feat(orders): добавлено отображение скидок в админке заказов

- Добавлен DiscountApplicationInline для просмотра истории скидок на странице заказа
- OrderAdmin: добавлены колонки subtotal_display и discount_display
- OrderAdmin: добавлен фильтр по applied_discount
- OrderAdmin: добавлена секция "Скидки" в fieldsets
- OrderItemInline: добавлено отображение скидки в inline
- OrderItemAdmin: добавлена колонка и фильтр по скидкам

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-11 00:43:26 +03:00
parent 6978f4e59f
commit f50b47736d

View File

@@ -6,6 +6,7 @@
""" """
from django.contrib import admin from django.contrib import admin
from django.utils.html import format_html from django.utils.html import format_html
from django.urls import reverse
from .models import Order, OrderItem, Transaction, PaymentMethod, Address, OrderStatus, Recipient, Delivery from .models import Order, OrderItem, Transaction, PaymentMethod, Address, OrderStatus, Recipient, Delivery
from tenants.admin_mixins import TenantAdminOnlyMixin from tenants.admin_mixins import TenantAdminOnlyMixin
@@ -27,14 +28,21 @@ class OrderItemInline(admin.TabularInline):
""" """
model = OrderItem model = OrderItem
extra = 1 extra = 1
fields = ['product', 'product_kit', 'quantity', 'price'] fields = ['product', 'product_kit', 'quantity', 'price', 'item_discount_inline']
readonly_fields = [] readonly_fields = []
def get_readonly_fields(self, request, obj=None): def get_readonly_fields(self, request, obj=None):
"""Делаем цену readonly для существующих позиций""" """Делаем цену readonly для существующих позиций"""
if obj and obj.pk: if obj and obj.pk:
return ['price'] return ['price', 'item_discount_inline']
return [] return ['item_discount_inline']
def item_discount_inline(self, obj):
"""Отображение скидки в inline"""
if obj.discount_amount and obj.discount_amount > 0:
return f'-{obj.discount_amount:.2f} руб.'
return '-'
item_discount_inline.short_description = 'Скидка'
class DeliveryInline(admin.StackedInline): class DeliveryInline(admin.StackedInline):
@@ -49,6 +57,46 @@ class DeliveryInline(admin.StackedInline):
verbose_name_plural = 'Доставка' verbose_name_plural = 'Доставка'
class DiscountApplicationInline(admin.TabularInline):
"""
Inline для просмотра истории применённых скидок.
"""
from discounts.models import DiscountApplication
model = DiscountApplication
extra = 0
can_delete = False
verbose_name = 'Применённая скидка'
verbose_name_plural = 'Скидки'
fields = [
'discount_link',
'promo_code_display',
'target',
'discount_amount',
'applied_at',
]
readonly_fields = fields
def discount_link(self, obj):
if obj.discount:
return format_html(
'<a href="/admin/discounts/discount/{}/">{}</a>',
obj.discount.id, obj.discount.name
)
return '-'
discount_link.short_description = 'Скидка'
def promo_code_display(self, obj):
return obj.promo_code.code if obj.promo_code else '-'
promo_code_display.short_description = 'Промокод'
def has_add_permission(self, request, obj=None):
return False
def has_change_permission(self, request, obj=None):
return False
@admin.register(Order) @admin.register(Order)
class OrderAdmin(TenantAdminOnlyMixin, admin.ModelAdmin): class OrderAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
"""Админ-панель для управления заказами.""" """Админ-панель для управления заказами."""
@@ -56,6 +104,8 @@ class OrderAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
'order_number', 'order_number',
'customer', 'customer',
'status', 'status',
'subtotal_display',
'discount_display',
'total_amount', 'total_amount',
'payment_status', 'payment_status',
'amount_paid', 'amount_paid',
@@ -66,6 +116,7 @@ class OrderAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
'status', 'status',
'payment_status', 'payment_status',
'created_at', 'created_at',
'applied_discount',
] ]
search_fields = [ search_fields = [
@@ -80,6 +131,8 @@ class OrderAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
'order_number', 'order_number',
'created_at', 'created_at',
'updated_at', 'updated_at',
'subtotal_display',
'discount_display',
'amount_due', 'amount_due',
'payment_status', 'payment_status',
] ]
@@ -94,8 +147,18 @@ class OrderAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
), ),
'description': 'Если получатель не указан, получателем является покупатель' 'description': 'Если получатель не указан, получателем является покупатель'
}), }),
('Скидки', {
'fields': (
'applied_discount',
'applied_promo_code',
'discount_amount',
),
'classes': ('collapse',)
}),
('Оплата', { ('Оплата', {
'fields': ( 'fields': (
'subtotal_display',
'discount_display',
'total_amount', 'total_amount',
'amount_paid', 'amount_paid',
'amount_due', 'amount_due',
@@ -112,7 +175,7 @@ class OrderAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
}), }),
) )
inlines = [OrderItemInline, DeliveryInline, TransactionInline] inlines = [OrderItemInline, DeliveryInline, TransactionInline, DiscountApplicationInline]
actions = [ actions = [
'mark_as_confirmed', 'mark_as_confirmed',
@@ -152,6 +215,23 @@ class OrderAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
self.message_user(request, f'{updated} заказ(ов) отмечено как оплаченные') self.message_user(request, f'{updated} заказ(ов) отмечено как оплаченные')
mark_as_paid.short_description = 'Отметить как оплаченные' mark_as_paid.short_description = 'Отметить как оплаченные'
def subtotal_display(self, obj):
"""Отображение subtotal (сумма товаров)"""
return f'{obj.subtotal:.2f} руб.' if obj.subtotal else '0.00 руб.'
subtotal_display.short_description = 'Подытог'
def discount_display(self, obj):
"""Отображение информации о скидке"""
if not obj.discount_amount or obj.discount_amount == 0:
return '-'
discount_info = f'-{obj.discount_amount:.2f} руб.'
if obj.applied_discount:
from django.utils.safestring import mark_safe
discount_link = f'<a href="/admin/discounts/discount/{obj.applied_discount.id}/">{obj.applied_discount.name}</a>'
return format_html('{}<br><small class="text-muted">{}</small>', discount_info, discount_link)
return discount_info
discount_display.short_description = 'Скидка'
@admin.register(Transaction) @admin.register(Transaction)
class TransactionAdmin(TenantAdminOnlyMixin, admin.ModelAdmin): class TransactionAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
@@ -202,12 +282,14 @@ class OrderItemAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
'item_name', 'item_name',
'quantity', 'quantity',
'price', 'price',
'item_discount_display',
'get_total_price', 'get_total_price',
] ]
list_filter = [ list_filter = [
'order__status', 'order__status',
'order__created_at', 'order__created_at',
'applied_discount',
] ]
search_fields = [ search_fields = [
@@ -216,7 +298,7 @@ class OrderItemAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
'product_kit__name', 'product_kit__name',
] ]
readonly_fields = ['created_at', 'get_total_price'] readonly_fields = ['created_at', 'get_total_price', 'item_discount_display']
fieldsets = ( fieldsets = (
('Заказ', { ('Заказ', {
@@ -226,7 +308,11 @@ class OrderItemAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
'fields': ('product', 'product_kit') 'fields': ('product', 'product_kit')
}), }),
('Информация', { ('Информация', {
'fields': ('quantity', 'price', 'get_total_price') 'fields': ('quantity', 'price', 'item_discount_display', 'get_total_price')
}),
('Скидка', {
'fields': ('applied_discount', 'discount_amount'),
'classes': ('collapse',)
}), }),
('Системная информация', { ('Системная информация', {
'fields': ('created_at',), 'fields': ('created_at',),
@@ -234,6 +320,16 @@ class OrderItemAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
}), }),
) )
def item_discount_display(self, obj):
"""Отображение скидки на позицию"""
if not obj.discount_amount or obj.discount_amount == 0:
return '-'
result = f'-{obj.discount_amount:.2f} руб.'
if obj.applied_discount:
result += format_html('<br><small class="text-muted">{}</small>', obj.applied_discount.name)
return result
item_discount_display.short_description = 'Скидка'
@admin.register(Address) @admin.register(Address)
class AddressAdmin(TenantAdminOnlyMixin, admin.ModelAdmin): class AddressAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):