Улучшена логика флага is_returned и добавлен запрет повторного completed для возвращённых заказов
Проблема:
1. Флаг is_returned управлялся в разных местах непоследовательно
2. При цепочке completed → cancelled → completed флаг оставался True
3. Можно было установить положительный статус для заказа с is_returned=True
без резервов (товар уже продан в другом заказе)
Решение:
1. ЕДИНАЯ ФУНКЦИЯ update_is_returned_flag():
- Флаг основан на РЕАЛЬНОМ состоянии заказа (наличие Sale)
- Логика: есть Sale → is_returned=False
- Нет Sale + был когда-то в положительном финальном статусе → is_returned=True
- Нет Sale + никогда не был в положительном статусе → is_returned=False
2. ВЫЗОВ update_is_returned_flag() в ключевых точках:
- После создания Sale (create_sale_on_order_completion)
- После отката Sale (rollback_sale_on_status_change)
- После освобождения резервов (release_reservations_on_cancellation)
3. ВАЛИДАЦИЯ в create_sale_on_order_completion:
- Запрещаем переход в положительный финальный статус (is_positive_end=True)
для заказов с is_returned=True, у которых нет резервов
- Даём понятное сообщение: резервы отсутствуют, товары могли быть проданы
в другом заказе, оставьте статус отрицательного исхода или создайте новый заказ
4. АВТОМАТИЧЕСКИЙ СБРОС is_returned:
- При законном переходе в положительный статус с резервами флаг сбрасывается
- Это позволяет исправить ошибочную отмену: cancelled → completed работает,
если резервы на месте (товар не ушёл в другой заказ)
5. УДАЛЕНА ДУБЛИРУЮЩАЯ ЛОГИКА:
- Убрали ручное управление is_returned в rollback_sale_on_status_change
- Убрали ручное управление is_returned в release_reservations_on_cancellation
- Теперь один источник истины через update_is_returned_flag()
Результат:
- Флаг is_returned всегда соответствует реальности (наличию Sale)
- Невозможно установить completed для возвращённого заказа без резервов
- Защита от двойного списания при переиспользовании витринных комплектов
- Понятные сообщения об ошибках для пользователя
- Предсказуемое поведение при любых комбинациях смены статусов
This commit is contained in:
@@ -10,6 +10,8 @@ from django.dispatch import receiver
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
from orders.models import Order, OrderItem
|
from orders.models import Order, OrderItem
|
||||||
from inventory.models import Reservation, Warehouse, Incoming, StockBatch, Sale, SaleBatchAllocation, Inventory, WriteOff, Stock, WriteOffDocumentItem
|
from inventory.models import Reservation, Warehouse, Incoming, StockBatch, Sale, SaleBatchAllocation, Inventory, WriteOff, Stock, WriteOffDocumentItem
|
||||||
from inventory.services import SaleProcessor
|
from inventory.services import SaleProcessor
|
||||||
@@ -17,6 +19,39 @@ from inventory.services.batch_manager import StockBatchManager
|
|||||||
from inventory.services.inventory_processor import InventoryProcessor
|
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)
|
@receiver(post_save, sender=Order)
|
||||||
def reserve_stock_on_order_create(sender, instance, created, **kwargs):
|
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 и резервы преобразуются в продажу.
|
||||||
|
|
||||||
КРИТИЧНО: Резервы обновляются ТОЛЬКО ПОСЛЕ успешного создания Sale!
|
КРИТИЧНО: Резервы обновляются ТОЛЬКО ПОСЛЕ успешного создания Sale!
|
||||||
|
|
||||||
|
ВАЛИДАЦИЯ:
|
||||||
|
- Запрещаем переход в положительный финальный статус для заказов с is_returned=True,
|
||||||
|
у которых нет резервов (товар уже продан в другом заказе).
|
||||||
|
|
||||||
Процесс:
|
Процесс:
|
||||||
1. Проверяем, изменился ли статус на 'completed'
|
1. Проверяем, изменился ли статус на 'completed'
|
||||||
2. Для каждого товара создаем Sale (автоматический FIFO-список)
|
2. ВАЛИДАЦИЯ: если is_returned=True и резервов нет → запрещаем
|
||||||
3. ТОЛЬКО после успешного создания Sale обновляем резервы на 'converted_to_sale'
|
3. Для каждого товара создаем Sale (автоматический FIFO-список)
|
||||||
|
4. ТОЛЬКО после успешного создания Sale обновляем резервы на 'converted_to_sale'
|
||||||
|
5. Обновляем флаг is_returned
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -89,12 +130,40 @@ def create_sale_on_order_completion(sender, instance, created, **kwargs):
|
|||||||
return # Только для обновлений
|
return # Только для обновлений
|
||||||
|
|
||||||
# Проверяем наличие статуса (может быть None при создании)
|
# Проверяем наличие статуса (может быть None при создании)
|
||||||
if not instance.status or instance.status.code != 'completed':
|
if not instance.status:
|
||||||
return # Только для статуса 'completed'
|
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 для этого заказа
|
# Защита от повторного списания: проверяем, не созданы ли уже Sale для этого заказа
|
||||||
if Sale.objects.filter(order=instance).exists():
|
if Sale.objects.filter(order=instance).exists():
|
||||||
return # Продажи уже созданы, выходим БЕЗ обновления резервов
|
# Продажи уже созданы — просто обновляем флаг is_returned и выходим
|
||||||
|
update_is_returned_flag(instance)
|
||||||
|
return
|
||||||
|
|
||||||
# Проверяем наличие резервов для этого заказа
|
# Проверяем наличие резервов для этого заказа
|
||||||
# Ищем резервы в статусах 'reserved' (новые) и 'released' (после отката)
|
# Ищем резервы в статусах 'reserved' (новые) и 'released' (после отката)
|
||||||
@@ -105,9 +174,12 @@ def create_sale_on_order_completion(sender, instance, created, **kwargs):
|
|||||||
|
|
||||||
if not reservations_to_update.exists():
|
if not reservations_to_update.exists():
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"⚠ Заказ {instance.order_number} переведён в 'completed', "
|
f"⚠ Заказ {instance.order_number} переведён в '{instance.status.name}', "
|
||||||
f"но нет резервов для обновления (все уже converted_to_sale или отсутствуют)"
|
f"но нет резервов для обновления (все уже converted_to_sale или отсутствуют)"
|
||||||
)
|
)
|
||||||
|
# Обновляем флаг is_returned и выходим
|
||||||
|
update_is_returned_flag(instance)
|
||||||
|
return
|
||||||
|
|
||||||
# Определяем склад (используем склад самовывоза из заказа или первый активный)
|
# Определяем склад (используем склад самовывоза из заказа или первый активный)
|
||||||
warehouse = instance.pickup_warehouse or Warehouse.objects.filter(is_active=True).first()
|
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"🎉 Заказ {instance.order_number} успешно обработан: создано {len(sales_created)} Sale, "
|
||||||
f"обновлено {reservations_to_update.count() if reservations_to_update.exists() else 0} резервов"
|
f"обновлено {reservations_to_update.count() if reservations_to_update.exists() else 0} резервов"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Обновляем флаг is_returned на основе фактического состояния
|
||||||
|
update_is_returned_flag(instance)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=Order)
|
@receiver(post_save, sender=Order)
|
||||||
@@ -503,16 +578,8 @@ def rollback_sale_on_status_change(sender, instance, created, **kwargs):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# === Обновляем is_returned ===
|
# === Обновляем is_returned ===
|
||||||
if is_cancellation:
|
# Используем единую функцию для обновления флага на основе фактического состояния
|
||||||
# Сценарий Б: устанавливаем is_returned = True
|
update_is_returned_flag(instance)
|
||||||
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")
|
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"🎉 Откат для заказа {instance.order_number} завершён успешно: "
|
f"🎉 Откат для заказа {instance.order_number} завершён успешно: "
|
||||||
@@ -639,27 +706,8 @@ def release_reservations_on_cancellation(sender, instance, created, **kwargs):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# === Обновляем is_returned ===
|
# === Обновляем is_returned ===
|
||||||
# Проверяем: был ли заказ когда-либо в статусе completed (продан)?
|
# Используем единую функцию для обновления флага
|
||||||
# Если да, то это возврат/отмена проданного товара
|
update_is_returned_flag(instance)
|
||||||
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}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=Order)
|
@receiver(post_save, sender=Order)
|
||||||
|
|||||||
Reference in New Issue
Block a user