Проблема #1: Резервы не создавались при создании заказа - Order сохранялся БЕЗ items → сигнал reserve_stock_on_order_create не мог создать резервы (items.all() был пустой) - OrderItem создавались ПОСЛЕ, но сигнал уже отработал Решение #1: Создание резервов через сигнал OrderItem - Доработан сигнал update_reservation_on_item_change - Убрано раннее возвращение для created=True - Теперь резервы создаются при добавлении OrderItem (любым способом) - Работает для всех сценариев: * Создание заказа с товарами * Добавление товаров при редактировании * Изменение количества Проблема #2: Риск расхождений при удалении заказа - Сигнал pre_delete освобождал резервы ДО удаления - Если удаление падало с ошибкой → резервы освобождены, Order не удалён - Возникало расхождение данных Решение #2: transaction.on_commit для освобождения резервов - Добавлен @transaction.atomic к сигналу release_stock_on_order_delete - Резервы получаются ДО удаления через list() - Освобождение происходит через transaction.on_commit() - Резервы освобождаются ТОЛЬКО если удаление успешно - Гарантия целостности данных Изменённые файлы: - myproject/inventory/signals.py - оба сигнала исправлены - RESERVATION_FIX.md - полная документация
230 lines
10 KiB
Markdown
230 lines
10 KiB
Markdown
# Исправление создания резервов для заказов
|
||
|
||
## 🐛 Проблема
|
||
|
||
При создании нового заказа со статусом "Черновик" (или любым другим) резервы товаров **не создавались**.
|
||
|
||
### Причина
|
||
|
||
**Неправильный порядок сохранения в `orders/views.py`:**
|
||
|
||
```python
|
||
def order_create(request):
|
||
# ...
|
||
order.save() # ← 1. Сигнал reserve_stock_on_order_create срабатывает
|
||
# НО instance.items.all() пустой!
|
||
|
||
formset.save() # ← 2. OrderItem создаются ПОСЛЕ
|
||
# Сигнал больше не сработает
|
||
```
|
||
|
||
**Что происходило:**
|
||
1. `order.save()` → сигнал `reserve_stock_on_order_create` срабатывал
|
||
2. Сигнал проверял `instance.items.all()` → **ПУСТО** (OrderItem ещё не созданы)
|
||
3. Цикл `for item in instance.items.all()` не выполнялся
|
||
4. Резервы НЕ создавались
|
||
5. `formset.save()` → OrderItem создавались, но сигнал уже отработал
|
||
|
||
---
|
||
|
||
## ✅ Решение #1: Создание резервов через сигнал OrderItem
|
||
|
||
### Изменённый файл: `inventory/signals.py`
|
||
|
||
**Сигнал:** `update_reservation_on_item_change` (строки 415-452)
|
||
|
||
### Было (ОШИБОЧНО):
|
||
```python
|
||
@receiver(post_save, sender=OrderItem)
|
||
def update_reservation_on_item_change(sender, instance, created, **kwargs):
|
||
if created:
|
||
return # ❌ Новые позиции игнорировались!
|
||
|
||
# Код обновления резервов...
|
||
```
|
||
|
||
### Стало (ИСПРАВЛЕНО):
|
||
```python
|
||
@receiver(post_save, sender=OrderItem)
|
||
def update_reservation_on_item_change(sender, instance, created, **kwargs):
|
||
"""
|
||
При создании или изменении позиции заказа управляем резервами.
|
||
|
||
Покрывает все сценарии:
|
||
- Создание заказа с товарами → создаёт резервы
|
||
- Редактирование + добавление товаров → создаёт резервы для новых
|
||
- Изменение количества → обновляет резервы
|
||
"""
|
||
# ✅ Убрали раннее возвращение для created=True
|
||
|
||
reservation = Reservation.objects.filter(
|
||
order_item=instance,
|
||
status='reserved'
|
||
).first()
|
||
|
||
if reservation:
|
||
# Резерв существует - обновляем количество
|
||
reservation.quantity = Decimal(str(instance.quantity))
|
||
reservation.save()
|
||
else:
|
||
# Резерва нет - создаём новый
|
||
warehouse = instance.order.pickup_warehouse or Warehouse.objects.filter(is_active=True).first()
|
||
|
||
if warehouse:
|
||
product = instance.product if instance.product else instance.product_kit
|
||
|
||
if product:
|
||
Reservation.objects.create(
|
||
order_item=instance,
|
||
product=product,
|
||
warehouse=warehouse,
|
||
quantity=Decimal(str(instance.quantity)),
|
||
status='reserved'
|
||
)
|
||
```
|
||
|
||
### Ключевые изменения:
|
||
1. ✅ Убрали `if created: return` - сигнал работает для ВСЕХ сохранений OrderItem
|
||
2. ✅ Резервы создаются при добавлении OrderItem (независимо от способа создания)
|
||
3. ✅ При редактировании только обновляются (если резерв уже есть)
|
||
4. ✅ Работает для любого статуса заказа (Черновик, Новый, и т.д.)
|
||
|
||
---
|
||
|
||
## ✅ Решение #2: Безопасное освобождение резервов при удалении
|
||
|
||
### Изменённый файл: `inventory/signals.py`
|
||
|
||
**Сигнал:** `release_stock_on_order_delete` (строки 392-413)
|
||
|
||
### Проблема с `pre_delete`:
|
||
|
||
**Сценарий риска:**
|
||
1. Сигнал `pre_delete` срабатывает → резервы освобождаются
|
||
2. Удаление Order **ПАДАЕТ** с ошибкой (например, из-за ограничений БД)
|
||
3. **Результат:** Order не удалён, но резервы освобождены! ❌ Расхождение!
|
||
|
||
### Было (РИСКОВАННО):
|
||
```python
|
||
@receiver(pre_delete, sender=Order)
|
||
def release_stock_on_order_delete(sender, instance, **kwargs):
|
||
reservations = Reservation.objects.filter(
|
||
order_item__order=instance,
|
||
status='reserved'
|
||
)
|
||
|
||
# ❌ Освобождаем резервы ДО удаления
|
||
# Если удаление упадёт - резервы уже изменены!
|
||
for res in reservations:
|
||
res.status = 'released'
|
||
res.released_at = timezone.now()
|
||
res.save()
|
||
```
|
||
|
||
### Стало (БЕЗОПАСНО):
|
||
```python
|
||
@receiver(pre_delete, sender=Order)
|
||
@transaction.atomic
|
||
def release_stock_on_order_delete(sender, instance, **kwargs):
|
||
"""
|
||
При удалении заказа освобождаем резервы ТОЛЬКО после успешного коммита.
|
||
"""
|
||
# ✅ Получаем резервы ДО удаления (пока Order существует)
|
||
# Используем list() чтобы выполнить запрос сейчас
|
||
reservations_to_release = list(
|
||
Reservation.objects.filter(
|
||
order_item__order=instance,
|
||
status='reserved'
|
||
)
|
||
)
|
||
|
||
# ✅ Освобождаем резервы ПОСЛЕ успешного коммита
|
||
def release_reservations():
|
||
for res in reservations_to_release:
|
||
res.status = 'released'
|
||
res.released_at = timezone.now()
|
||
res.save()
|
||
|
||
transaction.on_commit(release_reservations)
|
||
```
|
||
|
||
### Ключевые изменения:
|
||
1. ✅ Добавлен `@transaction.atomic` - гарантирует транзакцию
|
||
2. ✅ Используем `list()` для получения резервов ДО удаления
|
||
3. ✅ Используем `transaction.on_commit()` - освобождаем резервы ТОЛЬКО если удаление успешно
|
||
4. ✅ Гарантия целостности данных - нет риска расхождений
|
||
|
||
---
|
||
|
||
## 📊 Покрытие всех сценариев
|
||
|
||
| Сценарий | OrderItem | created | Резерв существует? | Действие |
|
||
|----------|-----------|---------|-------------------|----------|
|
||
| **Создание заказа с товарами** | Новый | True | Нет | Создать резерв ✅ |
|
||
| **Создание заказа без товаров** | - | - | - | Ничего ✅ |
|
||
| **Редактирование: добавить товар** | Новый | True | Нет | Создать резерв ✅ |
|
||
| **Редактирование: изменить количество** | Старый | False | Да | Обновить резерв ✅ |
|
||
| **Повторное сохранение заказа** | Старый | False | Да | Обновить резерв ✅ |
|
||
| **Удаление заказа (успешно)** | - | - | - | Освободить резервы ✅ |
|
||
| **Удаление заказа (с ошибкой)** | - | - | - | Резервы НЕ освобождаются ✅ |
|
||
|
||
---
|
||
|
||
## 🎯 Преимущества решения
|
||
|
||
### Решение #1 (OrderItem сигнал):
|
||
- ✅ **Универсальность** - работает для любого способа создания OrderItem
|
||
- ✅ **Простота** - минимальное изменение существующего сигнала
|
||
- ✅ **Независимость от статуса** - резервы создаются для любого статуса заказа
|
||
- ✅ **Гибкость** - можно добавлять товары при редактировании
|
||
|
||
### Решение #2 (transaction.on_commit):
|
||
- ✅ **Целостность данных** - резервы освобождаются только при успешном удалении
|
||
- ✅ **Безопасность** - нет риска расхождений при ошибках
|
||
- ✅ **Прозрачность** - очевидная логика работы
|
||
- ✅ **Надёжность** - гарантии через транзакции
|
||
|
||
---
|
||
|
||
## 🧪 Как проверить
|
||
|
||
### Тест 1: Создание заказа с товарами
|
||
1. Создайте новый заказ со статусом "Черновик"
|
||
2. Добавьте 2 товара по 10 штук каждый
|
||
3. Сохраните заказ
|
||
4. **Проверка:** В таблице `Reservation` должно быть 2 записи со `status='reserved'`
|
||
|
||
### Тест 2: Создание заказа без товаров
|
||
1. Создайте новый заказ без товаров
|
||
2. Сохраните заказ
|
||
3. **Проверка:** Резервы не создались (это нормально)
|
||
|
||
### Тест 3: Добавление товара при редактировании
|
||
1. Откройте существующий заказ
|
||
2. Добавьте новый товар (5 штук)
|
||
3. Сохраните заказ
|
||
4. **Проверка:** Создался новый резерв для добавленного товара
|
||
|
||
### Тест 4: Изменение количества
|
||
1. Откройте существующий заказ с товарами
|
||
2. Измените количество товара с 10 на 15
|
||
3. Сохраните заказ
|
||
4. **Проверка:** Резерв обновился (quantity=15)
|
||
|
||
### Тест 5: Удаление заказа
|
||
1. Удалите заказ с товарами
|
||
2. **Проверка:** Резервы переведены в `status='released'`
|
||
3. Попробуйте удалить заказ с нарушением ограничений БД
|
||
4. **Проверка:** Если удаление упало - резервы НЕ изменились
|
||
|
||
---
|
||
|
||
## 📝 Связанные файлы
|
||
|
||
- `myproject/inventory/signals.py` - оба исправления
|
||
- `myproject/orders/views.py` - порядок сохранения (не изменялся)
|
||
|
||
---
|
||
|
||
Дата исправления: 2024-12-01
|