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):

Reply via email to