Fixed critical bug: release reservations on draft->cancelled transition

This commit is contained in:
2025-12-01 11:57:06 +03:00
parent cdaf43afbd
commit 9f062f527d

View File

@@ -201,6 +201,9 @@ def rollback_sale_on_status_change(sender, instance, created, **kwargs):
Сценарии:
- А (ошибка): completed → draft/in_delivery → резервы возвращаются в 'reserved'
- Б (отмена): completed → cancelled → резервы освобождаются в 'released'
ПРИМЕЧАНИЕ: Этот сигнал ОБРАБАТЫВАЕТ ТОЛЬКО переход ОТ 'completed'!
Для перехода к 'cancelled' из любого статуса см. release_reservations_on_cancellation
"""
import logging
logger = logging.getLogger(__name__)
@@ -402,6 +405,99 @@ def rollback_sale_on_status_change(sender, instance, created, **kwargs):
)
@receiver(post_save, sender=Order)
@transaction.atomic
def release_reservations_on_cancellation(sender, instance, created, **kwargs):
"""
Сигнал: Освобождение резервов при переходе К cancelled из ЛЮБОГО статуса.
Триггер: любой_статус → cancelled
Процесс:
1. Проверяем что текущий статус = 'cancelled' (или is_negative_end)
2. Проверяем что предыдущий статус НЕ 'cancelled' (чтобы избежать повторной обработки)
3. Освобождаем все резервы в статусе 'reserved': status → 'released'
4. Устанавливаем released_at
ПРИМЕРЫ сценариев:
- draft → cancelled: резервы 'reserved''released'
- pending → cancelled: резервы 'reserved''released'
- completed → cancelled: обрабатывается rollback_sale_on_status_change ⚠️
ПРИМЕЧАНИЕ: Для completed → cancelled резервы в 'converted_to_sale',
поэтому этот сигнал их не затронет (обрабатывает rollback_sale_on_status_change).
"""
import logging
logger = logging.getLogger(__name__)
# Пропускаем новые заказы
if created:
return
# Проверяем наличие статуса
if not instance.status:
return
current_status = instance.status
# Проверяем: это статус отмены?
if not current_status.is_negative_end:
return # Не отмена, выходим
# === Получаем предыдущий статус ===
try:
history_count = instance.history.count()
if history_count < 2:
# Нет истории - значит заказ создан сразу в cancelled (необычно, но возможно)
# Продолжаем обработку
previous_status = None
else:
previous_record = instance.history.all()[1]
if not previous_record.status_id:
previous_status = None
else:
from orders.models import OrderStatus
previous_status = OrderStatus.objects.get(id=previous_record.status_id)
except (instance.history.model.DoesNotExist, OrderStatus.DoesNotExist, IndexError):
previous_status = None
# Проверяем: не был ли уже в cancelled?
if previous_status and previous_status.is_negative_end:
return # Уже был в отмене, не обрабатываем повторно
# === Освобождаем резервы ===
# Ищем только резервы в статусе 'reserved'
# Резервы в 'converted_to_sale' обрабатывает rollback_sale_on_status_change
reservations = Reservation.objects.filter(
order_item__order=instance,
status='reserved'
)
reservations_count = reservations.count()
if reservations_count > 0:
logger.info(
f"🔄 Переход к статусу '{current_status.name}' для заказа {instance.order_number}. "
f"Освобождаем {reservations_count} резервов..."
)
# Обновляем резервы через .save() чтобы сработал сигнал обновления Stock
for reservation in reservations:
reservation.status = 'released'
reservation.released_at = timezone.now()
reservation.save(update_fields=['status', 'released_at'])
logger.info(
f"✅ Освобождено {reservations_count} резервов: reserved → released"
)
else:
logger.debug(
f" Для заказа {instance.order_number} нет резервов в статусе 'reserved'"
)
@receiver(pre_delete, sender=Order)
@transaction.atomic
def release_stock_on_order_delete(sender, instance, **kwargs):