# Исправление критического бага дублирования резервов ## 🔥 СЕРЬЁЗНЕЙШИЙ БАГ ### Описание проблемы При изменении количества товара в заказе со статусом "Выполнен" (completed) создавался ДУБЛИКАТ резерва, что приводило к двойному списанию товара со склада. ### Сценарий воспроизведения 1. **Начальное состояние:** - Заказ в статусе "Выполнен" (completed) - OrderItem: quantity = 20 шт - Reservation: quantity = 20 шт, status = `converted_to_sale` - Sale: 20 шт создан - Товар списан со склада 2. **Пользователь увеличивает количество на 10 единиц (до 30 шт):** - OrderItem.quantity = 30 шт (было 20) - Срабатывает сигнал `update_reservation_on_item_change` 3. **ЧТО ПРОИСХОДИЛО (БАГ):** ```python # Старый код (ОШИБОЧНЫЙ): reservation = Reservation.objects.filter( order_item=instance, status='reserved' # ← ПРОБЛЕМА! ).first() ``` - Сигнал искал резерв в статусе `'reserved'` - НО резерв был в статусе `'converted_to_sale'` (уже продан!) - Резерв НЕ находился → `reservation = None` - **Создавался НОВЫЙ резерв на 30 шт!** 4. **Результат (ДУБЛИКАТ):** - **Старый резерв:** 20 шт, status = `converted_to_sale` (остался!) - **Новый резерв:** 30 шт, status = `reserved` (создан заново!) - **ИТОГО: 50 шт зарезервировано вместо 30!** 5. **При переводе обратно в "Выполнен":** - Создаётся Sale на 30 шт (дополнительно к существующим 20 шт) - Оба резерва переводятся в `converted_to_sale` - **На складе списывается 30 + 20 = 50 шт, хотя должно быть 30!** - **Товара становится на 10 шт меньше, чем должно быть!** --- ## ✅ РЕШЕНИЕ ### Изменения в коде **Файл:** `inventory/signals.py` **Функция:** `update_reservation_on_item_change` (строка 436) #### Было (ОШИБОЧНО): ```python @receiver(post_save, sender=OrderItem) def update_reservation_on_item_change(sender, instance, created, **kwargs): # Получаем резерв для этой позиции в статусе 'reserved' reservation = Reservation.objects.filter( order_item=instance, status='reserved' # ← ПРОБЛЕМА: пропускает резервы в других статусах! ).first() if reservation: # Обновляем количество reservation.quantity = Decimal(str(instance.quantity)) reservation.save() else: # Создаем новый резерв (даже если старый уже существует!) # ... ``` #### Стало (ИСПРАВЛЕНО): ```python @receiver(post_save, sender=OrderItem) @transaction.atomic # ← Добавлена транзакция для безопасности def update_reservation_on_item_change(sender, instance, created, **kwargs): """ КРИТИЧНО: Убран фильтр status='reserved' чтобы избежать дубликатов! Резерв ищется по order_item независимо от статуса. """ import logging logger = logging.getLogger(__name__) # Ищем резерв для этой позиции в ЛЮБОМ статусе reservation = Reservation.objects.filter( order_item=instance # ← Без фильтра по статусу! ).first() if reservation: # Резерв существует - обновляем ТОЛЬКО количество # НЕ меняем статус! (может быть 'converted_to_sale', 'reserved', 'released') old_quantity = reservation.quantity reservation.quantity = Decimal(str(instance.quantity)) reservation.save(update_fields=['quantity']) # ← Обновляем только quantity logger.info( f"✓ Резерв #{reservation.id} обновлён: quantity {old_quantity} → {reservation.quantity} " f"(статус: {reservation.status}, OrderItem #{instance.id}, заказ {instance.order.order_number})" ) else: # Создаём новый резерв ТОЛЬКО если НЕТ вообще # ... ``` ### Ключевые изменения 1. ✅ **Убран фильтр `status='reserved'`** - теперь ищем резерв в ЛЮБОМ статусе 2. ✅ **Добавлен `@transaction.atomic`** - обеспечивает атомарность операции 3. ✅ **Используем `save(update_fields=['quantity'])`** - обновляем только количество, статус НЕ меняется 4. ✅ **Добавлено логирование** - видим что происходит с резервами 5. ✅ **Обновлена документация** - объяснена критичность изменения --- ## 🎯 БЕЗОПАСНОСТЬ РЕШЕНИЯ ### Вопрос: "А если товар есть в разных заказах?" **Ответ: НЕТ, проблемы не будет!** ### Почему безопасно: **Один товар в разных заказах = разные OrderItem:** 1. **Заказ #1:** - OrderItem #100: товар "Роза", количество 20 шт - Reservation #1: `order_item=100`, product="Роза", quantity=20 2. **Заказ #2:** - OrderItem #101: товар "Роза", количество 30 шт - Reservation #2: `order_item=101`, product="Роза", quantity=30 ### Ключ поиска: ```python reservation = Reservation.objects.filter( order_item=instance # ← Фильтруем по КОНКРЕТНОМУ OrderItem! ).first() ``` **Фильтруем по `order_item=instance`, а НЕ по `product`!** Это гарантирует: - ✅ Для OrderItem #100 найдётся только Reservation #1 - ✅ Для OrderItem #101 найдётся только Reservation #2 - ✅ **Резервы НИКОГДА не пересекутся**, даже если товар одинаковый! - ✅ Каждый OrderItem имеет СВОЙ уникальный резерв - ✅ Резервы разных заказов НЕ конфликтуют (разные order_item) --- ## 📊 ПОКРЫТИЕ ВСЕХ СЦЕНАРИЕВ | Сценарий | OrderItem | Резерв существует? | Статус резерва | Действие | |----------|-----------|-------------------|----------------|----------| | **Создание заказа** | Новый | Нет | - | Создать резерв (status='reserved') ✅ | | **Добавление товара** | Новый | Нет | - | Создать резерв (status='reserved') ✅ | | **Изменение qty (черновик)** | Старый | Да | reserved | Обновить quantity ✅ | | **Изменение qty (выполнен)** | Старый | Да | converted_to_sale | Обновить quantity (БЕЗ смены статуса!) ✅ | | **Повторное сохранение** | Старый | Да | Любой | Обновить quantity ✅ | --- ## 🔧 ТЕСТИРОВАНИЕ ### Как проверить исправление: 1. **Создать заказ:** - Добавить товар (20 шт) - Перевести в статус "Выполнен" - Проверить: создался Sale на 20 шт, резерв в статусе `converted_to_sale` 2. **Изменить количество:** - Увеличить до 30 шт - Проверить резервы в debug странице `/inventory/debug/` - **Должен быть ОДИН резерв на 30 шт**, НЕ два! 3. **Проверить склад:** - Товара должно остаться на складе правильное количество - НЕ должно быть двойного списания --- ## 📝 КОММИТ ``` Исправлен критический баг дублирования резервов при изменении количества Проблема: - При изменении количества в OrderItem для заказа в статусе 'completed' - Создавался ДУБЛИКАТ резерва (старый + новый) - Это приводило к двойному списанию товара со склада - Фильтр status='reserved' пропускал резервы в статусе 'converted_to_sale' Решение: - Убран фильтр status='reserved' из поиска резерва - Теперь резерв ищется по order_item независимо от статуса - Обновляется только quantity, статус НЕ меняется - Добавлен @transaction.atomic для атомарности - Добавлено логирование операций с резервами - Используется save(update_fields=['quantity']) для оптимизации Безопасность: - Резервы разных заказов НЕ конфликтуют (разные order_item) - Один товар в разных заказах = разные OrderItem = разные Reservation - Дубликаты больше НЕ создаются Покрытие: - Создание заказа ✅ - Добавление товара ✅ - Изменение количества (черновик) ✅ - Изменение количества (выполнен) ✅ [ИСПРАВЛЕНО] - Повторное сохранение ✅ ``` --- ## ⚠️ ВАЖНО Это **критическое исправление**, которое влияет на: - ✅ Учёт товара на складе - ✅ Резервирование товаров - ✅ FIFO-списание - ✅ Продажи **Необходимо протестировать** перед продакшеном!