Fixed critical bug: release reservations on draft->cancelled transition
This commit is contained in:
@@ -201,6 +201,9 @@ def rollback_sale_on_status_change(sender, instance, created, **kwargs):
|
|||||||
Сценарии:
|
Сценарии:
|
||||||
- А (ошибка): completed → draft/in_delivery → резервы возвращаются в 'reserved'
|
- А (ошибка): completed → draft/in_delivery → резервы возвращаются в 'reserved'
|
||||||
- Б (отмена): completed → cancelled → резервы освобождаются в 'released'
|
- Б (отмена): completed → cancelled → резервы освобождаются в 'released'
|
||||||
|
|
||||||
|
ПРИМЕЧАНИЕ: Этот сигнал ОБРАБАТЫВАЕТ ТОЛЬКО переход ОТ 'completed'!
|
||||||
|
Для перехода к 'cancelled' из любого статуса см. release_reservations_on_cancellation
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
logger = logging.getLogger(__name__)
|
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)
|
@receiver(pre_delete, sender=Order)
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def release_stock_on_order_delete(sender, instance, **kwargs):
|
def release_stock_on_order_delete(sender, instance, **kwargs):
|
||||||
|
|||||||
Reference in New Issue
Block a user