changeset a72cad833857 in modules/web_shop_shopify:default
details:
https://hg.tryton.org/modules/web_shop_shopify?cmd=changeset&node=a72cad833857
description:
Support edit orders from Shopify
issue11085
review370521004
diffstat:
CHANGELOG | 1 +
sale.py | 81 ++++++++++++++++++++++++++++++++++++++++++++-----------------
tryton.cfg | 1 +
web.py | 68 ++++++++++++++++++++++++++++++++++++++++++++++-----
4 files changed, 121 insertions(+), 30 deletions(-)
diffs (302 lines):
diff -r 166c380fb612 -r a72cad833857 CHANGELOG
--- a/CHANGELOG Tue Jan 04 22:28:46 2022 +0100
+++ b/CHANGELOG Thu Jan 20 23:34:42 2022 +0100
@@ -1,3 +1,4 @@
+* Support edit orders from Shopify
* Define the metafields managed by Tryton
* Add support for Python 3.10
* Remove support for Python 3.6
diff -r 166c380fb612 -r a72cad833857 sale.py
--- a/sale.py Tue Jan 04 22:28:46 2022 +0100
+++ b/sale.py Thu Jan 20 23:34:42 2022 +0100
@@ -1,7 +1,9 @@
# 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 time
+from collections import defaultdict
from decimal import Decimal
+from itertools import zip_longest
import dateutil
import shopify
@@ -44,7 +46,7 @@
return amount
@classmethod
- def get_from_shopify(cls, shop, order):
+ def get_from_shopify(cls, shop, order, sale=None):
pool = Pool()
Party = pool.get('party.party')
Address = pool.get('party.address')
@@ -60,8 +62,10 @@
party = Party()
party.save()
- sale = shop.get_sale(party=party)
- sale.shopify_identifier = order.id
+ if not sale:
+ sale = shop.get_sale(party=party)
+ sale.shopify_identifier = order.id
+ assert sale.shopify_identifier == order.id
if order.location_id:
for shop_warehouse in shop.shopify_warehouses:
if shop_warehouse.shopify_id == str(order.location_id):
@@ -102,11 +106,37 @@
party=party, type='phone', value=order.phone)
sale.contact = contact_mechanism
+ refund_line_items = defaultdict(list)
+ for refund in order.refunds:
+ for refund_line_item in refund.refund_line_items:
+ refund_line_items[refund_line_item.line_item_id].append(
+ refund_line_item)
+
+ id2line = {
+ l.shopify_identifier: l for l in getattr(sale, 'lines', [])
+ if l.shopify_identifier}
+ shipping_lines = [
+ l for l in getattr(sale, 'lines', []) if not
+ l.shopify_identifier]
lines = []
for line_item in order.line_items:
- lines.append(Line.get_from_shopify(sale, line_item))
- for shipping_line in order.shipping_lines:
- lines.append(Line.get_from_shopify_shipping(sale, shipping_line))
+ line = id2line.pop(line_item.id, None)
+ quantity = line_item.quantity
+ for refund_line_item in refund_line_items[line_item.id]:
+ if refund_line_item.restock_type == 'cancel':
+ quantity -= refund_line_item.quantity
+ lines.append(Line.get_from_shopify(
+ sale, line_item, quantity, line=line))
+ for shipping_line, line in zip_longest(
+ order.shipping_lines, shipping_lines):
+ if shipping_line:
+ line = Line.get_from_shopify_shipping(
+ sale, shipping_line, line=line)
+ else:
+ line.quantity = 0
+ lines.append(line)
+ for line in id2line.values():
+ line.quantity = 0
sale.lines = lines
return sale
@@ -275,11 +305,11 @@
return super().set_shipment_cost()
@classmethod
- def get_from_shopify(cls, shop, order):
+ def get_from_shopify(cls, shop, order, sale=None):
pool = Pool()
Tax = pool.get('account.tax')
- sale = super().get_from_shopify(shop, order)
+ sale = super().get_from_shopify(shop, order, sale=sale)
sale.shipment_cost_method = 'order'
if order.shipping_lines:
@@ -305,24 +335,26 @@
__name__ = 'sale.line'
@classmethod
- def get_from_shopify(cls, sale, line_item):
+ def get_from_shopify(cls, sale, line_item, quantity, line=None):
pool = Pool()
Product = pool.get('product.product')
Tax = pool.get('account.tax')
- line = cls(type='line')
- line.sale = sale
- line.shopify_identifier = line_item.id
+ if not line:
+ line = cls(type='line')
+ line.sale = sale
+ line.shopify_identifier = line_item.id
+ assert line.shopify_identifier == line_item.id
if hasattr(line_item, 'variant_id'):
line.product = Product.search_shopify_identifier(
sale.web_shop, line_item.variant_id)
else:
line.product = None
if line.product:
- line._set_shopify_quantity(line.product, line_item.quantity)
+ line._set_shopify_quantity(line.product, quantity)
line.on_change_product()
else:
- line.quantity = line_item.quantity
+ line.quantity = quantity
line.description = line_item.title
line.taxes = []
total_discount = sum(
@@ -349,9 +381,10 @@
self.unit_price = unit_price
@classmethod
- def get_from_shopify_shipping(cls, sale, shipping_line):
- line = cls(type='line')
- line.sale = sale
+ def get_from_shopify_shipping(cls, sale, shipping_line, line=None):
+ if not line:
+ line = cls(type='line')
+ line.sale = sale
line.quantity = 1
line.unit_price = round_price(Decimal(shipping_line.discounted_price))
line.description = shipping_line.title
@@ -371,17 +404,18 @@
__name__ = 'sale.line'
@classmethod
- def get_from_shopify(cls, sale, line_item):
+ def get_from_shopify(cls, sale, line_item, quantity, line=None):
pool = Pool()
Tax = pool.get('account.tax')
- line = super().get_from_shopify(sale, line_item)
+ line = super().get_from_shopify(sale, line_item, quantity, line=line)
line.base_price = round_price(Tax.reverse_compute(
Decimal(line_item.price), line.taxes, sale.sale_date))
return line
@classmethod
- def get_from_shopify_shipping(cls, sale, shipping_line):
- line = super().get_from_shopify_shipping(sale, shipping_line)
+ def get_from_shopify_shipping(cls, sale, shipping_line, line=None):
+ line = super().get_from_shopify_shipping(
+ sale, shipping_line, line=line)
line.base_price = Decimal(shipping_line.price)
return line
@@ -413,8 +447,9 @@
__name__ = 'sale.line'
@classmethod
- def get_from_shopify_shipping(cls, sale, shipping_line):
- line = super().get_from_shopify_shipping(sale, shipping_line)
+ def get_from_shopify_shipping(cls, sale, shipping_line, line=None):
+ line = super().get_from_shopify_shipping(
+ sale, shipping_line, line=line)
line.shipment_cost = Decimal(shipping_line.price)
return line
diff -r 166c380fb612 -r a72cad833857 tryton.cfg
--- a/tryton.cfg Tue Jan 04 22:28:46 2022 +0100
+++ b/tryton.cfg Thu Jan 20 23:34:42 2022 +0100
@@ -8,6 +8,7 @@
product
product_attribute
sale
+ sale_amendment
sale_payment
stock
web_shop
diff -r 166c380fb612 -r a72cad833857 web.py
--- a/web.py Tue Jan 04 22:28:46 2022 +0100
+++ b/web.py Thu Jan 20 23:34:42 2022 +0100
@@ -1,5 +1,6 @@
# 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 datetime as dt
import time
from decimal import Decimal
@@ -14,6 +15,7 @@
MatchMixin, ModelSQL, ModelView, Unique, fields, sequence_ordered)
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Eval
+from trytond.tools import grouped_slice
from trytond.transaction import Transaction
from .common import IdentifierMixin, IdentifiersMixin
@@ -22,6 +24,7 @@
BACKOFF_TIME = config.getfloat(
'web_shop_shopify', 'api_backoff_time', default=1)
BACKOFF_TIME_FACTOR = 12
+EDIT_ORDER_DELAY = dt.timedelta(days=60 + 1)
class Shop(metaclass=PoolMeta):
@@ -509,26 +512,77 @@
@classmethod
def shopify_update_order(cls, shops=None):
- """Update existing Shopify Order"""
+ """Update existing sale from Shopify"""
pool = Pool()
Sale = pool.get('sale.sale')
- Payment = pool.get('account.payment')
if shops is None:
shops = cls.search([
('type', '=', 'shopify'),
])
cls.lock(shops)
+ now = dt.datetime.now()
for shop in shops:
sales = Sale.search([
('web_shop', '=', shop.id),
('shopify_identifier', '!=', None),
- ('state', 'in', ['quotation', 'confirmed', 'processing']),
+ ['OR',
+ ('state', 'in',
+ ['quotation', 'confirmed', 'processing']),
+ ('create_date', '>=', now - EDIT_ORDER_DELAY),
+ ],
])
+ for sub_sales in grouped_slice(sales, count=250):
+ cls._shopify_update_order(shop, list(sub_sales))
+
+ @classmethod
+ def _shopify_update_order(cls, shop, sales):
+ assert shop.type == 'shopify'
+ assert all(s.web_shop == shop for s in sales)
+ with shop.shopify_session():
+ orders = shopify.Order.find(
+ ids=','.join(str(s.shopify_identifier) for s in sales),
+ status='any')
+ id2order = {o.id: o for o in orders}
+
+ to_update = []
+ orders = []
+ for sale in sales:
+ try:
+ order = id2order[sale.shopify_identifier]
+ except KeyError:
+ continue
+ to_update.append(sale)
+ orders.append(order)
+ cls.shopify_update_sale(to_update, orders)
+
+ @classmethod
+ def shopify_update_sale(cls, sales, orders):
+ """Update sales based on Shopify orders"""
+ pool = Pool()
+ Amendment = pool.get('sale.amendment')
+ Payment = pool.get('account.payment')
+ Sale = pool.get('sale.sale')
+ assert len(sales) == len(orders)
+ to_update = {}
+ for sale, order in zip(sales, orders):
+ assert sale.shopify_identifier == order.id
+ shop = sale.web_shop
with shop.shopify_session():
- for sale in sales:
- order, = shopify.Order.find(ids=sale.shopify_identifier)
- Payment.get_from_shopify(sale, order)
- time.sleep(BACKOFF_TIME)
+ sale = Sale.get_from_shopify(shop, order, sale=sale)
+ if sale._changed_values:
+ sale.untaxed_amount_cache = None
+ sale.tax_amount_cache = None
+ sale.total_amount_cache = None
+ to_update[sale] = order
+ Payment.get_from_shopify(sale, order)
+ time.sleep(BACKOFF_TIME)
+ Sale.save(to_update.keys())
+ for sale, order in to_update.items():
+ sale.shopify_tax_adjustment = (
+ Decimal(order.current_total_price) - sale.total_amount)
+ Sale.store_cache(to_update.keys())
+ Amendment._clear_sale(to_update.keys())
+ Sale.__queue__.process(to_update.keys())
class ShopShopifyIdentifier(IdentifierMixin, ModelSQL, ModelView):