Add carrier selection for Shopify order

Closes #14102
This commit is contained in:
Cédric Krier
2025-07-03 16:49:46 +02:00
parent 807b593f1e
commit 567cae3c86
12 changed files with 175 additions and 11 deletions
+1
View File
@@ -1,3 +1,4 @@
* Add carrier selection
* Use GraphQL API
* Add option to notify customer about fulfillment
* Support taxes from shipping product
+59
View File
@@ -0,0 +1,59 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import re
from trytond.model import MatchMixin, ModelSQL, ModelView, fields
from trytond.pool import PoolMeta
class Carrier(metaclass=PoolMeta):
__name__ = 'carrier'
shopify_selections = fields.One2Many(
'carrier.selection.shopify', 'carrier', "Shopify Selections",
help="Define the criteria that will match this carrier "
"with the Shopify shipping methods.")
def shopify_match(self, shop, shipping_line, pattern=None):
pattern = pattern.copy() if pattern is not None else {}
pattern.setdefault('shop', shop.id)
pattern.setdefault('code', shipping_line.get('code'))
pattern.setdefault('title', shipping_line.get('title'))
for selection in self.shopify_selections:
if selection.match(pattern):
return True
return False
class SelectionShopify(MatchMixin, ModelSQL, ModelView):
__name__ = 'carrier.selection.shopify'
@classmethod
def __setup__(cls):
super().__setup__()
cls.__access__.add('carrier')
carrier = fields.Many2One(
'carrier', "Carrier", required=True, ondelete='CASCADE')
shop = fields.Many2One(
'web.shop', "Shop",
domain=[
('type', '=', 'shopify'),
])
code = fields.Char(
"Code",
help="The code of the shipping line.")
title = fields.Char(
"Title",
help="A regular expression to match the shipping line title.\n"
"Leave empty to allow any title.")
def match(self, pattern, match_none=False):
if 'title' in pattern:
pattern = pattern.copy()
title = pattern.pop('title') or ''
if (self.title is not None
and not re.search(self.title, title, flags=re.I)):
return False
return super().match(pattern, match_none=match_none)
+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tryton>
<data depends="carrier">
<record model="ir.ui.view" id="carrier_view_form">
<field name="model">carrier</field>
<field name="inherit" ref="carrier.carrier_view_form"/>
<field name="name">carrier_form</field>
</record>
<record model="ir.ui.view" id="carrier_selection_shopify_view_form">
<field name="model">carrier.selection.shopify</field>
<field name="type">form</field>
<field name="name">carrier_selection_shopify_form</field>
</record>
<record model="ir.ui.view" id="carrier_selection_shopify_view_list">
<field name="model">carrier.selection.shopify</field>
<field name="type">tree</field>
<field name="name">carrier_selection_shopify_list</field>
</record>
</data>
</tryton>
+14
View File
@@ -55,3 +55,17 @@ Shopify Inventory Item
The *Shopify Inventory Item* concept manages the Shopify inventory item of each
`Varient <product:model-product.product>`.
.. _model-carrier:
Carrier
=======
When the *Carrier Module* is activated, the `Carrier <carrier:model-carrier>`
is extended to allow to define the :guilabel:`Code` of the Shopify shipping
method.
.. seealso::
The `Carrier <carrier:model-carrier>` concept is introduced by the
:doc:`Carrier Module <carrier:index>`.
+16 -3
View File
@@ -721,6 +721,14 @@ class Sale_ShipmentCost(metaclass=PoolMeta):
return []
return super().set_shipment_cost()
@classmethod
def shopify_fields(cls):
fields = super().shopify_fields()
shipping_line = fields.setdefault('shippingLine', {})
shipping_line.setdefault('code')
shipping_line.setdefault('title')
return fields
@classmethod
def get_from_shopify(cls, shop, order, sale=None):
pool = Pool()
@@ -729,11 +737,16 @@ class Sale_ShipmentCost(metaclass=PoolMeta):
sale = super().get_from_shopify(shop, order, sale=sale)
shipment_cost_method = None
if order['shippingLines']:
if shipping_line := order['shippingLine']:
available_carriers = sale.on_change_with_available_carriers()
carrier = None
if available_carriers:
carrier = available_carriers[0]
for carrier in available_carriers:
if carrier.shopify_match(shop, shipping_line):
carrier = carrier
break
else:
if available_carriers:
carrier = available_carriers[0]
setattr_changed(sale, 'carrier', carrier)
if sale.carrier:
shipment_cost_method = 'order'
+1
View File
@@ -53,6 +53,7 @@ requires.append(get_require_version('trytond'))
tests_require = [
get_require_version('proteus'),
get_require_version('trytond_account_payment_clearing'),
get_require_version('trytond_carrier'),
get_require_version('trytond_customs'),
get_require_version('trytond_product_image'),
get_require_version('trytond_product_image_attribute'),
@@ -46,6 +46,7 @@ Activate modules::
>>> config = activate_modules([
... 'web_shop_shopify',
... 'account_payment_clearing',
... 'carrier',
... 'customs',
... 'product_measurements',
... 'product_image',
@@ -280,7 +281,7 @@ Create products::
... 'https://downloads.tryton.org/tests/shopify/chair-white.jpg').read()
>>> variant2.save()
Create carrier::
Create carriers::
>>> carrier_template = ProductTemplate()
>>> carrier_template.name = 'Carrier Product'
@@ -294,12 +295,23 @@ Create carrier::
>>> carrier_product.cost_price = Decimal('2')
>>> carrier_product.save()
>>> carrier = Carrier()
>>> party = Party(name='Carrier')
>>> carrier1 = Carrier()
>>> party = Party(name="Carrier 1")
>>> party.save()
>>> carrier.party = party
>>> carrier.carrier_product = carrier_product
>>> carrier.save()
>>> carrier1.party = party
>>> carrier1.carrier_product = carrier_product
>>> carrier1.save()
>>> carrier2 = Carrier()
>>> party = Party(name="Carrier 2")
>>> party.save()
>>> carrier2.party = party
>>> carrier2.carrier_product = carrier_product
>>> _ = carrier2.shopify_selections.new(code='SHIP')
>>> carrier2.save()
>>> CarrierSelection(carrier=carrier1).save()
>>> CarrierSelection(carrier=carrier2).save()
Fill warehouse::
@@ -562,7 +574,7 @@ Run fetch order::
'processing'
>>> payment.amount
Decimal('0')
>>> assertEqual(sale.carrier, carrier)
>>> assertEqual(sale.carrier, carrier2)
>>> sale.state
'quotation'
>>> sale.party.name
@@ -10,7 +10,7 @@ class WebShopShopifyTestCase(PartyCheckReplaceMixin, ModuleTestCase):
'Test Web Shop Shopify module'
module = 'web_shop_shopify'
extras = [
'customs', 'product_image', 'product_image_attribute',
'carrier', 'customs', 'product_image', 'product_image_attribute',
'product_measurements', 'sale_discount', 'sale_invoice_grouping',
'sale_secondary_unit', 'sale_shipment_cost', 'stock_package_shipping']
+7
View File
@@ -13,6 +13,7 @@ depends:
stock
web_shop
extras_depend:
carrier
customs
product_image
product_image_attribute
@@ -27,6 +28,7 @@ xml:
product.xml
web.xml
stock.xml
carrier.xml
message.xml
[register]
@@ -58,6 +60,11 @@ model:
wizard:
party.Replace
[register carrier]
model:
carrier.Carrier
carrier.SelectionShopify
[register customs]
model:
product.ShopifyInventoryItem_Customs
@@ -0,0 +1,8 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<data>
<xpath expr="/form" position="inside">
<field name="shopify_selections" colspan="4"/>
</xpath>
</data>
@@ -0,0 +1,17 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<form cursor="shop" col="2">
<label name="carrier"/>
<field name="carrier"/>
<separator string="Criteria" id="criteria" colspan="2"/>
<label name="shop"/>
<field name="shop"/>
<label name="code"/>
<field name="code"/>
<label name="title"/>
<field name="title"/>
</form>
@@ -0,0 +1,9 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tree>
<field name="carrier" expand="2"/>
<field name="shop" expand="1"/>
<field name="code" expand="1"/>
<field name="title" expand="1"/>
</tree>