Files
octopus/myproject/orders/admin.py
Andrey Smakotin f50b47736d 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>
2026-01-11 00:43:26 +03:00

581 lines
19 KiB
Python
Raw 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.
# -*- coding: utf-8 -*-
"""
Админка для приложения orders.
Все модели tenant-only, поэтому используют TenantAdminOnlyMixin
для скрытия от public admin (localhost/admin/).
"""
from django.contrib import admin
from django.utils.html import format_html
from django.urls import reverse
from .models import Order, OrderItem, Transaction, PaymentMethod, Address, OrderStatus, Recipient, Delivery
from tenants.admin_mixins import TenantAdminOnlyMixin
class TransactionInline(admin.TabularInline):
"""
Inline для управления транзакциями по заказу.
"""
model = Transaction
extra = 0
fields = ['transaction_type', 'amount', 'payment_method', 'transaction_date', 'created_by', 'reason', 'notes']
readonly_fields = ['transaction_date']
can_delete = False # Используйте refund вместо удаления
class OrderItemInline(admin.TabularInline):
"""
Inline для управления позициями заказа прямо в форме заказа.
"""
model = OrderItem
extra = 1
fields = ['product', 'product_kit', 'quantity', 'price', 'item_discount_inline']
readonly_fields = []
def get_readonly_fields(self, request, obj=None):
"""Делаем цену readonly для существующих позиций"""
if obj and obj.pk:
return ['price', 'item_discount_inline']
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):
"""
Inline для управления доставкой заказа.
"""
model = Delivery
extra = 0
max_num = 1
fields = ['delivery_type', 'address', 'pickup_warehouse', 'cost']
verbose_name = 'Доставка'
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)
class OrderAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
"""Админ-панель для управления заказами."""
list_display = [
'order_number',
'customer',
'status',
'subtotal_display',
'discount_display',
'total_amount',
'payment_status',
'amount_paid',
'created_at',
]
list_filter = [
'status',
'payment_status',
'created_at',
'applied_discount',
]
search_fields = [
'order_number',
'customer__name',
'customer__phone',
'customer__email',
'recipient__name',
]
readonly_fields = [
'order_number',
'created_at',
'updated_at',
'subtotal_display',
'discount_display',
'amount_due',
'payment_status',
]
fieldsets = (
('Основная информация', {
'fields': ('order_number', 'customer', 'status')
}),
('Получатель', {
'fields': (
'recipient',
),
'description': 'Если получатель не указан, получателем является покупатель'
}),
('Скидки', {
'fields': (
'applied_discount',
'applied_promo_code',
'discount_amount',
),
'classes': ('collapse',)
}),
('Оплата', {
'fields': (
'subtotal_display',
'discount_display',
'total_amount',
'amount_paid',
'amount_due',
'payment_status',
)
}),
('Дополнительно', {
'fields': ('is_anonymous', 'special_instructions'),
'classes': ('collapse',)
}),
('Системная информация', {
'fields': ('created_at', 'updated_at', 'modified_by'),
'classes': ('collapse',)
}),
)
inlines = [OrderItemInline, DeliveryInline, TransactionInline, DiscountApplicationInline]
actions = [
'mark_as_confirmed',
'mark_as_in_assembly',
'mark_as_in_delivery',
'mark_as_delivered',
'mark_as_paid',
]
def mark_as_confirmed(self, request, queryset):
"""Отметить заказы как подтвержденные"""
updated = queryset.update(status='confirmed')
self.message_user(request, f'{updated} заказ(ов) отмечено как подтвержденные')
mark_as_confirmed.short_description = 'Отметить как подтвержденные'
def mark_as_in_assembly(self, request, queryset):
"""Отметить заказы как в сборке"""
updated = queryset.update(status='in_assembly')
self.message_user(request, f'{updated} заказ(ов) отмечено как в сборке')
mark_as_in_assembly.short_description = 'Отметить как в сборке'
def mark_as_in_delivery(self, request, queryset):
"""Отметить заказы как в доставке"""
updated = queryset.update(status='in_delivery')
self.message_user(request, f'{updated} заказ(ов) отмечено как в доставке')
mark_as_in_delivery.short_description = 'Отметить как в доставке'
def mark_as_delivered(self, request, queryset):
"""Отметить заказы как доставленные"""
updated = queryset.update(status='delivered')
self.message_user(request, f'{updated} заказ(ов) отмечено как доставленные')
mark_as_delivered.short_description = 'Отметить как доставленные'
def mark_as_paid(self, request, queryset):
"""Отметить заказы как оплаченные"""
updated = queryset.update(is_paid=True)
self.message_user(request, f'{updated} заказ(ов) отмечено как оплаченные')
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)
class TransactionAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
"""
Админ-панель для управления транзакциями.
"""
list_display = [
'order',
'transaction_type',
'amount',
'payment_method',
'transaction_date',
'created_by',
'reason',
]
list_filter = [
'transaction_type',
'payment_method',
'transaction_date',
]
search_fields = [
'order__order_number',
'reason',
'notes',
]
readonly_fields = ['transaction_date']
fieldsets = (
('Информация о транзакции', {
'fields': ('order', 'transaction_type', 'amount', 'payment_method', 'transaction_date')
}),
('Дополнительно', {
'fields': ('related_payment', 'reason', 'created_by', 'notes')
}),
)
@admin.register(OrderItem)
class OrderItemAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
"""
Админ-панель для управления позициями заказов.
"""
list_display = [
'order',
'item_name',
'quantity',
'price',
'item_discount_display',
'get_total_price',
]
list_filter = [
'order__status',
'order__created_at',
'applied_discount',
]
search_fields = [
'order__order_number',
'product__name',
'product_kit__name',
]
readonly_fields = ['created_at', 'get_total_price', 'item_discount_display']
fieldsets = (
('Заказ', {
'fields': ('order',)
}),
('Товар/Комплект', {
'fields': ('product', 'product_kit')
}),
('Информация', {
'fields': ('quantity', 'price', 'item_discount_display', 'get_total_price')
}),
('Скидка', {
'fields': ('applied_discount', 'discount_amount'),
'classes': ('collapse',)
}),
('Системная информация', {
'fields': ('created_at',),
'classes': ('collapse',)
}),
)
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)
class AddressAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
"""
Админ-панель для управления адресами доставки заказов.
"""
list_display = [
'full_address',
'entrance',
'floor',
'confirm_address_with_recipient',
'created_at',
]
list_filter = [
'confirm_address_with_recipient',
'created_at',
]
search_fields = [
'street',
'building_number',
]
readonly_fields = ['created_at', 'updated_at']
fieldsets = (
('Адрес доставки', {
'fields': ('street', 'building_number', 'apartment_number', 'entrance', 'floor')
}),
('Доступ в здание', {
'fields': ('intercom_code',),
'classes': ('collapse',)
}),
('Дополнительная информация', {
'fields': ('delivery_instructions', 'confirm_address_with_recipient'),
'classes': ('collapse',)
}),
('Даты', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
@admin.register(Recipient)
class RecipientAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
"""
Админ-панель для управления получателями заказов.
"""
list_display = [
'name',
'phone',
'created_at',
]
list_filter = [
'created_at',
]
search_fields = [
'name',
'phone',
]
readonly_fields = ['created_at', 'updated_at']
fieldsets = (
('Информация о получателе', {
'fields': ('name', 'phone')
}),
('Даты', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
@admin.register(OrderStatus)
class OrderStatusAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
"""
Админ-панель для управления статусами заказов.
"""
list_display = [
'order_display',
'name',
'code',
'color_preview',
'is_system',
'is_positive_end',
'is_negative_end',
'orders_count',
]
list_filter = [
'is_system',
'is_positive_end',
'is_negative_end',
]
search_fields = [
'name',
'code',
'label',
]
readonly_fields = ['created_at', 'updated_at', 'created_by', 'updated_by']
fieldsets = (
('Основная информация', {
'fields': ('code', 'name', 'label', 'order')
}),
('Визуальное оформление', {
'fields': ('color',)
}),
('Тип статуса', {
'fields': ('is_system', 'is_positive_end', 'is_negative_end')
}),
('Дополнительно', {
'fields': ('description',),
'classes': ('collapse',)
}),
('Системная информация', {
'fields': ('created_at', 'updated_at', 'created_by', 'updated_by', 'get_orders_count'),
'classes': ('collapse',)
}),
)
ordering = ['order', 'name']
def get_readonly_fields(self, request, obj=None):
"""Делаем код readonly для системных статусов"""
readonly = list(self.readonly_fields)
readonly.append('get_orders_count')
if obj and obj.is_system:
readonly.append('code')
return readonly
def color_preview(self, obj):
"""Превью цвета статуса"""
return format_html(
'<div style="width: 30px; height: 20px; background-color: {}; border: 1px solid #ccc; border-radius: 3px;"></div>',
obj.color
)
color_preview.short_description = 'Цвет'
def order_display(self, obj):
"""Отображение порядкового номера с бейджем"""
return format_html(
'<span style="display: inline-block; background-color: #6c757d; color: white; padding: 2px 8px; border-radius: 10px; font-size: 11px;">{}</span>',
obj.order
)
order_display.short_description = 'Порядок'
def orders_count(self, obj):
"""Количество заказов в этом статусе"""
count = obj.orders_count
if count == 0:
return format_html('<span style="color: #999;">{}</span>', count)
return format_html('<span style="font-weight: bold;">{}</span>', count)
orders_count.short_description = 'Заказов'
def get_orders_count(self, obj):
"""Количество заказов для readonly поля"""
return obj.orders_count if obj else 0
get_orders_count.short_description = 'Количество заказов'
def has_delete_permission(self, request, obj=None):
"""Запрещаем удаление системных статусов и статусов с заказами"""
if obj:
if obj.is_system or obj.orders_count > 0:
return False
return super().has_delete_permission(request, obj)
@admin.register(PaymentMethod)
class PaymentMethodAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
"""
Админ-панель для управления способами оплаты.
"""
list_display = [
'order_display',
'name',
'code',
'description',
'is_active',
'is_system',
'payments_count',
]
list_filter = [
'is_active',
'is_system',
]
search_fields = [
'name',
'code',
'description',
]
readonly_fields = ['created_at', 'updated_at', 'created_by']
fieldsets = (
('Основная информация', {
'fields': ('code', 'name', 'description', 'order')
}),
('Настройки', {
'fields': ('is_active', 'is_system')
}),
('Системная информация', {
'fields': ('created_at', 'updated_at', 'created_by'),
'classes': ('collapse',)
}),
)
ordering = ['order', 'name']
def get_readonly_fields(self, request, obj=None):
"""Делаем код readonly для системных способов оплаты"""
readonly = list(self.readonly_fields)
if obj and obj.is_system:
readonly.append('code')
return readonly
def order_display(self, obj):
"""Отображение порядкового номера с бейджем"""
return format_html(
'<span style="display: inline-block; background-color: #6c757d; color: white; padding: 2px 8px; border-radius: 10px; font-size: 11px;">{}</span>',
obj.order
)
order_display.short_description = 'Порядок'
def payments_count(self, obj):
"""Количество платежей этим способом"""
count = obj.transactions.count()
if count == 0:
return format_html('<span style="color: #999;">{}</span>', count)
return format_html('<span style="font-weight: bold;">{}</span>', count)
payments_count.short_description = 'Платежей'
def has_delete_permission(self, request, obj=None):
"""Запрещаем удаление используемых способов оплаты"""
if obj:
# Разрешаем удаление только если нет связанных транзакций
if obj.transactions.exists():
return False
return super().has_delete_permission(request, obj)