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

229 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Исправление критического бага дублирования резервов
## 🔥 СЕРЬЁЗНЕЙШИЙ БАГ
### Описание проблемы
При изменении количества товара в заказе со статусом "Выполнен" (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-списание
- ✅ Продажи
**Необходимо протестировать** перед продакшеном!