Исправлено: агрегация резервов теперь использует quantity_base

КРИТИЧНО: Все агрегации Reservation.quantity заменены на quantity_base

Проблемы и решения:

🔴 КРИТИЧНО - BatchManager.write_off_by_fifo():

  - Проблема: суммировал quantity вместо quantity_base

  - Влияние: FIFO расчет свободного товара был некорректен

  - Решение: aggregate(Sum('quantity_base')) в строках 118, 125

🟡 СРЕДНЯЯ ВАЖНОСТЬ - ShowcaseManager:

  - reserve_showcase_item(): обновление quantity и quantity_base (строка 403)

  - release_showcase_reservation(): обновление обоих полей (строка 481)

  - Теперь витринные резервы полностью консистентны

🟡 СРЕДНЯЯ ВАЖНОСТЬ - TransformationService:

  - confirm(): проверка доступности через quantity_base (строка 254)

  - Корректная валидация при трансформации товаров

🟢 НИЗКАЯ ВАЖНОСТЬ - WriteOffDocumentService:

  - update_item(): синхронизация quantity и quantity_base (строка 175)

  - Полнота данных в резервах документов списания

🟢 НИЗКАЯ ВАЖНОСТЬ - Сигналы (signals.py):

  - update_order_item_reservation(): обновление обоих полей для товаров

  - Для обычных товаров: quantity_base = quantity_in_base_units (строка 1081)

  - Для комплектов: quantity_base = quantity (компоненты в базовых) (строка 1107)

  - Добавлено обновление sales_unit при изменении OrderItem

Архитектура:

- Принцип: quantity_base ВСЕГДА содержит количество в базовых единицах

- Все агрегации резервов используют quantity_base для корректных расчетов

- quantity сохраняется для совместимости и отображения

- sales_unit хранит ссылку на единицу продажи для аудита
This commit is contained in:
2026-01-02 14:46:02 +03:00
parent f34cfaeca0
commit d2b49cca56
5 changed files with 19 additions and 9 deletions

View File

@@ -115,14 +115,14 @@ class StockBatchManager:
status='reserved', status='reserved',
transformation_input__transformation=exclude_transformation transformation_input__transformation=exclude_transformation
) )
transformation_reserved_qty = transformation_reservations.aggregate(total=Sum('quantity'))['total'] or Decimal('0') transformation_reserved_qty = transformation_reservations.aggregate(total=Sum('quantity_base'))['total'] or Decimal('0')
# Исключаем резервы трансформации из общего расчета резервов # Исключаем резервы трансформации из общего расчета резервов
reservation_filter = reservation_filter.exclude(transformation_input__transformation=exclude_transformation) reservation_filter = reservation_filter.exclude(transformation_input__transformation=exclude_transformation)
if exclude_order: if exclude_order:
reservation_filter = reservation_filter.exclude(order_item__order=exclude_order) reservation_filter = reservation_filter.exclude(order_item__order=exclude_order)
total_reserved = reservation_filter.aggregate(total=Sum('quantity'))['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) batches = StockBatchManager.get_batches_for_fifo(product, warehouse)

View File

@@ -401,7 +401,8 @@ class ShowcaseManager:
) )
if not created: if not created:
reservation.quantity = (reservation.quantity or Decimal('0')) + quantity_per_item reservation.quantity = (reservation.quantity or Decimal('0')) + quantity_per_item
reservation.save(update_fields=['quantity']) reservation.quantity_base = (reservation.quantity_base or Decimal('0')) + quantity_per_item
reservation.save(update_fields=['quantity', 'quantity_base'])
return { return {
'success': True, 'success': True,
@@ -479,7 +480,8 @@ class ShowcaseManager:
if new_qty > 0: if new_qty > 0:
res.quantity = new_qty res.quantity = new_qty
res.save(update_fields=['quantity']) res.quantity_base = new_qty
res.save(update_fields=['quantity', 'quantity_base'])
released_amount = quantity_per_item released_amount = quantity_per_item
else: else:
# Полностью освобождаем резерв # Полностью освобождаем резерв

View File

@@ -251,7 +251,7 @@ class TransformationService:
status='reserved' status='reserved'
).exclude( ).exclude(
transformation_input__transformation=transformation transformation_input__transformation=transformation
).aggregate(total=models.Sum('quantity'))['total'] or Decimal('0') ).aggregate(total=models.Sum('quantity_base'))['total'] or Decimal('0')
available = stock.quantity_available - reserved_qty available = stock.quantity_available - reserved_qty
if trans_input.quantity > available: if trans_input.quantity > available:

View File

@@ -173,7 +173,8 @@ class WriteOffDocumentService:
# Обновляем резерв # Обновляем резерв
if item.reservation: if item.reservation:
item.reservation.quantity = quantity item.reservation.quantity = quantity
item.reservation.save(update_fields=['quantity']) item.reservation.quantity_base = quantity
item.reservation.save(update_fields=['quantity', 'quantity_base'])
item.quantity = quantity item.quantity = quantity

View File

@@ -1078,11 +1078,16 @@ def update_reservation_on_item_change(sender, instance, created, **kwargs):
# Обычный товар - один резерв, обновляем количество напрямую # Обычный товар - один резерв, обновляем количество напрямую
reservation = reservations.first() reservation = reservations.first()
old_quantity = reservation.quantity old_quantity = reservation.quantity
# Обновляем quantity и quantity_base
reservation.quantity = Decimal(str(instance.quantity)) reservation.quantity = Decimal(str(instance.quantity))
reservation.save(update_fields=['quantity']) reservation.quantity_base = instance.quantity_in_base_units or Decimal(str(instance.quantity))
reservation.sales_unit = instance.sales_unit
reservation.save(update_fields=['quantity', 'quantity_base', 'sales_unit'])
logger.info( logger.info(
f"✓ Резерв #{reservation.id} обновлён: quantity {old_quantity}{reservation.quantity} " f"✓ Резерв #{reservation.id} обновлён: quantity {old_quantity}{reservation.quantity}, "
f"quantity_base → {reservation.quantity_base} "
f"(статус: {reservation.status}, OrderItem #{instance.id}, заказ {instance.order.order_number})" f"(статус: {reservation.status}, OrderItem #{instance.id}, заказ {instance.order.order_number})"
) )
@@ -1104,8 +1109,10 @@ def update_reservation_on_item_change(sender, instance, created, **kwargs):
expected_qty = product_quantities.get(reservation.product_id, Decimal('0')) expected_qty = product_quantities.get(reservation.product_id, Decimal('0'))
if expected_qty > 0: if expected_qty > 0:
old_quantity = reservation.quantity old_quantity = reservation.quantity
# Компоненты комплекта всегда в базовых единицах
reservation.quantity = expected_qty reservation.quantity = expected_qty
reservation.save(update_fields=['quantity']) reservation.quantity_base = expected_qty
reservation.save(update_fields=['quantity', 'quantity_base'])
logger.info( logger.info(
f"✓ Резерв #{reservation.id} ({reservation.product.name}) обновлён: " f"✓ Резерв #{reservation.id} ({reservation.product.name}) обновлён: "