Исправление конфликта сигналов при отмене трансформации
Исправлена проблема, когда при отмене проведенной трансформации оба сигнала выполнялись последовательно:
- rollback_transformation_on_cancel возвращал резервы в 'reserved'
- release_reservations_on_draft_cancel ошибочно освобождал их в 'released'
Изменена проверка в release_reservations_on_draft_cancel: вместо проверки наличия партий Output (которые уже удалены) теперь проверяется статус резервов ('converted_to_transformation') или наличие поля converted_at, что работает независимо от порядка выполнения сигналов.
This commit is contained in:
@@ -70,7 +70,7 @@ class StockBatchManager:
|
||||
return batch
|
||||
|
||||
@staticmethod
|
||||
def write_off_by_fifo(product, warehouse, quantity_to_write_off, exclude_order=None):
|
||||
def write_off_by_fifo(product, warehouse, quantity_to_write_off, exclude_order=None, exclude_transformation=None):
|
||||
"""
|
||||
Списать товар по FIFO (старые партии первыми).
|
||||
ВАЖНО: Учитывает зарезервированное количество товара.
|
||||
@@ -83,6 +83,9 @@ class StockBatchManager:
|
||||
exclude_order: (опционально) объект Order - исключить резервы этого заказа из расчёта.
|
||||
Используется при переводе заказа в 'completed', когда резервы
|
||||
заказа ещё не переведены в 'converted_to_sale'.
|
||||
exclude_transformation: (опционально) объект Transformation - исключить резервы этой трансформации из расчёта.
|
||||
Используется при переводе трансформации в 'completed', когда резервы
|
||||
трансформации ещё не переведены в 'converted_to_transformation'.
|
||||
|
||||
Returns:
|
||||
list: [(batch, qty_written), ...] - какие партии и сколько списано
|
||||
@@ -96,12 +99,26 @@ class StockBatchManager:
|
||||
allocations = []
|
||||
|
||||
# Получаем общее количество зарезервированного товара (все резервы со статусом 'reserved')
|
||||
# Исключаем резервы заказа, для которого делается списание (если передан)
|
||||
# Исключаем резервы заказа или трансформации, для которых делается списание (если переданы)
|
||||
reservation_filter = Reservation.objects.filter(
|
||||
product=product,
|
||||
warehouse=warehouse,
|
||||
status='reserved'
|
||||
)
|
||||
|
||||
# Специальная обработка для трансформации: нужно списывать из зарезервированного товара трансформации
|
||||
transformation_reserved_qty = Decimal('0')
|
||||
if exclude_transformation:
|
||||
transformation_reservations = Reservation.objects.filter(
|
||||
product=product,
|
||||
warehouse=warehouse,
|
||||
status='reserved',
|
||||
transformation_input__transformation=exclude_transformation
|
||||
)
|
||||
transformation_reserved_qty = transformation_reservations.aggregate(total=Sum('quantity'))['total'] or Decimal('0')
|
||||
# Исключаем резервы трансформации из общего расчета резервов
|
||||
reservation_filter = reservation_filter.exclude(transformation_input__transformation=exclude_transformation)
|
||||
|
||||
if exclude_order:
|
||||
reservation_filter = reservation_filter.exclude(order_item__order=exclude_order)
|
||||
|
||||
@@ -110,8 +127,10 @@ class StockBatchManager:
|
||||
# Получаем партии по FIFO
|
||||
batches = StockBatchManager.get_batches_for_fifo(product, warehouse)
|
||||
|
||||
# Проходим партии, списывая только СВОБОДНОЕ количество
|
||||
reserved_remaining = total_reserved # Сколько резерва еще не распределено по партиям
|
||||
# Проходим партии, списывая товар
|
||||
# Если есть exclude_transformation, сначала списываем из зарезервированного товара трансформации
|
||||
reserved_remaining = total_reserved # Сколько резерва (кроме трансформации) еще не распределено по партиям
|
||||
transformation_reserved_remaining = transformation_reserved_qty # Сколько резерва трансформации еще не распределено
|
||||
|
||||
for batch in batches:
|
||||
if remaining <= 0:
|
||||
@@ -119,32 +138,57 @@ class StockBatchManager:
|
||||
|
||||
# Определяем сколько в этой партии зарезервировано (пропорционально)
|
||||
# Логика: старые партии "съедают" резерв первыми (как и при списании)
|
||||
batch_reserved = min(batch.quantity, reserved_remaining)
|
||||
reserved_remaining -= batch_reserved
|
||||
|
||||
batch_reserved_other = min(batch.quantity, reserved_remaining)
|
||||
reserved_remaining -= batch_reserved_other
|
||||
|
||||
# Если есть резерв трансформации, распределяем его по партиям
|
||||
batch_reserved_transformation = Decimal('0')
|
||||
if transformation_reserved_remaining > 0:
|
||||
# Распределяем резерв трансформации по партиям
|
||||
batch_reserved_transformation = min(batch.quantity - batch_reserved_other, transformation_reserved_remaining)
|
||||
transformation_reserved_remaining -= batch_reserved_transformation
|
||||
|
||||
# Общее зарезервированное в партии
|
||||
batch_reserved = batch_reserved_other + batch_reserved_transformation
|
||||
|
||||
# Свободное количество в партии
|
||||
batch_free = batch.quantity - batch_reserved
|
||||
|
||||
if batch_free <= 0:
|
||||
# Партия полностью зарезервирована → пропускаем
|
||||
continue
|
||||
# Если есть резерв трансформации в этой партии, списываем из него
|
||||
if batch_reserved_transformation > 0:
|
||||
# Списываем из зарезервированного товара трансформации
|
||||
qty_from_reserved = min(batch_reserved_transformation, remaining)
|
||||
batch.quantity -= qty_from_reserved
|
||||
batch.save(update_fields=['quantity', 'updated_at'])
|
||||
remaining -= qty_from_reserved
|
||||
allocations.append((batch, qty_from_reserved))
|
||||
|
||||
if remaining <= 0:
|
||||
break
|
||||
|
||||
# Если партия опустошена, деактивируем её
|
||||
if batch.quantity <= 0:
|
||||
batch.is_active = False
|
||||
batch.save(update_fields=['is_active'])
|
||||
continue
|
||||
|
||||
# Сколько можем списать из этой партии (только свободное)
|
||||
qty_from_this_batch = min(batch_free, remaining)
|
||||
# Если осталось списать, списываем из свободного
|
||||
if batch_free > 0 and remaining > 0:
|
||||
qty_from_this_batch = min(batch_free, remaining)
|
||||
|
||||
# Списываем
|
||||
batch.quantity -= qty_from_this_batch
|
||||
batch.save(update_fields=['quantity', 'updated_at'])
|
||||
# Списываем
|
||||
batch.quantity -= qty_from_this_batch
|
||||
batch.save(update_fields=['quantity', 'updated_at'])
|
||||
|
||||
remaining -= qty_from_this_batch
|
||||
remaining -= qty_from_this_batch
|
||||
|
||||
# Фиксируем распределение
|
||||
allocations.append((batch, qty_from_this_batch))
|
||||
# Фиксируем распределение
|
||||
allocations.append((batch, qty_from_this_batch))
|
||||
|
||||
# Если партия опустошена, деактивируем её
|
||||
if batch.quantity <= 0:
|
||||
batch.is_active = False
|
||||
batch.save(update_fields=['is_active'])
|
||||
# Если партия опустошена, деактивируем её
|
||||
if batch.quantity <= 0:
|
||||
batch.is_active = False
|
||||
batch.save(update_fields=['is_active'])
|
||||
|
||||
if remaining > 0:
|
||||
raise ValueError(
|
||||
|
||||
@@ -218,6 +218,20 @@ class TransformationService:
|
||||
if not transformation.outputs.exists():
|
||||
raise ValidationError("Добавьте хотя бы один выходной товар")
|
||||
|
||||
# Проверяем что сумма входных количеств равна сумме выходных
|
||||
total_input_quantity = sum(
|
||||
trans_input.quantity for trans_input in transformation.inputs.all()
|
||||
)
|
||||
total_output_quantity = sum(
|
||||
trans_output.quantity for trans_output in transformation.outputs.all()
|
||||
)
|
||||
|
||||
if total_input_quantity != total_output_quantity:
|
||||
raise ValidationError(
|
||||
f"Сумма входных количеств ({total_input_quantity}) должна быть равна "
|
||||
f"сумме выходных количеств ({total_output_quantity})"
|
||||
)
|
||||
|
||||
# Проверяем наличие всех входных товаров
|
||||
for trans_input in transformation.inputs.all():
|
||||
stock = Stock.objects.filter(
|
||||
|
||||
Reference in New Issue
Block a user