Files
octopus/FIX_RESERVATION_DUPLICATE_BUG.md
Andrey Smakotin 4597ddbd87 🔥 КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ: Дублирование резервов при изменении количества
Проблема:
- При изменении количества в OrderItem для заказа в статусе 'completed'
- Создавался ДУБЛИКАТ резерва (старый converted_to_sale + новый reserved)
- Это приводило к двойному списанию товара со склада (-10 лишних единиц)
- Фильтр status='reserved' пропускал существующие резервы в других статусах

Сценарий бага:
1. Заказ выполнен: 20 шт → резерв 20 шт (converted_to_sale)
2. Увеличить на 10 шт (до 30) → создаётся НОВЫЙ резерв 30 шт (reserved)
3. Итого: 20 + 30 = 50 шт зарезервировано вместо 30!
4. При переводе обратно в 'completed' → двойное списание (50 вместо 30)

Решение:
- Убран фильтр status='reserved' из update_reservation_on_item_change
- Теперь резерв ищется по order_item независимо от статуса
- Обновляется ТОЛЬКО quantity, статус НЕ меняется
- Добавлен @transaction.atomic для атомарности операции
- Добавлено логирование всех операций с резервами
- Используется save(update_fields=['quantity']) для оптимизации

Безопасность решения:
- Резервы разных заказов НЕ конфликтуют (разные order_item)
- Один товар в разных заказах = разные OrderItem = разные Reservation
- Каждый OrderItem имеет уникальный резерв
- Дубликаты больше НЕ создаются

Изменённые файлы:
- inventory/signals.py (функция update_reservation_on_item_change)
- FIX_RESERVATION_DUPLICATE_BUG.md (полная документация бага и решения)

Покрытие всех сценариев:
 Создание заказа с товарами
 Добавление товара при редактировании
 Изменение количества (черновик)
 Изменение количества (выполнен) - ИСПРАВЛЕНО
 Повторное сохранение заказа

КРИТИЧНО: Это исправление влияет на учёт товара и требует тестирования!
2025-12-01 10:22:28 +03:00

10 KiB
Raw Blame History

Исправление критического бага дублирования резервов

🔥 СЕРЬЁЗНЕЙШИЙ БАГ

Описание проблемы

При изменении количества товара в заказе со статусом "Выполнен" (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. ЧТО ПРОИСХОДИЛО (БАГ):

    # Старый код (ОШИБОЧНЫЙ):
    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)

Было (ОШИБОЧНО):

@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:
        # Создаем новый резерв (даже если старый уже существует!)
        # ...

Стало (ИСПРАВЛЕНО):

@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

Ключ поиска:

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-списание
  • Продажи

Необходимо протестировать перед продакшеном!