details:   https://code.tryton.org/tryton/commit/7ce694dea057
branch:    default
user:      Cédric Krier <[email protected]>
date:      Fri Mar 27 13:38:34 2026 +0100
description:
        Add support for pick-up delivery method from Shopify

        Closes #14581
diffstat:

 modules/web_shop_shopify/CHANGELOG                                          |  
 1 +
 modules/web_shop_shopify/message.xml                                        |  
 4 +
 modules/web_shop_shopify/sale.py                                            |  
35 +++--
 modules/web_shop_shopify/stock.py                                           |  
58 ++++++++++
 modules/web_shop_shopify/tests/scenario_web_shop_shopify_product_kit.rst    |  
19 ++-
 modules/web_shop_shopify/tests/scenario_web_shop_shopify_secondary_unit.rst |  
19 ++-
 6 files changed, 109 insertions(+), 27 deletions(-)

diffs (272 lines):

diff -r df818def97f8 -r 7ce694dea057 modules/web_shop_shopify/CHANGELOG
--- a/modules/web_shop_shopify/CHANGELOG        Thu Mar 26 10:58:16 2026 +0100
+++ b/modules/web_shop_shopify/CHANGELOG        Fri Mar 27 13:38:34 2026 +0100
@@ -1,3 +1,4 @@
+* Add support for pick-up delivery method
 * Log the update of sales
 * Add support for gift card products
 * Add support for Python 3.14
diff -r df818def97f8 -r 7ce694dea057 modules/web_shop_shopify/message.xml
--- a/modules/web_shop_shopify/message.xml      Thu Mar 26 10:58:16 2026 +0100
+++ b/modules/web_shop_shopify/message.xml      Fri Mar 27 13:38:34 2026 +0100
@@ -51,6 +51,10 @@
             <field name="text">Failed to set inventory with error:
 %(error)s</field>
         </record>
+        <record model="ir.message" 
id="msg_fulfillment_prepared_for_pickup_fail">
+            <field name="text">Failed to prepare for pickup fulfillments for 
shipments "%(shipments)s" and sales "%(sales)s" with error:
+%(error)s</field>
+        </record>
         <record model="ir.message" id="msg_fulfillment_fail">
             <field name="text">Failed to save fulfillment for sale "%(sale)s" 
with error:
 %(error)s</field>
diff -r df818def97f8 -r 7ce694dea057 modules/web_shop_shopify/sale.py
--- a/modules/web_shop_shopify/sale.py  Thu Mar 26 10:58:16 2026 +0100
+++ b/modules/web_shop_shopify/sale.py  Fri Mar 27 13:38:34 2026 +0100
@@ -251,6 +251,9 @@
                             'id': None,
                             },
                         },
