Исправлена ошибка списания товара при завершении заказа

Проблема: При переводе заказа в статус 'completed' возникала ошибка
"Не удалось создать Sale для заказа", т.к. резервы этого же заказа
блокировали списание товара.

Причина: write_off_by_fifo() считал все резервы со статусом 'reserved'
как занятые, включая резервы текущего заказа, которые ещё не были
переведены в 'converted_to_sale'.

Решение:
- Добавлен параметр exclude_order в write_off_by_fifo() для исключения
  резервов конкретного заказа из расчёта занятого товара
- SaleProcessor.create_sale() теперь передаёт order в write_off_by_fifo()
- Добавлены транзакции в views для атомарности операций с заказами:
  при ошибке в сигналах статус заказа откатывается вместе со всеми
  связанными изменениями

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-09 12:04:03 +03:00
parent 1d97da0d3e
commit 936d2275e4
3 changed files with 104 additions and 70 deletions

View File

@@ -70,7 +70,7 @@ class StockBatchManager:
return batch
@staticmethod
def write_off_by_fifo(product, warehouse, quantity_to_write_off):
def write_off_by_fifo(product, warehouse, quantity_to_write_off, exclude_order=None):
"""
Списать товар по FIFO (старые партии первыми).
ВАЖНО: Учитывает зарезервированное количество товара.
@@ -80,6 +80,9 @@ class StockBatchManager:
product: объект Product
warehouse: объект Warehouse
quantity_to_write_off: Decimal - сколько списать
exclude_order: (опционально) объект Order - исключить резервы этого заказа из расчёта.
Используется при переводе заказа в 'completed', когда резервы
заказа ещё не переведены в 'converted_to_sale'.
Returns:
list: [(batch, qty_written), ...] - какие партии и сколько списано
@@ -93,11 +96,16 @@ class StockBatchManager:
allocations = []
# Получаем общее количество зарезервированного товара (все резервы со статусом 'reserved')
total_reserved = Reservation.objects.filter(
# Исключаем резервы заказа, для которого делается списание (если передан)
reservation_filter = Reservation.objects.filter(
product=product,
warehouse=warehouse,
status='reserved'
).aggregate(total=Sum('quantity'))['total'] or Decimal('0')
)
if exclude_order:
reservation_filter = reservation_filter.exclude(order_item__order=exclude_order)
total_reserved = reservation_filter.aggregate(total=Sum('quantity'))['total'] or Decimal('0')
# Получаем партии по FIFO
batches = StockBatchManager.get_batches_for_fifo(product, warehouse)

View File

@@ -99,7 +99,11 @@ class SaleProcessor:
try:
# Списываем товар по FIFO
allocations = StockBatchManager.write_off_by_fifo(product, warehouse, quantity)
# exclude_order позволяет не считать резервы этого заказа как занятые
# (резервы переводятся в converted_to_sale ПОСЛЕ создания Sale)
allocations = StockBatchManager.write_off_by_fifo(
product, warehouse, quantity, exclude_order=order
)
# Фиксируем распределение для аудита
for batch, qty_allocated in allocations: