Исправлен race condition в списании партий товара

- Добавлен параметр lock в get_batches_for_fifo() для блокировки строк
- Используется select_for_update() в write_off_by_fifo() для предотвращения
  параллельной перезаписи quantity при одновременном списании из одной партии
- Защита от потери данных при параллельном завершении заказов
This commit is contained in:
2026-01-05 21:29:29 +03:00
parent 03794356d0
commit aed9290d7a

View File

@@ -20,7 +20,7 @@ class StockBatchManager:
""" """
@staticmethod @staticmethod
def get_batches_for_fifo(product, warehouse): def get_batches_for_fifo(product, warehouse, lock=False):
""" """
Получить все активные партии товара на складе для FIFO списания. Получить все активные партии товара на складе для FIFO списания.
Возвращает ВСЕ партии с quantity > 0, отсортированные по created_at. Возвращает ВСЕ партии с quantity > 0, отсортированные по created_at.
@@ -29,17 +29,25 @@ class StockBatchManager:
Args: Args:
product: объект Product product: объект Product
warehouse: объект Warehouse warehouse: объект Warehouse
lock: bool - использовать ли select_for_update() для блокировки строк
(защита от race condition при параллельном списании)
Returns: Returns:
QuerySet отсортированных партий QuerySet отсортированных партий
""" """
return StockBatch.objects.filter( queryset = StockBatch.objects.filter(
product=product, product=product,
warehouse=warehouse, warehouse=warehouse,
is_active=True, is_active=True,
quantity__gt=0 # Только партии с остатком quantity__gt=0 # Только партии с остатком
).order_by('created_at') # FIFO: старые первыми ).order_by('created_at') # FIFO: старые первыми
if lock:
# Блокируем строки для предотвращения race condition
queryset = queryset.select_for_update()
return queryset
@staticmethod @staticmethod
def create_batch(product, warehouse, quantity, cost_price): def create_batch(product, warehouse, quantity, cost_price):
""" """
@@ -127,8 +135,10 @@ class StockBatchManager:
total_reserved = reservation_filter.aggregate(total=Sum('quantity_base'))['total'] or Decimal('0') total_reserved = reservation_filter.aggregate(total=Sum('quantity_base'))['total'] or Decimal('0')
# Получаем партии по FIFO # Получаем партии по FIFO с блокировкой строк
batches = StockBatchManager.get_batches_for_fifo(product, warehouse) # Используем select_for_update() для предотвращения race condition
# при параллельном списании из одной партии
batches = StockBatchManager.get_batches_for_fifo(product, warehouse, lock=True)
# Проходим партии, списывая товар # Проходим партии, списывая товар
# Если есть exclude_transformation, сначала списываем из зарезервированного товара трансформации # Если есть exclude_transformation, сначала списываем из зарезервированного товара трансформации