🔥 КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ: Дублирование резервов при изменении количества

Проблема:
- При изменении количества в 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 (полная документация бага и решения)

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

КРИТИЧНО: Это исправление влияет на учёт товара и требует тестирования!
This commit is contained in:
2025-12-01 10:22:28 +03:00
parent 5300b83565
commit 4597ddbd87
2 changed files with 257 additions and 9 deletions

View File

@@ -434,30 +434,45 @@ def release_stock_on_order_delete(sender, instance, **kwargs):
@receiver(post_save, sender=OrderItem)
@transaction.atomic
def update_reservation_on_item_change(sender, instance, created, **kwargs):
"""
Сигнал: При создании или изменении позиции заказа управляем резервами.
Процесс:
1. Ищем существующий резерв для этой позиции
2. Если резерв ЕСТЬ - обновляем количество
1. Ищем существующий резерв для этой позиции (в ЛЮБОМ статусе)
2. Если резерв ЕСТЬ - обновляем ТОЛЬКО количество (статус НЕ меняем!)
3. Если резерва НЕТ - создаем новый
Покрывает все сценарии:
- Создание заказа с товарами → создаёт резервы
- Редактирование + добавление товаров → создаёт резервы для новых
- Изменение количества → обновляет резервы
- Изменение количества → обновляет резервы (даже если уже converted_to_sale)
КРИТИЧНО: Убран фильтр status='reserved' чтобы избежать дубликатов!
Резерв ищется по order_item независимо от статуса.
Это предотвращает создание нового резерва для заказа в статусе 'completed'.
"""
# Получаем резерв для этой позиции в статусе 'reserved'
import logging
logger = logging.getLogger(__name__)
# Ищем резерв для этой позиции в ЛЮБОМ статусе (не только 'reserved')
# КРИТИЧНО: Убран фильтр status='reserved' для предотвращения дубликатов
reservation = Reservation.objects.filter(
order_item=instance,
status='reserved'
order_item=instance
).first()
if reservation:
# Резерв существует - обновляем его количество
# Резерв существует - обновляем ТОЛЬКО количество
# НЕ меняем статус! (может быть 'converted_to_sale', 'reserved', 'released')
old_quantity = reservation.quantity
reservation.quantity = Decimal(str(instance.quantity))
reservation.save()
reservation.save(update_fields=['quantity'])
logger.info(
f"✓ Резерв #{reservation.id} обновлён: quantity {old_quantity}{reservation.quantity} "
f"(статус: {reservation.status}, OrderItem #{instance.id}, заказ {instance.order.order_number})"
)
else:
# Резерва нет - создаем новый
# Это происходит при создании нового OrderItem (через форму или при редактировании)
@@ -467,13 +482,18 @@ def update_reservation_on_item_change(sender, instance, created, **kwargs):
product = instance.product if instance.product else instance.product_kit
if product:
Reservation.objects.create(
reservation = Reservation.objects.create(
order_item=instance,
product=product,
warehouse=warehouse,
quantity=Decimal(str(instance.quantity)),
status='reserved'
)
logger.info(
f"✓ Создан новый резерв #{reservation.id}: {product.name}, quantity={reservation.quantity} "
f"(OrderItem #{instance.id}, заказ {instance.order.order_number})"
)
@receiver(post_save, sender=Incoming)