diff --git a/myproject/inventory/services/sale_processor.py b/myproject/inventory/services/sale_processor.py index 968c51d..190bbb9 100644 --- a/myproject/inventory/services/sale_processor.py +++ b/myproject/inventory/services/sale_processor.py @@ -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_base = quantity - unit_name_snapshot = '' + # quantity УЖЕ в базовых единицах, конверсия не нужна + quantity_base = quantity + 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, diff --git a/myproject/inventory/signals.py b/myproject/inventory/signals.py index d0ecd57..ce3a962 100644 --- a/myproject/inventory/signals.py +++ b/myproject/inventory/signals.py @@ -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: # Логируем ошибку и прерываем процесс