Files
octopus/myproject/discounts/models/base.py
Andrey Smakotin 241625eba7 feat(discounts): добавлено приложение скидок
Создано новое Django приложение для управления скидками:

Модели:
- BaseDiscount: абстрактный базовый класс с общими полями
- Discount: основная модель скидки (процент/фикс, на заказ/товар/категорию)
- PromoCode: промокоды для активации скидок
- DiscountApplication: история применения скидок

Сервисы:
- DiscountCalculator: расчёт скидок для корзины и заказов
- DiscountApplier: применение скидок к заказам (атомарно)
- DiscountValidator: валидация промокодов и условий

Админ-панель:
- DiscountAdmin: управление скидками
- PromoCodeAdmin: управление промокодами
- DiscountApplicationAdmin: история применения (только чтение)

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

172 lines
5.3 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 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', 'На категорию товаров'),
]
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