feat: Add new inventory and POS components, including a script to reproduce a POS checkout sale price bug.

This commit is contained in:
2026-01-25 15:26:57 +03:00
parent 5a66d492c8
commit f75e861bb8
5 changed files with 410 additions and 217 deletions

View File

@@ -366,7 +366,7 @@ def create_sale_on_order_completion(sender, instance, created, **kwargs):
# === ЗАЩИТА ОТ ПРЕЖДЕВРЕМЕННОГО СОЗДАНИЯ SALE ===
# Проверяем, есть ли уже Sale для этого заказа
if Sale.objects.filter(order=instance).exists():
logger.info(f"Заказ {instance.order_number}: Sale уже существуют, пропускаем")
logger.info(f"Заказ {instance.order_number}: Sale уже существуют, пропускаем")
update_is_returned_flag(instance)
return
@@ -376,7 +376,7 @@ def create_sale_on_order_completion(sender, instance, created, **kwargs):
previous_status = getattr(instance, '_previous_status', None)
if previous_status and previous_status.is_positive_end:
logger.info(
f"🔄 Заказ {instance.order_number}: повторный переход в положительный статус "
f"Заказ {instance.order_number}: повторный переход в положительный статус "
f"({previous_status.name}{instance.status.name}). Проверяем Sale..."
)
if Sale.objects.filter(order=instance).exists():
@@ -454,13 +454,66 @@ def create_sale_on_order_completion(sender, instance, created, **kwargs):
)
continue
# === РАСЧЕТ ЦЕНЫ ===
# Рассчитываем фактическую стоимость продажи всего комплекта с учетом скидок
# 1. Базовая стоимость позиции
item_subtotal = Decimal(str(item.price)) * Decimal(str(item.quantity))
# 2. Скидки
item_discount = Decimal(str(item.discount_amount)) if item.discount_amount is not None else Decimal('0')
# Скидка на заказ (распределенная)
instance.refresh_from_db()
order_total = instance.subtotal if hasattr(instance, 'subtotal') else Decimal('0')
delivery_cost = Decimal(str(instance.delivery.cost)) if hasattr(instance, 'delivery') and instance.delivery else Decimal('0')
order_discount_amount = (order_total - (Decimal(str(instance.total_amount)) - delivery_cost)) if order_total > 0 else Decimal('0')
if order_total > 0 and order_discount_amount > 0:
item_order_discount = order_discount_amount * (item_subtotal / order_total)
else:
item_order_discount = Decimal('0')
kit_net_total = item_subtotal - item_discount - item_order_discount
if kit_net_total < 0:
kit_net_total = Decimal('0')
# 3. Суммарная каталожная стоимость всех компонентов (для пропорции)
total_catalog_price = Decimal('0')
for reservation in kit_reservations:
qty = reservation.quantity_base or reservation.quantity
price = reservation.product.actual_price or Decimal('0')
total_catalog_price += price * qty
# 4. Коэффициент распределения
if total_catalog_price > 0:
ratio = kit_net_total / total_catalog_price
else:
# Если каталожная цена 0, распределяем просто по количеству или 0
ratio = Decimal('0')
# Создаем Sale для каждого компонента комплекта
for reservation in kit_reservations:
try:
# Рассчитываем цену продажи компонента пропорционально цене комплекта
# Используем actual_price компонента как цену продажи
component_sale_price = reservation.product.actual_price
# Рассчитываем цену продажи компонента пропорционально
catalog_price = reservation.product.actual_price or Decimal('0')
if ratio > 0:
# Распределяем реальную выручку
component_sale_price = catalog_price * ratio
else:
# Если выручка 0 или каталожные цены 0
if total_catalog_price == 0 and kit_net_total > 0:
# Крайний случай: товаров на 0 руб, а продали за деньги (услуга?)
# Распределяем равномерно
count = kit_reservations.count()
component_qty = reservation.quantity_base or reservation.quantity
if count > 0 and component_qty > 0:
component_sale_price = (kit_net_total / count) / component_qty
else:
component_sale_price = Decimal('0')
else:
component_sale_price = Decimal('0')
sale = SaleProcessor.create_sale(
product=reservation.product,
warehouse=warehouse,
@@ -472,7 +525,8 @@ def create_sale_on_order_completion(sender, instance, created, **kwargs):
sales_created.append(sale)
logger.info(
f"✓ Sale создан для компонента комплекта '{kit.name}': "
f"{reservation.product.name} - {reservation.quantity_base or reservation.quantity} шт. (базовых единиц)"
f"{reservation.product.name} - {reservation.quantity_base or reservation.quantity} шт. "
f"(цена: {component_sale_price})"
)
except ValueError as e:
logger.error(
@@ -548,6 +602,21 @@ def create_sale_on_order_completion(sender, instance, created, **kwargs):
else:
base_price = price_with_discount
# LOGGING DEBUG INFO
# print(f"DEBUG SALE PRICE CALCULATION for Item #{item.id} ({product.name}):")
# print(f" Price: {item.price}, Qty: {item.quantity}, Subtotal: {item_subtotal}")
# print(f" Item Discount: {item_discount}, Order Discount Share: {item_order_discount}, Total Discount: {total_discount}")
# print(f" Price w/ Discount: {price_with_discount}")
# print(f" Sales Unit: {item.sales_unit}, Conversion: {item.conversion_factor_snapshot}")
# print(f" FINAL BASE PRICE: {base_price}")
# print(f" Sales Unit Object: {item.sales_unit}")
# if item.sales_unit:
# print(f" Sales Unit Conversion: {item.sales_unit.conversion_factor}")
logger.info(f"DEBUG SALE PRICE CALCULATION for Item #{item.id} ({product.name}):")
logger.info(f" Price: {item.price}, Qty: {item.quantity}, Subtotal: {item_subtotal}")
logger.info(f" FINAL BASE PRICE: {base_price}")
# Создаем Sale (с автоматическим FIFO-списанием)
sale = SaleProcessor.create_sale(
product=product,