Добавлено поле 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>
223 lines
6.2 KiB
Python
223 lines
6.2 KiB
Python
# -*- 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',
|
||
'combine_mode_display',
|
||
'is_auto',
|
||
'is_active',
|
||
'current_usage_count',
|
||
'validity_period',
|
||
]
|
||
|
||
list_filter = [
|
||
'discount_type',
|
||
'scope',
|
||
'combine_mode',
|
||
'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', 'combine_mode')
|
||
}),
|
||
('Ограничения', {
|
||
'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 combine_mode_display(self, obj):
|
||
"""Отображение режима объединения с иконкой."""
|
||
icons = {
|
||
'stack': '📚', # слои
|
||
'max_only': '🏆', # максимум
|
||
'exclusive': '🚫', # запрет
|
||
}
|
||
labels = {
|
||
'stack': 'Склад.',
|
||
'max_only': 'Макс.',
|
||
'exclusive': 'Исключ.',
|
||
}
|
||
icon = icons.get(obj.combine_mode, '')
|
||
label = labels.get(obj.combine_mode, obj.combine_mode)
|
||
return f'{icon} {label}' if icon else label
|
||
combine_mode_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 = "Промокод"
|