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:
2025-12-12 04:58:26 +03:00
parent 2d253584ba
commit e54d7d04d7
7 changed files with 399 additions and 14 deletions

View 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('')