""" Management-команда для тестового импорта клиентов из XLSX/CSV файлов. Использование: python manage.py test_import путь/к/файлу.xlsx --schema=anatol [--update] [--export-errors] Примеры: python manage.py test_import ../customers_mixflowers.by_2025-12-14_20-35-36.xlsx --schema=anatol python manage.py test_import ../customers.csv --schema=anatol --update python manage.py test_import ../file.xlsx --schema=anatol --export-errors """ from django.core.management.base import BaseCommand from django.core.files import File from django_tenants.utils import schema_context from customers.services.import_export import CustomerImporter import os try: from openpyxl import Workbook from openpyxl.styles import Font, PatternFill, Alignment except ImportError: Workbook = None class Command(BaseCommand): help = 'Тестовый импорт клиентов из XLSX/CSV файла' def add_arguments(self, parser): parser.add_argument('file_path', type=str, help='Путь к файлу для импорта') parser.add_argument( '--schema', type=str, required=True, help='Имя схемы БД тенанта (пример: anatol)' ) parser.add_argument( '--update', action='store_true', help='Обновлять существующих клиентов (по email/телефону)', ) parser.add_argument( '--export-errors', action='store_true', help='Экспортировать все проблемные строки в отдельный XLSX файл', ) def handle(self, *args, **options): file_path = options['file_path'] schema_name = options['schema'] update_existing = options['update'] export_errors = options.get('export_errors', False) if not os.path.exists(file_path): self.stdout.write(self.style.ERROR(f'Файл не найден: {file_path}')) return self.stdout.write(f'Импорт из файла: {file_path}') self.stdout.write(f'Схема тенанта: {schema_name}') self.stdout.write(f'Режим обновления: {"ВКЛ" if update_existing else "ВЫКЛ"}') self.stdout.write('-' * 60) # Выполняем импорт в контексте схемы тенанта with schema_context(schema_name): importer = CustomerImporter() with open(file_path, 'rb') as f: # Создаём простой объект-обёртку для файла class FakeUploadedFile: def __init__(self, file_obj, name): self.file = file_obj self.name = name def __getattr__(self, attr): # Делегируем все остальные методы внутреннему файловому объекту return getattr(self.file, attr) fake_file = FakeUploadedFile(f, os.path.basename(file_path)) result = importer.import_from_file(fake_file, update_existing=update_existing) self.stdout.write(self.style.SUCCESS(f"\n{result['message']}")) self.stdout.write('-' * 60) self.stdout.write(f"Создано: {result['created']}") self.stdout.write(f"Обновлено: {result['updated']}") self.stdout.write(f"Пропущено: {result['skipped']}") self.stdout.write(f"Ошибок: {len(result['errors'])}") if result['errors']: self.stdout.write('\n' + self.style.WARNING('ОШИБКИ:')) # Показываем первые 20 ошибок, остальные — просто счётчик for idx, error in enumerate(result['errors'][:20], 1): row = error.get('row', '?') email = error.get('email', '') phone = error.get('phone', '') reason = error.get('reason', '') self.stdout.write( f" [{idx}] Строка {row}: {email or phone or '(пусто)'} - {reason}" ) if len(result['errors']) > 20: self.stdout.write(f" ... и ещё {len(result['errors']) - 20} ошибок") # Экспорт ошибок в XLSX if export_errors: self._export_errors_to_xlsx(file_path, result['real_errors']) def _export_errors_to_xlsx(self, original_file_path, errors): """ Экспортирует все проблемные строки в отдельный XLSX файл. """ if Workbook is None: self.stdout.write(self.style.ERROR('\nНевозможно экспортировать ошибки: openpyxl не установлен')) return # Формируем имя файла для ошибок base_name = os.path.splitext(os.path.basename(original_file_path))[0] error_file = f"{base_name}_ERRORS.xlsx" error_path = os.path.join(os.path.dirname(original_file_path) or '.', error_file) # Создаём новую книгу Excel wb = Workbook() ws = wb.active ws.title = "Ошибки импорта" # Заголовки с форматированием headers = ['Строка', 'Email', 'Телефон', 'Причина ошибки'] for col_num, header in enumerate(headers, 1): cell = ws.cell(row=1, column=col_num, value=header) cell.font = Font(bold=True, color="FFFFFF") cell.fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid") cell.alignment = Alignment(horizontal="center", vertical="center") # Данные об ошибках for idx, error in enumerate(errors, 2): ws.cell(row=idx, column=1, value=error.get('row', '')) ws.cell(row=idx, column=2, value=error.get('email', '')) ws.cell(row=idx, column=3, value=error.get('phone', '')) ws.cell(row=idx, column=4, value=error.get('reason', '')) # Автоподбор ширины колонок for column in ws.columns: max_length = 0 column_letter = column[0].column_letter for cell in column: try: if cell.value: max_length = max(max_length, len(str(cell.value))) except: pass adjusted_width = min(max_length + 2, 80) ws.column_dimensions[column_letter].width = adjusted_width # Сохраняем файл try: wb.save(error_path) self.stdout.write(self.style.SUCCESS(f"\n✓ Файл с ошибками сохранён: {error_path}")) self.stdout.write(f" Всего строк с ошибками: {len(errors)}") except Exception as e: self.stdout.write(self.style.ERROR(f"\n✗ Ошибка при сохранении файла: {e}"))