From df5dc2325383d8964b56db2f176d89054ac3a77e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Fri, 27 Mar 2026 13:38:34 +0100 Subject: [PATCH] Add support for pick-up delivery method from Shopify Closes #14581 --- 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 +++++++++++++++++++ .../scenario_web_shop_shopify_product_kit.rst | 19 ++++-- ...enario_web_shop_shopify_secondary_unit.rst | 19 ++++-- 6 files changed, 109 insertions(+), 27 deletions(-) diff --git a/modules/web_shop_shopify/CHANGELOG b/modules/web_shop_shopify/CHANGELOG index c7f59b66c5..9ae17ec544 100644 --- a/modules/web_shop_shopify/CHANGELOG +++ b/modules/web_shop_shopify/CHANGELOG @@ -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 --git a/modules/web_shop_shopify/message.xml b/modules/web_shop_shopify/message.xml index 8467a1fae2..3323a1e631 100644 --- a/modules/web_shop_shopify/message.xml +++ b/modules/web_shop_shopify/message.xml @@ -49,6 +49,10 @@ this repository contains the full copyright notices and license terms. --> Failed to set inventory with error: +%(error)s + + + Failed to prepare for pickup fulfillments for shipments "%(shipments)s" and sales "%(sales)s" with error: %(error)s diff --git a/modules/web_shop_shopify/sale.py b/modules/web_shop_shopify/sale.py index 1175d07b49..b207cf09f5 100644 --- a/modules/web_shop_shopify/sale.py +++ b/modules/web_shop_shopify/sale.py @@ -251,6 +251,9 @@ class Sale(IdentifierMixin, metaclass=PoolMeta): 'id': None, }, }, + 'deliveryMethod': { + 'methodType': None, + }, 'lineItems(first: 100)': { 'nodes': { 'lineItem': { @@ -264,6 +267,7 @@ class Sale(IdentifierMixin, metaclass=PoolMeta): 'endCursor': None, }, }, + 'status': None, }, 'pageInfo': { 'hasNextPage': None, @@ -348,23 +352,9 @@ class Sale(IdentifierMixin, metaclass=PoolMeta): 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 @@ class Sale(IdentifierMixin, metaclass=PoolMeta): 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 @@ class Sale(IdentifierMixin, metaclass=PoolMeta): 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 --git a/modules/web_shop_shopify/stock.py b/modules/web_shop_shopify/stock.py index fd847eb28f..fafa1e7d6f 100644 --- a/modules/web_shop_shopify/stock.py +++ b/modules/web_shop_shopify/stock.py @@ -13,12 +13,24 @@ from trytond.pool import Pool, PoolMeta 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 @@ class ShipmentOut(metaclass=PoolMeta): 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 --git a/modules/web_shop_shopify/tests/scenario_web_shop_shopify_product_kit.rst b/modules/web_shop_shopify/tests/scenario_web_shop_shopify_product_kit.rst index 42b33d15c7..65f6f6a6f8 100644 --- a/modules/web_shop_shopify/tests/scenario_web_shop_shopify_product_kit.rst +++ b/modules/web_shop_shopify/tests/scenario_web_shop_shopify_product_kit.rst @@ -36,6 +36,7 @@ Activate modules:: >>> 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 @@ Activate modules:: >>> 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 @@ Create an order on Shopify:: ... '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 --git a/modules/web_shop_shopify/tests/scenario_web_shop_shopify_secondary_unit.rst b/modules/web_shop_shopify/tests/scenario_web_shop_shopify_secondary_unit.rst index 867b56d1d7..0d53e1e295 100644 --- a/modules/web_shop_shopify/tests/scenario_web_shop_shopify_secondary_unit.rst +++ b/modules/web_shop_shopify/tests/scenario_web_shop_shopify_secondary_unit.rst @@ -36,6 +36,7 @@ Activate modules:: >>> 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 @@ Activate modules:: >>> 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 @@ Create an order on Shopify:: ... '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',