Проблема: при входе в localhost/admin/ (public схема) возникала ошибка "relation user_roles_userrole does not exist", так как tenant-only таблицы не существуют в public схеме. Решение: - Создан TenantAdminOnlyMixin для скрытия tenant-only моделей от public admin - Применён миксин ко всем ModelAdmin классам в tenant-only приложениях: user_roles, customers, orders, inventory, products - Добавлена проверка _is_public_schema() в RoleBasedPermissionBackend для предотвращения запросов к tenant-only таблицам в public схеме Теперь: - localhost/admin/ показывает только public модели (Client, Domain, User) - shop.localhost/admin/ показывает все модели магазина 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
166 lines
6.6 KiB
Python
166 lines
6.6 KiB
Python
from django.contrib import admin
|
||
from django.db import models
|
||
from django.utils.html import format_html
|
||
from .models import Customer, WalletTransaction, ContactChannel
|
||
from tenants.admin_mixins import TenantAdminOnlyMixin
|
||
|
||
|
||
class IsSystemCustomerFilter(admin.SimpleListFilter):
|
||
title = 'Системный клиент'
|
||
parameter_name = 'is_system_customer'
|
||
|
||
def lookups(self, request, model_admin):
|
||
return (
|
||
('yes', 'Системный'),
|
||
('no', 'Обычный'),
|
||
)
|
||
|
||
def queryset(self, request, queryset):
|
||
if self.value() == 'yes':
|
||
return queryset.filter(is_system_customer=True)
|
||
if self.value() == 'no':
|
||
return queryset.filter(is_system_customer=False)
|
||
return queryset
|
||
|
||
|
||
@admin.register(Customer)
|
||
class CustomerAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
||
"""
|
||
Административный интерфейс для управления клиентами цветочного магазина.
|
||
TenantAdminOnlyMixin - скрывает от public admin (таблица только в tenant схемах).
|
||
"""
|
||
list_display = (
|
||
'full_name',
|
||
'email',
|
||
'phone',
|
||
'wallet_balance_display',
|
||
'is_system_customer',
|
||
'created_at'
|
||
)
|
||
list_filter = (
|
||
IsSystemCustomerFilter,
|
||
'created_at'
|
||
)
|
||
search_fields = (
|
||
'name',
|
||
'email',
|
||
'phone'
|
||
)
|
||
date_hierarchy = 'created_at'
|
||
ordering = ('-created_at',)
|
||
readonly_fields = ('created_at', 'updated_at', 'is_system_customer', 'wallet_balance_display')
|
||
|
||
fieldsets = (
|
||
('Основная информация', {
|
||
'fields': ('name', 'email', 'phone', 'is_system_customer')
|
||
}),
|
||
('Кошелёк', {
|
||
'fields': ('wallet_balance_display',),
|
||
}),
|
||
('Заметки', {
|
||
'fields': ('notes',)
|
||
}),
|
||
('Даты', {
|
||
'fields': ('created_at', 'updated_at'),
|
||
'classes': ('collapse',)
|
||
}),
|
||
)
|
||
|
||
def wallet_balance_display(self, obj):
|
||
"""Отображение баланса кошелька с цветом"""
|
||
balance = obj.wallet_balance
|
||
if balance > 0:
|
||
return format_html(
|
||
'<span style="color: green; font-weight: bold;">{} руб.</span>',
|
||
balance
|
||
)
|
||
return f'{balance} руб.'
|
||
wallet_balance_display.short_description = 'Баланс кошелька'
|
||
|
||
def get_readonly_fields(self, request, obj=None):
|
||
"""Делаем все поля read-only для системного клиента"""
|
||
if obj and obj.is_system_customer:
|
||
# Для системного клиента все поля только для чтения
|
||
return ['name', 'email', 'phone', 'is_system_customer', 'wallet_balance_display', 'notes', 'created_at', 'updated_at']
|
||
return self.readonly_fields
|
||
|
||
def has_delete_permission(self, request, obj=None):
|
||
"""Запрет на удаление системного клиента"""
|
||
if obj and obj.is_system_customer:
|
||
return False
|
||
return super().has_delete_permission(request, obj)
|
||
|
||
def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
|
||
"""Добавляем предупреждение для системного клиента"""
|
||
extra_context = extra_context or {}
|
||
if object_id:
|
||
obj = self.get_object(request, object_id)
|
||
if obj and obj.is_system_customer:
|
||
extra_context['readonly'] = True
|
||
from django.contrib import messages
|
||
messages.warning(request, 'Это системный клиент. Редактирование запрещено для обеспечения корректной работы системы.')
|
||
return super().changeform_view(request, object_id, form_url, extra_context)
|
||
|
||
|
||
class ContactChannelInline(admin.TabularInline):
|
||
"""Inline для управления каналами связи клиента"""
|
||
model = ContactChannel
|
||
extra = 1
|
||
fields = ('channel_type', 'value', 'is_primary', 'notes')
|
||
|
||
|
||
class WalletTransactionInline(admin.TabularInline):
|
||
"""Inline для отображения транзакций кошелька"""
|
||
model = WalletTransaction
|
||
extra = 0
|
||
can_delete = False
|
||
readonly_fields = ('created_at', 'transaction_type', 'signed_amount', 'balance_after', 'order', 'description', 'created_by')
|
||
fields = ('created_at', 'transaction_type', 'signed_amount', 'balance_after', 'order', 'description', 'created_by')
|
||
ordering = ('-created_at',)
|
||
|
||
def has_add_permission(self, request, obj=None):
|
||
"""Запрещаем ручное создание транзакций - только через сервис"""
|
||
return False
|
||
|
||
|
||
# Добавляем inline в CustomerAdmin
|
||
CustomerAdmin.inlines = [ContactChannelInline, WalletTransactionInline]
|
||
|
||
|
||
@admin.register(WalletTransaction)
|
||
class WalletTransactionAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
|
||
"""
|
||
Админка для просмотра всех транзакций кошелька.
|
||
TenantAdminOnlyMixin - скрывает от public admin.
|
||
"""
|
||
list_display = ('created_at', 'customer', 'transaction_type', 'amount_display', 'balance_after', 'order', 'created_by')
|
||
list_filter = ('transaction_type', 'balance_category', 'created_at')
|
||
search_fields = ('customer__name', 'customer__email', 'customer__phone', 'description')
|
||
readonly_fields = ('customer', 'transaction_type', 'signed_amount', 'balance_category', 'balance_after', 'order', 'description', 'created_at', 'created_by')
|
||
date_hierarchy = 'created_at'
|
||
ordering = ('-created_at',)
|
||
|
||
def amount_display(self, obj):
|
||
"""Отображение суммы с цветом"""
|
||
amount = obj.signed_amount
|
||
if amount > 0:
|
||
return format_html(
|
||
'<span style="color: green; font-weight: bold;">+{} руб.</span>',
|
||
amount
|
||
)
|
||
elif amount < 0:
|
||
return format_html(
|
||
'<span style="color: red; font-weight: bold;">{} руб.</span>',
|
||
amount
|
||
)
|
||
return f'{amount} руб.'
|
||
amount_display.short_description = 'Сумма'
|
||
|
||
def has_add_permission(self, request):
|
||
"""Запрещаем ручное создание - только через сервис"""
|
||
return False
|
||
|
||
def has_delete_permission(self, request, obj=None):
|
||
"""Запрещаем удаление - аудит должен быть неизменяем"""
|
||
return False
|