Исправлен порядок обработки ShowcaseItem при переходе cancelled → completed

Проблема:
- При переходе cancelled → completed срабатывали ОБА сигнала:
  1. reserve_stock_on_uncancellation переводил ShowcaseItem: available → reserved
  2. create_sale_on_order_completion искал букеты в available, но они уже в reserved
  3. Букеты оставались в reserved вместо sold

Решение:
- inventory/signals.py: в reserve_stock_on_uncancellation добавлена проверка current_status.is_positive_end
- Если текущий статус положительный финальный (completed) - ShowcaseItem НЕ трогаем
- Оставляем в available для финализации в create_sale_on_order_completion
- Если текущий статус нейтральный (draft/pending) - переводим available → reserved как раньше

Flow теперь работает корректно:
1. cancelled → draft/pending: ShowcaseItem available → reserved 
2. cancelled → completed: ShowcaseItem available → sold  (ИСПРАВЛЕНО!)
   - reserve_stock_on_uncancellation пропускает (видит is_positive_end)
   - create_sale_on_order_completion финализирует: available → sold

Защита от race condition между двумя сигналами.
This commit is contained in:
2026-01-05 09:41:29 +03:00
parent 6095729409
commit 0faae69c63

View File

@@ -1236,37 +1236,47 @@ def reserve_stock_on_uncancellation(sender, instance, created, **kwargs):
# === Возвращаем ShowcaseItem из available обратно в reserved === # === Возвращаем ShowcaseItem из available обратно в reserved ===
# При отмене (cancelled) ShowcaseItem переходит в 'available'. # При отмене (cancelled) ShowcaseItem переходит в 'available'.
# При возврате к нейтральному статусу нужно вернуть в 'reserved'. # При возврате к нейтральному статусу нужно вернуть в 'reserved'.
# НО: При переходе в положительный финальный статус - НЕ трогаем!
# (create_sale_on_order_completion сам переведёт available → sold)
from inventory.models import ShowcaseItem from inventory.models import ShowcaseItem
# Находим все ShowcaseItem которые были освобождены при отмене # Проверяем: если текущий статус положительный финальный, то пропускаем
# (у них sold_order_item сброшен в return_to_available, нужно найти через резервы) if current_status.is_positive_end:
for order_item in showcase_order_items: logger.info(
kit = order_item.product_kit f"🔄 Переход к положительному финальному статусу '{current_status.name}'. "
f"ShowcaseItem остаются в 'available' для финализации в create_sale_on_order_completion."
# Находим ShowcaseItem этого комплекта в статусе 'available'
# Их sold_order_item = None, поэтому ищем через product_kit
available_items = ShowcaseItem.objects.filter(
product_kit=kit,
status='available',
sold_order_item__isnull=True
) )
else:
# Переход к нейтральному статусу - возвращаем в reserved
# Находим все ShowcaseItem которые были освобождены при отмене
# (у них sold_order_item сброшен в return_to_available, нужно найти через резервы)
for order_item in showcase_order_items:
kit = order_item.product_kit
if available_items.exists(): # Находим ShowcaseItem этого комплекта в статусе 'available'
logger.info( # Их sold_order_item = None, поэтому ищем через product_kit
f" 🔄 Найдено {available_items.count()} ShowcaseItem комплекта '{kit.name}' в статусе 'available'. " available_items = ShowcaseItem.objects.filter(
f"Возвращаем в reserved..." product_kit=kit,
status='available',
sold_order_item__isnull=True
) )
for item in available_items: if available_items.exists():
try: logger.info(
item.return_to_reserved(order_item) f" 🔄 Найдено {available_items.count()} ShowcaseItem комплекта '{kit.name}' в статусе 'available'. "
logger.info( f"Возвращаем в reserved..."
f" ✅ ShowcaseItem #{item.id}: available → reserved (привязан к OrderItem #{order_item.id})" )
)
except Exception as e: for item in available_items:
logger.error( try:
f" ❌ Ошибка возврата ShowcaseItem #{item.id} в reserved: {e}" item.return_to_reserved(order_item)
) logger.info(
f" ✅ ShowcaseItem #{item.id}: available → reserved (привязан к OrderItem #{order_item.id})"
)
except Exception as e:
logger.error(
f" ❌ Ошибка возврата ShowcaseItem #{item.id} в reserved: {e}"
)
@receiver(pre_delete, sender=Order) @receiver(pre_delete, sender=Order)