Files
octopus/myproject/discounts/models/promo_code.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

148 lines
4.6 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
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