feat(discounts): добавлено приложение скидок

Создано новое Django приложение для управления скидками:

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

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

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-11 00:30:14 +03:00
parent 27cb9ba09d
commit 241625eba7
14 changed files with 1524 additions and 0 deletions

View File

@@ -0,0 +1,203 @@
# -*- coding: utf-8 -*-
from django.contrib import admin
from .models import Discount, PromoCode, DiscountApplication
@admin.register(Discount)
class DiscountAdmin(admin.ModelAdmin):
"""Админ-панель для управления скидками."""
list_display = [
'name',
'discount_type',
'value_display',
'scope',
'is_auto',
'is_active',
'current_usage_count',
'validity_period',
]
list_filter = [
'discount_type',
'scope',
'is_auto',
'is_active',
]
search_fields = [
'name',
'description',
]
readonly_fields = [
'current_usage_count',
'created_at',
]
fieldsets = (
('Основная информация', {
'fields': ('name', 'description', 'is_active', 'priority')
}),
('Параметры скидки', {
'fields': ('discount_type', 'value', 'scope')
}),
('Ограничения', {
'fields': (
'start_date',
'end_date',
'max_usage_count',
'current_usage_count',
'is_auto'
)
}),
('Условия применения', {
'fields': (
'min_order_amount',
'products',
'categories',
'excluded_products'
)
}),
('Метаданные', {
'fields': ('created_at', 'created_by'),
'classes': ('collapse',)
}),
)
def value_display(self, obj):
if obj.discount_type == 'percentage':
return f"{obj.value}%"
return f"{obj.value} руб."
value_display.short_description = "Значение"
def validity_period(self, obj):
if obj.start_date and obj.end_date:
return f"{obj.start_date.date()} - {obj.end_date.date()}"
elif obj.start_date:
return f"с {obj.start_date.date()}"
elif obj.end_date:
return f"до {obj.end_date.date()}"
return "Бессрочная"
validity_period.short_description = "Период действия"
@admin.register(PromoCode)
class PromoCodeAdmin(admin.ModelAdmin):
"""Админ-панель для управления промокодами."""
list_display = [
'code',
'discount_name',
'is_active',
'current_uses',
'usage_limit',
'validity_period',
]
list_filter = [
'is_active',
'discount__scope',
]
search_fields = [
'code',
'discount__name',
]
readonly_fields = [
'current_uses',
'created_at',
]
fieldsets = (
('Основная информация', {
'fields': ('code', 'discount', 'is_active')
}),
('Ограничения', {
'fields': (
'max_uses_per_user',
'max_total_uses',
'current_uses',
'start_date',
'end_date',
)
}),
('Метаданные', {
'fields': ('created_at', 'created_by'),
'classes': ('collapse',)
}),
)
def discount_name(self, obj):
return obj.discount.name
discount_name.short_description = "Скидка"
def usage_limit(self, obj):
if obj.max_total_uses:
return f"{obj.current_uses} / {obj.max_total_uses}"
return str(obj.current_uses)
usage_limit.short_description = "Использования"
def validity_period(self, obj):
if obj.start_date and obj.end_date:
return f"{obj.start_date.date()} - {obj.end_date.date()}"
elif obj.start_date:
return f"с {obj.start_date.date()}"
elif obj.end_date:
return f"до {obj.end_date.date()}"
return "Бессрочный"
validity_period.short_description = "Период действия"
@admin.register(DiscountApplication)
class DiscountApplicationAdmin(admin.ModelAdmin):
"""Админ-панель для истории применения скидок."""
list_display = [
'order_link',
'discount_name',
'promo_code_display',
'target',
'discount_amount',
'customer',
'applied_at',
]
list_filter = [
'target',
'applied_at',
'discount__discount_type',
]
readonly_fields = [
'order',
'order_item',
'discount',
'promo_code',
'target',
'base_amount',
'discount_amount',
'final_amount',
'customer',
'applied_at',
'applied_by',
]
def has_add_permission(self, request):
return False # Только чтение
def has_change_permission(self, request, obj=None):
return False # Только чтение
def order_link(self, obj):
from django.urls import reverse
url = reverse('admin:orders_order_change', args=[obj.order.id])
return f'<a href="{url}">#{obj.order.order_number}</a>'
order_link.short_description = "Заказ"
order_link.allow_tags = True
def discount_name(self, obj):
return obj.discount.name if obj.discount else '-'
discount_name.short_description = "Скидка"
def promo_code_display(self, obj):
return obj.promo_code.code if obj.promo_code else '-'
promo_code_display.short_description = "Промокод"