Исправлена проблема с резервами при откате из статуса 'Выполнен'

Проблема:
- При откате заказа из статуса 'completed' в 'возврат' или другой статус
- Резервы правильно обновлялись на 'reserved' или 'released'
- НО Stock.quantity_reserved не обновлялся
- В результате товар показывался как полностью свободный, хотя был резерв

Причина:
- В сигнале rollback_sale_on_status_change использовался .update()
- Это не вызывало сигнал update_stock_on_reservation_change
- Stock не пересчитывался автоматически

Решение:
- Заменен .update() на .save(update_fields=[...]) в сигнале отката
- Теперь при изменении резервов автоматически срабатывает сигнал
- Stock корректно обновляется в обоих направлениях:
  * completed → резервы converted_to_sale → Stock обновляется
  * откат → резервы reserved/released → Stock обновляется
- Убран костыль с ручным вызовом refresh_from_batches()

Результат:
- Элегантное единообразное решение для всех сценариев
- Stock автоматически синхронизируется с резервами
- Работает корректно при любых изменениях статуса заказа
This commit is contained in:
2025-12-01 02:40:40 +03:00
parent e4cb175db2
commit a5a983b198

View File

@@ -360,42 +360,24 @@ def rollback_sale_on_status_change(sender, instance, created, **kwargs):
reservations_count = reservations.count() reservations_count = reservations.count()
if reservations_count > 0: if reservations_count > 0:
# Используем update() вместо save() для массового обновления # Обновляем резервы через .save() чтобы сработал сигнал обновления Stock
# Это предотвращает повторный вызов сигнала update_stock_on_reservation_change # Сигнал update_stock_on_reservation_change автоматически обновит Stock
# и двойное обновление Stock for reservation in reservations:
update_fields = {'status': reservation_target_status} reservation.status = reservation_target_status
if reservation_target_status == 'released': if reservation_target_status == 'released':
update_fields['released_at'] = timezone.now() reservation.released_at = timezone.now()
# converted_at оставляем (для истории) # converted_at оставляем (для истории)
reservations.update(**update_fields) # Используем save() с указанием измененных полей
update_fields = ['status']
if reservation_target_status == 'released':
update_fields.append('released_at')
reservation.save(update_fields=update_fields)
logger.info( logger.info(
f"✓ Обновлено {reservations_count} резервов: " f"✓ Обновлено {reservations_count} резервов: "
f"converted_to_sale → {reservation_target_status}" f"converted_to_sale → {reservation_target_status}"
) )
# Обновляем Stock вручную, т.к. update() не вызывает сигналы
# Группируем по product + warehouse для эффективности
reservation_groups = reservations.values_list('product_id', 'warehouse_id').distinct()
for product_id, warehouse_id in reservation_groups:
try:
stock = Stock.objects.get(
product_id=product_id,
warehouse_id=warehouse_id
)
stock.refresh_from_batches()
logger.debug(
f" Stock обновлен после изменения резервов: "
f"product_id={product_id}, warehouse_id={warehouse_id}"
)
except Stock.DoesNotExist:
logger.warning(
f" Stock не найден для product_id={product_id}, warehouse_id={warehouse_id}"
)
else: else:
logger.warning( logger.warning(
f"⚠ Для заказа {instance.order_number} нет резервов в статусе 'converted_to_sale'" f"⚠ Для заказа {instance.order_number} нет резервов в статусе 'converted_to_sale'"