From 7b1922c186db463086e567b715b9c12563c8c316 Mon Sep 17 00:00:00 2001 From: Andrey Smakotin Date: Mon, 1 Dec 2025 02:57:44 +0300 Subject: [PATCH] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D0=BA=D0=B0=20=D1=80=D0=B5=D0=B7=D0=B5=D1=80=D0=B2=D0=BE=D0=B2?= =?UTF-8?q?=20=D0=BF=D1=80=D0=B8=20=D0=BF=D0=B5=D1=80=D0=B5=D1=85=D0=BE?= =?UTF-8?q?=D0=B4=D0=B5=20=D0=9E=D0=A2=D0=9C=D0=95=D0=9D=D0=81=D0=9D=20?= =?UTF-8?q?=E2=86=92=20=D0=92=D0=AB=D0=9F=D0=9E=D0=9B=D0=9D=D0=95=D0=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Проблема: - При смене статуса заказа ОТМЕНЁН → ВЫПОЛНЕН - Sale создавался и товар списывался корректно ✓ - НО резервы оставались в статусе 'released' вместо 'converted_to_sale' - Это приводило к некорректной истории и возможным проблемам при откате Причина: - Сигнал искал только резервы в статусе 'reserved' - После отмены резервы были в статусе 'released' - При повторном выполнении они не обновлялись Решение: - Изменён фильтр резервов: берём ВСЕ кроме 'converted_to_sale' - Теперь обрабатываются резервы в любом статусе (reserved, released, и др.) - Элегантное решение без хардкода конкретных статусов Дополнительно: - Добавлен @transaction.atomic к сигналам обновления Stock - Защита от race conditions при одновременном изменении резервов - Минимальные издержки, максимальная надёжность Результат: - Корректная работа при ЛЮБЫХ переходах статусов: * reserved → converted_to_sale ✓ * released → converted_to_sale ✓ * повторный вызов → пропуск ✓ - Целостность данных гарантирована транзакциями - Элегантный код без костылей --- myproject/inventory/signals.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/myproject/inventory/signals.py b/myproject/inventory/signals.py index ab006a9..e1614d3 100644 --- a/myproject/inventory/signals.py +++ b/myproject/inventory/signals.py @@ -96,17 +96,18 @@ def create_sale_on_order_completion(sender, instance, created, **kwargs): if Sale.objects.filter(order=instance).exists(): return # Продажи уже созданы, выходим БЕЗ обновления резервов - # Проверяем наличие резервов ДО начала операции + # Проверяем наличие резервов для этого заказа + # Ищем резервы в статусах 'reserved' (новые) и 'released' (после отката) + # Исключаем уже обработанные 'converted_to_sale' reservations_to_update = Reservation.objects.filter( - order_item__order=instance, - status='reserved' - ) + order_item__order=instance + ).exclude(status='converted_to_sale') if not reservations_to_update.exists(): logger.warning( - f"⚠ Заказ {instance.order_number} переведён в 'completed', но нет резервов в статусе 'reserved'" + f"⚠ Заказ {instance.order_number} переведён в 'completed', " + f"но нет резервов для обновления (все уже converted_to_sale или отсутствуют)" ) - # Продолжаем выполнение - возможно, это повторный вызов или резервы уже обработаны # Определяем склад (используем склад самовывоза из заказа или первый активный) warehouse = instance.pickup_warehouse or Warehouse.objects.filter(is_active=True).first() @@ -708,6 +709,7 @@ def update_stock_on_writeoff(sender, instance, created, **kwargs): @receiver(post_save, sender=Reservation) +@transaction.atomic def update_stock_on_reservation_change(sender, instance, created, **kwargs): """ Сигнал: При создании или изменении резерва (Reservation) обновляем Stock. @@ -755,6 +757,7 @@ def update_stock_on_reservation_change(sender, instance, created, **kwargs): @receiver(post_delete, sender=Reservation) +@transaction.atomic def update_stock_on_reservation_delete(sender, instance, **kwargs): """ Сигнал: При удалении резерва (Reservation) обновляем Stock.