Files
octopus/myproject/discounts/services/applier.py
Andrey Smakotin f57e639dbe feat(discounts): добавлено комбинирование скидок по режимам
Добавлено поле combine_mode с тремя режимами:
- stack - складывать с другими скидками
- max_only - применять только максимальную
- exclusive - отменяет все остальные скидки

Изменения:
- Модель Discount: добавлено поле combine_mode
- Calculator: новый класс DiscountCombiner, методы возвращают списки скидок
- Applier: создание нескольких DiscountApplication записей
- Admin: отображение combine_mode с иконками
- POS API: возвращает списки применённых скидок
- POS UI: отображение нескольких скидок с иконками режимов

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 12:56:38 +03:00

314 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from decimal import Decimal
from django.db import transaction
from django.core.exceptions import ValidationError
class DiscountApplier:
"""
Сервис для применения скидок к заказам.
Поддерживает комбинирование скидок по combine_mode.
Все операции атомарны.
"""
@staticmethod
@transaction.atomic
def apply_promo_code(order, promo_code, user=None):
"""
Применить промокод к заказу.
Поддерживает комбинирование скидок.
Args:
order: Order
promo_code: str
user: CustomUser (применивший скидку)
Returns:
dict: {
'success': bool,
'discounts': [{'discount': Discount, 'amount': Decimal}, ...],
'total_amount': Decimal,
'error': str
}
"""
from discounts.models import PromoCode, DiscountApplication
from discounts.services.calculator import DiscountCalculator
# Удаляем предыдущую скидку на заказ
if order.applied_promo_code:
DiscountApplier._remove_order_discount_only(order)
# Рассчитываем скидку
result = DiscountCalculator.calculate_order_discount(order, promo_code)
if result['error']:
return {
'success': False,
'error': result['error']
}
promo = result['promo_code']
discounts_data = result['discounts']
total_amount = result['total_amount']
# Применяем первую скидку в applied_discount (для обратной совместимости с Order)
if discounts_data:
first_discount = discounts_data[0]['discount']
order.applied_discount = first_discount
order.applied_promo_code = promo.code
order.discount_amount = total_amount
order.save(update_fields=['applied_discount', 'applied_promo_code', 'discount_amount'])
# Пересчитываем total_amount
order.calculate_total()
# Регистрируем использование промокода
promo.record_usage(order.customer)
# Создаем записи о применении для каждой скидки
for disc_data in discounts_data:
discount = disc_data['discount']
amount = disc_data['amount']
DiscountApplication.objects.create(
order=order,
discount=discount,
promo_code=promo,
target='order',
base_amount=order.subtotal,
discount_amount=amount,
final_amount=order.subtotal - amount,
customer=order.customer,
applied_by=user or order.modified_by
)
# Увеличиваем счетчик использований скидки
discount.current_usage_count += 1
discount.save(update_fields=['current_usage_count'])
return {
'success': True,
'discounts': discounts_data,
'total_amount': total_amount
}
@staticmethod
@transaction.atomic
def apply_auto_discounts(order, user=None):
"""
Применить автоматические скидки к заказу и позициям.
Поддерживает комбинирование скидок.
Args:
order: Order
user: CustomUser
Returns:
dict: {
'order_discounts': [...],
'item_discounts': [...],
'total_discount': Decimal
}
"""
from discounts.models import Discount, DiscountApplication
from discounts.services.calculator import DiscountCalculator
result = {
'order_discounts': [],
'item_discounts': [],
'total_discount': Decimal('0')
}
# 1. Применяем скидки на заказ (может быть несколько)
order_result = DiscountCalculator.calculate_order_discount(order)
if order_result['discounts'] and not order_result['error']:
total_order_amount = order_result['total_amount']
# Сохраняем первую скидку в applied_discount (для совместимости)
first_discount_data = order_result['discounts'][0]
order.applied_discount = first_discount_data['discount']
order.discount_amount = total_order_amount
order.save(update_fields=['applied_discount', 'discount_amount'])
# Создаем записи о применении для всех скидок
for disc_data in order_result['discounts']:
discount = disc_data['discount']
amount = disc_data['amount']
DiscountApplication.objects.create(
order=order,
discount=discount,
target='order',
base_amount=order.subtotal,
discount_amount=amount,
final_amount=order.subtotal - amount,
customer=order.customer,
applied_by=user
)
# Увеличиваем счетчик
discount.current_usage_count += 1
discount.save(update_fields=['current_usage_count'])
result['order_discounts'] = order_result['discounts']
result['total_discount'] += total_order_amount
# 2. Применяем скидки на позиции
available_product_discounts = list(DiscountCalculator.get_available_discounts(
scope='product',
auto_only=True
))
for item in order.items.all():
item_result = DiscountCalculator.calculate_item_discount(
item, available_product_discounts
)
if item_result['discounts']:
total_item_amount = item_result['total_amount']
# Сохраняем первую скидку в applied_discount (для совместимости)
first_discount_data = item_result['discounts'][0]
item.applied_discount = first_discount_data['discount']
item.discount_amount = total_item_amount
item.save(update_fields=['applied_discount', 'discount_amount'])
# Создаем записи о применении для всех скидок
base_amount = item.price * item.quantity
for disc_data in item_result['discounts']:
discount = disc_data['discount']
amount = disc_data['amount']
DiscountApplication.objects.create(
order=order,
order_item=item,
discount=discount,
target='order_item',
base_amount=base_amount,
discount_amount=amount,
final_amount=item.get_total_price(),
customer=order.customer,
applied_by=user
)
# Увеличиваем счетчик
discount.current_usage_count += 1
discount.save(update_fields=['current_usage_count'])
result['item_discounts'].append({
'item': item,
'discounts': item_result['discounts'],
'total_amount': total_item_amount
})
result['total_discount'] += total_item_amount
# Пересчитываем итоговую сумму
order.calculate_total()
return result
@staticmethod
@transaction.atomic
def remove_discount_from_order(order):
"""
Удалить скидку с заказа.
Args:
order: Order
"""
DiscountApplier._remove_order_discount_only(order)
# Удаляем скидки с позиций
order.items.update(
applied_discount=None,
discount_amount=Decimal('0')
)
# Удаляем записи о применении
from discounts.models import DiscountApplication
DiscountApplication.objects.filter(order=order).delete()
# Пересчитываем
order.calculate_total()
@staticmethod
@transaction.atomic
def apply_manual_discount(order, discount, user=None):
"""
Применить скидку вручную к заказу.
Args:
order: Order
discount: Discount
user: CustomUser (применивший скидку)
Returns:
dict: {
'success': bool,
'discount_amount': Decimal,
'error': str
}
"""
from discounts.models import DiscountApplication
# Проверяем scope скидки
if discount.scope != 'order':
return {'success': False, 'error': 'Эта скидка не применяется к заказу'}
# Проверяем мин. сумму
if discount.min_order_amount and order.subtotal < discount.min_order_amount:
return {'success': False, 'error': f'Мин. сумма заказа: {discount.min_order_amount} руб.'}
# Удаляем предыдущую скидку на заказ
DiscountApplier._remove_order_discount_only(order)
# Рассчитываем сумму
discount_amount = discount.calculate_discount_amount(Decimal(order.subtotal))
# Применяем к заказу
order.applied_discount = discount
order.discount_amount = discount_amount
order.save(update_fields=['applied_discount', 'discount_amount'])
# Пересчитываем total_amount
order.calculate_total()
# Создаем запись о применении
DiscountApplication.objects.create(
order=order,
discount=discount,
target='order',
base_amount=order.subtotal,
discount_amount=discount_amount,
final_amount=order.subtotal - discount_amount,
customer=order.customer,
applied_by=user
)
# Увеличиваем счетчик использований скидки
discount.current_usage_count += 1
discount.save(update_fields=['current_usage_count'])
return {
'success': True,
'discount_amount': discount_amount
}
@staticmethod
def _remove_order_discount_only(order):
"""
Удалить только скидку с заказа (не трогая позиции).
Args:
order: Order
"""
from discounts.models import DiscountApplication
# Удаляем записи о применении скидок к заказу
DiscountApplication.objects.filter(order=order, target='order').delete()
order.applied_discount = None
order.applied_promo_code = None
order.discount_amount = Decimal('0')
order.save(update_fields=['applied_discount', 'applied_promo_code', 'discount_amount'])