details:   https://code.tryton.org/tryton/commit/ecedcfd87dba
branch:    default
user:      Cédric Krier <[email protected]>
date:      Thu Apr 16 11:11:01 2026 +0200
description:
        Ensure to refund Shopify order only once

        There are no guarantee from Shopify that the fulfillableQuantity will be
        updated just after the refund creation but the sale may be re-processed 
via the
        Shopify webhooks just after the refund.
        To avoid to refund multiple times, we need to keep track that the 
refund was
        already be made.

        Closes #14776
diffstat:

 modules/web_shop_shopify/sale.py |  26 ++++++++++++++++++++++++--
 1 files changed, 24 insertions(+), 2 deletions(-)

diffs (64 lines):

diff -r 2763a98a25e4 -r ecedcfd87dba modules/web_shop_shopify/sale.py
--- a/modules/web_shop_shopify/sale.py  Thu May 07 18:12:20 2026 +0200
+++ b/modules/web_shop_shopify/sale.py  Thu Apr 16 11:11:01 2026 +0200
@@ -7,7 +7,7 @@
 import dateutil
 
 from trytond.i18n import gettext
-from trytond.model import ModelView, Unique, fields
+from trytond.model import ModelView, Unique, Workflow, fields
 from trytond.modules.currency.fields import Monetary
 from trytond.modules.product import round_price
 from trytond.modules.sale.exceptions import SaleConfirmError
@@ -154,6 +154,8 @@
     shopify_amount_to_pay = Monetary(
         "Shopify Amount to Pay",
         currency='currency', digits='currency', readonly=True)
+    shopify_refunded = fields.Boolean(
+        "Shopify Refunded", readonly=True)
     shopify_status_url = fields.Char("Shopify Status URL", readonly=True)
     shopify_resource = 'orders'
 
@@ -561,6 +563,23 @@
         return invoice
 
     @classmethod
+    @Workflow.transition('processing')
+    def proceed(cls, sales):
+        if shopify_refunded_sales := [s for s in sales if s.shopify_refunded]:
+            cls.write(shopify_refunded_sales, {'shopify_refunded': False})
+        super().proceed(sales)
+
+    @classmethod
+    def _process_invoice_fulfillment_states(cls, sales):
+        shopify_refunded_sales = [
+            s for s in sales if s.shopify_refunded]
+        super()._process_invoice_fulfillment_states(sales)
+        if shopify_refunded_sales := [
+                s for s in shopify_refunded_sales
+                if s.shipment_state != 'sent']:
+            cls.write(shopify_refunded_sales, {'shopify_refunded': False})
+
+    @classmethod
     @ModelView.button
     def process(cls, sales):
         for sale in sales:
@@ -625,7 +644,8 @@
         # TODO: manage drop shipment
 
         shopify_id = id2gid('Order', self.shopify_identifier)
-        if self.shipment_state == 'sent' or self.state == 'done':
+        if ((self.shipment_state == 'sent' or self.state == 'done')
+                and not self.shopify_refunded):
             # TODO: manage shopping refund
             refund = self.get_shopify_refund(
                 shipping=self.shipment_state == 'none')
@@ -645,6 +665,8 @@
                         'fields': graphql.selection(self.shopify_fields()),
                         }, {'id': shopify_id}).data['order']
                 Payment.get_from_shopify(self, order)
+            self.shopify_refunded = True
+            self.save()
 
         shopify_id = id2gid('Order', self.shopify_identifier)
         order = shopify_request(

Reply via email to