From 567cae3c865a9d67831c1999d0e88df5874be92c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Thu, 3 Jul 2025 16:49:46 +0200 Subject: [PATCH] Add carrier selection for Shopify order Closes #14102 --- modules/web_shop_shopify/CHANGELOG | 1 + modules/web_shop_shopify/carrier.py | 59 +++++++++++++++++++ modules/web_shop_shopify/carrier.xml | 23 ++++++++ modules/web_shop_shopify/doc/design.rst | 14 +++++ modules/web_shop_shopify/sale.py | 19 +++++- modules/web_shop_shopify/setup.py | 1 + .../tests/scenario_web_shop_shopify.rst | 26 +++++--- modules/web_shop_shopify/tests/test_module.py | 2 +- modules/web_shop_shopify/tryton.cfg | 7 +++ .../web_shop_shopify/view/carrier_form.xml | 8 +++ .../view/carrier_selection_shopify_form.xml | 17 ++++++ .../view/carrier_selection_shopify_list.xml | 9 +++ 12 files changed, 175 insertions(+), 11 deletions(-) create mode 100644 modules/web_shop_shopify/carrier.py create mode 100644 modules/web_shop_shopify/carrier.xml create mode 100644 modules/web_shop_shopify/view/carrier_form.xml create mode 100644 modules/web_shop_shopify/view/carrier_selection_shopify_form.xml create mode 100644 modules/web_shop_shopify/view/carrier_selection_shopify_list.xml diff --git a/modules/web_shop_shopify/CHANGELOG b/modules/web_shop_shopify/CHANGELOG index 1791fb7e29..b8fe8143a5 100644 --- a/modules/web_shop_shopify/CHANGELOG +++ b/modules/web_shop_shopify/CHANGELOG @@ -1,3 +1,4 @@ +* Add carrier selection * Use GraphQL API * Add option to notify customer about fulfillment * Support taxes from shipping product diff --git a/modules/web_shop_shopify/carrier.py b/modules/web_shop_shopify/carrier.py new file mode 100644 index 0000000000..dca9cdbddd --- /dev/null +++ b/modules/web_shop_shopify/carrier.py @@ -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) diff --git a/modules/web_shop_shopify/carrier.xml b/modules/web_shop_shopify/carrier.xml new file mode 100644 index 0000000000..21119ec934 --- /dev/null +++ b/modules/web_shop_shopify/carrier.xml @@ -0,0 +1,23 @@ + + + + + + carrier + + carrier_form + + + + carrier.selection.shopify + form + carrier_selection_shopify_form + + + carrier.selection.shopify + tree + carrier_selection_shopify_list + + + diff --git a/modules/web_shop_shopify/doc/design.rst b/modules/web_shop_shopify/doc/design.rst index 06218fc64f..20f2f6712d 100644 --- a/modules/web_shop_shopify/doc/design.rst +++ b/modules/web_shop_shopify/doc/design.rst @@ -55,3 +55,17 @@ Shopify Inventory Item The *Shopify Inventory Item* concept manages the Shopify inventory item of each `Varient `. + +.. _model-carrier: + +Carrier +======= + +When the *Carrier Module* is activated, the `Carrier ` +is extended to allow to define the :guilabel:`Code` of the Shopify shipping +method. + +.. seealso:: + + The `Carrier ` concept is introduced by the + :doc:`Carrier Module `. diff --git a/modules/web_shop_shopify/sale.py b/modules/web_shop_shopify/sale.py index b587fb6261..d9408981f6 100644 --- a/modules/web_shop_shopify/sale.py +++ b/modules/web_shop_shopify/sale.py @@ -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' diff --git a/modules/web_shop_shopify/setup.py b/modules/web_shop_shopify/setup.py index a20deb2e4f..50cc0c211d 100755 --- a/modules/web_shop_shopify/setup.py +++ b/modules/web_shop_shopify/setup.py @@ -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'), diff --git a/modules/web_shop_shopify/tests/scenario_web_shop_shopify.rst b/modules/web_shop_shopify/tests/scenario_web_shop_shopify.rst index acfc0f81e7..02723a51db 100644 --- a/modules/web_shop_shopify/tests/scenario_web_shop_shopify.rst +++ b/modules/web_shop_shopify/tests/scenario_web_shop_shopify.rst @@ -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 diff --git a/modules/web_shop_shopify/tests/test_module.py b/modules/web_shop_shopify/tests/test_module.py index 259044c3e0..7b521a4849 100644 --- a/modules/web_shop_shopify/tests/test_module.py +++ b/modules/web_shop_shopify/tests/test_module.py @@ -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'] diff --git a/modules/web_shop_shopify/tryton.cfg b/modules/web_shop_shopify/tryton.cfg index 4ec83c4a36..a5b31fa861 100644 --- a/modules/web_shop_shopify/tryton.cfg +++ b/modules/web_shop_shopify/tryton.cfg @@ -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 diff --git a/modules/web_shop_shopify/view/carrier_form.xml b/modules/web_shop_shopify/view/carrier_form.xml new file mode 100644 index 0000000000..65c67d8fb5 --- /dev/null +++ b/modules/web_shop_shopify/view/carrier_form.xml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/modules/web_shop_shopify/view/carrier_selection_shopify_form.xml b/modules/web_shop_shopify/view/carrier_selection_shopify_form.xml new file mode 100644 index 0000000000..24818f631b --- /dev/null +++ b/modules/web_shop_shopify/view/carrier_selection_shopify_form.xml @@ -0,0 +1,17 @@ + + +
+