+                    'deliveryMethod': {
+                        'methodType': None,
+                        },
                     'lineItems(first: 100)': {
                         'nodes': {
                             'lineItem': {
@@ -264,6 +267,7 @@
                             'endCursor': None,
                             },
                         },
+                    'status': None,
                     },
                 'pageInfo': {
                     'hasNextPage': None,
@@ -348,23 +352,9 @@
             else:
                 invoice_address = None
         else:
-            shipment_address = sale.party.address_get(type='delivery')
+            shipment_address = None
             invoice_address = sale.party.address_get(type='invoice')
 
-        if shipment_address:
-            setattr_changed(sale, 'shipment_address', shipment_address)
-        if invoice_address or shipment_address:
-            setattr_changed(
-                sale, 'invoice_address', invoice_address or shipment_address)
-
-        if not party.addresses:
-            address = Address(party=party)
-            address.save()
-            if not sale.shipment_address:
-                sale.shipment_address = address
-            if not sale.invoice_address:
-                sale.invoice_address = address
-
         setattr_changed(sale, 'reference', order['name'])
         setattr_changed(sale, 'shopify_status_url', order['statusPageUrl'])
         setattr_changed(sale, 'comment', order['note'])
@@ -419,6 +409,10 @@
                     break
             else:
                 continue
+            if fulfillment_order['status'] != 'CANCELLED':
+                method_type = fulfillment_order['deliveryMethod']['methodType']
+                if method_type == 'PICK_UP':
+                    shipment_address = sale.warehouse.address
             shopify_line_items = graphql.iterate(
                 QUERY_FULFILLMENT_ORDER % {
                     'fields': graphql.selection({
@@ -436,6 +430,17 @@
                     line2warehouses[gid2id(line_item['lineItem']['id'])].add(
                         warehouse)
 
+        if shipment_address:
+            setattr_changed(sale, 'shipment_address', shipment_address)
+        if invoice_address or shipment_address:
+            setattr_changed(
+                sale, 'invoice_address', invoice_address or shipment_address)
+
+        if not party.addresses and not sale.invoice_address:
+            address = Address(party=party)
+            address.save()
+            sale.invoice_address = address
+
         id2line = {
             l.shopify_identifier: l for l in getattr(sale, 'lines', [])
             if l.shopify_identifier}
diff -r df818def97f8 -r 7ce694dea057 modules/web_shop_shopify/stock.py
--- a/modules/web_shop_shopify/stock.py Thu Mar 26 10:58:16 2026 +0100
+++ b/modules/web_shop_shopify/stock.py Fri Mar 27 13:38:34 2026 +0100
@@ -13,12 +13,24 @@
 from . import graphql
 from .common import IdentifierMixin, id2gid
 from .exceptions import ShopifyError
+from .shopify_retry import GraphQLException
 
 QUERY_FULFILLMENT_ORDERS = '''\
 query FulfillmentOrders($orderId: ID!) {
     order(id: $orderId) %(fields)s
 }'''
 
+MUTATION_FULFILLMENT_ORDER_LINE_ITEMS_PREPARED_FOR_PICKUP = '''\
+mutation fulfillmentOrderLineItemsPreparedForPickup(\
+        $input: FulfillmentOrderLineItemsPreparedForPickupInput!) {
+    fulfillmentOrderLineItemsPreparedForPickup(input: $input) {
+        userErrors {
+            field
+            message
+        }
+    }
+}'''
+
 
 class ShipmentOut(metaclass=PoolMeta):
     __name__ = 'stock.shipment.out'
@@ -145,6 +157,52 @@
                         shipment=shipment.rec_name))
         super().draft(shipments)
 
+    @classmethod
+    @ModelView.button
+    @Workflow.transition('packed')
+    def pack(cls, shipments):
+        super().pack(shipments)
+        if pickup := [
+                s for s in shipments
+                if s.delivery_address == s.warehouse.address]:
+            cls.__queue__._shopify_prepared_for_pickup(pickup)
+
+    @classmethod
+    def _shopify_prepared_for_pickup(cls, shipments):
+        mutation = MUTATION_FULFILLMENT_ORDER_LINE_ITEMS_PREPARED_FOR_PICKUP
+        fullfilments = defaultdict(set)
+        for shipment in shipments:
+            if shipment.state == 'packed':
+                for record in shipment.shopify_identifiers:
+                    fullfilments[record.sale.web_shop].add(record)
+        for shop, records in fullfilments.items():
+            with shop.shopify_session():
+                fullfilment_ids = list({
+                        id2gid('FulfillmentOrder', r.shopify_identifier)
+                        for r in records})
+                try:
+                    result = shopify.GraphQL().execute(
+                        mutation, {
+                            'input': list(fullfilment_ids),
+                            }
+                        )['data']['fulfillmentOrderLineItemsPreparedForPickup']
+                    if errors := result.get('userErrors'):
+                        raise GraphQLException({'errors': errors})
+                except GraphQLException as e:
+                    shipments = ", ".join(
+                        r.shipment.rec_name for r in records[:5])
+                    sales = ", ".join(r.sale.rec_name for r in records[:5])
+                    if len(records) > 5:
+                        shipments += "..."
+                        sales += "..."
+                    raise ShopifyError(gettext(
+                            'web_shop_shopify'
+                            '.msg_fulfillment_prepared_for_pickup_fail',
+                            shipments=shipments,
+                            sales=sales,
+                            error="\n".join(
+                                err['message'] for err in e.errors))) from e
+
 
 class ShipmentShopifyIdentifier(IdentifierMixin, ModelSQL, ModelView):
     __name__ = 'stock.shipment.shopify_identifier'
diff -r df818def97f8 -r 7ce694dea057 
modules/web_shop_shopify/tests/scenario_web_shop_shopify_product_kit.rst
--- a/modules/web_shop_shopify/tests/scenario_web_shop_shopify_product_kit.rst  
Thu Mar 26 10:58:16 2026 +0100
+++ b/modules/web_shop_shopify/tests/scenario_web_shop_shopify_product_kit.rst  
Fri Mar 27 13:38:34 2026 +0100
@@ -36,6 +36,7 @@
     >>> Account = Model.get('account.account')
     >>> Category = Model.get('product.category')
     >>> Cron = Model.get('ir.cron')
+    >>> Country = Model.get('country.country')
     >>> Location = Model.get('stock.location')
     >>> PaymentJournal = Model.get('account.payment.journal')
     >>> Product = Model.get('product.product')
@@ -45,6 +46,11 @@
     >>> Uom = Model.get('product.uom')
     >>> WebShop = Model.get('web.shop')
 
+Create country::
+
+    >>> belgium = Country(name="Belgium", code='BE')
+    >>> belgium.save()
+
 Get company::
 
     >>> company = get_company()
@@ -161,16 +167,17 @@
     ...         'lastName': "Customer",
     ...         'email': (''.join(
     ...                 random.choice(string.ascii_letters) for _ in range(10))
-    ...             + '@example.com'),
-    ...         'addresses': [{
-    ...                 'address1': "Street",
-    ...                 'city': "City",
-    ...                 'countryCode': 'BE',
-    ...                 }],
+    ...             + '@example.com')
     ...         })
 
     >>> order = tools.create_order({
     ...         'customerId': customer['id'],
+    ...         'shippingAddress': {
+    ...                 'lastName': "Customer",
+    ...                 'address1': "Street",
+    ...                 'city': "City",
+    ...                 'countryCode': 'BE',
+    ...                 },
     ...         'lineItems': [{
     ...             'variantId': id2gid(
     ...                 'ProductVariant',
diff -r df818def97f8 -r 7ce694dea057 
modules/web_shop_shopify/tests/scenario_web_shop_shopify_secondary_unit.rst
--- 
a/modules/web_shop_shopify/tests/scenario_web_shop_shopify_secondary_unit.rst   
    Thu Mar 26 10:58:16 2026 +0100
+++ 
b/modules/web_shop_shopify/tests/scenario_web_shop_shopify_secondary_unit.rst   
    Fri Mar 27 13:38:34 2026 +0100
@@ -36,6 +36,7 @@
     >>> Account = Model.get('account.account')
     >>> Category = Model.get('product.category')
     >>> Cron = Model.get('ir.cron')
+    >>> Country = Model.get('country.country')
     >>> Location = Model.get('stock.location')
     >>> PaymentJournal = Model.get('account.payment.journal')
     >>> Product = Model.get('product.product')
@@ -45,6 +46,11 @@
     >>> Uom = Model.get('product.uom')
     >>> WebShop = Model.get('web.shop')
 
+Create country::
+
+    >>> belgium = Country(name="Belgium", code='BE')
+    >>> belgium.save()
+
 Get company::
 
     >>> company = get_company()
@@ -149,16 +155,17 @@
     ...         'lastName': "Customer",
     ...         'email': (''.join(
     ...                 random.choice(string.ascii_letters) for _ in range(10))
-    ...             + '@example.com'),
-    ...         'addresses': [{
-    ...                 'address1': "Street",
-    ...                 'city': "City",
-    ...                 'countryCode': 'BE',
-    ...                 }],
+    ...             + '@example.com')
     ...         })
 
     >>> order = tools.create_order({
     ...         'customerId': customer['id'],
+    ...         'shippingAddress': {
+    ...                 'lastName': "Customer",
+    ...                 'address1': "Street",
+    ...                 'city': "City",
+    ...                 'countryCode': 'BE',
+    ...                 },
     ...         'lineItems': [{
     ...             'variantId': id2gid(
     ...                 'ProductVariant',

Reply via email to