Исправлено: резервирование и списание с учетом единиц продажи

- Проблема: при заказе 1 ветки резервировался 1 банч вместо 1/15

- Решение: используем quantity_in_base_units из OrderItem

- Изменения:

  - signals.py: reserve_stock_on_order_create использует quantity_in_base_units

  - signals.py: _create_or_update_reservation сохраняет sales_unit

  - signals.py: create_sale_on_order_completion берет quantity из резерва

  - sale_processor.py: уточнена документация параметра quantity
This commit is contained in:
2026-01-02 13:45:22 +03:00
parent 0d801680d7
commit c5e1ea06f9
2 changed files with 53 additions and 19 deletions

View File

@@ -61,19 +61,19 @@ class SaleProcessor:
Процесс: Процесс:
1. Создаем запись Sale 1. Создаем запись Sale
2. Конвертируем количество в базовые единицы (если указана sales_unit) 2. Списываем товар по FIFO из партий
3. Списываем товар по FIFO из партий 3. Фиксируем распределение в SaleBatchAllocation для аудита
4. Фиксируем распределение в SaleBatchAllocation для аудита
Args: Args:
product: объект Product product: объект Product
warehouse: объект Warehouse warehouse: объект Warehouse
quantity: Decimal - количество товара (в единицах продажи, если указана sales_unit) quantity: Decimal - количество товара В БАЗОВЫХ ЕДИНИЦАХ.
Для списания со склада всегда используются базовые единицы.
sale_price: Decimal - цена продажи sale_price: Decimal - цена продажи
order: (опционально) объект Order order: (опционально) объект Order
document_number: (опционально) номер документа document_number: (опционально) номер документа
sales_unit: (опционально) объект ProductSalesUnit - единица продажи. sales_unit: (опционально) объект ProductSalesUnit - единица продажи.
Если указана, quantity конвертируется в базовые единицы товара. Используется ТОЛЬКО для сохранения снимка (не для конверсии).
Returns: Returns:
Объект Sale Объект Sale
@@ -87,13 +87,9 @@ class SaleProcessor:
if sale_price < 0: if sale_price < 0:
raise ValueError("Цена продажи не может быть отрицательной") raise ValueError("Цена продажи не может быть отрицательной")
# Конвертируем количество в базовые единицы, если указана единица продажи # quantity УЖЕ в базовых единицах, конверсия не нужна
if sales_unit:
quantity_base = sales_unit.convert_to_base(quantity)
unit_name_snapshot = sales_unit.name
else:
quantity_base = quantity quantity_base = quantity
unit_name_snapshot = '' unit_name_snapshot = sales_unit.name if sales_unit else ''
# Создаем запись Sale # Создаем запись Sale
# ВАЖНО: Устанавливаем processed=True сразу, чтобы сигнал process_sale_fifo не сработал # ВАЖНО: Устанавливаем processed=True сразу, чтобы сигнал process_sale_fifo не сработал
@@ -101,7 +97,7 @@ class SaleProcessor:
sale = Sale.objects.create( sale = Sale.objects.create(
product=product, product=product,
warehouse=warehouse, warehouse=warehouse,
quantity=quantity, # В единицах продажи (для истории/отчётов) quantity=quantity_base, # В базовых единицах (для истории/отчётов)
quantity_base=quantity_base, # В базовых единицах (для списания) quantity_base=quantity_base, # В базовых единицах (для списания)
sale_price=sale_price, sale_price=sale_price,
order=order, order=order,

View File

@@ -149,8 +149,16 @@ def reserve_stock_on_order_create(sender, instance, created, **kwargs):
# Для каждого товара в заказе # Для каждого товара в заказе
for item in instance.items.all(): for item in instance.items.all():
if item.product: if item.product:
# Обычный товар - резервируем как раньше # Обычный товар - резервируем с учетом единиц продажи
_create_or_update_reservation(item, item.product, warehouse, Decimal(str(item.quantity))) # Используем quantity_in_base_units если заполнено, иначе quantity
reservation_quantity = item.quantity_in_base_units if item.quantity_in_base_units else Decimal(str(item.quantity))
_create_or_update_reservation(
item,
item.product,
warehouse,
reservation_quantity,
sales_unit=item.sales_unit
)
elif item.product_kit and item.kit_snapshot: elif item.product_kit and item.kit_snapshot:
# Комплект - резервируем КОМПОНЕНТЫ из снимка # Комплект - резервируем КОМПОНЕНТЫ из снимка
@@ -173,9 +181,17 @@ def reserve_stock_on_order_create(sender, instance, created, **kwargs):
) )
def _create_or_update_reservation(order_item, product, warehouse, quantity, product_kit=None): def _create_or_update_reservation(order_item, product, warehouse, quantity, product_kit=None, sales_unit=None):
""" """
Вспомогательная функция для создания или обновления резерва. Вспомогательная функция для создания или обновления резерва.
Args:
order_item: Позиция заказа
product: Товар
warehouse: Склад
quantity: Количество (в базовых единицах)
product_kit: Комплект (для резервов компонентов)
sales_unit: Единица продажи (опционально)
""" """
# Формируем фильтр для поиска существующего резерва # Формируем фильтр для поиска существующего резерва
filter_kwargs = { filter_kwargs = {
@@ -191,6 +207,8 @@ def _create_or_update_reservation(order_item, product, warehouse, quantity, prod
if existing_reservation: if existing_reservation:
# Резерв уже существует - обновляем его # Резерв уже существует - обновляем его
existing_reservation.quantity = quantity existing_reservation.quantity = quantity
existing_reservation.quantity_base = quantity # quantity уже в базовых единицах
existing_reservation.sales_unit = sales_unit
existing_reservation.status = 'reserved' existing_reservation.status = 'reserved'
existing_reservation.save() existing_reservation.save()
else: else:
@@ -201,6 +219,8 @@ def _create_or_update_reservation(order_item, product, warehouse, quantity, prod
product_kit=product_kit, product_kit=product_kit,
warehouse=warehouse, warehouse=warehouse,
quantity=quantity, quantity=quantity,
quantity_base=quantity, # quantity уже в базовых единицах
sales_unit=sales_unit,
status='reserved' status='reserved'
) )
@@ -377,17 +397,35 @@ def create_sale_on_order_completion(sender, instance, created, **kwargs):
continue continue
try: try:
# Находим резерв для этого OrderItem
item_reservation = Reservation.objects.filter(
order_item=item,
product=product
).exclude(status='converted_to_sale').first()
if item_reservation:
# Используем quantity из резерва (уже в базовых единицах)
sale_quantity = item_reservation.quantity
else:
# Fallback: используем quantity_in_base_units из OrderItem
sale_quantity = item.quantity_in_base_units if item.quantity_in_base_units else Decimal(str(item.quantity))
logger.warning(
f"Не найден резерв для OrderItem {item.id}. "
f"Используем quantity_in_base_units: {sale_quantity}"
)
# Создаем Sale (с автоматическим FIFO-списанием) # Создаем Sale (с автоматическим FIFO-списанием)
sale = SaleProcessor.create_sale( sale = SaleProcessor.create_sale(
product=product, product=product,
warehouse=warehouse, warehouse=warehouse,
quantity=Decimal(str(item.quantity)), quantity=sale_quantity,
sale_price=Decimal(str(item.price)), sale_price=Decimal(str(item.price)),
order=instance, order=instance,
document_number=instance.order_number document_number=instance.order_number,
sales_unit=item.sales_unit # Передаем sales_unit в Sale
) )
sales_created.append(sale) sales_created.append(sale)
logger.info(f"✓ Sale создан для {product.name}: {item.quantity} шт.") logger.info(f"✓ Sale создан для {product.name}: {sale_quantity} шт. (базовых единиц)")
except ValueError as e: except ValueError as e:
# Логируем ошибку и прерываем процесс # Логируем ошибку и прерываем процесс