Исправлено: резервирование и списание с учетом единиц продажи
- Проблема: при заказе 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
|
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,
|
||||||
|
|||||||
@@ -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:
|
||||||
# Логируем ошибку и прерываем процесс
|
# Логируем ошибку и прерываем процесс
|
||||||
|
|||||||
Reference in New Issue
Block a user