""" Сервисы для расчета цен комплектов (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(status='active').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(status='active').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 }