Restock product inventory when an order is cancelled #9

Merged
Ghasemi merged 1 commit from fix/restock-on-cancel into master 2026-09-05 08:16:25 -04:00
2 changed files with 38 additions and 0 deletions

View file

@ -1,11 +1,14 @@
from itertools import groupby from itertools import groupby
from django.db import transaction from django.db import transaction
from django.db.models import F
from django.utils import timezone from django.utils import timezone
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from utils.exceptions import Conflict from utils.exceptions import Conflict
from apps.catalog.models import Product
from .models import Cart, Notification, Order, OrderGroup, OrderItem, OrderStatusLog 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 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): def _transition(order, to_status, *, changed_by=None, note='', **extra_fields):
from_status = order.status from_status = order.status
order.status = to_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) 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: if to_status == Order.Status.CANCELLED:
_restock_order_items(order)
notify( notify(
recipient=order.store.owner, recipient=order.store.owner,
type=Notification.Type.ORDER_CANCELLED, type=Notification.Type.ORDER_CANCELLED,

View file

@ -50,6 +50,30 @@ class StatusTransitionTests(OrdersTestCase):
self.order.refresh_from_db() self.order.refresh_from_db()
self.assertEqual(self.order.status, Order.Status.CANCELLED) 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): def test_cannot_cancel_after_handed_to_courier(self):
self._as_seller() self._as_seller()
for action in ('confirm', 'mark-ready', 'mark-shipped'): for action in ('confirm', 'mark-ready', 'mark-shipped'):