Улучшена логика флага 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 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)
|
||||
|
||||
Reference in New Issue
Block a user