Исправлено: резервирование и списание с учетом единиц продажи
- Проблема: при заказе 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:
@@ -61,19 +61,19 @@ class SaleProcessor:
|
||||
|
||||
Процесс:
|
||||
1. Создаем запись Sale
|
||||
2. Конвертируем количество в базовые единицы (если указана sales_unit)
|
||||
3. Списываем товар по FIFO из партий
|
||||
4. Фиксируем распределение в SaleBatchAllocation для аудита
|
||||
2. Списываем товар по FIFO из партий
|
||||
3. Фиксируем распределение в SaleBatchAllocation для аудита
|
||||
|
||||
Args:
|
||||
product: объект Product
|
||||
warehouse: объект Warehouse
|
||||
quantity: Decimal - количество товара (в единицах продажи, если указана sales_unit)
|
||||
quantity: Decimal - количество товара В БАЗОВЫХ ЕДИНИЦАХ.
|
||||
Для списания со склада всегда используются базовые единицы.
|
||||
sale_price: Decimal - цена продажи
|
||||
order: (опционально) объект Order
|
||||
document_number: (опционально) номер документа
|
||||
sales_unit: (опционально) объект ProductSalesUnit - единица продажи.
|
||||
Если указана, quantity конвертируется в базовые единицы товара.
|
||||
Используется ТОЛЬКО для сохранения снимка (не для конверсии).
|
||||
|
||||
Returns:
|
||||
Объект Sale
|
||||
@@ -87,13 +87,9 @@ class SaleProcessor:
|
||||
if sale_price < 0:
|
||||
raise ValueError("Цена продажи не может быть отрицательной")
|
||||
|
||||
# Конвертируем количество в базовые единицы, если указана единица продажи
|
||||
if sales_unit:
|
||||
quantity_base = sales_unit.convert_to_base(quantity)
|
||||
unit_name_snapshot = sales_unit.name
|
||||
else:
|
||||
# quantity УЖЕ в базовых единицах, конверсия не нужна
|
||||
quantity_base = quantity
|
||||
unit_name_snapshot = ''
|
||||
unit_name_snapshot = sales_unit.name if sales_unit else ''
|
||||
|
||||
# Создаем запись Sale
|
||||
# ВАЖНО: Устанавливаем processed=True сразу, чтобы сигнал process_sale_fifo не сработал
|
||||
@@ -101,7 +97,7 @@ class SaleProcessor:
|
||||
sale = Sale.objects.create(
|
||||
product=product,
|
||||
warehouse=warehouse,
|
||||
quantity=quantity, # В единицах продажи (для истории/отчётов)
|
||||
quantity=quantity_base, # В базовых единицах (для истории/отчётов)
|
||||
quantity_base=quantity_base, # В базовых единицах (для списания)
|
||||
sale_price=sale_price,
|
||||
order=order,
|
||||
|
||||
@@ -149,8 +149,16 @@ def reserve_stock_on_order_create(sender, instance, created, **kwargs):
|
||||
# Для каждого товара в заказе
|
||||
for item in instance.items.all():
|
||||
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:
|
||||
# Комплект - резервируем КОМПОНЕНТЫ из снимка
|
||||
@@ -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 = {
|
||||
@@ -191,6 +207,8 @@ def _create_or_update_reservation(order_item, product, warehouse, quantity, prod
|
||||
if existing_reservation:
|
||||
# Резерв уже существует - обновляем его
|
||||
existing_reservation.quantity = quantity
|
||||
existing_reservation.quantity_base = quantity # quantity уже в базовых единицах
|
||||
existing_reservation.sales_unit = sales_unit
|
||||
existing_reservation.status = 'reserved'
|
||||
existing_reservation.save()
|
||||
else:
|
||||
@@ -201,6 +219,8 @@ def _create_or_update_reservation(order_item, product, warehouse, quantity, prod
|
||||
product_kit=product_kit,
|
||||
warehouse=warehouse,
|
||||
quantity=quantity,
|
||||
quantity_base=quantity, # quantity уже в базовых единицах
|
||||
sales_unit=sales_unit,
|
||||
status='reserved'
|
||||
)
|
||||
|
||||
@@ -377,17 +397,35 @@ def create_sale_on_order_completion(sender, instance, created, **kwargs):
|
||||
continue
|
||||
|
||||
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 = SaleProcessor.create_sale(
|
||||
product=product,
|
||||
warehouse=warehouse,
|
||||
quantity=Decimal(str(item.quantity)),
|
||||
quantity=sale_quantity,
|
||||
sale_price=Decimal(str(item.price)),
|
||||
order=instance,
|
||||
document_number=instance.order_number
|
||||
document_number=instance.order_number,
|
||||
sales_unit=item.sales_unit # Передаем sales_unit в 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:
|
||||
# Логируем ошибку и прерываем процесс
|
||||
|
||||
Reference in New Issue
Block a user