Files
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

178 lines
5.5 KiB
Python
Raw Permalink 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 django.db import models
from django.core.exceptions import ValidationError
class BaseDiscount(models.Model):
"""
Абстрактный базовый класс для всех типов скидок.
Содержит общие поля и логику валидации.
"""
DISCOUNT_TYPE_CHOICES = [
('percentage', 'Процент'),
('fixed_amount', 'Фиксированная сумма'),
]
SCOPE_CHOICES = [
('order', 'На весь заказ'),
('product', 'На товар'),
('category', 'На категорию товаров'),
]
COMBINE_MODE_CHOICES = [
('stack', 'Складывать (суммировать)'),
('max_only', 'Только максимум'),
('exclusive', 'Исключающая (отменяет остальные)'),
]
name = models.CharField(
max_length=200,
verbose_name="Название скидки"
)
description = models.TextField(
blank=True,
verbose_name="Описание"
)
discount_type = models.CharField(
max_length=20,
choices=DISCOUNT_TYPE_CHOICES,
verbose_name="Тип скидки"
)
value = models.DecimalField(
max_digits=10,
decimal_places=2,
verbose_name="Значение",
help_text="Процент (0-100) или сумма в рублях"
)
scope = models.CharField(
max_length=20,
choices=SCOPE_CHOICES,
default='order',
verbose_name="Уровень применения"
)
is_active = models.BooleanField(
default=True,
verbose_name="Активна",
db_index=True
)
start_date = models.DateTimeField(
null=True,
blank=True,
verbose_name="Дата начала действия"
)
end_date = models.DateTimeField(
null=True,
blank=True,
verbose_name="Дата окончания действия"
)
max_usage_count = models.PositiveIntegerField(
null=True,
blank=True,
verbose_name="Макс. количество использований",
help_text="Оставьте пустым для безлимитного использования"
)
current_usage_count = models.PositiveIntegerField(
default=0,
verbose_name="Текущее количество использований"
)
priority = models.PositiveIntegerField(
default=0,
verbose_name="Приоритет",
help_text="Более высокий приоритет применяется первым"
)
created_at = models.DateTimeField(
auto_now_add=True,
verbose_name="Дата создания"
)
created_by = models.ForeignKey(
'accounts.CustomUser',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='created_discounts',
verbose_name="Создал"
)
class Meta:
abstract = True
ordering = ['-priority', '-created_at']
indexes = [
models.Index(fields=['is_active']),
models.Index(fields=['scope']),
models.Index(fields=['discount_type']),
]
def __str__(self):
if self.discount_type == 'percentage':
return f"{self.name} ({self.value}%)"
return f"{self.name} (-{self.value} руб.)"
def clean(self):
"""Валидация значений скидки"""
if self.discount_type == 'percentage':
if self.value < 0 or self.value > 100:
raise ValidationError({
'value': 'Процентная скидка должна быть от 0 до 100'
})
elif self.discount_type == 'fixed_amount':
if self.value < 0:
raise ValidationError({
'value': 'Фиксированная скидка не может быть отрицательной'
})
if self.start_date and self.end_date and self.start_date > self.end_date:
raise ValidationError({
'end_date': 'Дата окончания не может быть раньше даты начала'
})
def calculate_discount_amount(self, base_amount):
"""
Вычислить сумму скидки для заданной базовой суммы.
Args:
base_amount: Десятичное число - базовая сумма
Returns:
Decimal: Сумма скидки (не может превышать base_amount)
"""
if self.discount_type == 'percentage':
return base_amount * self.value / 100
else: # fixed_amount
return min(self.value, base_amount) # Скидка не может превышать сумму
def is_valid_now(self):
"""
Проверить, что скидка активна в текущий момент времени.
Returns:
bool: True если скидка активна
"""
from django.utils import timezone
if not self.is_active:
return False
now = timezone.now()
if self.start_date and now < self.start_date:
return False
if self.end_date and now > self.end_date:
return False
if self.max_usage_count and self.current_usage_count >= self.max_usage_count:
return False
return True