Исправлена повторная продажа витринных комплектов при возврате в статус completed

Проблема:
При отмене заказа (completed → cancelled) и последующем возврате в completed
витринные комплекты оставались зарезервированными и не уходили со склада.
Резервы не конвертировались в продажи, Sale не создавались.

Причина:
При откате заказа (уход от completed) мы обнуляли reservation.order_item = None
для витринных комплектов. Это разрывало связь между резервом и позицией заказа.

При повторном переходе в completed сигнал create_sale_on_order_completion
искал резервы по фильтру:
  Reservation.objects.filter(order_item=item, product_kit=kit)

Но так как order_item был None, резервы не находились и Sale не создавались.

Решение:
Разделили семантику полей Reservation:
- order_item - принадлежность к позиции заказа (часть жизненного цикла заказа)
- cart_lock_expires_at, locked_by_user, cart_session_id - блокировки корзины

При откате заказа (completed → другой_статус):
- НЕ трогаем order_item - он остаётся привязанным к OrderItem
- Очищаем ТОЛЬКО cart lock поля (expires_at, locked_by_user, session_id)
- Резервы витринных комплектов: status = reserved

При повторном переходе в completed:
- create_sale_on_order_completion находит резервы (order_item сохранён!)
- Создаёт Sale для каждого компонента
- Конвертирует резервы: reserved → converted_to_sale
- Витринный экземпляр помечается как проданный через ShowcaseManager

Изменения в 3 местах:
1. rollback_sale_on_status_change - откат от completed
2. release_reservations_on_cancellation - переход к cancelled
3. release_stock_on_order_delete - удаление заказа

Во всех случаях для витринных комплектов сохраняем order_item, очищаем
только cart lock поля.

Результат:
Теперь витринные комплекты можно продавать/отменять/продавать снова
через смену статуса заказа в админке - как в реальной жизни.
This commit is contained in:
2025-12-11 23:17:12 +03:00
parent d44687649c
commit 2a3898fb44

View File

@@ -456,18 +456,18 @@ def rollback_sale_on_status_change(sender, instance, created, **kwargs):
if showcase_count > 0:
for reservation in showcase_kit_reservations:
reservation.status = 'reserved'
# Очищаем блокировки корзины при отмене заказа
reservation.order_item = None
# Очищаем ТОЛЬКО блокировки корзины при отмене заказа
# НЕ трогаем 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', 'order_item', 'cart_lock_expires_at', 'locked_by_user', 'cart_session_id'])
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 (возвращены на витрину, блокировки сняты)"
f"converted_to_sale → reserved (возвращены на витрину, блокировки сняты, order_item сохранён)"
)
else:
logger.warning(
@@ -622,15 +622,15 @@ 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=['order_item', 'cart_lock_expires_at', 'locked_by_user', 'cart_session_id'])
reservation.save(update_fields=['cart_lock_expires_at', 'locked_by_user', 'cart_session_id'])
logger.info(
f" Найдено {showcase_count} резервов витринных комплектов - остаются в reserved (на витрине, блокировки сняты)"
f" Найдено {showcase_count} резервов витринных комплектов - остаются в reserved (на витрине, блокировки сняты, order_item сохранён)"
)
if normal_count == 0 and showcase_count == 0:
@@ -792,13 +792,13 @@ def release_stock_on_order_delete(sender, instance, **kwargs):
res.released_at = timezone.now()
res.save()
# Витринные комплекты остаются зарезервированными, но отвязываем их от заказа и снимаем блокировки
# Витринные комплекты остаются зарезервированными, но отвязываем блокировки корзины
# НЕ трогаем order_item - он нужен если заказ снова перейдёт в completed
for res in showcase_reservations:
res.order_item = None
res.cart_lock_expires_at = None
res.locked_by_user = None
res.cart_session_id = None
res.save(update_fields=['order_item', 'cart_lock_expires_at', 'locked_by_user', 'cart_session_id'])
res.save(update_fields=['cart_lock_expires_at', 'locked_by_user', 'cart_session_id'])
transaction.on_commit(release_reservations)