# -*- 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(
'{}',
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'{obj.applied_discount.name}'
return format_html('{}
{}', 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('
{}', 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(
'