From 595cf6a018878b47f3c7279e7adfc59038c810c4 Mon Sep 17 00:00:00 2001 From: Andrey Smakotin Date: Sun, 4 Jan 2026 22:04:51 +0300 Subject: [PATCH] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=B1=D0=B0=D0=B3=D0=B8=20=D1=81=20=D0=B4?= =?UTF-8?q?=D1=83=D0=B1=D0=BB=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=D0=BC=20=D1=80=D0=B5=D0=B7=D0=B5=D1=80=D0=B2=D0=BE=D0=B2?= =?UTF-8?q?=20=D0=B8=20Sale=20=D0=B4=D0=BB=D1=8F=20=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=D1=80=D0=B8=D0=BD=D0=BD=D1=8B=D1=85=20=D0=BA=D0=BE=D0=BC=D0=BF?= =?UTF-8?q?=D0=BB=D0=B5=D0=BA=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Проблемы: 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 --- .../inventory/services/showcase_manager.py | 7 +- myproject/inventory/signals.py | 74 ++++++++++++++++--- 2 files changed, 68 insertions(+), 13 deletions(-) diff --git a/myproject/inventory/services/showcase_manager.py b/myproject/inventory/services/showcase_manager.py index 7ffe3f4..1b4f75c 100644 --- a/myproject/inventory/services/showcase_manager.py +++ b/myproject/inventory/services/showcase_manager.py @@ -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() diff --git a/myproject/inventory/signals.py b/myproject/inventory/signals.py index c297adb..49bf505 100644 --- a/myproject/inventory/signals.py +++ b/myproject/inventory/signals.py @@ -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 ===