fix: Улучшения системы ценообразования комплектов
Исправлены 4 проблемы: 1. Расчёт цены первого товара - улучшена валидация в getProductPrice и calculateFinalPrice 2. Отображение actual_price в Select2 вместо обычной цены 3. Количество по умолчанию = 1 для новых форм компонентов 4. Auto-select текста при клике на поле количества для удобства редактирования Изменённые файлы: - products/forms.py: добавлен __init__ в KitItemForm для quantity.initial = 1 - products/templates/includes/select2-product-init.html: обновлена formatSelectResult - products/templates/productkit_create.html: добавлен focus handler для auto-select - products/templates/productkit_edit.html: добавлен focus handler для auto-select 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
231
myproject/products/services/kit_pricing.py
Normal file
231
myproject/products/services/kit_pricing.py
Normal file
@@ -0,0 +1,231 @@
|
||||
"""
|
||||
Сервисы для расчета цен комплектов (ProductKit).
|
||||
Извлекает сложную бизнес-логику из модели.
|
||||
"""
|
||||
from decimal import Decimal, InvalidOperation
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class KitPriceCalculator:
|
||||
"""
|
||||
Калькулятор цен для ProductKit.
|
||||
Реализует различные методы ценообразования комплектов.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def calculate_price_with_substitutions(kit, stock_manager=None):
|
||||
"""
|
||||
Расчёт цены комплекта с учётом доступных замен компонентов.
|
||||
|
||||
Метод определяет цену комплекта, учитывая доступные товары-заменители
|
||||
и применяет выбранный метод ценообразования.
|
||||
|
||||
Args:
|
||||
kit (ProductKit): Комплект для расчета
|
||||
stock_manager: Объект управления складом (если не указан, используется стандартный)
|
||||
|
||||
Returns:
|
||||
Decimal: Расчетная цена комплекта, или 0 в случае ошибки
|
||||
"""
|
||||
from ..utils.stock_manager import StockManager
|
||||
|
||||
if stock_manager is None:
|
||||
stock_manager = StockManager()
|
||||
|
||||
# Если указана ручная цена, используем её
|
||||
if kit.pricing_method == 'manual' and kit.price:
|
||||
return kit.price
|
||||
|
||||
total_cost = Decimal('0.00')
|
||||
total_sale = Decimal('0.00')
|
||||
|
||||
for kit_item in kit.kit_items.select_related('product', 'variant_group'):
|
||||
try:
|
||||
best_product = kit_item.get_best_available_product(stock_manager)
|
||||
|
||||
if not best_product:
|
||||
# Если товар недоступен, используем цену первого в списке
|
||||
available_products = kit_item.get_available_products()
|
||||
best_product = available_products[0] if available_products else None
|
||||
|
||||
if best_product:
|
||||
item_cost = best_product.cost_price
|
||||
item_price = best_product.price
|
||||
item_quantity = kit_item.quantity or Decimal('1.00')
|
||||
|
||||
# Проверяем корректность значений перед умножением
|
||||
if item_cost and item_quantity:
|
||||
total_cost += item_cost * item_quantity
|
||||
if item_price and item_quantity:
|
||||
total_sale += item_price * item_quantity
|
||||
except (AttributeError, TypeError, InvalidOperation) as e:
|
||||
# Логируем ошибку, но продолжаем вычисления
|
||||
logger.warning(
|
||||
f"Ошибка при расчёте цены для комплекта {kit.name} (item: {kit_item}): {e}"
|
||||
)
|
||||
continue # Пропускаем ошибочный элемент и продолжаем с остальными
|
||||
|
||||
# Применяем метод ценообразования
|
||||
try:
|
||||
if kit.pricing_method == 'from_sale_prices':
|
||||
return total_sale
|
||||
elif kit.pricing_method == 'from_cost_plus_percent' and kit.markup_percent is not None:
|
||||
return total_cost * (Decimal('1') + kit.markup_percent / Decimal('100'))
|
||||
elif kit.pricing_method == 'from_cost_plus_amount' and kit.markup_amount is not None:
|
||||
return total_cost + kit.markup_amount
|
||||
elif kit.pricing_method == 'manual' and kit.price:
|
||||
return kit.price
|
||||
|
||||
return total_sale
|
||||
except (TypeError, InvalidOperation) as e:
|
||||
logger.error(
|
||||
f"Ошибка при применении метода ценообразования для комплекта {kit.name}: {e}"
|
||||
)
|
||||
# Возвращаем ручную цену если есть, иначе 0
|
||||
if kit.pricing_method == 'manual' and kit.price:
|
||||
return kit.price
|
||||
return Decimal('0.00')
|
||||
|
||||
|
||||
class KitCostCalculator:
|
||||
"""
|
||||
Калькулятор себестоимости для ProductKit.
|
||||
Включает расчет и валидацию себестоимости комплекта.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def calculate_cost(kit):
|
||||
"""
|
||||
Расчёт себестоимости комплекта на основе себестоимости компонентов.
|
||||
|
||||
Args:
|
||||
kit (ProductKit): Комплект для расчета
|
||||
|
||||
Returns:
|
||||
Decimal: Себестоимость комплекта (может быть 0 если есть проблемы)
|
||||
"""
|
||||
total_cost = Decimal('0.00')
|
||||
|
||||
for kit_item in kit.kit_items.select_related('product', 'variant_group'):
|
||||
# Получаем продукт - либо конкретный, либо первый из группы вариантов
|
||||
product = kit_item.product
|
||||
if not product and kit_item.variant_group:
|
||||
# Берем первый продукт из группы вариантов
|
||||
product = kit_item.variant_group.products.filter(is_active=True).first()
|
||||
|
||||
if product and product.cost_price:
|
||||
item_cost = product.cost_price
|
||||
item_quantity = kit_item.quantity or Decimal('1.00')
|
||||
total_cost += item_cost * item_quantity
|
||||
|
||||
return total_cost
|
||||
|
||||
@staticmethod
|
||||
def validate_and_calculate_cost(kit):
|
||||
"""
|
||||
Расчёт себестоимости с полной валидацией.
|
||||
Проверяет, что все компоненты имеют себестоимость > 0.
|
||||
|
||||
Args:
|
||||
kit (ProductKit): Комплект для валидации и расчета
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
'total_cost': Decimal or None,
|
||||
'is_valid': bool,
|
||||
'problems': list of dicts {
|
||||
'component_name': str,
|
||||
'reason': str,
|
||||
'kit_item_id': int
|
||||
}
|
||||
}
|
||||
"""
|
||||
total_cost = Decimal('0.00')
|
||||
problems = []
|
||||
|
||||
if not kit.kit_items.exists():
|
||||
# Комплект без компонентов не может иметь корректную себестоимость
|
||||
return {
|
||||
'total_cost': None,
|
||||
'is_valid': False,
|
||||
'problems': [{
|
||||
'component_name': 'Комплект',
|
||||
'reason': 'Комплект не содержит компонентов'
|
||||
}]
|
||||
}
|
||||
|
||||
for kit_item in kit.kit_items.select_related('product', 'variant_group'):
|
||||
# Получаем продукт
|
||||
product = kit_item.product
|
||||
product_name = ''
|
||||
|
||||
if not product and kit_item.variant_group:
|
||||
# Берем первый активный продукт из группы вариантов
|
||||
product = kit_item.variant_group.products.filter(is_active=True).first()
|
||||
if kit_item.variant_group:
|
||||
product_name = f"[Варианты] {kit_item.variant_group.name}"
|
||||
|
||||
if not product:
|
||||
# Товар не найден или группа вариантов пуста
|
||||
if kit_item.variant_group:
|
||||
problems.append({
|
||||
'component_name': f"[Варианты] {kit_item.variant_group.name}",
|
||||
'reason': 'Группа не содержит активных товаров',
|
||||
'kit_item_id': kit_item.id
|
||||
})
|
||||
else:
|
||||
problems.append({
|
||||
'component_name': 'Неизвестный компонент',
|
||||
'reason': 'Товар не выбран и нет группы вариантов',
|
||||
'kit_item_id': kit_item.id
|
||||
})
|
||||
continue
|
||||
|
||||
# Используем имя товара, если не установили выше
|
||||
if not product_name:
|
||||
product_name = product.name
|
||||
|
||||
# Проверяем наличие себестоимости
|
||||
if product.cost_price is None:
|
||||
problems.append({
|
||||
'component_name': product_name,
|
||||
'reason': 'Себестоимость не определена',
|
||||
'kit_item_id': kit_item.id
|
||||
})
|
||||
continue
|
||||
|
||||
# Проверяем, что себестоимость > 0
|
||||
if product.cost_price == Decimal('0.00') or product.cost_price <= 0:
|
||||
problems.append({
|
||||
'component_name': product_name,
|
||||
'reason': 'Себестоимость равна 0',
|
||||
'kit_item_id': kit_item.id
|
||||
})
|
||||
continue
|
||||
|
||||
# Если всё OK - добавляем в сумму
|
||||
try:
|
||||
item_quantity = kit_item.quantity or Decimal('1.00')
|
||||
if item_quantity > 0:
|
||||
total_cost += product.cost_price * item_quantity
|
||||
except (TypeError, InvalidOperation) as e:
|
||||
logger.warning(
|
||||
f"Ошибка при расчете себестоимости компонента {product_name} "
|
||||
f"комплекта {kit.name}: {e}"
|
||||
)
|
||||
problems.append({
|
||||
'component_name': product_name,
|
||||
'reason': 'Ошибка при расчете',
|
||||
'kit_item_id': kit_item.id
|
||||
})
|
||||
|
||||
# Если есть проблемы, себестоимость не валидна
|
||||
is_valid = len(problems) == 0
|
||||
|
||||
return {
|
||||
'total_cost': total_cost if is_valid else None,
|
||||
'is_valid': is_valid,
|
||||
'problems': problems
|
||||
}
|
||||
Reference in New Issue
Block a user