Создано новое Django приложение для управления скидками: Модели: - BaseDiscount: абстрактный базовый класс с общими полями - Discount: основная модель скидки (процент/фикс, на заказ/товар/категорию) - PromoCode: промокоды для активации скидок - DiscountApplication: история применения скидок Сервисы: - DiscountCalculator: расчёт скидок для корзины и заказов - DiscountApplier: применение скидок к заказам (атомарно) - DiscountValidator: валидация промокодов и условий Админ-панель: - DiscountAdmin: управление скидками - PromoCodeAdmin: управление промокодами - DiscountApplicationAdmin: история применения (только чтение) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
148 lines
4.6 KiB
Python
148 lines
4.6 KiB
Python
from django.db import models
|
||
from django.core.exceptions import ValidationError
|
||
from django.utils import timezone
|
||
|
||
|
||
class PromoCode(models.Model):
|
||
"""
|
||
Промокод для активации скидки.
|
||
Связывает код с одной скидкой.
|
||
"""
|
||
|
||
code = models.CharField(
|
||
max_length=50,
|
||
unique=True,
|
||
verbose_name="Код промокода",
|
||
help_text="Уникальный код (например: SALE2025, WINTER10)"
|
||
)
|
||
|
||
discount = models.ForeignKey(
|
||
'Discount',
|
||
on_delete=models.CASCADE,
|
||
related_name='promo_codes',
|
||
verbose_name="Скидка"
|
||
)
|
||
|
||
# Ограничения использования
|
||
max_uses_per_user = models.PositiveIntegerField(
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Макс. использований на клиента",
|
||
help_text="Оставьте пустым для безлимитного использования"
|
||
)
|
||
|
||
max_total_uses = models.PositiveIntegerField(
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Макс. общее количество использований"
|
||
)
|
||
|
||
current_uses = models.PositiveIntegerField(
|
||
default=0,
|
||
verbose_name="Текущее количество использований"
|
||
)
|
||
|
||
is_active = models.BooleanField(
|
||
default=True,
|
||
verbose_name="Активен"
|
||
)
|
||
|
||
start_date = models.DateTimeField(
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Дата начала действия"
|
||
)
|
||
|
||
end_date = models.DateTimeField(
|
||
null=True,
|
||
blank=True,
|
||
verbose_name="Дата окончания действия"
|
||
)
|
||
|
||
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_promo_codes',
|
||
verbose_name="Создал"
|
||
)
|
||
|
||
class Meta:
|
||
verbose_name = "Промокод"
|
||
verbose_name_plural = "Промокоды"
|
||
indexes = [
|
||
models.Index(fields=['code']),
|
||
models.Index(fields=['is_active']),
|
||
]
|
||
|
||
def __str__(self):
|
||
return f"{self.code} -> {self.discount.name}"
|
||
|
||
def clean(self):
|
||
"""Валидация промокода"""
|
||
super().clean()
|
||
if self.code:
|
||
self.code = self.code.strip().upper()
|
||
|
||
def save(self, *args, **kwargs):
|
||
"""Приводим код к верхнему регистру при сохранении"""
|
||
if self.code:
|
||
self.code = self.code.strip().upper()
|
||
super().save(*args, **kwargs)
|
||
|
||
def is_valid(self, customer=None):
|
||
"""
|
||
Проверить валидность промокода.
|
||
|
||
Args:
|
||
customer: Customer для проверки использований на пользователя
|
||
|
||
Returns:
|
||
tuple: (is_valid, error_message)
|
||
"""
|
||
now = timezone.now()
|
||
|
||
if not self.is_active:
|
||
return False, "Промокод неактивен"
|
||
|
||
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_total_uses and self.current_uses >= self.max_total_uses:
|
||
return False, "Промокод полностью использован"
|
||
|
||
if customer and self.max_uses_per_user:
|
||
# Проверяем использования этим клиентом
|
||
uses = DiscountApplication.objects.filter(
|
||
promo_code=self,
|
||
customer=customer
|
||
).count()
|
||
|
||
if uses >= self.max_uses_per_user:
|
||
return False, f"Вы уже использовали этот промокод максимальное количество раз ({self.max_uses_per_user})"
|
||
|
||
return True, None
|
||
|
||
def record_usage(self, customer=None):
|
||
"""
|
||
Зарегистрировать использование промокода.
|
||
|
||
Args:
|
||
customer: Customer (опционально)
|
||
"""
|
||
self.current_uses += 1
|
||
self.save(update_fields=['current_uses'])
|
||
|
||
|
||
# Импортируем здесь, чтобы избежать циклического импорта
|
||
from .application import DiscountApplication
|