Add support for pick-up delivery method from Shopify

Closes #14581
This commit is contained in:
Cédric Krier
2026-03-27 13:38:34 +01:00
parent 41f63dbb1a
commit df5dc23253
6 changed files with 109 additions and 27 deletions
+1
View File
@@ -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
+4
View File
@@ -49,6 +49,10 @@ this repository contains the full copyright notices and license terms. -->
</record>
<record model="ir.message" id="msg_inventory_set_fail">
<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">
+20 -15
View File
@@ -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}
+58
View File
@@ -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'
@@ -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',
@@ -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',