From 03794356d0a978919dc70cdebf1ccee32f071281 Mon Sep 17 00:00:00 2001 From: Andrey Smakotin Date: Mon, 5 Jan 2026 09:51:00 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D0=B0=D0=B2=D1=82=D0=BE=D0=BC=D0=B0=D1=82=D0=B8=D1=87?= =?UTF-8?q?=D0=B5=D1=81=D0=BA=D0=B8=D0=B9=20=D0=BF=D1=80=D0=BE=D0=BC=D0=B5?= =?UTF-8?q?=D0=B6=D1=83=D1=82=D0=BE=D1=87=D0=BD=D1=8B=D0=B9=20=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D1=85=D0=BE=D0=B4=20cancelled=20=E2=86=92=20draft?= =?UTF-8?q?=20=E2=86=92=20completed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Проблема: - Прямой переход 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 ✅ --- myproject/orders/models/order.py | 61 ++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/myproject/orders/models/order.py b/myproject/orders/models/order.py index 9e32dee..7d2cfc8 100644 --- a/myproject/orders/models/order.py +++ b/myproject/orders/models/order.py @@ -178,6 +178,8 @@ class Order(models.Model): def save(self, *args, **kwargs): from django.db import transaction from django.core.exceptions import ValidationError + import logging + logger = logging.getLogger(__name__) # Генерируем уникальный номер заказа при создании (начиная с 100 для 3-значного поиска) if not self.order_number: @@ -188,6 +190,65 @@ class Order(models.Model): else: 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 к любому не-отменённому статусу if self.pk: # Только при редактировании