Исправлено создание резервов при сохранении заказов
Проблема #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:
229
RESERVATION_FIX.md
Normal file
229
RESERVATION_FIX.md
Normal 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
|
||||||
@@ -390,41 +390,51 @@ def rollback_sale_on_status_change(sender, instance, created, **kwargs):
|
|||||||
|
|
||||||
|
|
||||||
@receiver(pre_delete, sender=Order)
|
@receiver(pre_delete, sender=Order)
|
||||||
|
@transaction.atomic
|
||||||
def release_stock_on_order_delete(sender, instance, **kwargs):
|
def release_stock_on_order_delete(sender, instance, **kwargs):
|
||||||
"""
|
"""
|
||||||
Сигнал: При удалении/отмене заказа освободить резервы.
|
Сигнал: При удалении/отмене заказа освободить резервы.
|
||||||
|
|
||||||
Процесс:
|
Процесс:
|
||||||
1. Ищем все резервы для этого заказа
|
1. Ищем все резервы для этого заказа ДО удаления
|
||||||
2. Меняем статус резерва на 'released'
|
2. Освобождаем резервы ПОСЛЕ успешного коммита транзакции
|
||||||
3. Фиксируем время освобождения
|
3. Это гарантирует, что резервы освободятся только если удаление успешно
|
||||||
"""
|
"""
|
||||||
# Находим все резервы для этого заказа
|
# Находим все резервы для этого заказа ДО удаления
|
||||||
reservations = Reservation.objects.filter(
|
# Используем list() чтобы выполнить запрос сейчас, пока Order ещё существует
|
||||||
|
reservations_to_release = list(
|
||||||
|
Reservation.objects.filter(
|
||||||
order_item__order=instance,
|
order_item__order=instance,
|
||||||
status='reserved'
|
status='reserved'
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# Освобождаем каждый резерв
|
# Освобождаем резервы ПОСЛЕ успешного коммита транзакции
|
||||||
for res in reservations:
|
# Это гарантирует целостность: резервы освободятся только если удаление прошло успешно
|
||||||
|
def release_reservations():
|
||||||
|
for res in reservations_to_release:
|
||||||
res.status = 'released'
|
res.status = 'released'
|
||||||
res.released_at = timezone.now()
|
res.released_at = timezone.now()
|
||||||
res.save()
|
res.save()
|
||||||
|
|
||||||
|
transaction.on_commit(release_reservations)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=OrderItem)
|
@receiver(post_save, sender=OrderItem)
|
||||||
def update_reservation_on_item_change(sender, instance, created, **kwargs):
|
def update_reservation_on_item_change(sender, instance, created, **kwargs):
|
||||||
"""
|
"""
|
||||||
Сигнал: Если изменилось количество товара в позиции заказа,
|
Сигнал: При создании или изменении позиции заказа управляем резервами.
|
||||||
обновить резерв.
|
|
||||||
|
|
||||||
Процесс:
|
Процесс:
|
||||||
1. Если это новая позиция - игнорируем (резерв уже создан через Order)
|
1. Ищем существующий резерв для этой позиции
|
||||||
2. Если изменилось количество - обновляем резерв или создаем новый
|
2. Если резерв ЕСТЬ - обновляем количество
|
||||||
"""
|
3. Если резерва НЕТ - создаем новый
|
||||||
if created:
|
|
||||||
return # Новые позиции обрабатываются через Order signal
|
|
||||||
|
|
||||||
|
Покрывает все сценарии:
|
||||||
|
- Создание заказа с товарами → создаёт резервы
|
||||||
|
- Редактирование + добавление товаров → создаёт резервы для новых
|
||||||
|
- Изменение количества → обновляет резервы
|
||||||
|
"""
|
||||||
# Получаем резерв для этой позиции в статусе 'reserved'
|
# Получаем резерв для этой позиции в статусе 'reserved'
|
||||||
reservation = Reservation.objects.filter(
|
reservation = Reservation.objects.filter(
|
||||||
order_item=instance,
|
order_item=instance,
|
||||||
@@ -432,11 +442,12 @@ def update_reservation_on_item_change(sender, instance, created, **kwargs):
|
|||||||
).first()
|
).first()
|
||||||
|
|
||||||
if reservation:
|
if reservation:
|
||||||
# Резерв существует - обновляем его
|
# Резерв существует - обновляем его количество
|
||||||
reservation.quantity = Decimal(str(instance.quantity))
|
reservation.quantity = Decimal(str(instance.quantity))
|
||||||
reservation.save()
|
reservation.save()
|
||||||
else:
|
else:
|
||||||
# Резерва нет - создаем новый
|
# Резерва нет - создаем новый
|
||||||
|
# Это происходит при создании нового OrderItem (через форму или при редактировании)
|
||||||
warehouse = instance.order.pickup_warehouse or Warehouse.objects.filter(is_active=True).first()
|
warehouse = instance.order.pickup_warehouse or Warehouse.objects.filter(is_active=True).first()
|
||||||
|
|
||||||
if warehouse:
|
if warehouse:
|
||||||
|
|||||||
Reference in New Issue
Block a user