Исправлены баги витринных комплектов: резервы и валидация восстановления заказов

This commit is contained in:
2026-01-04 22:53:53 +03:00
parent 595cf6a018
commit 8041ceb04a
3 changed files with 224 additions and 64 deletions

View File

@@ -732,76 +732,102 @@ def rollback_sale_on_status_change(sender, instance, created, **kwargs):
f"converted_to_sale → {reservation_target_status}"
)
# Витринные временные комплекты ВСЕГДА возвращаются в reserved (остаются на витрине)
# Витринные временные комплекты: логика зависит от сценария
if showcase_count > 0:
for reservation in showcase_kit_reservations:
reservation.status = 'reserved'
# Очищаем ТОЛЬКО блокировки корзины при отмене заказа
# НЕ трогаем order_item - он нужен для повторной продажи при возврате в completed
reservation.cart_lock_expires_at = None
reservation.locked_by_user = None
reservation.cart_session_id = None
# Не трогаем showcase и product_kit - они остаются привязанными
# converted_at оставляем (для истории)
reservation.save(update_fields=['status', 'cart_lock_expires_at', 'locked_by_user', 'cart_session_id'])
if is_cancellation:
# Сценарий Б: Отмена - возвращаем на витрину
for reservation in showcase_kit_reservations:
reservation.status = 'reserved'
# КРИТИЧНО: Отвязываем резервы от заказа при отмене
reservation.order_item = None
# Очищаем блокировки корзины
reservation.cart_lock_expires_at = None
reservation.locked_by_user = None
reservation.cart_session_id = None
# showcase_item и product_kit остаются - букет на витрине
# converted_at оставляем (для истории)
reservation.save(update_fields=['status', 'order_item', 'cart_lock_expires_at', 'locked_by_user', 'cart_session_id'])
logger.info(
f"✓ Обновлено {showcase_count} резервов витринных комплектов: "
f"converted_to_sale → reserved (возвращены на витрину, блокировки сняты, order_item сохранён)"
)
logger.info(
f"✓ Обновлено {showcase_count} резервов витринных комплектов: "
f"converted_to_sale → reserved (возвращены на витрину, отвязаны от заказа, блокировки сняты)"
)
else:
# Сценарий А: Возврат к нейтральному - резервы ОСТАЮТСЯ в заказе
for reservation in showcase_kit_reservations:
reservation.status = 'reserved'
# Очищаем ТОЛЬКО блокировки корзины
# order_item НЕ ТРОГАЕМ - резерв остаётся за заказом!
reservation.cart_lock_expires_at = None
reservation.locked_by_user = None
reservation.cart_session_id = None
# converted_at оставляем (для истории)
reservation.save(update_fields=['status', 'cart_lock_expires_at', 'locked_by_user', 'cart_session_id'])
logger.info(
f"✓ Обновлено {showcase_count} резервов витринных комплектов: "
f"converted_to_sale → reserved (остаются в заказе, блокировки сняты)"
)
else:
logger.warning(
f"⚠ Для заказа {instance.order_number} нет резервов в статусе 'converted_to_sale'"
)
# === Возвращаем витринные экземпляры обратно на витрину ===
from inventory.models import ShowcaseItem
# ТОЛЬКО при отмене (отрицательный статус)!
if is_cancellation:
from inventory.models import ShowcaseItem
# Находим все ShowcaseItem, проданные в рамках этого заказа
showcase_items = ShowcaseItem.objects.filter(
sold_order_item__order=instance,
status='sold'
)
showcase_items_count = showcase_items.count()
if showcase_items_count > 0:
logger.info(
f"🔄 Возвращаем {showcase_items_count} витринных экземпляров обратно на витрину..."
# Находим все ShowcaseItem, проданные в рамках этого заказа
showcase_items = ShowcaseItem.objects.filter(
sold_order_item__order=instance,
status='sold'
)
# Возвращаем каждый экземпляр на витрину
for item in showcase_items:
item.status = 'available'
item.sold_order_item = None
item.sold_at = None
# showcase и product_kit не трогаем - букет остаётся на той же витрине
item.save(update_fields=['status', 'sold_order_item', 'sold_at', 'updated_at'])
showcase_items_count = showcase_items.count()
# КРИТИЧНО: Восстанавливаем связь между ShowcaseItem и Reservation
# Находим все резервы этого комплекта для данного OrderItem
order_item = item.sold_order_item if hasattr(item, '_original_sold_order_item') else None
if not order_item:
# Пытаемся найти через заказ и product_kit
order_items = instance.items.filter(product_kit=item.product_kit)
if order_items.exists():
order_item = order_items.first()
if showcase_items_count > 0:
logger.info(
f"🔄 Возвращаем {showcase_items_count} витринных экземпляров обратно на витрину..."
)
if order_item:
# Восстанавливаем связь showcase_item в резервах
reservations_updated = Reservation.objects.filter(
order_item=order_item,
product_kit=item.product_kit,
status='reserved'
).update(showcase_item=item)
# Возвращаем каждый экземпляр на витрину
for item in showcase_items:
item.status = 'available'
item.sold_order_item = None
item.sold_at = None
# showcase и product_kit не трогаем - букет остаётся на той же витрине
item.save(update_fields=['status', 'sold_order_item', 'sold_at', 'updated_at'])
if reservations_updated > 0:
logger.debug(
f" ✅ Восстановлена связь для {reservations_updated} резервов ShowcaseItem #{item.id}"
)
# КРИТИЧНО: Восстанавливаем связь между ShowcaseItem и Reservation
# Находим все резервы этого комплекта для данного OrderItem
order_item = item.sold_order_item if hasattr(item, '_original_sold_order_item') else None
if not order_item:
# Пытаемся найти через заказ и product_kit
order_items = instance.items.filter(product_kit=item.product_kit)
if order_items.exists():
order_item = order_items.first()
if order_item:
# Восстанавливаем связь showcase_item в резервах
reservations_updated = Reservation.objects.filter(
order_item=order_item,
product_kit=item.product_kit,
status='reserved'
).update(showcase_item=item)
if reservations_updated > 0:
logger.debug(
f" ✅ Восстановлена связь для {reservations_updated} резервов ShowcaseItem #{item.id}"
)
logger.info(
f"{showcase_items_count} витринных экземпляров вернулись на витрину: sold → available со связью с резервами"
)
else:
# Сценарий А: Возврат к нейтральному - ShowcaseItem ОСТАЁТСЯ sold
logger.info(
f"{showcase_items_count} витринных экземпляров вернулись на витрину: sold → available со связью с резервами"
f" Сценарий А: Витринные экземпляры остаются в статусе 'sold' (заказ в нейтральном статусе)"
)
# === Обновляем is_returned ===
@@ -916,15 +942,16 @@ def release_reservations_on_cancellation(sender, instance, created, **kwargs):
if showcase_count > 0:
# Для витринных комплектов очищаем блокировки корзины
# НЕ трогаем order_item - он нужен для повторной продажи если статус вернётся в completed
# КРИТИЧНО: Отвязываем резервы от заказа при отмене
for reservation in showcase_kit_reservations:
reservation.order_item = None
reservation.cart_lock_expires_at = None
reservation.locked_by_user = None
reservation.cart_session_id = None
reservation.save(update_fields=['cart_lock_expires_at', 'locked_by_user', 'cart_session_id'])
reservation.save(update_fields=['order_item', 'cart_lock_expires_at', 'locked_by_user', 'cart_session_id'])
logger.info(
f" Найдено {showcase_count} резервов витринных комплектов - остаются в reserved (на витрине, блокировки сняты, order_item сохранён)"
f" Найдено {showcase_count} резервов витринных комплектов - остаются в reserved (на витрине, отвязаны от заказа, блокировки сняты)"
)
if normal_count == 0 and showcase_count == 0:
@@ -996,7 +1023,7 @@ def reserve_stock_on_uncancellation(sender, instance, created, **kwargs):
return # Не было перехода от cancelled, выходим
# === Резервируем товар заново ===
# Ищем резервы в статусе 'released'
# Ищем резервы в статусе 'released' (обычные резервы)
reservations = Reservation.objects.filter(
order_item__order=instance,
status='released'
@@ -1024,6 +1051,52 @@ def reserve_stock_on_uncancellation(sender, instance, created, **kwargs):
f" Для заказа {instance.order_number} нет резервов в статусе 'released'"
)
# === Привязываем витринные резервы обратно к заказу ===
# Витринные резервы остаются в статусе 'reserved' при отмене,
# но отвязываются от order_item. При возврате нужно привязать их обратно.
# Находим все OrderItem витринных комплектов в этом заказе
showcase_order_items = instance.items.filter(
product_kit__is_temporary=True,
product_kit__showcase__isnull=False
).select_related('product_kit')
showcase_items_count = showcase_order_items.count()
if showcase_items_count > 0:
logger.info(
f"🔄 Найдено {showcase_items_count} витринных комплектов в заказе. "
f"Привязываем резервы обратно к заказу..."
)
for order_item in showcase_order_items:
kit = order_item.product_kit
# Находим витринные резервы для этого комплекта
# (они в статусе 'reserved', но order_item=None)
showcase_reservations = Reservation.objects.filter(
product_kit=kit,
showcase__isnull=False,
status='reserved',
order_item__isnull=True
)
if showcase_reservations.exists():
# Привязываем резервы обратно к OrderItem
updated_count = showcase_reservations.update(order_item=order_item)
logger.info(
f" ✅ Витринный комплект '{kit.name}': привязано {updated_count} резервов к OrderItem #{order_item.id}"
)
else:
logger.warning(
f" ⚠ Витринный комплект '{kit.name}': не найдено витринных резервов без order_item"
)
logger.info(
f"✅ Обработано {showcase_items_count} витринных комплектов"
)
@receiver(pre_delete, sender=Order)
@transaction.atomic

View File

@@ -292,6 +292,7 @@
<th>Единица продажи</th>
<th>Статус</th>
<th>Заказ</th>
<th>Витрина</th>
<th>Создан</th>
<th>Преобразован</th>
<th>Освобожден</th>
@@ -341,6 +342,15 @@
-
{% endif %}
</td>
<td>
{% if res.showcase %}
<span class="badge bg-warning text-dark" title="Витрина: {{ res.showcase.name }}">🌺 {{ res.showcase.name }}</span>
{% elif res.showcase_item %}
<span class="badge bg-warning text-dark" title="Витрина: {{ res.showcase_item.showcase.name }}">🌺 {{ res.showcase_item.showcase.name }}</span>
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
<td class="text-muted-small">{{ res.reserved_at|date:"d.m.Y H:i:s" }}</td>
<td class="text-muted-small">
{% if res.converted_at %}{{ res.converted_at|date:"d.m.Y H:i:s" }}{% else %}-{% endif %}
@@ -350,7 +360,7 @@
</td>
</tr>
{% empty %}
<tr><td colspan="11" class="text-center text-muted">Нет резервов</td></tr>
<tr><td colspan="12" class="text-center text-muted">Нет резервов</td></tr>
{% endfor %}
</tbody>
</table>

View File

@@ -177,6 +177,8 @@ class Order(models.Model):
def save(self, *args, **kwargs):
from django.db import transaction
from django.core.exceptions import ValidationError
# Генерируем уникальный номер заказа при создании (начиная с 100 для 3-значного поиска)
if not self.order_number:
last_order = Order.objects.order_by('-order_number').first()
@@ -186,6 +188,81 @@ class Order(models.Model):
else:
self.order_number = 100
# === ВАЛИДАЦИЯ: Проверяем доступность витринных комплектов ===
# При переходе ИЗ cancelled к любому не-отменённому статусу
if self.pk: # Только при редактировании
try:
# Получаем старый статус из БД
old_instance = Order.objects.get(pk=self.pk)
old_status = old_instance.status
new_status = self.status
# Проверяем: переход от cancelled к не-cancelled?
if (old_status and old_status.is_negative_end and
new_status and not new_status.is_negative_end):
# Находим все витринные комплекты в этом заказе
from orders.models import OrderItem
showcase_items = OrderItem.objects.filter(
order=self,
product_kit__is_temporary=True,
product_kit__showcase__isnull=False
).select_related('product_kit')
if showcase_items.exists():
# Проверяем доступность резервов для каждого комплекта
from inventory.models import Reservation
unavailable_kits = []
for item in showcase_items:
kit = item.product_kit
# КРИТИЧНО: Ищем ВСЕ витринные резервы этого комплекта
# Проверяем не привязаны ли они к ДРУГОМУ заказу (в ЛЮБОМ статусе)
occupied_reservations = Reservation.objects.filter(
product_kit=kit,
showcase__isnull=False,
order_item__isnull=False # Привязаны к какому-то заказу
).exclude(
order_item__order=self # Исключаем текущий заказ
).select_related('order_item__order')
if occupied_reservations.exists():
# Резервы заняты другим заказом - блокируем переход
occupied_res = occupied_reservations.first()
other_order_number = occupied_res.order_item.order.order_number
other_order_status = occupied_res.order_item.order.status.name if occupied_res.order_item.order.status else 'неизвестен'
unavailable_kits.append(
f"Витринный комплект '{kit.name}' занят заказом #{other_order_number} (статус: {other_order_status})"
)
else:
# Проверяем что вообще есть резервы для этого комплекта
any_reservations = Reservation.objects.filter(
product_kit=kit,
showcase__isnull=False
).exists()
if not any_reservations:
# Комплект демонтирован или удалён
unavailable_kits.append(
f"Витринный комплект '{kit.name}' больше не существует на витрине"
)
# Если есть недоступные комплекты - блокируем переход
if unavailable_kits:
error_message = (
f"Невозможно восстановить заказ #{self.order_number}. "
f"Витринные комплекты уже проданы:\n\n" +
"\n".join(f"\u2022 {msg}" for msg in unavailable_kits) +
f"\n\nОтмените сначала соответствующие заказы или удалите эти позиции из заказа."
)
raise ValidationError(error_message)
except Order.DoesNotExist:
# Заказ ещё не создан в БД (не должно произойти, но на всякий случай)
pass
# Оборачиваем в транзакцию чтобы ValidationError в сигналах откатывал save()
with transaction.atomic():
super().save(*args, **kwargs)