diff --git a/myproject/inventory/signals.py b/myproject/inventory/signals.py index 5c2c68f..6fe3eb9 100644 --- a/myproject/inventory/signals.py +++ b/myproject/inventory/signals.py @@ -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):