Исправлены баги витринных комплектов: резервы и валидация восстановления заказов
This commit is contained in:
@@ -732,22 +732,41 @@ def rollback_sale_on_status_change(sender, instance, created, **kwargs):
|
||||
f"converted_to_sale → {reservation_target_status}"
|
||||
)
|
||||
|
||||
# Витринные временные комплекты ВСЕГДА возвращаются в reserved (остаются на витрине)
|
||||
# Витринные временные комплекты: логика зависит от сценария
|
||||
if showcase_count > 0:
|
||||
if is_cancellation:
|
||||
# Сценарий Б: Отмена - возвращаем на витрину
|
||||
for reservation in showcase_kit_reservations:
|
||||
reservation.status = 'reserved'
|
||||
# Очищаем ТОЛЬКО блокировки корзины при отмене заказа
|
||||
# НЕ трогаем order_item - он нужен для повторной продажи при возврате в completed
|
||||
# КРИТИЧНО: Отвязываем резервы от заказа при отмене
|
||||
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 (возвращены на витрину, отвязаны от заказа, блокировки сняты)"
|
||||
)
|
||||
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
|
||||
# Не трогаем showcase и product_kit - они остаются привязанными
|
||||
# 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 (возвращены на витрину, блокировки сняты, order_item сохранён)"
|
||||
f"converted_to_sale → reserved (остаются в заказе, блокировки сняты)"
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
@@ -755,6 +774,8 @@ def rollback_sale_on_status_change(sender, instance, created, **kwargs):
|
||||
)
|
||||
|
||||
# === Возвращаем витринные экземпляры обратно на витрину ===
|
||||
# ТОЛЬКО при отмене (отрицательный статус)!
|
||||
if is_cancellation:
|
||||
from inventory.models import ShowcaseItem
|
||||
|
||||
# Находим все ShowcaseItem, проданные в рамках этого заказа
|
||||
@@ -803,6 +824,11 @@ def rollback_sale_on_status_change(sender, instance, created, **kwargs):
|
||||
logger.info(
|
||||
f"✅ {showcase_items_count} витринных экземпляров вернулись на витрину: sold → available со связью с резервами"
|
||||
)
|
||||
else:
|
||||
# Сценарий А: Возврат к нейтральному - ShowcaseItem ОСТАЁТСЯ sold
|
||||
logger.info(
|
||||
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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user