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