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',