Исправлено создание резервов при сохранении заказов

Проблема #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 - полная документация
This commit is contained in:
2025-12-01 01:10:58 +03:00
parent e0437cdb5a
commit 293e8640ef
2 changed files with 260 additions and 20 deletions

229
RESERVATION_FIX.md Normal file
View File

@@ -0,0 +1,229 @@
# Исправление создания резервов для заказов
## 🐛 Проблема
При создании нового заказа со статусом "Черновик" (или любым другим) резервы товаров **не создавались**.
### Причина
**Неправильный порядок сохранения в `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