Исправлены баги с дублированием резервов и 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:
|
for showcase_item in showcase_items_locked:
|
||||||
# Проверка статуса перед продажей
|
# Проверяем статус перед продажей
|
||||||
if showcase_item.status == 'sold':
|
if showcase_item.status == 'sold':
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
f'Экземпляр "{showcase_item}" уже продан'
|
f'Экземпляр "{showcase_item}" уже продан'
|
||||||
@@ -200,6 +200,11 @@ class ShowcaseManager:
|
|||||||
)
|
)
|
||||||
|
|
||||||
for reservation in reservations:
|
for reservation in reservations:
|
||||||
|
# Проверяем что резерв ещё НЕ обработан
|
||||||
|
if reservation.status == 'converted_to_sale':
|
||||||
|
# Этот резерв уже преобразован в продажу, пропускаем
|
||||||
|
continue
|
||||||
|
|
||||||
# Сначала устанавливаем order_item для правильного определения цены
|
# Сначала устанавливаем order_item для правильного определения цены
|
||||||
reservation.order_item = order_item
|
reservation.order_item = order_item
|
||||||
reservation.save()
|
reservation.save()
|
||||||
|
|||||||
@@ -132,8 +132,9 @@ def reserve_stock_on_item_create(sender, instance, created, **kwargs):
|
|||||||
1. Проверяем, новая ли позиция (создана только что)
|
1. Проверяем, новая ли позиция (создана только что)
|
||||||
2. Для обычных товаров - создаём резерв с учетом единиц продажи
|
2. Для обычных товаров - создаём резерв с учетом единиц продажи
|
||||||
3. Для комплектов - резервируем компоненты (группируя одинаковые товары)
|
3. Для комплектов - резервируем компоненты (группируя одинаковые товары)
|
||||||
4. Статус резерва = 'reserved'
|
4. Для ВИТРИННЫХ комплектов - НЕ создаём резервы (они уже есть от ShowcaseItem)
|
||||||
5. Проверяем на существующие резервы (защита от дубликатов)
|
5. Статус резерва = 'reserved'
|
||||||
|
6. Проверяем на существующие резервы (защита от дубликатов)
|
||||||
"""
|
"""
|
||||||
from collections import defaultdict
|
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:
|
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)
|
product_quantities = defaultdict(Decimal)
|
||||||
|
|
||||||
@@ -350,9 +366,21 @@ def create_sale_on_order_completion(sender, instance, created, **kwargs):
|
|||||||
).exclude(status='converted_to_sale')
|
).exclude(status='converted_to_sale')
|
||||||
|
|
||||||
if not kit_reservations.exists():
|
if not kit_reservations.exists():
|
||||||
logger.warning(
|
# Проверяем, может быть витринный комплект уже продан через ShowcaseManager?
|
||||||
f"⚠ Комплект '{kit.name}': не найдено резервов компонентов"
|
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
|
continue
|
||||||
|
|
||||||
# Создаем Sale для каждого компонента комплекта
|
# Создаем Sale для каждого компонента комплекта
|
||||||
@@ -750,8 +778,30 @@ def rollback_sale_on_status_change(sender, instance, created, **kwargs):
|
|||||||
# showcase и product_kit не трогаем - букет остаётся на той же витрине
|
# showcase и product_kit не трогаем - букет остаётся на той же витрине
|
||||||
item.save(update_fields=['status', 'sold_order_item', 'sold_at', 'updated_at'])
|
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(
|
logger.info(
|
||||||
f"✓ {showcase_items_count} витринных экземпляров вернулись на витрину: sold → available"
|
f"✅ {showcase_items_count} витринных экземпляров вернулись на витрину: sold → available со связью с резервами"
|
||||||
)
|
)
|
||||||
|
|
||||||
# === Обновляем is_returned ===
|
# === Обновляем is_returned ===
|
||||||
|
|||||||
Reference in New Issue
Block a user