Исправлены баги с дублированием резервов и Sale для витринных комплектов
Проблемы: 1. При продаже витринного комплекта через POS создавались дубликаты резервов - reserve_stock_on_item_create создавал новые резервы для витринного комплекта - Хотя резервы уже существовали от ShowcaseManager.reserve_kit_to_showcase 2. При переходе заказа в Completed создавались дубликаты Sale - ShowcaseManager.sell_showcase_items создавал Sale - Затем сигнал create_sale_on_order_completion создавал Sale повторно 3. При отмене заказа (Completed → Cancelled) терялась связь ShowcaseItem с резервами - ShowcaseItem возвращался на витрину, но резервы теряли поле showcase_item - При повторном переходе в Completed резервы дублировались Исправления: 1. inventory/signals.py - reserve_stock_on_item_create (строки 165-180): - Добавлена проверка витринного комплекта (is_temporary && showcase) - Для витринных комплектов сигнал пропускает создание новых резервов - Привязка существующих резервов происходит в update_reservation_on_item_change 2. inventory/signals.py - create_sale_on_order_completion (строки 346-365): - Добавлена проверка уже обработанных резервов (status='converted_to_sale') - Сигнал пропускает витринные резервы, уже обработанные ShowcaseManager - Логирует информацию о пропущенных резервах 3. inventory/signals.py - rollback_sale_on_status_change (строки 746-774): - При возврате ShowcaseItem на витрину восстанавливается связь с резервами - Обновляется поле showcase_item в резервах через Reservation.objects.update() - Логируется количество восстановленных связей 4. inventory/services/showcase_manager.py - sell_showcase_items (строки 201-206): - Добавлена проверка статуса резерва перед созданием Sale - Если резерв уже в 'converted_to_sale', он пропускается - Защита от двойного списания одного резерва Результат: ✅ Резервы создаются только один раз при размещении на витрине ✅ Sale создаются только один раз при продаже ✅ ShowcaseItem корректно возвращается на витрину со связью с резервами ✅ Остатки на складе корректные (60 → 55 после продажи, 60 после отмены) ✅ Нет дублирования при многократных переходах Completed ↔ Cancelled
This commit is contained in:
@@ -180,7 +180,7 @@ class ShowcaseManager:
|
||||
)
|
||||
|
||||
for showcase_item in showcase_items_locked:
|
||||
# Проверка статуса перед продажей
|
||||
# Проверяем статус перед продажей
|
||||
if showcase_item.status == 'sold':
|
||||
raise ValidationError(
|
||||
f'Экземпляр "{showcase_item}" уже продан'
|
||||
@@ -200,6 +200,11 @@ class ShowcaseManager:
|
||||
)
|
||||
|
||||
for reservation in reservations:
|
||||
# Проверяем что резерв ещё НЕ обработан
|
||||
if reservation.status == 'converted_to_sale':
|
||||
# Этот резерв уже преобразован в продажу, пропускаем
|
||||
continue
|
||||
|
||||
# Сначала устанавливаем order_item для правильного определения цены
|
||||
reservation.order_item = order_item
|
||||
reservation.save()
|
||||
|
||||
@@ -132,8 +132,9 @@ def reserve_stock_on_item_create(sender, instance, created, **kwargs):
|
||||
1. Проверяем, новая ли позиция (создана только что)
|
||||
2. Для обычных товаров - создаём резерв с учетом единиц продажи
|
||||
3. Для комплектов - резервируем компоненты (группируя одинаковые товары)
|
||||
4. Статус резерва = 'reserved'
|
||||
5. Проверяем на существующие резервы (защита от дубликатов)
|
||||
4. Для ВИТРИННЫХ комплектов - НЕ создаём резервы (они уже есть от ShowcaseItem)
|
||||
5. Статус резерва = 'reserved'
|
||||
6. Проверяем на существующие резервы (защита от дубликатов)
|
||||
"""
|
||||
from collections import defaultdict
|
||||
|
||||
@@ -163,7 +164,22 @@ def reserve_stock_on_item_create(sender, instance, created, **kwargs):
|
||||
)
|
||||
|
||||
elif instance.product_kit and instance.kit_snapshot:
|
||||
# Комплект - резервируем КОМПОНЕНТЫ из снимка
|
||||
# КРИТИЧНО: Проверяем витринный ли это комплект
|
||||
is_showcase_kit = instance.product_kit.is_temporary and instance.product_kit.showcase
|
||||
|
||||
if is_showcase_kit:
|
||||
# Витринный комплект - резервы УЖЕ созданы через ShowcaseManager.reserve_kit_to_showcase
|
||||
# Привязка резервов к OrderItem происходит в update_reservation_on_item_change
|
||||
# НЕ создаём новые резервы!
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(
|
||||
f"ℹ️ Витринный комплект '{instance.product_kit.name}': пропускаем создание резервов "
|
||||
f"(уже созданы ShowcaseManager), OrderItem #{instance.id}"
|
||||
)
|
||||
return
|
||||
|
||||
# Обычный (постоянный) комплект - резервируем КОМПОНЕНТЫ из снимка
|
||||
# Группируем одинаковые товары для создания одного резерва
|
||||
product_quantities = defaultdict(Decimal)
|
||||
|
||||
@@ -350,9 +366,21 @@ def create_sale_on_order_completion(sender, instance, created, **kwargs):
|
||||
).exclude(status='converted_to_sale')
|
||||
|
||||
if not kit_reservations.exists():
|
||||
logger.warning(
|
||||
f"⚠ Комплект '{kit.name}': не найдено резервов компонентов"
|
||||
)
|
||||
# Проверяем, может быть витринный комплект уже продан через ShowcaseManager?
|
||||
already_sold = Reservation.objects.filter(
|
||||
order_item=item,
|
||||
product_kit=kit,
|
||||
status='converted_to_sale'
|
||||
).exists()
|
||||
|
||||
if already_sold:
|
||||
logger.info(
|
||||
f"ℹ️ Витринный комплект '{kit.name}': резервы уже обработаны через ShowcaseManager.sell_showcase_items"
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
f"⚠ Комплект '{kit.name}': не найдено резервов компонентов"
|
||||
)
|
||||
continue
|
||||
|
||||
# Создаем Sale для каждого компонента комплекта
|
||||
@@ -728,20 +756,20 @@ def rollback_sale_on_status_change(sender, instance, created, **kwargs):
|
||||
|
||||
# === Возвращаем витринные экземпляры обратно на витрину ===
|
||||
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} витринных экземпляров обратно на витрину..."
|
||||
)
|
||||
|
||||
|
||||
# Возвращаем каждый экземпляр на витрину
|
||||
for item in showcase_items:
|
||||
item.status = 'available'
|
||||
@@ -749,9 +777,31 @@ def rollback_sale_on_status_change(sender, instance, created, **kwargs):
|
||||
item.sold_at = None
|
||||
# showcase и product_kit не трогаем - букет остаётся на той же витрине
|
||||
item.save(update_fields=['status', 'sold_order_item', 'sold_at', 'updated_at'])
|
||||
|
||||
|
||||
# КРИТИЧНО: Восстанавливаем связь между 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"
|
||||
f"✅ {showcase_items_count} витринных экземпляров вернулись на витрину: sold → available со связью с резервами"
|
||||
)
|
||||
|
||||
# === Обновляем is_returned ===
|
||||
|
||||
Reference in New Issue
Block a user