from decimal import Decimal from django.db import models from django.utils import timezone class DiscountCalculator: """ Калькулятор скидок для заказов. Рассчитывает применимые скидки и их суммы. """ @staticmethod def get_available_discounts(scope=None, customer=None, auto_only=False): """ Получить список доступных скидок. Args: scope: 'order', 'product', 'category' или None для всех customer: Customer для проверки условий auto_only: Только автоматические скидки Returns: QuerySet[Discount]: Активные скидки, отсортированные по приоритету """ from discounts.models import Discount now = timezone.now() qs = Discount.objects.filter(is_active=True) # Фильтр по scope if scope: qs = qs.filter(scope=scope) # Фильтр по auto if auto_only: qs = qs.filter(is_auto=True) # Фильтр по дате qs = qs.filter( models.Q(start_date__isnull=True) | models.Q(start_date__lte=now), models.Q(end_date__isnull=True) | models.Q(end_date__gte=now) ) # Фильтр по лимиту использований qs = qs.filter( models.Q(max_usage_count__isnull=True) | models.Q(current_usage_count__lt=models.F('max_usage_count')) ) return qs.order_by('-priority', '-created_at') @staticmethod def calculate_order_discount(order, promo_code=None): """ Рассчитать скидку на весь заказ. Args: order: Order объект promo_code: Строка промокода (опционально) Returns: dict: { 'discount': Discount или None, 'promo_code': PromoCode или None, 'discount_amount': Decimal, 'error': str или None } """ from discounts.models import PromoCode from discounts.services.validator import DiscountValidator subtotal = Decimal(str(order.subtotal)) result = { 'discount': None, 'promo_code': None, 'discount_amount': Decimal('0'), 'error': None } # 1. Проверяем промокод первым if promo_code: is_valid, promo, error = DiscountValidator.validate_promo_code( promo_code, order.customer, subtotal ) if not is_valid: result['error'] = error return result discount = promo.discount # Проверяем scope скидки if discount.scope != 'order': result['error'] = "Этот промокод применяется только к товарам" return result result['discount'] = discount result['promo_code'] = promo result['discount_amount'] = discount.calculate_discount_amount(subtotal) return result # 2. Если нет промокода, проверяем автоматические скидки auto_discounts = DiscountCalculator.get_available_discounts( scope='order', auto_only=True ) for discount in auto_discounts: # Проверяем мин. сумму заказа if discount.min_order_amount and subtotal < discount.min_order_amount: continue # Применяем первую подходящую автоматическую скидку result['discount'] = discount result['discount_amount'] = discount.calculate_discount_amount(subtotal) break return result @staticmethod def calculate_item_discount(order_item, available_discounts=None): """ Рассчитать скидку на позицию заказа. Args: order_item: OrderItem объект available_discounts: Предварительно полученный список скидок Returns: dict: { 'discount': Discount или None, 'discount_amount': Decimal, } """ result = { 'discount': None, 'discount_amount': Decimal('0') } # Определяем продукт product = None if order_item.product: product = order_item.product elif order_item.product_kit: product = order_item.product_kit if not product: return result base_amount = Decimal(str(order_item.price)) * Decimal(str(order_item.quantity)) if not available_discounts: available_discounts = DiscountCalculator.get_available_discounts( scope='product', auto_only=True ) for discount in available_discounts: # Проверяем, применяется ли скидка к этому товару if not discount.applies_to_product(product): continue result['discount'] = discount result['discount_amount'] = discount.calculate_discount_amount(base_amount) break # Проверяем скидки по категориям if not result['discount']: category_discounts = DiscountCalculator.get_available_discounts( scope='category', auto_only=True ) for discount in category_discounts: if discount.applies_to_product(product): result['discount'] = discount result['discount_amount'] = discount.calculate_discount_amount(base_amount) break return result @staticmethod def calculate_cart_discounts(cart_items, promo_code=None, customer=None): """ Рассчитать скидки для корзины (применяется в POS до создания заказа). Args: cart_items: Список словарей {'type': 'product'|'kit', 'id': int, 'quantity': Decimal, 'price': Decimal} promo_code: Промокод (опционально) customer: Customer (опционально) Returns: dict: { 'order_discount': {...}, # Как в calculate_order_discount 'item_discounts': [ {'cart_index': int, 'discount': Discount, 'discount_amount': Decimal}, ... ], 'total_discount': Decimal, 'final_total': Decimal } """ from products.models import Product, ProductKit cart_subtotal = Decimal('0') for item in cart_items: cart_subtotal += Decimal(str(item['price'])) * Decimal(str(item['quantity'])) # Создаем фейковый объект для расчета скидки на заказ class FakeOrder: def __init__(self, subtotal, customer): self.subtotal = subtotal self.customer = customer fake_order = FakeOrder(cart_subtotal, customer) # Скидка на заказ order_discount = DiscountCalculator.calculate_order_discount( fake_order, promo_code ) # Скидки на позиции item_discounts = [] items_total_discount = Decimal('0') available_product_discounts = list(DiscountCalculator.get_available_discounts( scope='product', auto_only=True )) available_category_discounts = list(DiscountCalculator.get_available_discounts( scope='category', auto_only=True )) for idx, item in enumerate(cart_items): # Загружаем продукт product = None if item.get('type') == 'product': try: product = Product.objects.get(id=item['id']) except Product.DoesNotExist: pass elif item.get('type') == 'kit': try: product = ProductKit.objects.get(id=item['id']) except ProductKit.DoesNotExist: pass if not product: continue base_amount = Decimal(str(item['price'])) * Decimal(str(item['quantity'])) # Проверяем скидки на товары applied_discount = None discount_amount = Decimal('0') for discount in available_product_discounts: if discount.applies_to_product(product): applied_discount = discount discount_amount = discount.calculate_discount_amount(base_amount) break # Если не нашли скидку на товар, проверяем категории if not applied_discount: for discount in available_category_discounts: if discount.applies_to_product(product): applied_discount = discount discount_amount = discount.calculate_discount_amount(base_amount) break if applied_discount: item_discounts.append({ 'cart_index': idx, 'discount': applied_discount, 'discount_amount': discount_amount }) items_total_discount += discount_amount total_discount = order_discount['discount_amount'] + items_total_discount final_total = max(cart_subtotal - total_discount, Decimal('0')) return { 'order_discount': order_discount, 'item_discounts': item_discounts, 'total_discount': total_discount, 'final_total': final_total }