feat: Добавлены команды управления данными тенантов и исправлены фильтры по статусу товаров
Добавлено: - Команда clear_tenant_data для полной очистки данных тенанта без удаления схемы * Очищает все таблицы через TRUNCATE CASCADE * Сбрасывает ID-последовательности * Сохраняет схему БД и запись Client * Поддержка флага --noinput для автоматизации - Команда init_tenant_data для инициализации системных данных тенанта * Создаёт системного клиента (АНОНИМНЫЙ ПОКУПАТЕЛЬ для POS) * Создаёт 8 системных статусов заказов * Создаёт 5 системных способов оплаты * Поддержка флага --reset для пересоздания данных Исправлено: - Заменены устаревшие фильтры is_active на status='active' для Product и ProductKit * products/views/category_views.py: исправлены фильтры в build_category_tree и get_context_data * products/services/kit_pricing.py: исправлены фильтры при получении товаров из variant_group * products/models/kits.py: исправлен фильтр в get_available_products * Устранена ошибка FieldError при работе со списком категорий Улучшено: - Команда clear_tenant_data теперь предлагает пользователю инициализировать системные данные после очистки - Добавлена детальная информация о процессе очистки и инициализации данных
This commit is contained in:
200
myproject/tenants/management/commands/clear_tenant_data.py
Normal file
200
myproject/tenants/management/commands/clear_tenant_data.py
Normal file
@@ -0,0 +1,200 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Management команда для полной очистки данных тенанта без удаления схемы и самого тенанта.
|
||||
|
||||
ВАЖНО: Это необратимая операция! Все данные тенанта будут удалены, но схема и запись в Client останутся.
|
||||
|
||||
Использование:
|
||||
# Очистка всех данных тенанта
|
||||
python manage.py clear_tenant_data --schema=anatol --noinput
|
||||
|
||||
# С подтверждением
|
||||
python manage.py clear_tenant_data --schema=anatol
|
||||
"""
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db import connection
|
||||
from django.apps import apps
|
||||
from django_tenants.utils import schema_context
|
||||
from tenants.models import Client
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Полная очистка всех данных в схеме тенанта без удаления схемы и Client'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--schema',
|
||||
type=str,
|
||||
required=True,
|
||||
help='Имя схемы БД тенанта для очистки (пример: anatol)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--noinput',
|
||||
action='store_true',
|
||||
help='Не запрашивать подтверждение (для автоматизации)'
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
self.stdout.write(self.style.SUCCESS('\n=== Очистка данных тенанта ===\n'))
|
||||
|
||||
# Получаем параметры
|
||||
schema_name = options.get('schema')
|
||||
noinput = options.get('noinput', False)
|
||||
|
||||
# Проверяем что тенант существует
|
||||
try:
|
||||
tenant = Client.objects.get(schema_name=schema_name)
|
||||
except Client.DoesNotExist:
|
||||
self.stdout.write(self.style.ERROR(f'\nОШИБКА: Тенант со схемой "{schema_name}" не найден\n'))
|
||||
return
|
||||
|
||||
# Получаем список всех таблиц в схеме (кроме служебных django)
|
||||
tables = self._get_tenant_tables(schema_name)
|
||||
|
||||
# Показываем информацию о том что будет удалено
|
||||
self.stdout.write('='*70)
|
||||
self.stdout.write(self.style.WARNING('ВНИМАНИЕ! Следующие данные будут удалены:'))
|
||||
self.stdout.write('='*70)
|
||||
self.stdout.write(f'\n[Тенант]')
|
||||
self.stdout.write(f' Название: {tenant.name}')
|
||||
self.stdout.write(f' Схема: {tenant.schema_name}')
|
||||
self.stdout.write(f' Владелец: {tenant.owner_email}')
|
||||
|
||||
self.stdout.write(f'\n[База данных]')
|
||||
self.stdout.write(f' Схема: "{schema_name}" (НЕ будет удалена)')
|
||||
self.stdout.write(f' Будет очищено таблиц: {len(tables)}')
|
||||
|
||||
if tables:
|
||||
self.stdout.write(f'\n[Таблицы для очистки]')
|
||||
for table in tables[:20]: # Показываем первые 20
|
||||
self.stdout.write(f' - {table}')
|
||||
if len(tables) > 20:
|
||||
self.stdout.write(f' ... и ещё {len(tables) - 20} таблиц')
|
||||
|
||||
self.stdout.write(f'\n[Что НЕ будет удалено]')
|
||||
self.stdout.write(f' ✓ Запись Client (тенант)')
|
||||
self.stdout.write(f' ✓ Схема БД "{schema_name}"')
|
||||
self.stdout.write(f' ✓ Домены тенанта')
|
||||
|
||||
self.stdout.write('\n' + '='*70)
|
||||
|
||||
# Запрашиваем подтверждение
|
||||
if not noinput:
|
||||
confirm = input(self.style.WARNING('\nВы уверены? Введите "yes" для подтверждения: '))
|
||||
if confirm.lower() not in ['yes', 'да', 'y']:
|
||||
self.stdout.write(self.style.ERROR('\nОтменено\n'))
|
||||
return
|
||||
|
||||
# Начинаем очистку
|
||||
self.stdout.write('\n' + '='*70)
|
||||
self.stdout.write(self.style.SUCCESS('Начинаю очистку данных...'))
|
||||
self.stdout.write('='*70 + '\n')
|
||||
|
||||
try:
|
||||
deleted_count = self._clear_tenant_data(schema_name, tables)
|
||||
|
||||
# Итоговое сообщение
|
||||
self.stdout.write('\n' + '='*70)
|
||||
self.stdout.write(self.style.SUCCESS('УСПЕХ: Данные тенанта очищены!'))
|
||||
self.stdout.write('='*70)
|
||||
self.stdout.write(self.style.WARNING('\nИтоги очистки:'))
|
||||
self.stdout.write(f' - Очищено таблиц: {deleted_count}')
|
||||
self.stdout.write(f' - Схема "{schema_name}" сохранена')
|
||||
self.stdout.write(f' - Тенант "{tenant.name}" сохранён')
|
||||
|
||||
# Предлагаем инициализировать системные данные
|
||||
self._suggest_init_data(schema_name)
|
||||
|
||||
except Exception as e:
|
||||
self.stdout.write(self.style.ERROR(f'\nОШИБКА при очистке данных: {e}'))
|
||||
self.stdout.write(self.style.ERROR(' Данные могут быть частично удалены. Проверьте состояние БД.\n'))
|
||||
raise
|
||||
|
||||
def _get_tenant_tables(self, schema_name):
|
||||
"""
|
||||
Получает список всех таблиц в схеме тенанта (кроме служебных).
|
||||
"""
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("""
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = %s
|
||||
AND table_type = 'BASE TABLE'
|
||||
AND table_name NOT IN (
|
||||
'django_migrations',
|
||||
'django_content_type',
|
||||
'django_admin_log',
|
||||
'django_session'
|
||||
)
|
||||
ORDER BY table_name
|
||||
""", [schema_name])
|
||||
|
||||
return [row[0] for row in cursor.fetchall()]
|
||||
|
||||
def _clear_tenant_data(self, schema_name, tables):
|
||||
"""
|
||||
Очищает все данные в таблицах тенанта.
|
||||
Использует TRUNCATE CASCADE для удаления связанных данных.
|
||||
"""
|
||||
deleted_count = 0
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
# Устанавливаем search_path на схему тенанта
|
||||
cursor.execute(f'SET search_path TO {schema_name}')
|
||||
|
||||
# Отключаем триггеры и ограничения для быстрой очистки
|
||||
self.stdout.write('[1] Отключение триггеров и ограничений...')
|
||||
cursor.execute('SET session_replication_role = replica;')
|
||||
|
||||
# Очищаем каждую таблицу
|
||||
self.stdout.write(f'[2] Очистка {len(tables)} таблиц...')
|
||||
for i, table in enumerate(tables, 1):
|
||||
try:
|
||||
# Используем TRUNCATE CASCADE для удаления связанных данных
|
||||
cursor.execute(f'TRUNCATE TABLE {table} CASCADE;')
|
||||
deleted_count += 1
|
||||
|
||||
if i % 10 == 0:
|
||||
self.stdout.write(f' Очищено таблиц: {i}/{len(tables)}')
|
||||
|
||||
except Exception as e:
|
||||
self.stdout.write(
|
||||
self.style.WARNING(f' Предупреждение: не удалось очистить таблицу {table}: {e}')
|
||||
)
|
||||
|
||||
# Включаем триггеры и ограничения обратно
|
||||
self.stdout.write('[3] Включение триггеров и ограничений...')
|
||||
cursor.execute('SET session_replication_role = DEFAULT;')
|
||||
|
||||
# Сбрасываем последовательности (sequences) для ID
|
||||
self.stdout.write('[4] Сброс последовательностей (ID sequences)...')
|
||||
cursor.execute("""
|
||||
SELECT 'SELECT SETVAL(' ||
|
||||
quote_literal(quote_ident(sequence_schema) || '.' || quote_ident(sequence_name)) ||
|
||||
', 1, false);'
|
||||
FROM information_schema.sequences
|
||||
WHERE sequence_schema = %s;
|
||||
""", [schema_name])
|
||||
|
||||
for row in cursor.fetchall():
|
||||
try:
|
||||
cursor.execute(row[0])
|
||||
except Exception as e:
|
||||
# Игнорируем ошибки при сбросе последовательностей
|
||||
pass
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(f' OK: Все данные очищены'))
|
||||
|
||||
return deleted_count
|
||||
|
||||
def _suggest_init_data(self, schema_name):
|
||||
"""
|
||||
Предлагает пользователю инициализировать системные данные.
|
||||
"""
|
||||
self.stdout.write('\n' + '-'*70)
|
||||
self.stdout.write(self.style.WARNING('Рекомендация:'))
|
||||
self.stdout.write('-'*70)
|
||||
self.stdout.write('Для работы системы необходимы системные данные.')
|
||||
self.stdout.write('Выполните команду для их создания:\n')
|
||||
self.stdout.write(self.style.SUCCESS(f' python manage.py init_tenant_data --schema={schema_name}'))
|
||||
self.stdout.write('')
|
||||
Reference in New Issue
Block a user