# -*- coding: utf-8 -*- from django.contrib import admin from django.contrib import messages from django.utils import timezone from django.utils.html import format_html from django.urls import reverse from django.db import transaction from .models import Client, Domain, TenantRegistration, Subscription class DomainInline(admin.TabularInline): """ Inline для управления доменами тенанта. """ model = Domain extra = 1 fields = ['domain', 'is_primary'] @admin.register(Client) class ClientAdmin(admin.ModelAdmin): """ Админ-панель для управления тенантами (магазинами). ВАЖНО: Эта админка доступна только в Public схеме! """ list_display = [ 'name', 'schema_name', 'owner_email', 'is_active', 'created_at', 'subscription_status', ] list_filter = [ 'is_active', 'created_at', ] search_fields = [ 'name', 'schema_name', 'owner_email', 'owner_name', ] # Убираем schema_name из readonly при создании нового тенанта def get_readonly_fields(self, request, obj=None): if obj: # Редактирование существующего тенанта return ['schema_name', 'created_at'] else: # Создание нового тенанта return ['created_at'] fieldsets = ( ('Информация о магазине', { 'fields': ('name', 'schema_name', 'is_active') }), ('Владелец', { 'fields': ('owner_name', 'owner_email', 'phone') }), ('Дополнительно', { 'fields': ('created_at', 'notes'), 'classes': ('collapse',) }), ) inlines = [DomainInline] def subscription_status(self, obj): """Отображение статуса подписки""" try: sub = obj.subscription if sub.is_expired(): return format_html('Истекла {}', sub.expires_at.date()) elif sub.days_left() <= 7: return format_html('Осталось {} дн.', sub.days_left()) else: return format_html('{} до {}', sub.get_plan_display(), sub.expires_at.date()) except Subscription.DoesNotExist: return format_html('Нет подписки') subscription_status.short_description = "Подписка" def has_delete_permission(self, request, obj=None): """ Запрещаем удаление тенантов через админку (для безопасности). Удаление должно происходить через специальную команду. """ return False @admin.register(Domain) class DomainAdmin(admin.ModelAdmin): """ Админ-панель для управления доменами. """ list_display = [ 'domain', 'tenant', 'is_primary', ] list_filter = [ 'is_primary', ] search_fields = [ 'domain', 'tenant__name', 'tenant__schema_name', ] @admin.register(TenantRegistration) class TenantRegistrationAdmin(admin.ModelAdmin): """ Админка для управления заявками на регистрацию """ list_display = ("shop_name", "schema_name", "owner_email", "status", "created_at", "actions_column") list_filter = ("status", "created_at") search_fields = ("shop_name", "schema_name", "owner_email", "owner_name") readonly_fields = ("created_at", "processed_at", "processed_by", "tenant") fieldsets = ( ("Информация о магазине", { "fields": ("shop_name", "schema_name") }), ("Владелец", { "fields": ("owner_name", "owner_email", "phone") }), ("Статус", { "fields": ("status", "created_at", "processed_at", "processed_by", "rejection_reason") }), ("Результат", { "fields": ("tenant",) }), ) actions = ['approve_registrations', 'reject_registrations', 'resend_password_setup_email'] def actions_column(self, obj): """Кнопки действий для каждой заявки""" if obj.status == TenantRegistration.STATUS_PENDING: approve_url = reverse('admin:tenants_tenantregistration_change', args=[obj.pk]) return format_html( '
' 'Активировать' 'Отклонить' '
', approve_url, approve_url ) elif obj.status == TenantRegistration.STATUS_APPROVED: if obj.tenant: tenant_url = reverse('admin:tenants_client_change', args=[obj.tenant.pk]) return format_html('Перейти к тенанту →', tenant_url) return "Одобрено" else: return "Отклонено" actions_column.short_description = "Действия" def approve_registrations(self, request, queryset): """Массовая активация заявок""" approved_count = 0 for registration in queryset.filter(status=TenantRegistration.STATUS_PENDING): try: self._approve_registration(registration, request.user) approved_count += 1 except Exception as e: messages.error(request, f"Ошибка при активации '{registration.shop_name}': {str(e)}") if approved_count > 0: messages.success(request, f"Успешно активировано заявок: {approved_count}") approve_registrations.short_description = "✓ Активировать выбранные заявки" def reject_registrations(self, request, queryset): """Массовое отклонение заявок""" rejected_count = queryset.filter(status=TenantRegistration.STATUS_PENDING).update( status=TenantRegistration.STATUS_REJECTED, processed_at=timezone.now(), processed_by=request.user, rejection_reason="Отклонено массовым действием" ) if rejected_count > 0: messages.success(request, f"Отклонено заявок: {rejected_count}") reject_registrations.short_description = "✗ Отклонить выбранные заявки" def resend_password_setup_email(self, request, queryset): """Повторно отправить письмо с ссылкой для установки пароля""" from tenants.services import send_password_setup_email sent_count = 0 for registration in queryset.filter(status=TenantRegistration.STATUS_APPROVED): try: send_password_setup_email(registration) sent_count += 1 except Exception as e: messages.error(request, f"Не удалось отправить письмо для {registration.shop_name}: {e}") if sent_count > 0: messages.success(request, f"Письма повторно отправлены: {sent_count}") resend_password_setup_email.short_description = "📧 Отправить письмо повторно" def save_model(self, request, obj, form, change): """ Отлавливаем изменение статуса на APPROVED и запускаем процесс активации """ # Если это изменение существующего объекта if change: # Получаем старое значение из БД try: old_obj = TenantRegistration.objects.get(pk=obj.pk) old_status = old_obj.status except TenantRegistration.DoesNotExist: old_status = None # Если статус изменился на APPROVED if old_status == TenantRegistration.STATUS_PENDING and obj.status == TenantRegistration.STATUS_APPROVED: # Не сохраняем пока - сначала активируем try: self._approve_registration(obj, request.user) messages.success(request, f"Заявка '{obj.shop_name}' успешно активирована!") return # _approve_registration уже сохраняет объект except Exception as e: messages.error(request, f"Ошибка при активации: {str(e)}") # Откатываем статус обратно obj.status = old_status super().save_model(request, obj, form, change) return # Обычное сохранение для всех остальных случаев super().save_model(request, obj, form, change) def changeform_view(self, request, object_id=None, form_url='', extra_context=None): """ Обработка действий активации/отклонения через GET параметры """ if object_id and request.method == 'GET': registration = TenantRegistration.objects.get(pk=object_id) if 'approve' in request.GET and registration.status == TenantRegistration.STATUS_PENDING: try: self._approve_registration(registration, request.user) messages.success(request, f"Заявка '{registration.shop_name}' успешно активирована!") return self._redirect_to_changelist() except Exception as e: messages.error(request, f"Ошибка при активации: {str(e)}") elif 'reject' in request.GET and registration.status == TenantRegistration.STATUS_PENDING: # Обрабатываем отклонение registration.status = TenantRegistration.STATUS_REJECTED registration.processed_at = timezone.now() registration.processed_by = request.user registration.rejection_reason = "Отклонено администратором" registration.save() messages.warning(request, f"Заявка '{registration.shop_name}' отклонена.") return self._redirect_to_changelist() return super().changeform_view(request, object_id, form_url, extra_context) @transaction.atomic def _approve_registration(self, registration, admin_user): """ Активация заявки: создание тенанта, домена и триальной подписки """ import logging logger = logging.getLogger(__name__) try: # Проверяем, не создан ли уже тенант if Client.objects.filter(schema_name=registration.schema_name).exists(): raise ValueError(f"Тенант с schema_name '{registration.schema_name}' уже существует!") # Создаем тенант logger.info(f"Создание тенанта: {registration.schema_name}") client = Client.objects.create( schema_name=registration.schema_name, name=registration.shop_name, owner_email=registration.owner_email, owner_name=registration.owner_name, phone=registration.phone, is_active=True ) logger.info(f"Тенант создан: {client.id}") # Создаем домен (динамически определяется из настроек) # Локально: schema_name.localhost:8000 # Продакшен: schema_name.mix.smaa.by from django.conf import settings domain_base = settings.TENANT_DOMAIN_BASE domain_name = f"{registration.schema_name}.{domain_base}" logger.info(f"Создание домена: {domain_name}") domain = Domain.objects.create( domain=domain_name, tenant=client, is_primary=True ) logger.info(f"Домен создан: {domain.id}") # Применяем миграции для нового тенанта logger.info(f"Применение миграций для тенанта: {registration.schema_name}") from django.core.management import call_command try: call_command('migrate_schemas', schema_name=registration.schema_name, verbosity=1) logger.info(f"Миграции успешно применены для тенанта: {registration.schema_name}") except Exception as e: logger.error(f"Ошибка при применении миграций: {str(e)}", exc_info=True) raise # Создаем триальную подписку на 90 дней logger.info(f"Создание триальной подписки для тенанта: {client.id}") subscription = Subscription.create_trial(client) logger.info(f"Подписка создана: {subscription.id}, истекает: {subscription.expires_at}") # Автоматически создаем суперпользователя для тенанта logger.info(f"Создание суперпользователя для тенанта: {client.id}") from django.db import connection from django.contrib.auth import get_user_model from django.conf import settings # Переключаемся на схему тенанта connection.set_tenant(client) User = get_user_model() # Проверяем, не существует ли уже пользователь с таким email if not User.objects.filter(email=settings.TENANT_ADMIN_EMAIL).exists(): superuser = User.objects.create_superuser( email=settings.TENANT_ADMIN_EMAIL, name=settings.TENANT_ADMIN_NAME, password=settings.TENANT_ADMIN_PASSWORD ) logger.info(f"Суперпользователь создан: {superuser.id} ({superuser.email})") else: logger.warning(f"Пользователь с email {settings.TENANT_ADMIN_EMAIL} уже существует в тенанте") # Создаем аккаунт владельца тенанта logger.info(f"Создание аккаунта владельца для тенанта: {client.id}") if not User.objects.filter(email=registration.owner_email).exists(): owner = User.objects.create_user( email=registration.owner_email, name=registration.owner_name, password=None, # Пароль будет установлен через ссылку is_staff=False, # SECURITY: Владелец НЕ может входить в админку is_superuser=False # SECURITY: Владелец НЕ суперпользователь ) # Помечаем email как подтвержденный, так как владелец регистрировался с ним owner.is_email_confirmed = True owner.email_confirmed_at = timezone.now() owner.is_active = False # Неактивен до установки пароля owner.save() logger.info(f"Аккаунт владельца создан: {owner.id} ({owner.email})") # Создаем системные роли пользователей (ПЕРЕД назначением роли владельцу) logger.info(f"Создание системных ролей для тенанта: {client.id}") from user_roles.services import RoleService try: RoleService.create_default_roles() logger.info("Системные роли успешно созданы") except Exception as e: logger.error(f"Ошибка при создании ролей: {e}", exc_info=True) # Не прерываем процесс, т.к. это не критично # Назначаем роль owner владельцу try: from user_roles.models import Role RoleService.assign_role_to_user(owner, Role.OWNER, created_by=None) logger.info(f"Роль owner назначена владельцу {owner.email}") except Exception as e: logger.error(f"Ошибка при назначении роли owner: {e}", exc_info=True) # Не прерываем процесс else: logger.warning(f"Пользователь с email {registration.owner_email} уже существует в тенанте") # Создаем системного клиента для анонимных продаж logger.info(f"Создание системного клиента для тенанта: {client.id}") from customers.models import Customer try: system_customer, created = Customer.get_or_create_system_customer() if created: logger.info(f"Системный клиент создан: {system_customer.id} ({system_customer.name})") else: logger.info(f"Системный клиент уже существует: {system_customer.id} ({system_customer.name})") except Exception as e: logger.error(f"Ошибка при создании системного клиента: {e}", exc_info=True) # Не прерываем процесс, т.к. это не критично # Создаем системные статусы заказов logger.info(f"Создание системных статусов заказов для тенанта: {client.id}") from orders.services.order_status_service import OrderStatusService try: OrderStatusService.create_default_statuses() logger.info("Системные статусы заказов успешно созданы") except Exception as e: logger.error(f"Ошибка при создании статусов заказов: {e}", exc_info=True) # Не прерываем процесс, т.к. это не критично # Создаем системные способы оплаты logger.info(f"Создание системных способов оплаты для тенанта: {client.id}") from django.core.management import call_command try: # Вызываем команду создания способов оплаты # Это единственный источник истины для списка способов оплаты call_command('create_payment_methods') logger.info("Системные способы оплаты успешно созданы") except Exception as e: logger.error(f"Ошибка при создании способов оплаты: {e}", exc_info=True) # Не прерываем процесс, т.к. это не критично # Возвращаемся в public схему connection.set_schema_to_public() # Отправляем письмо владельцу с ссылкой для установки пароля logger.info(f"Отправка письма владельцу с ссылкой установки пароля: {registration.owner_email}") try: from tenants.services import send_password_setup_email send_password_setup_email(registration) logger.info(f"Письмо успешно отправлено владельцу {registration.owner_email}") except Exception as e: logger.error(f"Ошибка при отправке письма владельцу: {e}", exc_info=True) # Не прерываем процесс одобрения, если письмо не отправилось # Админ может повторно отправить письмо через действие в админке # Обновляем статус заявки registration.status = TenantRegistration.STATUS_APPROVED registration.processed_at = timezone.now() registration.processed_by = admin_user registration.tenant = client registration.save() logger.info(f"Заявка {registration.id} успешно активирована") return client except Exception as e: logger.error(f"Ошибка при активации заявки {registration.id}: {str(e)}", exc_info=True) raise # Перебрасываем исключение для отображения в админке def _redirect_to_changelist(self): """Редирект на список заявок""" from django.shortcuts import redirect return redirect('admin:tenants_tenantregistration_changelist') @admin.register(Subscription) class SubscriptionAdmin(admin.ModelAdmin): """ Админка для управления подписками """ list_display = ("client", "plan", "started_at", "expires_at", "is_active", "days_left_display", "is_expired_display") list_filter = ("plan", "is_active", "auto_renew") search_fields = ("client__name", "client__schema_name") readonly_fields = ("created_at", "updated_at", "days_left_display", "is_expired_display") fieldsets = ( ("Основная информация", { "fields": ("client", "plan", "is_active", "auto_renew") }), ("Период действия", { "fields": ("started_at", "expires_at", "days_left_display", "is_expired_display") }), ("Служебная информация", { "fields": ("created_at", "updated_at") }), ) def days_left_display(self, obj): """Отображение оставшихся дней""" if not obj or not obj.expires_at: return "—" days = obj.days_left() if days == 0: return format_html('Истекла') elif days <= 7: return format_html('{} дн.', days) else: return format_html('{} дн.', days) days_left_display.short_description = "Осталось" def is_expired_display(self, obj): """Отображение статуса истечения""" if not obj or not obj.expires_at: return "—" if obj.is_expired(): return format_html('Да') else: return format_html('Нет') is_expired_display.short_description = "Истекла"