Files
octopus/myproject/customers/admin.py
Andrey Smakotin eb6a3c1874 Исправлена ошибка public admin для мультитенантной архитектуры
Проблема: при входе в 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>
2025-12-31 01:05:47 +03:00

166 lines
6.6 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.
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