From 6431c28e0fd67bac03737760015539cecd700784 Mon Sep 17 00:00:00 2001 From: Ali Asadi Date: Sat, 5 Sep 2026 15:08:02 +0330 Subject: [PATCH] Restock product inventory when an order is cancelled checkout() decrements stock_quantity/increments sold_count per order item, but cancellation never reversed it, so a cancelled order's stock stayed permanently reduced. Add _restock_order_items(), called from _transition() on the CANCELLED transition, using F() expressions to reverse both counters atomically; wrap _transition() itself in @transaction.atomic so the status change and restock can't partially apply. Co-Authored-By: Claude Sonnet 5 --- apps/orders/services.py | 14 ++++++++++++ apps/orders/tests/test_status_transitions.py | 24 ++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/apps/orders/services.py b/apps/orders/services.py index ad2db7d..8c1b436 100644 --- a/apps/orders/services.py +++ b/apps/orders/services.py @@ -1,11 +1,14 @@ from itertools import groupby from django.db import transaction +from django.db.models import F from django.utils import timezone from rest_framework.exceptions import ValidationError from utils.exceptions import Conflict +from apps.catalog.models import Product + from .models import Cart, Notification, Order, OrderGroup, OrderItem, OrderStatusLog @@ -136,6 +139,16 @@ def checkout(user, address, delivery_type, scheduled_at, payment_method, notes): return order_group +def _restock_order_items(order): + """Reverses the stock_quantity/sold_count decrement made at checkout (see checkout()).""" + for item in order.items.select_related('product').filter(product__isnull=False): + Product.objects.filter(pk=item.product_id).update( + stock_quantity=F('stock_quantity') + item.quantity, + sold_count=F('sold_count') - item.quantity, + ) + + +@transaction.atomic def _transition(order, to_status, *, changed_by=None, note='', **extra_fields): from_status = order.status order.status = to_status @@ -147,6 +160,7 @@ def _transition(order, to_status, *, changed_by=None, note='', **extra_fields): OrderStatusLog.objects.create(order=order, from_status=from_status, to_status=to_status, changed_by=changed_by, note=note) if to_status == Order.Status.CANCELLED: + _restock_order_items(order) notify( recipient=order.store.owner, type=Notification.Type.ORDER_CANCELLED, diff --git a/apps/orders/tests/test_status_transitions.py b/apps/orders/tests/test_status_transitions.py index 5d2de47..4365e49 100644 --- a/apps/orders/tests/test_status_transitions.py +++ b/apps/orders/tests/test_status_transitions.py @@ -50,6 +50,30 @@ class StatusTransitionTests(OrdersTestCase): self.order.refresh_from_db() self.assertEqual(self.order.status, Order.Status.CANCELLED) + def test_cancelling_an_order_restocks_its_products(self): + self.product1.refresh_from_db() + stock_after_checkout = self.product1.stock_quantity + sold_after_checkout = self.product1.sold_count + self.assertEqual(stock_after_checkout, 49) # base.py seeds 50, checkout bought 1 + self.assertEqual(sold_after_checkout, 1) + + self.client.force_authenticate(user=self.customer) + response = self.client.post(f'/api/v1/orders/{self.order.uuid}/cancel/') + self.assertEqual(response.status_code, 200) + + self.product1.refresh_from_db() + self.assertEqual(self.product1.stock_quantity, stock_after_checkout + 1) + self.assertEqual(self.product1.sold_count, sold_after_checkout - 1) + + def test_cancelling_after_confirm_also_restocks(self): + self._as_seller() + self.client.post(f'/api/v1/seller/orders/{self.order.uuid}/confirm/') + self.client.post(f'/api/v1/seller/orders/{self.order.uuid}/cancel/') + + self.product1.refresh_from_db() + self.assertEqual(self.product1.stock_quantity, 50) + self.assertEqual(self.product1.sold_count, 0) + def test_cannot_cancel_after_handed_to_courier(self): self._as_seller() for action in ('confirm', 'mark-ready', 'mark-shipped'): -- 2.45.3