Рефакторинг: вынос логики онбординга тенанта в сервисный слой
Создан TenantOnboardingService как единый источник истины для: - Активации заявки на регистрацию тенанта - Создания Client, Domain, Subscription - Инициализации системных данных (Customer, статусы, способы оплаты, склад, витрина) Новые сервисы: - TenantOnboardingService (tenants/services/onboarding.py) - WarehouseService (inventory/services/warehouse_service.py) - ShowcaseService (inventory/services/showcase_service.py) - PaymentMethodService (orders/services/payment_method_service.py) Рефакторинг: - admin.py: 220 строк → 5 строк (делегирование сервису) - init_tenant_data.py: 259 строк → 68 строк - activate_registration.py: использует сервис - Тесты обновлены для вызова сервиса напрямую При создании тенанта автоматически создаются склад и витрина по умолчанию. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,10 +6,14 @@ from .batch_manager import StockBatchManager
|
|||||||
from .sale_processor import SaleProcessor
|
from .sale_processor import SaleProcessor
|
||||||
from .inventory_processor import InventoryProcessor
|
from .inventory_processor import InventoryProcessor
|
||||||
from .showcase_manager import ShowcaseManager
|
from .showcase_manager import ShowcaseManager
|
||||||
|
from .warehouse_service import WarehouseService
|
||||||
|
from .showcase_service import ShowcaseService
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'StockBatchManager',
|
'StockBatchManager',
|
||||||
'SaleProcessor',
|
'SaleProcessor',
|
||||||
'InventoryProcessor',
|
'InventoryProcessor',
|
||||||
'ShowcaseManager',
|
'ShowcaseManager',
|
||||||
|
'WarehouseService',
|
||||||
|
'ShowcaseService',
|
||||||
]
|
]
|
||||||
|
|||||||
70
myproject/inventory/services/showcase_service.py
Normal file
70
myproject/inventory/services/showcase_service.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Сервис для работы с витринами.
|
||||||
|
"""
|
||||||
|
from inventory.models import Showcase, Warehouse
|
||||||
|
from inventory.services.warehouse_service import WarehouseService
|
||||||
|
|
||||||
|
|
||||||
|
class ShowcaseService:
|
||||||
|
"""Сервис управления витринами"""
|
||||||
|
|
||||||
|
# Константы для витрины по умолчанию
|
||||||
|
DEFAULT_NAME = 'Основная витрина'
|
||||||
|
DEFAULT_DESCRIPTION = 'Витрина по умолчанию, созданная автоматически при регистрации'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_default(cls) -> Showcase | None:
|
||||||
|
"""
|
||||||
|
Получить витрину по умолчанию.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Showcase или None если не найдена
|
||||||
|
"""
|
||||||
|
return Showcase.objects.filter(is_default=True, is_active=True).first()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_or_create_default(cls, warehouse: Warehouse = None) -> tuple[Showcase, bool]:
|
||||||
|
"""
|
||||||
|
Получить или создать витрину по умолчанию.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
warehouse: Склад для витрины. Если не указан, используется склад по умолчанию.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple (Showcase, created: bool)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: если склад не найден и не может быть создан
|
||||||
|
"""
|
||||||
|
if warehouse is None:
|
||||||
|
warehouse, _ = WarehouseService.get_or_create_default()
|
||||||
|
|
||||||
|
return Showcase.objects.get_or_create(
|
||||||
|
warehouse=warehouse,
|
||||||
|
is_default=True,
|
||||||
|
defaults={
|
||||||
|
'name': cls.DEFAULT_NAME,
|
||||||
|
'description': cls.DEFAULT_DESCRIPTION,
|
||||||
|
'is_active': True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def reset_default(cls, warehouse: Warehouse = None) -> Showcase:
|
||||||
|
"""
|
||||||
|
Удалить и пересоздать витрину по умолчанию.
|
||||||
|
Используется при --reset в init_tenant_data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
warehouse: Склад для витрины
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Новый Showcase
|
||||||
|
"""
|
||||||
|
if warehouse is None:
|
||||||
|
warehouse, _ = WarehouseService.get_or_create_default()
|
||||||
|
|
||||||
|
Showcase.objects.filter(warehouse=warehouse, is_default=True).delete()
|
||||||
|
showcase, _ = cls.get_or_create_default(warehouse)
|
||||||
|
return showcase
|
||||||
54
myproject/inventory/services/warehouse_service.py
Normal file
54
myproject/inventory/services/warehouse_service.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Сервис для работы со складами.
|
||||||
|
"""
|
||||||
|
from inventory.models import Warehouse
|
||||||
|
|
||||||
|
|
||||||
|
class WarehouseService:
|
||||||
|
"""Сервис управления складами"""
|
||||||
|
|
||||||
|
# Константы для склада по умолчанию
|
||||||
|
DEFAULT_NAME = 'Основной склад'
|
||||||
|
DEFAULT_DESCRIPTION = 'Склад по умолчанию, созданный автоматически при регистрации'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_default(cls) -> Warehouse | None:
|
||||||
|
"""
|
||||||
|
Получить склад по умолчанию.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Warehouse или None если не найден
|
||||||
|
"""
|
||||||
|
return Warehouse.objects.filter(is_default=True, is_active=True).first()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_or_create_default(cls) -> tuple[Warehouse, bool]:
|
||||||
|
"""
|
||||||
|
Получить или создать склад по умолчанию.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple (Warehouse, created: bool)
|
||||||
|
"""
|
||||||
|
return Warehouse.objects.get_or_create(
|
||||||
|
is_default=True,
|
||||||
|
defaults={
|
||||||
|
'name': cls.DEFAULT_NAME,
|
||||||
|
'description': cls.DEFAULT_DESCRIPTION,
|
||||||
|
'is_active': True,
|
||||||
|
'is_pickup_point': True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def reset_default(cls) -> Warehouse:
|
||||||
|
"""
|
||||||
|
Удалить и пересоздать склад по умолчанию.
|
||||||
|
Используется при --reset в init_tenant_data.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Новый Warehouse
|
||||||
|
"""
|
||||||
|
Warehouse.objects.filter(is_default=True).delete()
|
||||||
|
warehouse, _ = cls.get_or_create_default()
|
||||||
|
return warehouse
|
||||||
@@ -1,56 +1,19 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Management команда для создания стандартных способов оплаты.
|
||||||
|
"""
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from orders.models import PaymentMethod
|
from orders.services import PaymentMethodService
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
help = 'Создаёт стандартные способы оплаты для цветочного магазина'
|
help = 'Создаёт стандартные способы оплаты для цветочного магазина'
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
payment_methods = [
|
results = PaymentMethodService.create_default_methods()
|
||||||
{
|
|
||||||
'code': 'account_balance',
|
|
||||||
'name': 'С баланса счёта',
|
|
||||||
'description': 'Оплата из кошелька клиента',
|
|
||||||
'is_system': True,
|
|
||||||
'order': 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'cash',
|
|
||||||
'name': 'Наличными',
|
|
||||||
'description': 'Оплата наличными деньгами',
|
|
||||||
'is_system': True,
|
|
||||||
'order': 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'card',
|
|
||||||
'name': 'Картой',
|
|
||||||
'description': 'Оплата банковской картой',
|
|
||||||
'is_system': True,
|
|
||||||
'order': 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'online',
|
|
||||||
'name': 'Онлайн',
|
|
||||||
'description': 'Онлайн оплата через платежную систему',
|
|
||||||
'is_system': True,
|
|
||||||
'order': 3
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'legal_entity',
|
|
||||||
'name': 'Безнал от ЮРЛИЦ',
|
|
||||||
'description': 'Безналичный расчёт от юридических лиц',
|
|
||||||
'is_system': True,
|
|
||||||
'order': 4
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
created_count = 0
|
created_count = 0
|
||||||
for method_data in payment_methods:
|
for method, created in results:
|
||||||
method, created = PaymentMethod.objects.get_or_create(
|
|
||||||
code=method_data['code'],
|
|
||||||
defaults=method_data
|
|
||||||
)
|
|
||||||
if created:
|
if created:
|
||||||
created_count += 1
|
created_count += 1
|
||||||
self.stdout.write(
|
self.stdout.write(
|
||||||
|
|||||||
@@ -2,4 +2,10 @@
|
|||||||
Сервисный слой для приложения orders.
|
Сервисный слой для приложения orders.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__all__ = []
|
from .order_status_service import OrderStatusService
|
||||||
|
from .payment_method_service import PaymentMethodService
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'OrderStatusService',
|
||||||
|
'PaymentMethodService',
|
||||||
|
]
|
||||||
|
|||||||
90
myproject/orders/services/payment_method_service.py
Normal file
90
myproject/orders/services/payment_method_service.py
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Сервис для работы со способами оплаты.
|
||||||
|
"""
|
||||||
|
from orders.models import PaymentMethod
|
||||||
|
|
||||||
|
|
||||||
|
class PaymentMethodService:
|
||||||
|
"""Сервис управления способами оплаты"""
|
||||||
|
|
||||||
|
# Системные способы оплаты
|
||||||
|
DEFAULT_METHODS = [
|
||||||
|
{
|
||||||
|
'code': 'account_balance',
|
||||||
|
'name': 'С баланса счёта',
|
||||||
|
'description': 'Оплата из кошелька клиента',
|
||||||
|
'is_system': True,
|
||||||
|
'order': 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'code': 'cash',
|
||||||
|
'name': 'Наличными',
|
||||||
|
'description': 'Оплата наличными деньгами',
|
||||||
|
'is_system': True,
|
||||||
|
'order': 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'code': 'card',
|
||||||
|
'name': 'Картой',
|
||||||
|
'description': 'Оплата банковской картой',
|
||||||
|
'is_system': True,
|
||||||
|
'order': 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'code': 'online',
|
||||||
|
'name': 'Онлайн',
|
||||||
|
'description': 'Онлайн оплата через платежную систему',
|
||||||
|
'is_system': True,
|
||||||
|
'order': 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'code': 'legal_entity',
|
||||||
|
'name': 'Безнал от ЮРЛИЦ',
|
||||||
|
'description': 'Безналичный расчёт от юридических лиц',
|
||||||
|
'is_system': True,
|
||||||
|
'order': 4
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_by_code(cls, code: str) -> PaymentMethod | None:
|
||||||
|
"""
|
||||||
|
Получить способ оплаты по коду.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
code: Код способа оплаты
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PaymentMethod или None
|
||||||
|
"""
|
||||||
|
return PaymentMethod.objects.filter(code=code).first()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_default_methods(cls) -> list[tuple[PaymentMethod, bool]]:
|
||||||
|
"""
|
||||||
|
Создать системные способы оплаты.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Список кортежей (PaymentMethod, created)
|
||||||
|
"""
|
||||||
|
results = []
|
||||||
|
for method_data in cls.DEFAULT_METHODS:
|
||||||
|
method, created = PaymentMethod.objects.get_or_create(
|
||||||
|
code=method_data['code'],
|
||||||
|
defaults=method_data
|
||||||
|
)
|
||||||
|
results.append((method, created))
|
||||||
|
return results
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def reset_default_methods(cls) -> list[PaymentMethod]:
|
||||||
|
"""
|
||||||
|
Удалить и пересоздать системные способы оплаты.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Список созданных PaymentMethod
|
||||||
|
"""
|
||||||
|
PaymentMethod.objects.filter(is_system=True).delete()
|
||||||
|
results = cls.create_default_methods()
|
||||||
|
return [method for method, _ in results]
|
||||||
@@ -4,7 +4,6 @@ from django.contrib import messages
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.db import transaction
|
|
||||||
from .models import Client, Domain, TenantRegistration, Subscription
|
from .models import Client, Domain, TenantRegistration, Subscription
|
||||||
|
|
||||||
|
|
||||||
@@ -263,188 +262,12 @@ class TenantRegistrationAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
return super().changeform_view(request, object_id, form_url, extra_context)
|
return super().changeform_view(request, object_id, form_url, extra_context)
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def _approve_registration(self, registration, admin_user):
|
def _approve_registration(self, registration, admin_user):
|
||||||
"""
|
"""
|
||||||
Активация заявки: создание тенанта, домена и триальной подписки
|
Активация заявки: делегирует всю работу TenantOnboardingService.
|
||||||
"""
|
"""
|
||||||
import logging
|
from tenants.services import TenantOnboardingService
|
||||||
logger = logging.getLogger(__name__)
|
return TenantOnboardingService.activate_registration(registration, admin_user)
|
||||||
|
|
||||||
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 (без порта!)
|
|
||||||
# Продакшен: schema_name.mix.smaa.by
|
|
||||||
from django.conf import settings
|
|
||||||
domain_base = settings.TENANT_DOMAIN_BASE
|
|
||||||
# Убираем порт из домена (django-tenants ищет по hostname без порта)
|
|
||||||
if ':' in domain_base:
|
|
||||||
domain_base = domain_base.split(':')[0]
|
|
||||||
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):
|
def _redirect_to_changelist(self):
|
||||||
"""Редирект на список заявок"""
|
"""Редирект на список заявок"""
|
||||||
|
|||||||
@@ -9,9 +9,8 @@ Management команда для ручной активации заявки н
|
|||||||
python manage.py activate_registration mixflowers
|
python manage.py activate_registration mixflowers
|
||||||
"""
|
"""
|
||||||
from django.core.management.base import BaseCommand, CommandError
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
from django.db import transaction
|
from django.conf import settings
|
||||||
from django.utils import timezone
|
from tenants.models import TenantRegistration, Client, Domain
|
||||||
from tenants.models import TenantRegistration, Client, Domain, Subscription
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
@@ -33,22 +32,20 @@ class Command(BaseCommand):
|
|||||||
schema_name = options['schema_name']
|
schema_name = options['schema_name']
|
||||||
force = options.get('force', False)
|
force = options.get('force', False)
|
||||||
|
|
||||||
|
# Ищем заявку
|
||||||
try:
|
try:
|
||||||
# Ищем заявку
|
|
||||||
registration = TenantRegistration.objects.get(schema_name=schema_name)
|
registration = TenantRegistration.objects.get(schema_name=schema_name)
|
||||||
except TenantRegistration.DoesNotExist:
|
except TenantRegistration.DoesNotExist:
|
||||||
raise CommandError(f'Заявка с schema_name "{schema_name}" не найдена')
|
raise CommandError(f'Заявка с schema_name "{schema_name}" не найдена')
|
||||||
|
|
||||||
self.stdout.write(f'Найдена заявка: {registration.shop_name} ({registration.schema_name})')
|
self._print_registration_info(registration)
|
||||||
self.stdout.write(f'Статус: {registration.get_status_display()}')
|
|
||||||
self.stdout.write(f'Email: {registration.owner_email}')
|
|
||||||
self.stdout.write('')
|
|
||||||
|
|
||||||
# Проверяем статус
|
# Проверяем статус
|
||||||
if registration.status == TenantRegistration.STATUS_APPROVED and not force:
|
if registration.status == TenantRegistration.STATUS_APPROVED and not force:
|
||||||
self.stdout.write(self.style.WARNING('Эта заявка уже была одобрена!'))
|
self.stdout.write(self.style.WARNING('Эта заявка уже была одобрена!'))
|
||||||
if registration.tenant:
|
if registration.tenant:
|
||||||
self.stdout.write(f'Связанный тенант: {registration.tenant.name} (ID: {registration.tenant.id})')
|
self.stdout.write(f'Связанный тенант: {registration.tenant.name} (ID: {registration.tenant.id})')
|
||||||
|
return
|
||||||
else:
|
else:
|
||||||
self.stdout.write(self.style.WARNING('Но тенант не был создан. Используйте --force для пересоздания'))
|
self.stdout.write(self.style.WARNING('Но тенант не был создан. Используйте --force для пересоздания'))
|
||||||
if not self._confirm('Продолжить с --force?'):
|
if not self._confirm('Продолжить с --force?'):
|
||||||
@@ -63,74 +60,47 @@ class Command(BaseCommand):
|
|||||||
'Используйте --force для пересоздания (ОПАСНО: удалит все данные!)'
|
'Используйте --force для пересоздания (ОПАСНО: удалит все данные!)'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Активируем
|
# Если force - удаляем старый тенант
|
||||||
|
if force and existing_client:
|
||||||
|
self._delete_existing_tenant(existing_client)
|
||||||
|
|
||||||
|
# Активация через сервис
|
||||||
|
self._activate(registration)
|
||||||
|
|
||||||
|
def _print_registration_info(self, registration):
|
||||||
|
"""Вывод информации о заявке."""
|
||||||
|
self.stdout.write(f'Найдена заявка: {registration.shop_name} ({registration.schema_name})')
|
||||||
|
self.stdout.write(f'Статус: {registration.get_status_display()}')
|
||||||
|
self.stdout.write(f'Email: {registration.owner_email}')
|
||||||
|
self.stdout.write('')
|
||||||
|
|
||||||
|
def _delete_existing_tenant(self, client):
|
||||||
|
"""Удаление существующего тенанта."""
|
||||||
|
self.stdout.write(self.style.WARNING(f'Удаление существующего тенанта {client.id}...'))
|
||||||
|
try:
|
||||||
|
client.subscription.delete()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
Domain.objects.filter(tenant=client).delete()
|
||||||
|
client.delete()
|
||||||
|
self.stdout.write(self.style.SUCCESS('Старый тенант удален'))
|
||||||
|
|
||||||
|
def _activate(self, registration):
|
||||||
|
"""Активация заявки через сервис."""
|
||||||
self.stdout.write('')
|
self.stdout.write('')
|
||||||
self.stdout.write(self.style.WARNING('НАЧИНАЮ АКТИВАЦИЮ...'))
|
self.stdout.write(self.style.WARNING('НАЧИНАЮ АКТИВАЦИЮ...'))
|
||||||
self.stdout.write('')
|
self.stdout.write('')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with transaction.atomic():
|
from tenants.services import TenantOnboardingService
|
||||||
# Если force - удаляем старый тенант
|
client = TenantOnboardingService.activate_registration(registration, admin_user=None)
|
||||||
if force and existing_client:
|
|
||||||
self.stdout.write(self.style.WARNING(f'Удаление существующего тенанта {existing_client.id}...'))
|
|
||||||
# Удаляем подписку
|
|
||||||
try:
|
|
||||||
existing_client.subscription.delete()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
# Удаляем домены
|
|
||||||
Domain.objects.filter(tenant=existing_client).delete()
|
|
||||||
# Удаляем тенант (это также удалит схему из PostgreSQL)
|
|
||||||
existing_client.delete()
|
|
||||||
self.stdout.write(self.style.SUCCESS('Старый тенант удален'))
|
|
||||||
|
|
||||||
# Создаем тенант
|
# Выводим результат
|
||||||
self.stdout.write(f'Создание тенанта: {registration.schema_name}')
|
domain = Domain.objects.filter(tenant=client, is_primary=True).first()
|
||||||
client = Client.objects.create(
|
domain_base = settings.TENANT_DOMAIN_BASE
|
||||||
schema_name=registration.schema_name,
|
if ':' in domain_base:
|
||||||
name=registration.shop_name,
|
domain_base = domain_base.split(':')[0]
|
||||||
owner_email=registration.owner_email,
|
domain_name = f"{registration.schema_name}.{domain_base}"
|
||||||
owner_name=registration.owner_name,
|
|
||||||
phone=registration.phone,
|
|
||||||
is_active=True
|
|
||||||
)
|
|
||||||
self.stdout.write(self.style.SUCCESS(f'✓ Тенант создан (ID: {client.id})'))
|
|
||||||
|
|
||||||
# Создаем домен
|
|
||||||
domain_base = getattr(settings, 'TENANT_DOMAIN_BASE', 'localhost')
|
|
||||||
# Убираем порт из домена (django-tenants ищет по hostname без порта)
|
|
||||||
if ':' in domain_base:
|
|
||||||
domain_base = domain_base.split(':')[0]
|
|
||||||
domain_name = f"{registration.schema_name}.{domain_base}"
|
|
||||||
self.stdout.write(f'Создание домена: {domain_name}')
|
|
||||||
domain = Domain.objects.create(
|
|
||||||
domain=domain_name,
|
|
||||||
tenant=client,
|
|
||||||
is_primary=True
|
|
||||||
)
|
|
||||||
self.stdout.write(self.style.SUCCESS(f'✓ Домен создан (ID: {domain.id})'))
|
|
||||||
|
|
||||||
# Создаем триальную подписку
|
|
||||||
self.stdout.write('Создание триальной подписки на 90 дней')
|
|
||||||
subscription = Subscription.create_trial(client)
|
|
||||||
self.stdout.write(self.style.SUCCESS(
|
|
||||||
f'✓ Подписка создана (ID: {subscription.id}), '
|
|
||||||
f'истекает: {subscription.expires_at.strftime("%Y-%m-%d")}'
|
|
||||||
))
|
|
||||||
|
|
||||||
# Инициализация системных данных
|
|
||||||
self.stdout.write('Инициализация системных данных...')
|
|
||||||
from django.core.management import call_command
|
|
||||||
call_command('init_tenant_data', schema=client.schema_name)
|
|
||||||
self.stdout.write(self.style.SUCCESS('✓ Системные данные созданы'))
|
|
||||||
|
|
||||||
# Обновляем заявку
|
|
||||||
registration.status = TenantRegistration.STATUS_APPROVED
|
|
||||||
registration.processed_at = timezone.now()
|
|
||||||
registration.processed_by = None # Активировано через команду
|
|
||||||
registration.tenant = client
|
|
||||||
registration.save()
|
|
||||||
self.stdout.write(self.style.SUCCESS('✓ Заявка обновлена'))
|
|
||||||
|
|
||||||
self.stdout.write('')
|
self.stdout.write('')
|
||||||
self.stdout.write(self.style.SUCCESS('=' * 60))
|
self.stdout.write(self.style.SUCCESS('=' * 60))
|
||||||
@@ -140,7 +110,7 @@ class Command(BaseCommand):
|
|||||||
self.stdout.write(f'Магазин: {client.name}')
|
self.stdout.write(f'Магазин: {client.name}')
|
||||||
self.stdout.write(f'Schema: {client.schema_name}')
|
self.stdout.write(f'Schema: {client.schema_name}')
|
||||||
self.stdout.write(f'Домен: http://{domain_name}:8000/')
|
self.stdout.write(f'Домен: http://{domain_name}:8000/')
|
||||||
self.stdout.write(f'Подписка до: {subscription.expires_at.strftime("%Y-%m-%d")} ({subscription.days_left()} дней)')
|
self.stdout.write(f'Подписка до: {client.subscription.expires_at.strftime("%Y-%m-%d")} ({client.subscription.days_left()} дней)')
|
||||||
self.stdout.write('')
|
self.stdout.write('')
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -150,6 +120,6 @@ class Command(BaseCommand):
|
|||||||
raise CommandError('Активация не удалась')
|
raise CommandError('Активация не удалась')
|
||||||
|
|
||||||
def _confirm(self, question):
|
def _confirm(self, question):
|
||||||
"""Запрос подтверждения у пользователя"""
|
"""Запрос подтверждения у пользователя."""
|
||||||
answer = input(f'{question} (yes/no): ')
|
answer = input(f'{question} (yes/no): ')
|
||||||
return answer.lower() in ['yes', 'y', 'да']
|
return answer.lower() in ['yes', 'y', 'да']
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ Management команда для инициализации всех систе
|
|||||||
- Системного клиента (анонимный покупатель для POS)
|
- Системного клиента (анонимный покупатель для POS)
|
||||||
- Системные статусы заказов
|
- Системные статусы заказов
|
||||||
- Системные способы оплаты
|
- Системные способы оплаты
|
||||||
|
- Склад по умолчанию
|
||||||
|
- Витрину по умолчанию
|
||||||
|
|
||||||
Использование:
|
Использование:
|
||||||
# Инициализация для конкретного тенанта
|
# Инициализация для конкретного тенанта
|
||||||
@@ -15,12 +17,11 @@ Management команда для инициализации всех систе
|
|||||||
python manage.py init_tenant_data --schema=anatol --reset
|
python manage.py init_tenant_data --schema=anatol --reset
|
||||||
"""
|
"""
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from django.db import connection
|
|
||||||
from django_tenants.utils import get_tenant_model, schema_context
|
from django_tenants.utils import get_tenant_model, schema_context
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
help = 'Инициализация всех системных данных тенанта (клиент, статусы, способы оплаты)'
|
help = 'Инициализация всех системных данных тенанта (клиент, статусы, способы оплаты, склад, витрина)'
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
@@ -48,135 +49,16 @@ class Command(BaseCommand):
|
|||||||
return
|
return
|
||||||
|
|
||||||
self.stdout.write(self.style.SUCCESS('\n=== Инициализация системных данных тенанта ===\n'))
|
self.stdout.write(self.style.SUCCESS('\n=== Инициализация системных данных тенанта ===\n'))
|
||||||
self.stdout.write(f'Тенант: {tenant.name} ({schema_name})\n')
|
self.stdout.write(f'Тенант: {tenant.name} ({schema_name})')
|
||||||
|
if reset:
|
||||||
|
self.stdout.write(self.style.WARNING('Режим: RESET (пересоздание данных)\n'))
|
||||||
|
else:
|
||||||
|
self.stdout.write('')
|
||||||
|
|
||||||
# Переключаемся на схему тенанта
|
# Переключаемся на схему тенанта и вызываем сервис
|
||||||
with schema_context(schema_name):
|
with schema_context(schema_name):
|
||||||
# 1. Создаём системного клиента
|
from tenants.services import TenantOnboardingService
|
||||||
self.stdout.write('\n' + '='*70)
|
TenantOnboardingService.init_tenant_data(reset=reset)
|
||||||
self.stdout.write('[1] Создание системного клиента...')
|
|
||||||
self.stdout.write('='*70)
|
|
||||||
|
|
||||||
from customers.models import Customer
|
|
||||||
|
|
||||||
if reset:
|
|
||||||
# Удаляем существующего системного клиента
|
|
||||||
system_customers = Customer.objects.filter(email="system@pos.customer")
|
|
||||||
if system_customers.exists():
|
|
||||||
count = system_customers.count()
|
|
||||||
system_customers.delete()
|
|
||||||
self.stdout.write(self.style.WARNING(f' Удалено системных клиентов: {count}'))
|
|
||||||
|
|
||||||
try:
|
|
||||||
system_customer, created = Customer.get_or_create_system_customer()
|
|
||||||
if created:
|
|
||||||
self.stdout.write(self.style.SUCCESS(f' ✓ Системный клиент создан: {system_customer.name}'))
|
|
||||||
self.stdout.write(f' Email: {system_customer.email}')
|
|
||||||
self.stdout.write(f' ID: {system_customer.id}')
|
|
||||||
else:
|
|
||||||
self.stdout.write(self.style.WARNING(f' • Системный клиент уже существует: {system_customer.name}'))
|
|
||||||
self.stdout.write(f' ID: {system_customer.id}')
|
|
||||||
except Exception as e:
|
|
||||||
self.stdout.write(self.style.ERROR(f' ✗ Ошибка при создании системного клиента: {e}'))
|
|
||||||
|
|
||||||
# 2. Создаём системные статусы заказов
|
|
||||||
self.stdout.write('\n' + '='*70)
|
|
||||||
self.stdout.write('[2] Создание системных статусов заказов...')
|
|
||||||
self.stdout.write('='*70)
|
|
||||||
|
|
||||||
from orders.models import OrderStatus
|
|
||||||
from orders.services.order_status_service import OrderStatusService
|
|
||||||
|
|
||||||
if reset:
|
|
||||||
count = OrderStatus.objects.filter(is_system=True).count()
|
|
||||||
if count > 0:
|
|
||||||
OrderStatus.objects.filter(is_system=True).delete()
|
|
||||||
self.stdout.write(self.style.WARNING(f' Удалено системных статусов: {count}'))
|
|
||||||
|
|
||||||
try:
|
|
||||||
OrderStatusService.create_default_statuses()
|
|
||||||
statuses = OrderStatus.objects.filter(is_system=True).order_by('order')
|
|
||||||
self.stdout.write(self.style.SUCCESS(f' ✓ Создано системных статусов: {statuses.count()}'))
|
|
||||||
|
|
||||||
for status in statuses:
|
|
||||||
end_type = ''
|
|
||||||
if status.is_positive_end:
|
|
||||||
end_type = ' [Успешный]'
|
|
||||||
elif status.is_negative_end:
|
|
||||||
end_type = ' [Отрицательный]'
|
|
||||||
self.stdout.write(f' - {status.name:<20} ({status.code:<15}){end_type}')
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.stdout.write(self.style.ERROR(f' ✗ Ошибка при создании статусов: {e}'))
|
|
||||||
|
|
||||||
# 3. Создаём системные способы оплаты
|
|
||||||
self.stdout.write('\n' + '='*70)
|
|
||||||
self.stdout.write('[3] Создание системных способов оплаты...')
|
|
||||||
self.stdout.write('='*70)
|
|
||||||
|
|
||||||
from orders.models import PaymentMethod
|
|
||||||
|
|
||||||
if reset:
|
|
||||||
count = PaymentMethod.objects.filter(is_system=True).count()
|
|
||||||
if count > 0:
|
|
||||||
PaymentMethod.objects.filter(is_system=True).delete()
|
|
||||||
self.stdout.write(self.style.WARNING(f' Удалено системных способов оплаты: {count}'))
|
|
||||||
|
|
||||||
payment_methods = [
|
|
||||||
{
|
|
||||||
'code': 'account_balance',
|
|
||||||
'name': 'С баланса счёта',
|
|
||||||
'description': 'Оплата из кошелька клиента',
|
|
||||||
'is_system': True,
|
|
||||||
'order': 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'cash',
|
|
||||||
'name': 'Наличными',
|
|
||||||
'description': 'Оплата наличными деньгами',
|
|
||||||
'is_system': True,
|
|
||||||
'order': 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'card',
|
|
||||||
'name': 'Картой',
|
|
||||||
'description': 'Оплата банковской картой',
|
|
||||||
'is_system': True,
|
|
||||||
'order': 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'online',
|
|
||||||
'name': 'Онлайн',
|
|
||||||
'description': 'Онлайн оплата через платежную систему',
|
|
||||||
'is_system': True,
|
|
||||||
'order': 3
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'code': 'legal_entity',
|
|
||||||
'name': 'Безнал от ЮРЛИЦ',
|
|
||||||
'description': 'Безналичный расчёт от юридических лиц',
|
|
||||||
'is_system': True,
|
|
||||||
'order': 4
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
created_count = 0
|
|
||||||
try:
|
|
||||||
for method_data in payment_methods:
|
|
||||||
method, created = PaymentMethod.objects.get_or_create(
|
|
||||||
code=method_data['code'],
|
|
||||||
defaults=method_data
|
|
||||||
)
|
|
||||||
if created:
|
|
||||||
created_count += 1
|
|
||||||
self.stdout.write(self.style.SUCCESS(f' ✓ Создан способ оплаты: {method.name}'))
|
|
||||||
else:
|
|
||||||
self.stdout.write(self.style.WARNING(f' • Уже существует: {method.name}'))
|
|
||||||
|
|
||||||
self.stdout.write(self.style.SUCCESS(f'\n Готово! Создано {created_count} новых способов оплаты.'))
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.stdout.write(self.style.ERROR(f' ✗ Ошибка при создании способов оплаты: {e}'))
|
|
||||||
|
|
||||||
# Итоговое сообщение
|
# Итоговое сообщение
|
||||||
self.stdout.write('\n' + '='*70)
|
self.stdout.write('\n' + '='*70)
|
||||||
|
|||||||
12
myproject/tenants/services/__init__.py
Normal file
12
myproject/tenants/services/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Сервисы для работы с тенантами.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .onboarding import TenantOnboardingService
|
||||||
|
from .email import send_password_setup_email
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'TenantOnboardingService',
|
||||||
|
'send_password_setup_email',
|
||||||
|
]
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
Сервисы для работы с тенантами
|
Сервис отправки email для тенантов.
|
||||||
"""
|
"""
|
||||||
import uuid
|
import uuid
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
268
myproject/tenants/services/onboarding.py
Normal file
268
myproject/tenants/services/onboarding.py
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Сервис онбординга новых тенантов.
|
||||||
|
|
||||||
|
Единственный источник истины для создания тенанта и всех связанных сущностей.
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from django.db import connection, transaction
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.core.management import call_command
|
||||||
|
|
||||||
|
from tenants.models import Client, Domain, TenantRegistration, Subscription
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class TenantOnboardingService:
|
||||||
|
"""
|
||||||
|
Сервис онбординга нового тенанта.
|
||||||
|
|
||||||
|
Отвечает за полный цикл создания тенанта:
|
||||||
|
- Создание Client (тенант)
|
||||||
|
- Создание Domain (домен)
|
||||||
|
- Создание Subscription (подписка)
|
||||||
|
- Миграции схемы
|
||||||
|
- Создание пользователей (admin, owner)
|
||||||
|
- Инициализация системных данных
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@transaction.atomic
|
||||||
|
def activate_registration(cls, registration: TenantRegistration, admin_user=None) -> Client:
|
||||||
|
"""
|
||||||
|
Полная активация заявки на регистрацию тенанта.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
registration: Заявка на регистрацию
|
||||||
|
admin_user: Администратор, одобривший заявку (опционально)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Client: Созданный тенант
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: Если тенант уже существует
|
||||||
|
"""
|
||||||
|
logger.info(f"Начало активации заявки: {registration.schema_name}")
|
||||||
|
|
||||||
|
# 1. Проверка уникальности
|
||||||
|
if Client.objects.filter(schema_name=registration.schema_name).exists():
|
||||||
|
raise ValueError(f"Тенант с schema_name '{registration.schema_name}' уже существует!")
|
||||||
|
|
||||||
|
# 2. Создание тенанта
|
||||||
|
client = cls._create_client(registration)
|
||||||
|
|
||||||
|
# 3. Создание домена
|
||||||
|
domain = cls._create_domain(client, registration)
|
||||||
|
|
||||||
|
# 4. Применение миграций
|
||||||
|
cls._run_migrations(client)
|
||||||
|
|
||||||
|
# 5. Создание подписки
|
||||||
|
subscription = cls._create_subscription(client)
|
||||||
|
|
||||||
|
# 6. Инициализация данных тенанта (в контексте схемы)
|
||||||
|
connection.set_tenant(client)
|
||||||
|
try:
|
||||||
|
cls._init_tenant_users(registration)
|
||||||
|
cls._init_tenant_data()
|
||||||
|
finally:
|
||||||
|
connection.set_schema_to_public()
|
||||||
|
|
||||||
|
# 7. Отправка email владельцу
|
||||||
|
cls._send_welcome_email(registration)
|
||||||
|
|
||||||
|
# 8. Обновление статуса заявки
|
||||||
|
cls._finalize_registration(registration, client, admin_user)
|
||||||
|
|
||||||
|
logger.info(f"Заявка {registration.id} успешно активирована. Тенант: {client.id}")
|
||||||
|
return client
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def init_tenant_data(cls, reset: bool = False):
|
||||||
|
"""
|
||||||
|
Инициализация системных данных тенанта.
|
||||||
|
|
||||||
|
Вызывается в контексте схемы тенанта (connection.set_tenant уже выполнен).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
reset: Если True, удаляет и пересоздаёт данные
|
||||||
|
"""
|
||||||
|
from customers.models import Customer
|
||||||
|
from orders.services import OrderStatusService, PaymentMethodService
|
||||||
|
from inventory.services import WarehouseService, ShowcaseService
|
||||||
|
|
||||||
|
# 1. Системный клиент
|
||||||
|
logger.info("Создание системного клиента...")
|
||||||
|
if reset:
|
||||||
|
Customer.objects.filter(is_system_customer=True).delete()
|
||||||
|
customer, created = Customer.get_or_create_system_customer()
|
||||||
|
logger.info(f"Системный клиент: {'создан' if created else 'уже существует'}")
|
||||||
|
|
||||||
|
# 2. Статусы заказов
|
||||||
|
logger.info("Создание статусов заказов...")
|
||||||
|
if reset:
|
||||||
|
from orders.models import OrderStatus
|
||||||
|
OrderStatus.objects.filter(is_system=True).delete()
|
||||||
|
OrderStatusService.create_default_statuses()
|
||||||
|
logger.info("Статусы заказов созданы")
|
||||||
|
|
||||||
|
# 3. Способы оплаты
|
||||||
|
logger.info("Создание способов оплаты...")
|
||||||
|
if reset:
|
||||||
|
PaymentMethodService.reset_default_methods()
|
||||||
|
else:
|
||||||
|
PaymentMethodService.create_default_methods()
|
||||||
|
logger.info("Способы оплаты созданы")
|
||||||
|
|
||||||
|
# 4. Склад по умолчанию
|
||||||
|
logger.info("Создание склада по умолчанию...")
|
||||||
|
if reset:
|
||||||
|
warehouse = WarehouseService.reset_default()
|
||||||
|
else:
|
||||||
|
warehouse, created = WarehouseService.get_or_create_default()
|
||||||
|
logger.info(f"Склад по умолчанию: {warehouse.name}")
|
||||||
|
|
||||||
|
# 5. Витрина по умолчанию
|
||||||
|
logger.info("Создание витрины по умолчанию...")
|
||||||
|
if reset:
|
||||||
|
showcase = ShowcaseService.reset_default(warehouse)
|
||||||
|
else:
|
||||||
|
showcase, created = ShowcaseService.get_or_create_default(warehouse)
|
||||||
|
logger.info(f"Витрина по умолчанию: {showcase.name}")
|
||||||
|
|
||||||
|
# ==================== Приватные методы ====================
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _create_client(cls, registration: TenantRegistration) -> Client:
|
||||||
|
"""Создание тенанта (Client)."""
|
||||||
|
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}")
|
||||||
|
return client
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _create_domain(cls, client: Client, registration: TenantRegistration) -> Domain:
|
||||||
|
"""Создание домена для тенанта."""
|
||||||
|
domain_base = settings.TENANT_DOMAIN_BASE
|
||||||
|
# Убираем порт из домена (django-tenants ищет по hostname без порта)
|
||||||
|
if ':' in domain_base:
|
||||||
|
domain_base = domain_base.split(':')[0]
|
||||||
|
|
||||||
|
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}")
|
||||||
|
return domain
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _run_migrations(cls, client: Client):
|
||||||
|
"""Применение миграций для схемы тенанта."""
|
||||||
|
logger.info(f"Применение миграций для тенанта: {client.schema_name}")
|
||||||
|
try:
|
||||||
|
call_command('migrate_schemas', schema_name=client.schema_name, verbosity=1)
|
||||||
|
logger.info("Миграции успешно применены")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при применении миграций: {e}", exc_info=True)
|
||||||
|
raise
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _create_subscription(cls, client: Client) -> Subscription:
|
||||||
|
"""Создание триальной подписки."""
|
||||||
|
logger.info(f"Создание триальной подписки для тенанта: {client.id}")
|
||||||
|
subscription = Subscription.create_trial(client)
|
||||||
|
logger.info(f"Подписка создана: {subscription.id}, истекает: {subscription.expires_at}")
|
||||||
|
return subscription
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _init_tenant_users(cls, registration: TenantRegistration):
|
||||||
|
"""Создание пользователей тенанта (admin и owner)."""
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
# Суперпользователь (системный admin)
|
||||||
|
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}")
|
||||||
|
else:
|
||||||
|
logger.warning(f"Пользователь {settings.TENANT_ADMIN_EMAIL} уже существует")
|
||||||
|
|
||||||
|
# Владелец тенанта
|
||||||
|
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,
|
||||||
|
is_superuser=False
|
||||||
|
)
|
||||||
|
owner.is_email_confirmed = True
|
||||||
|
owner.email_confirmed_at = timezone.now()
|
||||||
|
owner.is_active = False # Неактивен до установки пароля
|
||||||
|
owner.save()
|
||||||
|
logger.info(f"Аккаунт владельца создан: {owner.id}")
|
||||||
|
|
||||||
|
# Роли
|
||||||
|
cls._assign_owner_role(owner)
|
||||||
|
else:
|
||||||
|
logger.warning(f"Пользователь {registration.owner_email} уже существует")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _assign_owner_role(cls, owner):
|
||||||
|
"""Назначение роли owner владельцу тенанта."""
|
||||||
|
try:
|
||||||
|
from user_roles.services import RoleService
|
||||||
|
from user_roles.models import Role
|
||||||
|
|
||||||
|
RoleService.create_default_roles()
|
||||||
|
RoleService.assign_role_to_user(owner, Role.OWNER, created_by=None)
|
||||||
|
logger.info(f"Роль owner назначена: {owner.email}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при назначении роли: {e}", exc_info=True)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _init_tenant_data(cls):
|
||||||
|
"""Инициализация системных данных тенанта."""
|
||||||
|
try:
|
||||||
|
cls.init_tenant_data(reset=False)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при инициализации данных: {e}", exc_info=True)
|
||||||
|
# Не прерываем процесс
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _send_welcome_email(cls, registration: TenantRegistration):
|
||||||
|
"""Отправка приветственного письма владельцу."""
|
||||||
|
try:
|
||||||
|
from tenants.services.email 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)
|
||||||
|
# Не прерываем процесс
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _finalize_registration(cls, registration: TenantRegistration, client: Client, admin_user):
|
||||||
|
"""Финализация заявки на регистрацию."""
|
||||||
|
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} финализирована")
|
||||||
@@ -9,6 +9,8 @@
|
|||||||
4. Создание способов оплаты
|
4. Создание способов оплаты
|
||||||
5. Создание системных статусов заказов
|
5. Создание системных статусов заказов
|
||||||
6. Создание системного клиента
|
6. Создание системного клиента
|
||||||
|
7. Создание склада по умолчанию
|
||||||
|
8. Создание витрины по умолчанию
|
||||||
"""
|
"""
|
||||||
from django.test import TestCase, TransactionTestCase
|
from django.test import TestCase, TransactionTestCase
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
@@ -19,6 +21,7 @@ from django_tenants.utils import schema_context
|
|||||||
from tenants.models import Client, Domain, TenantRegistration
|
from tenants.models import Client, Domain, TenantRegistration
|
||||||
from orders.models import PaymentMethod, OrderStatus
|
from orders.models import PaymentMethod, OrderStatus
|
||||||
from customers.models import Customer
|
from customers.models import Customer
|
||||||
|
from inventory.models import Warehouse, Showcase
|
||||||
|
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
@@ -77,12 +80,9 @@ class TenantCreationIntegrationTest(TransactionTestCase):
|
|||||||
status=TenantRegistration.STATUS_PENDING
|
status=TenantRegistration.STATUS_PENDING
|
||||||
)
|
)
|
||||||
|
|
||||||
# 2. Активируем заявку (как в админке)
|
# 2. Активируем заявку через сервис
|
||||||
from tenants.admin import TenantRegistrationAdmin
|
from tenants.services import TenantOnboardingService
|
||||||
from django.contrib.admin.sites import AdminSite
|
tenant = TenantOnboardingService.activate_registration(registration, self.admin_user)
|
||||||
|
|
||||||
admin = TenantRegistrationAdmin(TenantRegistration, AdminSite())
|
|
||||||
tenant = admin._approve_registration(registration, self.admin_user)
|
|
||||||
|
|
||||||
# 3. Проверяем что тенант создан
|
# 3. Проверяем что тенант создан
|
||||||
self.assertIsNotNone(tenant)
|
self.assertIsNotNone(tenant)
|
||||||
@@ -145,11 +145,8 @@ class TenantCreationIntegrationTest(TransactionTestCase):
|
|||||||
status=TenantRegistration.STATUS_PENDING
|
status=TenantRegistration.STATUS_PENDING
|
||||||
)
|
)
|
||||||
|
|
||||||
from tenants.admin import TenantRegistrationAdmin
|
from tenants.services import TenantOnboardingService
|
||||||
from django.contrib.admin.sites import AdminSite
|
tenant = TenantOnboardingService.activate_registration(registration, self.admin_user)
|
||||||
|
|
||||||
admin = TenantRegistrationAdmin(TenantRegistration, AdminSite())
|
|
||||||
tenant = admin._approve_registration(registration, self.admin_user)
|
|
||||||
|
|
||||||
# Проверяем статусы заказов
|
# Проверяем статусы заказов
|
||||||
with schema_context('test_shop_statuses'):
|
with schema_context('test_shop_statuses'):
|
||||||
@@ -224,11 +221,8 @@ class TenantCreationIntegrationTest(TransactionTestCase):
|
|||||||
status=TenantRegistration.STATUS_PENDING
|
status=TenantRegistration.STATUS_PENDING
|
||||||
)
|
)
|
||||||
|
|
||||||
from tenants.admin import TenantRegistrationAdmin
|
from tenants.services import TenantOnboardingService
|
||||||
from django.contrib.admin.sites import AdminSite
|
tenant = TenantOnboardingService.activate_registration(registration, self.admin_user)
|
||||||
|
|
||||||
admin = TenantRegistrationAdmin(TenantRegistration, AdminSite())
|
|
||||||
tenant = admin._approve_registration(registration, self.admin_user)
|
|
||||||
|
|
||||||
# Проверяем системного клиента
|
# Проверяем системного клиента
|
||||||
with schema_context('test_shop_customers'):
|
with schema_context('test_shop_customers'):
|
||||||
@@ -242,6 +236,71 @@ class TenantCreationIntegrationTest(TransactionTestCase):
|
|||||||
self.assertTrue(system_customer.is_system_customer)
|
self.assertTrue(system_customer.is_system_customer)
|
||||||
self.assertEqual(system_customer.name, 'Анонимный клиент')
|
self.assertEqual(system_customer.name, 'Анонимный клиент')
|
||||||
|
|
||||||
|
def test_new_tenant_gets_default_warehouse(self):
|
||||||
|
"""
|
||||||
|
Тест: Новый тенант получает склад по умолчанию.
|
||||||
|
"""
|
||||||
|
# Создаём и активируем тенант
|
||||||
|
registration = TenantRegistration.objects.create(
|
||||||
|
shop_name='Тестовый магазин склад',
|
||||||
|
schema_name='test_shop_warehouse',
|
||||||
|
owner_email='owner_warehouse@test.com',
|
||||||
|
owner_name='Тест Владелец Склад',
|
||||||
|
phone='+375291111118',
|
||||||
|
status=TenantRegistration.STATUS_PENDING
|
||||||
|
)
|
||||||
|
|
||||||
|
from tenants.services import TenantOnboardingService
|
||||||
|
tenant = TenantOnboardingService.activate_registration(registration, self.admin_user)
|
||||||
|
|
||||||
|
# Проверяем склад по умолчанию
|
||||||
|
with schema_context('test_shop_warehouse'):
|
||||||
|
default_warehouse = Warehouse.objects.filter(is_default=True).first()
|
||||||
|
|
||||||
|
self.assertIsNotNone(
|
||||||
|
default_warehouse,
|
||||||
|
"Склад по умолчанию не создан"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(default_warehouse.is_default)
|
||||||
|
self.assertTrue(default_warehouse.is_active)
|
||||||
|
self.assertTrue(default_warehouse.is_pickup_point)
|
||||||
|
self.assertEqual(default_warehouse.name, 'Основной склад')
|
||||||
|
|
||||||
|
def test_new_tenant_gets_default_showcase(self):
|
||||||
|
"""
|
||||||
|
Тест: Новый тенант получает витрину по умолчанию.
|
||||||
|
"""
|
||||||
|
# Создаём и активируем тенант
|
||||||
|
registration = TenantRegistration.objects.create(
|
||||||
|
shop_name='Тестовый магазин витрина',
|
||||||
|
schema_name='test_shop_showcase',
|
||||||
|
owner_email='owner_showcase@test.com',
|
||||||
|
owner_name='Тест Владелец Витрина',
|
||||||
|
phone='+375291111119',
|
||||||
|
status=TenantRegistration.STATUS_PENDING
|
||||||
|
)
|
||||||
|
|
||||||
|
from tenants.services import TenantOnboardingService
|
||||||
|
tenant = TenantOnboardingService.activate_registration(registration, self.admin_user)
|
||||||
|
|
||||||
|
# Проверяем витрину по умолчанию
|
||||||
|
with schema_context('test_shop_showcase'):
|
||||||
|
default_showcase = Showcase.objects.filter(is_default=True).first()
|
||||||
|
|
||||||
|
self.assertIsNotNone(
|
||||||
|
default_showcase,
|
||||||
|
"Витрина по умолчанию не создана"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(default_showcase.is_default)
|
||||||
|
self.assertTrue(default_showcase.is_active)
|
||||||
|
self.assertEqual(default_showcase.name, 'Основная витрина')
|
||||||
|
|
||||||
|
# Проверяем что витрина привязана к складу по умолчанию
|
||||||
|
self.assertIsNotNone(default_showcase.warehouse)
|
||||||
|
self.assertTrue(default_showcase.warehouse.is_default)
|
||||||
|
|
||||||
def test_new_tenant_gets_superuser(self):
|
def test_new_tenant_gets_superuser(self):
|
||||||
"""
|
"""
|
||||||
Тест: Новый тенант получает суперпользователя для доступа к админке.
|
Тест: Новый тенант получает суперпользователя для доступа к админке.
|
||||||
@@ -256,11 +315,8 @@ class TenantCreationIntegrationTest(TransactionTestCase):
|
|||||||
status=TenantRegistration.STATUS_PENDING
|
status=TenantRegistration.STATUS_PENDING
|
||||||
)
|
)
|
||||||
|
|
||||||
from tenants.admin import TenantRegistrationAdmin
|
from tenants.services import TenantOnboardingService
|
||||||
from django.contrib.admin.sites import AdminSite
|
tenant = TenantOnboardingService.activate_registration(registration, self.admin_user)
|
||||||
|
|
||||||
admin = TenantRegistrationAdmin(TenantRegistration, AdminSite())
|
|
||||||
tenant = admin._approve_registration(registration, self.admin_user)
|
|
||||||
|
|
||||||
# Проверяем суперпользователя
|
# Проверяем суперпользователя
|
||||||
with schema_context('test_shop_admin'):
|
with schema_context('test_shop_admin'):
|
||||||
@@ -290,11 +346,8 @@ class TenantCreationIntegrationTest(TransactionTestCase):
|
|||||||
status=TenantRegistration.STATUS_PENDING
|
status=TenantRegistration.STATUS_PENDING
|
||||||
)
|
)
|
||||||
|
|
||||||
from tenants.admin import TenantRegistrationAdmin
|
from tenants.services import TenantOnboardingService
|
||||||
from django.contrib.admin.sites import AdminSite
|
tenant = TenantOnboardingService.activate_registration(registration, self.admin_user)
|
||||||
|
|
||||||
admin = TenantRegistrationAdmin(TenantRegistration, AdminSite())
|
|
||||||
tenant = admin._approve_registration(registration, self.admin_user)
|
|
||||||
|
|
||||||
# Проверяем домен
|
# Проверяем домен
|
||||||
domain = Domain.objects.filter(tenant=tenant).first()
|
domain = Domain.objects.filter(tenant=tenant).first()
|
||||||
@@ -325,11 +378,8 @@ class TenantCreationIntegrationTest(TransactionTestCase):
|
|||||||
self.assertEqual(registration.status, TenantRegistration.STATUS_PENDING)
|
self.assertEqual(registration.status, TenantRegistration.STATUS_PENDING)
|
||||||
|
|
||||||
# Активируем
|
# Активируем
|
||||||
from tenants.admin import TenantRegistrationAdmin
|
from tenants.services import TenantOnboardingService
|
||||||
from django.contrib.admin.sites import AdminSite
|
tenant = TenantOnboardingService.activate_registration(registration, self.admin_user)
|
||||||
|
|
||||||
admin = TenantRegistrationAdmin(TenantRegistration, AdminSite())
|
|
||||||
tenant = admin._approve_registration(registration, self.admin_user)
|
|
||||||
|
|
||||||
# Обновляем объект из БД
|
# Обновляем объект из БД
|
||||||
registration.refresh_from_db()
|
registration.refresh_from_db()
|
||||||
@@ -357,11 +407,8 @@ class TenantCreationIntegrationTest(TransactionTestCase):
|
|||||||
status=TenantRegistration.STATUS_PENDING
|
status=TenantRegistration.STATUS_PENDING
|
||||||
)
|
)
|
||||||
|
|
||||||
from tenants.admin import TenantRegistrationAdmin
|
from tenants.services import TenantOnboardingService
|
||||||
from django.contrib.admin.sites import AdminSite
|
tenant = TenantOnboardingService.activate_registration(registration, self.admin_user)
|
||||||
|
|
||||||
admin = TenantRegistrationAdmin(TenantRegistration, AdminSite())
|
|
||||||
tenant = admin._approve_registration(registration, self.admin_user)
|
|
||||||
|
|
||||||
# Проверяем тенант
|
# Проверяем тенант
|
||||||
self.assertIsNotNone(tenant)
|
self.assertIsNotNone(tenant)
|
||||||
@@ -389,6 +436,17 @@ class TenantCreationIntegrationTest(TransactionTestCase):
|
|||||||
superuser = User.objects.filter(email=settings.TENANT_ADMIN_EMAIL).first()
|
superuser = User.objects.filter(email=settings.TENANT_ADMIN_EMAIL).first()
|
||||||
self.assertIsNotNone(superuser, "Должен быть суперпользователь")
|
self.assertIsNotNone(superuser, "Должен быть суперпользователь")
|
||||||
|
|
||||||
|
# 5. Склад по умолчанию
|
||||||
|
default_warehouse = Warehouse.objects.filter(is_default=True).first()
|
||||||
|
self.assertIsNotNone(default_warehouse, "Должен быть склад по умолчанию")
|
||||||
|
self.assertTrue(default_warehouse.is_active)
|
||||||
|
|
||||||
|
# 6. Витрина по умолчанию
|
||||||
|
default_showcase = Showcase.objects.filter(is_default=True).first()
|
||||||
|
self.assertIsNotNone(default_showcase, "Должна быть витрина по умолчанию")
|
||||||
|
self.assertTrue(default_showcase.is_active)
|
||||||
|
self.assertEqual(default_showcase.warehouse, default_warehouse)
|
||||||
|
|
||||||
# Проверяем заявку
|
# Проверяем заявку
|
||||||
registration.refresh_from_db()
|
registration.refresh_from_db()
|
||||||
self.assertEqual(registration.status, TenantRegistration.STATUS_APPROVED)
|
self.assertEqual(registration.status, TenantRegistration.STATUS_APPROVED)
|
||||||
@@ -403,4 +461,6 @@ class TenantCreationIntegrationTest(TransactionTestCase):
|
|||||||
print(f"Статусов заказов: {order_statuses_count}")
|
print(f"Статусов заказов: {order_statuses_count}")
|
||||||
print(f"Системный клиент: ✓")
|
print(f"Системный клиент: ✓")
|
||||||
print(f"Суперпользователь: ✓")
|
print(f"Суперпользователь: ✓")
|
||||||
|
print(f"Склад по умолчанию: ✓")
|
||||||
|
print(f"Витрина по умолчанию: ✓")
|
||||||
print("=" * 70)
|
print("=" * 70)
|
||||||
|
|||||||
Reference in New Issue
Block a user