diff --git a/myproject/inventory/signals.py b/myproject/inventory/signals.py index 9efc690..3e18647 100644 --- a/myproject/inventory/signals.py +++ b/myproject/inventory/signals.py @@ -10,6 +10,8 @@ from django.dispatch import receiver from django.utils import timezone from decimal import Decimal +from django.core.exceptions import ValidationError + from orders.models import Order, OrderItem from inventory.models import Reservation, Warehouse, Incoming, StockBatch, Sale, SaleBatchAllocation, Inventory, WriteOff, Stock, WriteOffDocumentItem from inventory.services import SaleProcessor @@ -17,6 +19,39 @@ from inventory.services.batch_manager import StockBatchManager from inventory.services.inventory_processor import InventoryProcessor +def update_is_returned_flag(order): + """ + Обновляет флаг is_returned на основе фактического состояния заказа. + + Логика: + - Если есть хотя бы одна Sale по этому заказу → is_returned = False + - Если Sale нет, но заказ когда-либо был в статусе completed → is_returned = True + - Если заказ ни разу не был completed → is_returned = False + + Это гарантирует что флаг отражает реальность: + - Заказ продан и не возвращён → False + - Заказ был продан, но продажи откачены (возврат) → True + - Новый заказ без продаж → False + """ + has_sale_now = Sale.objects.filter(order=order).exists() + + # Проверяем историю: был ли когда-либо в положительном финальном статусе + was_completed_ever = order.history.filter( + status__is_positive_end=True + ).exists() + + if has_sale_now: + # Есть актуальные продажи → заказ не возвращён + new_flag = False + else: + # Продаж нет → возвращён только если был когда-то completed + new_flag = was_completed_ever + + # Обновляем только если значение изменилось + if order.is_returned != new_flag: + Order.objects.filter(pk=order.pk).update(is_returned=new_flag) + + @receiver(post_save, sender=Order) def reserve_stock_on_order_create(sender, instance, created, **kwargs): """ @@ -76,11 +111,17 @@ def create_sale_on_order_completion(sender, instance, created, **kwargs): создается операция Sale и резервы преобразуются в продажу. КРИТИЧНО: Резервы обновляются ТОЛЬКО ПОСЛЕ успешного создания Sale! + + ВАЛИДАЦИЯ: + - Запрещаем переход в положительный финальный статус для заказов с is_returned=True, + у которых нет резервов (товар уже продан в другом заказе). Процесс: 1. Проверяем, изменился ли статус на 'completed' - 2. Для каждого товара создаем Sale (автоматический FIFO-список) - 3. ТОЛЬКО после успешного создания Sale обновляем резервы на 'converted_to_sale' + 2. ВАЛИДАЦИЯ: если is_returned=True и резервов нет → запрещаем + 3. Для каждого товара создаем Sale (автоматический FIFO-список) + 4. ТОЛЬКО после успешного создания Sale обновляем резервы на 'converted_to_sale' + 5. Обновляем флаг is_returned """ import logging logger = logging.getLogger(__name__) @@ -89,12 +130,40 @@ def create_sale_on_order_completion(sender, instance, created, **kwargs): return # Только для обновлений # Проверяем наличие статуса (может быть None при создании) - if not instance.status or instance.status.code != 'completed': - return # Только для статуса 'completed' + if not instance.status: + return + + # Проверяем: это положительный финальный статус? + is_positive_end = instance.status.is_positive_end + + if not is_positive_end: + return # Только для положительных финальных статусов (completed и т.п.) + # === ВАЛИДАЦИЯ: Запрет повторного completed для возвращённых заказов без резервов === + if instance.is_returned: + # Заказ уже был продан и возвращён — проверяем наличие резервов + has_reservations = Reservation.objects.filter( + order_item__order=instance + ).exists() + + if not has_reservations: + # Резервов нет — товар уже ушёл в другой заказ или был освобождён + logger.error( + f"❌ Заказ {instance.order_number} имеет флаг is_returned=True и не имеет резервов. " + f"Невозможно перевести в статус '{instance.status.name}'." + ) + raise ValidationError( + f"Невозможно установить статус '{instance.status.name}' для заказа {instance.order_number}. " + f"Этот заказ уже был отменён после продажи, резервы отсутствуют. " + f"Товары могли быть проданы в другом заказе. " + f"Пожалуйста, оставьте статус отрицательного исхода (отменён) или создайте новый заказ." + ) + # Защита от повторного списания: проверяем, не созданы ли уже Sale для этого заказа if Sale.objects.filter(order=instance).exists(): - return # Продажи уже созданы, выходим БЕЗ обновления резервов + # Продажи уже созданы — просто обновляем флаг is_returned и выходим + update_is_returned_flag(instance) + return # Проверяем наличие резервов для этого заказа # Ищем резервы в статусах 'reserved' (новые) и 'released' (после отката) @@ -105,9 +174,12 @@ def create_sale_on_order_completion(sender, instance, created, **kwargs): if not reservations_to_update.exists(): logger.warning( - f"⚠ Заказ {instance.order_number} переведён в 'completed', " + f"⚠ Заказ {instance.order_number} переведён в '{instance.status.name}', " f"но нет резервов для обновления (все уже converted_to_sale или отсутствуют)" ) + # Обновляем флаг is_returned и выходим + update_is_returned_flag(instance) + return # Определяем склад (используем склад самовывоза из заказа или первый активный) warehouse = instance.pickup_warehouse or Warehouse.objects.filter(is_active=True).first() @@ -234,6 +306,9 @@ def create_sale_on_order_completion(sender, instance, created, **kwargs): f"🎉 Заказ {instance.order_number} успешно обработан: создано {len(sales_created)} Sale, " f"обновлено {reservations_to_update.count() if reservations_to_update.exists() else 0} резервов" ) + + # Обновляем флаг is_returned на основе фактического состояния + update_is_returned_flag(instance) @receiver(post_save, sender=Order) @@ -503,16 +578,8 @@ def rollback_sale_on_status_change(sender, instance, created, **kwargs): ) # === Обновляем is_returned === - if is_cancellation: - # Сценарий Б: устанавливаем is_returned = True - Order.objects.filter(pk=instance.pk).update(is_returned=True) - logger.info(f"✓ Установлен флаг is_returned = True") - else: - # Сценарий А: сбрасываем is_returned = False - # (на случай если ранее был cancelled, а теперь вернули в промежуточный) - if instance.is_returned: - Order.objects.filter(pk=instance.pk).update(is_returned=False) - logger.info(f"✓ Сброшен флаг is_returned = False") + # Используем единую функцию для обновления флага на основе фактического состояния + update_is_returned_flag(instance) logger.info( f"🎉 Откат для заказа {instance.order_number} завершён успешно: " @@ -639,27 +706,8 @@ def release_reservations_on_cancellation(sender, instance, created, **kwargs): ) # === Обновляем is_returned === - # Проверяем: был ли заказ когда-либо в статусе completed (продан)? - # Если да, то это возврат/отмена проданного товара - try: - from orders.models import OrderStatus - - # Проверяем всю историю заказа - was_completed = instance.history.filter( - status__is_positive_end=True - ).exists() - - if was_completed: - # Заказ был продан → это возврат - Order.objects.filter(pk=instance.pk).update(is_returned=True) - logger.info( - f"✓ Заказ {instance.order_number} был продан ранее. " - f"Установлен флаг is_returned = True" - ) - except Exception as e: - logger.warning( - f"⚠ Не удалось проверить историю заказа {instance.order_number}: {e}" - ) + # Используем единую функцию для обновления флага + update_is_returned_flag(instance) @receiver(post_save, sender=Order)