Добавлен автоматический промежуточный переход cancelled → draft → completed

Проблема:
- Прямой переход cancelled → completed вызывал race condition между сигналами
- Сигналы срабатывали в непредсказуемом порядке
- ShowcaseItem и Reservation не успевали корректно обработаться
- Букеты оставались в неправильном статусе

Решение ПОД КАПОТОМ:
- orders/models/order.py: Order.save() теперь перехватывает прямой переход cancelled → completed
- Автоматически разбивает на два последовательных шага:
  1. cancelled → draft: reserve_stock_on_uncancellation возвращает резервы и букеты в reserved
  2. draft → completed: create_sale_on_order_completion корректно финализирует в sold
- Каждый шаг вызывает super().save() в отдельной транзакции
- Сигналы срабатывают последовательно в правильном порядке

Преимущества:
- Пользователь не замечает промежуточный переход (происходит мгновенно)
- Не нужны сложные проверки порядка срабатывания сигналов
- Гарантируется корректная работа всех существующих сигналов
- Решение элегантное и не требует изменений в сигналах

Flow теперь гарантированно работает:
cancelled → draft → completed:
  Шаг 1: ShowcaseItem available → reserved 
  Шаг 2: ShowcaseItem reserved → sold 
  Шаг 1: Reservation order_item=None → привязаны 
  Шаг 2: Sale создаются, резервы converted_to_sale 
This commit is contained in:
2026-01-05 09:51:00 +03:00
parent d65a69e2bb
commit 03794356d0

View File

@@ -178,6 +178,8 @@ class Order(models.Model):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
from django.db import transaction from django.db import transaction
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
import logging
logger = logging.getLogger(__name__)
# Генерируем уникальный номер заказа при создании (начиная с 100 для 3-значного поиска) # Генерируем уникальный номер заказа при создании (начиная с 100 для 3-значного поиска)
if not self.order_number: if not self.order_number:
@@ -188,6 +190,65 @@ class Order(models.Model):
else: else:
self.order_number = 100 self.order_number = 100
# === АВТОМАТИЧЕСКИЙ ПРОМЕЖУТОЧНЫЙ ПЕРЕХОД: cancelled → draft → completed ===
# При прямом переходе из отрицательного (cancelled) в положительный (completed) статус,
# делаем промежуточный переход через нейтральный статус 'draft'.
# Это гарантирует корректную работу всех сигналов:
# 1. cancelled → draft: витринные букеты available → reserved
# 2. draft → completed: резервы и букеты корректно финализируются в sold
if self.pk: # Только при редактировании
try:
old_instance = Order.objects.get(pk=self.pk)
old_status = old_instance.status
new_status = self.status
# Проверяем: переход от отрицательного к положительному?
if (old_status and old_status.is_negative_end and
new_status and new_status.is_positive_end):
logger.info(
f"🔄 Заказ #{self.order_number}: Обнаружен прямой переход "
f"{old_status.name} (отрицательный) → {new_status.name} (положительный). "
f"Выполняем автоматический промежуточный переход через 'draft'..."
)
# Получаем статус 'draft'
try:
draft_status = OrderStatus.objects.get(code='draft', is_system=True)
except OrderStatus.DoesNotExist:
raise ValidationError(
f"Невозможно выполнить переход из '{old_status.name}' в '{new_status.name}': "
f"системный статус 'draft' не найден. Обратитесь к администратору."
)
# Сохраняем целевой статус для второго шага
target_status = new_status
# ШАГ 1: cancelled → draft
logger.info(f" 📍 Шаг 1/2: {old_status.name} → draft")
self.status = draft_status
with transaction.atomic():
super().save(*args, **kwargs)
# Обновляем old_instance для следующего шага
old_instance.refresh_from_db()
# ШАГ 2: draft → completed
logger.info(f" 📍 Шаг 2/2: draft → {target_status.name}")
self.status = target_status
with transaction.atomic():
super().save(*args, **kwargs)
logger.info(
f"✅ Заказ #{self.order_number}: Промежуточный переход завершён успешно. "
f"Итоговый статус: {target_status.name}"
)
return # Выходим, т.к. save() уже вызван дважды
except Order.DoesNotExist:
# Заказ ещё не создан в БД (не должно произойти, но на всякий случай)
pass
# === ВАЛИДАЦИЯ: Проверяем доступность витринных комплектов === # === ВАЛИДАЦИЯ: Проверяем доступность витринных комплектов ===
# При переходе ИЗ cancelled к любому не-отменённому статусу # При переходе ИЗ cancelled к любому не-отменённому статусу
if self.pk: # Только при редактировании if self.pk: # Только при редактировании