Проблема #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 - полная документация
10 KiB
10 KiB
Исправление создания резервов для заказов
🐛 Проблема
При создании нового заказа со статусом "Черновик" (или любым другим) резервы товаров не создавались.
Причина
Неправильный порядок сохранения в orders/views.py:
def order_create(request):
# ...
order.save() # ← 1. Сигнал reserve_stock_on_order_create срабатывает
# НО instance.items.all() пустой!
formset.save() # ← 2. OrderItem создаются ПОСЛЕ
# Сигнал больше не сработает
Что происходило:
order.save()→ сигналreserve_stock_on_order_createсрабатывал- Сигнал проверял
instance.items.all()→ ПУСТО (OrderItem ещё не созданы) - Цикл
for item in instance.items.all()не выполнялся - Резервы НЕ создавались
formset.save()→ OrderItem создавались, но сигнал уже отработал
✅ Решение #1: Создание резервов через сигнал OrderItem
Изменённый файл: inventory/signals.py
Сигнал: update_reservation_on_item_change (строки 415-452)
Было (ОШИБОЧНО):
@receiver(post_save, sender=OrderItem)
def update_reservation_on_item_change(sender, instance, created, **kwargs):
if created:
return # ❌ Новые позиции игнорировались!
# Код обновления резервов...
Стало (ИСПРАВЛЕНО):
@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'
)
Ключевые изменения:
- ✅ Убрали
if created: return- сигнал работает для ВСЕХ сохранений OrderItem - ✅ Резервы создаются при добавлении OrderItem (независимо от способа создания)
- ✅ При редактировании только обновляются (если резерв уже есть)
- ✅ Работает для любого статуса заказа (Черновик, Новый, и т.д.)
✅ Решение #2: Безопасное освобождение резервов при удалении
Изменённый файл: inventory/signals.py
Сигнал: release_stock_on_order_delete (строки 392-413)
Проблема с pre_delete:
Сценарий риска:
- Сигнал
pre_deleteсрабатывает → резервы освобождаются - Удаление Order ПАДАЕТ с ошибкой (например, из-за ограничений БД)
- Результат: Order не удалён, но резервы освобождены! ❌ Расхождение!
Было (РИСКОВАННО):
@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()
Стало (БЕЗОПАСНО):
@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)
Ключевые изменения:
- ✅ Добавлен
@transaction.atomic- гарантирует транзакцию - ✅ Используем
list()для получения резервов ДО удаления - ✅ Используем
transaction.on_commit()- освобождаем резервы ТОЛЬКО если удаление успешно - ✅ Гарантия целостности данных - нет риска расхождений
📊 Покрытие всех сценариев
| Сценарий | OrderItem | created | Резерв существует? | Действие |
|---|---|---|---|---|
| Создание заказа с товарами | Новый | True | Нет | Создать резерв ✅ |
| Создание заказа без товаров | - | - | - | Ничего ✅ |
| Редактирование: добавить товар | Новый | True | Нет | Создать резерв ✅ |
| Редактирование: изменить количество | Старый | False | Да | Обновить резерв ✅ |
| Повторное сохранение заказа | Старый | False | Да | Обновить резерв ✅ |
| Удаление заказа (успешно) | - | - | - | Освободить резервы ✅ |
| Удаление заказа (с ошибкой) | - | - | - | Резервы НЕ освобождаются ✅ |
🎯 Преимущества решения
Решение #1 (OrderItem сигнал):
- ✅ Универсальность - работает для любого способа создания OrderItem
- ✅ Простота - минимальное изменение существующего сигнала
- ✅ Независимость от статуса - резервы создаются для любого статуса заказа
- ✅ Гибкость - можно добавлять товары при редактировании
Решение #2 (transaction.on_commit):
- ✅ Целостность данных - резервы освобождаются только при успешном удалении
- ✅ Безопасность - нет риска расхождений при ошибках
- ✅ Прозрачность - очевидная логика работы
- ✅ Надёжность - гарантии через транзакции
🧪 Как проверить
Тест 1: Создание заказа с товарами
- Создайте новый заказ со статусом "Черновик"
- Добавьте 2 товара по 10 штук каждый
- Сохраните заказ
- Проверка: В таблице
Reservationдолжно быть 2 записи соstatus='reserved'
Тест 2: Создание заказа без товаров
- Создайте новый заказ без товаров
- Сохраните заказ
- Проверка: Резервы не создались (это нормально)
Тест 3: Добавление товара при редактировании
- Откройте существующий заказ
- Добавьте новый товар (5 штук)
- Сохраните заказ
- Проверка: Создался новый резерв для добавленного товара
Тест 4: Изменение количества
- Откройте существующий заказ с товарами
- Измените количество товара с 10 на 15
- Сохраните заказ
- Проверка: Резерв обновился (quantity=15)
Тест 5: Удаление заказа
- Удалите заказ с товарами
- Проверка: Резервы переведены в
status='released' - Попробуйте удалить заказ с нарушением ограничений БД
- Проверка: Если удаление упало - резервы НЕ изменились
📝 Связанные файлы
myproject/inventory/signals.py- оба исправленияmyproject/orders/views.py- порядок сохранения (не изменялся)
Дата исправления: 2024-12-01