From 12df5f106c1a3dfbca566323e5e1e3a0bef3880e Mon Sep 17 00:00:00 2001 From: DokoGen Date: Mon, 3 Aug 2026 09:15:28 +0400 Subject: [PATCH] =?UTF-8?q?Revert=20"feat:=20import=5Fmapper=20+=20=D0=BA?= =?UTF-8?q?=D0=BD=D0=BE=D0=BF=D0=BA=D0=B0=20=D0=B2=20UI"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit dd909792d42b9be83c21c92c89bff976592e9430. --- dokogen/import_mapper.py | 467 --------------------------------------- dokogen/ui.py | 13 -- 2 files changed, 480 deletions(-) delete mode 100644 dokogen/import_mapper.py diff --git a/dokogen/import_mapper.py b/dokogen/import_mapper.py deleted file mode 100644 index 8ffcc83..0000000 --- a/dokogen/import_mapper.py +++ /dev/null @@ -1,467 +0,0 @@ -# -*- coding: utf-8 -*- -"""DokoGen — Визуальный редактор маппинга импорта из Excel -Показывает, какие ячейки Excel → какие поля программы, -позволяет добавлять/удалять/изменять привязки.""" - -import tkinter as tk -from tkinter import ttk, messagebox, filedialog -import re -import json -import os -from typing import Dict, List, Optional - -try: - import openpyxl -except ImportError: - openpyxl = None - - -# ============================================================ -# БАЗОВЫЙ МАППИНГ (стандартные правила импорта) -# ============================================================ - -# Откуда эти правила: из importer.py (поля __import_152_common и др.) -# Формат: (регулярка Excel_заголовка, имя_поля_программы, описание) - -DEFAULT_MAPPINGS = [ - # Общие сведения - ('полное наименование организации', 'fullName', 'Полное название организации'), - ('сокращенное наименование организации', 'shortName', 'Сокращенное название'), - ('адрес организации \\(юридический\\)', 'address_legal', 'Юридический адрес'), - ('адрес организации \\(фактический\\)', 'address_factual', 'Фактический адрес'), - ('инн', 'inn', 'ИНН'), - ('огрн', 'ogrn', 'ОГРН'), - ('дата.*огрн', 'ogrn_date', 'Дата ОГРН'), - ('кпп', 'kpp', 'КПП'), - ('должность руководителя', 'chief_position', 'Должность руководителя'), - ('фио руководителя', 'chief_fio', 'ФИО руководителя'), - ('должность.*ответственн', 'responsible_position', 'Должность ответственного'), - ('фио.*ответственн', 'responsible_fio', 'ФИО ответственного'), - ('должность.*защит.*информ|должность.*администрат', 'admin_position', 'Должность администратора'), - ('фио.*защит.*информ|фио.*администрат', 'admin_fio', 'ФИО администратора'), - ('номер.*договора|номер.*контракта', 'contract_number', 'Номер договора'), - ('дата.*договора|дата.*контракта', 'contract_date', 'Дата договора'), - ('названи.*документ', 'paper_documents', 'Перечень бумажных документов'), - ('место.*хранен', 'paper_storage', 'Место хранения документов'), - ('основной оквэд', 'okved_main', 'Основной ОКВЭД'), - ('электронная почта', 'email', 'Email'), - ('телефон', 'phone', 'Телефон'), - # ИС - ('название ис|наименование ис|наименование информационной', 'is_name', 'Название ИС'), - ('сотрудник', 'is_employee', 'Сотрудник ИС'), - ('должность сотрудника', 'is_position', 'Должность сотрудника ИС'), - ('цель обработки пд', 'is_purpose', 'Цель обработки ПДн'), - ('количество записей|количество субъект', 'is_pd_count', 'Кол-во записей ПДн'), - ('обработки пд|способ обработки', 'is_processing_mode', 'Способ обработки'), - ('интернет', 'is_internet', 'Наличие интернета'), - # Комиссия - ('состав комиссии', 'section_commission', 'Секция: состав комиссии'), - ('перечень сотрудников', 'section_employees', 'Секция: сотрудники с доступом'), -] - -# Поля для информационных систем (используются при импорте листов ИС) -IS_FIELD_MAP = { - 'is_name': 'name', - 'is_description': 'description', - 'is_pd_count': 'pd_count', - 'is_purpose': 'purpose', - 'is_processing_mode': 'processing_modes', - 'is_internet': 'is_internet', - 'is_employee': 'employee', - 'is_position': 'position', -} - - -def scan_excel(filepath: str) -> Dict: - """Сканирует Excel-файл и возвращает структуру: листы, строки, колонки.""" - if not openpyxl: - return {'error': 'openpyxl не установлен'} - - result = { - 'sheets': [], - 'total_cells': 0, - 'detected_mappings': [], - 'unmapped_cells': [], - } - - try: - wb = openpyxl.load_workbook(filepath, data_only=True, read_only=False) - - for sheet_name in wb.sheetnames: - ws = wb[sheet_name] - sheet_info = { - 'name': sheet_name, - 'rows': 0, - 'cols': 0, - 'cells': [], - } - - for row_idx, row in enumerate(ws.iter_rows(min_row=1, values_only=True), 1): - has_data = False - row_cells = [] - for col_idx, val in enumerate(row, 1): - if val is not None: - str_val = str(val).strip() - if str_val: - has_data = True - cell_info = { - 'row': row_idx, - 'col': col_idx, - 'value': str_val[:200], - 'sheet': sheet_name, - } - row_cells.append(cell_info) - result['total_cells'] += 1 - - if row_cells: - sheet_info['cells'].extend(row_cells) - sheet_info['rows'] += 1 - elif not has_data and sheet_info['rows'] > 0: - break # пустая строка после данных = конец - - result['sheets'].append(sheet_info) - - wb.close() - - # Анализируем: какие ячейки подходят под маппинг - for sheet in result['sheets']: - for cell in sheet['cells']: - val_lower = cell['value'].lower() - mapped = False - for pattern, field, desc in DEFAULT_MAPPINGS: - if re.search(pattern, val_lower): - result['detected_mappings'].append({ - 'pattern': pattern, - 'field': field, - 'description': desc, - 'cell': cell, - 'value': cell['value'][:100], - }) - mapped = True - break - # Определяем тип листа - sheet_type = 'common' - if 'сотруд' in sheet['name'].lower(): - sheet_type = 'employees' - elif sheet['name'].lower() not in ('общие сведения', 'сотрудники', ''): - sheet_type = 'is' - - if not mapped and cell['col'] <= 2: - result['unmapped_cells'].append({ - 'cell': cell, - 'sheet_type': sheet_type, - 'suggested_field': _suggest_field(cell['value'], sheet_type), - }) - - except Exception as e: - result['error'] = str(e) - - return result - - -def _suggest_field(value: str, sheet_type: str) -> str: - """Пытается предположить, какому полю может соответствовать ячейка.""" - val = value.lower().strip() - - # Пробуем найти совпадение в базовых маппингах - for pattern, field, desc in DEFAULT_MAPPINGS: - if re.search(pattern, val): - return field - - # По ключевым словам - keywords = { - 'full_name': ['полное наименование', 'наименование организации', 'полное название'], - 'short_name': ['сокращенное наименование', 'краткое наименование', 'сокращ'], - 'address': ['адрес', 'место нахождения'], - 'chief_fio': ['руководител', 'глава', 'директор'], - 'inn': ['инн', 'идентификационный номер'], - 'ogrn': ['огрн', 'основной государствен'], - 'kpp': ['кпп'], - 'phone': ['телефон', 'контактный'], - 'email': ['почта', 'email', 'e-mail'], - 'is_name': ['наименование ис', 'название ис', 'наименование информационной'], - 'is_software': ['программ', 'по'], - 'is_pd': ['персональные данные', 'пдн', 'перечень пд'], - } - - for field, kws in keywords.items(): - for kw in kws: - if kw in val: - return field - - return 'unknown_' + value[:20].replace(' ', '_').lower() - - -# ============================================================ -# GUI: РЕДАКТОР МАППИНГА -# ============================================================ - -class ImportMapperWindow: - """Окно редактора импорта: показывает откуда что берётся, - позволяет изменять привязки, добавлять новые, удалять.""" - - def __init__(self, parent, excel_path: str = None): - self.parent = parent - self.excel_path = excel_path - self.mappings = list(DEFAULT_MAPPINGS) # копия для редактирования - self.scan_result = None - self.custom_mappings = [] - self.removed_mappings = set() - - self.window = tk.Toplevel(parent) - self.window.title("Редактор импорта из Excel") - self.window.geometry("1000x700") - self.window.minsize(800, 500) - self.window.transient(parent) - self.window.grab_set() - - self._build_ui() - - if excel_path: - self._load_excel(excel_path) - - def _build_ui(self): - # Верхняя панель — выбор файла - top = ttk.Frame(self.window, padding=5) - top.pack(fill=tk.X) - ttk.Label(top, text="Файл опросника:").pack(side=tk.LEFT) - self.file_var = tk.StringVar(value=self.excel_path or '') - ttk.Entry(top, textvariable=self.file_var, width=60).pack(side=tk.LEFT, padx=5) - ttk.Button(top, text="Обзор", command=self._browse_file).pack(side=tk.LEFT, padx=2) - ttk.Button(top, text="Сканировать", command=self._scan_file).pack(side=tk.LEFT, padx=5) - - # Основная область - main = ttk.PanedWindow(self.window, orient=tk.HORIZONTAL) - main.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) - - # Левая панель — Обнаруженные привязки - left = ttk.LabelFrame(main, text="Обнаруженные привязки", padding=5) - main.add(left, weight=1) - - self.mapping_tree = ttk.Treeview(left, columns=('field', 'excel', 'sheet', 'status'), - show='tree headings', height=20) - self.mapping_tree.heading('field', text='Поле программы') - self.mapping_tree.heading('excel', text='Значение в Excel') - self.mapping_tree.heading('sheet', text='Лист') - self.mapping_tree.heading('status', text='Статус') - self.mapping_tree.column('field', width=180) - self.mapping_tree.column('excel', width=250) - self.mapping_tree.column('sheet', width=100) - self.mapping_tree.column('status', width=80) - self.mapping_tree.pack(fill=tk.BOTH, expand=True) - - # Кнопки управления привязками - btn_frame = ttk.Frame(left) - btn_frame.pack(fill=tk.X, pady=3) - ttk.Button(btn_frame, text="✏ Изменить привязку", command=self._edit_mapping).pack(side=tk.LEFT, padx=2) - ttk.Button(btn_frame, text="🗑 Удалить привязку", command=self._remove_mapping).pack(side=tk.LEFT, padx=2) - - # Правая панель — Непривязанные ячейки - right = ttk.LabelFrame(main, text="Непривязанные ячейки", padding=5) - main.add(right, weight=1) - - self.unmapped_tree = ttk.Treeview(right, columns=('value', 'sheet', 'suggest'), - show='tree headings', height=20) - self.unmapped_tree.heading('value', text='Значение в Excel') - self.unmapped_tree.heading('sheet', text='Лист') - self.unmapped_tree.heading('suggest', text='Предполагаемое поле') - self.unmapped_tree.column('value', width=220) - self.unmapped_tree.column('sheet', width=100) - self.unmapped_tree.column('suggest', width=150) - self.unmapped_tree.pack(fill=tk.BOTH, expand=True) - - btn_frame2 = ttk.Frame(right) - btn_frame2.pack(fill=tk.X, pady=3) - ttk.Button(btn_frame2, text="➕ Добавить привязку", command=self._add_mapping).pack(side=tk.LEFT, padx=2) - - # Нижняя панель — статистика и кнопки - bottom = ttk.Frame(self.window, padding=5) - bottom.pack(fill=tk.X) - self.status_label = ttk.Label(bottom, text="Загрузите Excel-файл для анализа") - self.status_label.pack(side=tk.LEFT) - ttk.Button(bottom, text="💾 Сохранить настройки импорта", command=self._save_settings).pack(side=tk.RIGHT, padx=2) - ttk.Button(bottom, text="❌ Закрыть", command=self.window.destroy).pack(side=tk.RIGHT, padx=2) - - def _browse_file(self): - path = filedialog.askopenfilename( - title="Выберите опросный лист (Excel)", - filetypes=[("Excel", "*.xlsx *.xls")] - ) - if path: - self.file_var.set(path) - self.excel_path = path - self._load_excel(path) - - def _load_excel(self, path: str): - self.status_label.config(text="Сканирование...") - self.window.update() - - result = scan_excel(path) - self.scan_result = result - - if 'error' in result: - messagebox.showerror("Ошибка", result['error']) - self.status_label.config(text=f"Ошибка: {result['error']}") - return - - # Заполняем дерево привязок - self._populate_mappings(result) - # Заполняем дерево непривязанных - self._populate_unmapped(result) - - self.status_label.config( - text=f"Всего ячеек: {result['total_cells']} | " - f"Привязок: {len(result['detected_mappings'])} | " - f"Непривязанных: {len(result['unmapped_cells'])}" - ) - - def _populate_mappings(self, result: Dict): - self.mapping_tree.delete(*self.mapping_tree.get_children()) - for m in result['detected_mappings']: - status = '✅' if m['field'] not in self.removed_mappings else '❌ удалено' - self.mapping_tree.insert('', tk.END, values=( - m['field'], - m['value'][:60], - m['cell']['sheet'], - status - )) - # Добавляем пользовательские привязки - for cm in self.custom_mappings: - self.mapping_tree.insert('', tk.END, values=( - cm['field'], - cm['excel_value'][:60], - cm['sheet'], - '➕ добавлено' - )) - - def _populate_unmapped(self, result: Dict): - self.unmapped_tree.delete(*self.unmapped_tree.get_children()) - for u in result['unmapped_cells']: - self.unmapped_tree.insert('', tk.END, values=( - u['cell']['value'][:60], - u['cell']['sheet'], - u['suggested_field'] - )) - - def _scan_file(self): - path = self.file_var.get().strip() - if path and os.path.isfile(path): - self._load_excel(path) - else: - messagebox.showwarning("Предупреждение", "Укажите существующий файл") - - def _edit_mapping(self): - selection = self.mapping_tree.selection() - if not selection: - messagebox.showinfo("Подсказка", "Выберите привязку для редактирования") - return - item = self.mapping_tree.item(selection[0]) - values = item['values'] - if not values: - return - - old_field = values[0] - new_field = self._ask_field_name("Изменить поле", old_field) - if new_field and new_field != old_field: - # Обновляем в custom_mappings или removed_mappings - for cm in self.custom_mappings: - if cm['field'] == old_field: - cm['field'] = new_field - if old_field in self.removed_mappings: - self.removed_mappings.discard(old_field) - self._populate_mappings(self.scan_result) - - def _remove_mapping(self): - selection = self.mapping_tree.selection() - if not selection: - return - item = self.mapping_tree.item(selection[0]) - values = item['values'] - if values and values[0]: - field = values[0] - self.removed_mappings.add(field) - self._populate_mappings(self.scan_result) - self.status_label.config(text=f"Привязка '{field}' удалена") - - def _add_mapping(self): - selection = self.unmapped_tree.selection() - if not selection: - messagebox.showinfo("Подсказка", "Выберите ячейку из правого списка") - return - item = self.unmapped_tree.item(selection[0]) - values = item['values'] - if not values: - return - - excel_val = values[0] - suggested = values[2] if len(values) > 2 else '' - sheet = values[1] if len(values) > 1 else '' - - field = self._ask_field_name("Новая привязка", suggested) - if field: - self.custom_mappings.append({ - 'field': field, - 'excel_value': excel_val, - 'sheet': sheet, - }) - self._populate_mappings(self.scan_result) - # Удаляем из непривязанных - self.unmapped_tree.delete(selection[0]) - self.status_label.config(text=f"Добавлена привязка: {excel_val} → {field}") - - def _ask_field_name(self, title: str, default: str) -> Optional[str]: - """Диалог ввода имени поля.""" - dialog = tk.Toplevel(self.window) - dialog.title(title) - dialog.geometry("500x250") - dialog.transient(self.window) - dialog.grab_set() - - ttk.Label(dialog, text="Имя поля в программе:").pack(pady=(10, 0)) - var = tk.StringVar(value=default) - entry = ttk.Entry(dialog, textvariable=var, width=50) - entry.pack(padx=10, pady=5) - entry.select_range(0, tk.END) - entry.focus() - - # Подсказка: существующие поля - ttk.Label(dialog, text="Подсказка: доступные поля", font=('', 8)).pack() - fields_text = tk.Text(dialog, height=6, width=60) - fields_text.pack(padx=10, pady=5) - existing = sorted(set(m[1] for m in DEFAULT_MAPPINGS)) - fields_text.insert(tk.END, ', '.join(existing)) - fields_text.configure(state='disabled') - - result = [] - def _ok(): - result.append(var.get().strip()) - dialog.destroy() - def _cancel(): - dialog.destroy() - - ttk.Button(dialog, text="OK", command=_ok).pack(side=tk.LEFT, padx=10, pady=10) - ttk.Button(dialog, text="Отмена", command=_cancel).pack(side=tk.RIGHT, padx=10, pady=10) - - dialog.wait_window() - return result[0] if result else None - - def _save_settings(self): - """Сохраняет пользовательские настройки маппинга в JSON.""" - path = filedialog.asksaveasfilename( - defaultextension=".json", - filetypes=[("JSON", "*.json")], - title="Сохранить настройки импорта" - ) - if not path: - return - - settings = { - 'removed_mappings': list(self.removed_mappings), - 'custom_mappings': self.custom_mappings, - } - try: - with open(path, 'w', encoding='utf-8') as f: - json.dump(settings, f, ensure_ascii=False, indent=2) - messagebox.showinfo("Готово", f"Настройки сохранены:\n{path}") - except Exception as e: - messagebox.showerror("Ошибка", str(e)) diff --git a/dokogen/ui.py b/dokogen/ui.py index 2259446..e0dce99 100644 --- a/dokogen/ui.py +++ b/dokogen/ui.py @@ -100,7 +100,6 @@ class DokoGenApp: ttk.Button(toolbar, text="📂 Открыть", command=self.open_data).pack(side=tk.LEFT, padx=2) ttk.Button(toolbar, text="💾 Сохранить", command=self.save_data).pack(side=tk.LEFT, padx=2) ttk.Button(toolbar, text="📥 Импорт", command=self.import_from_survey).pack(side=tk.LEFT, padx=2) - ttk.Button(toolbar, text="✏ Редактор импорта", command=self.open_import_mapper).pack(side=tk.LEFT, padx=2) ttk.Button(toolbar, text="📁 Шаблоны", command=self.select_templates).pack(side=tk.LEFT, padx=2) ttk.Button(toolbar, text="⚡ Генерация", command=self.generate_all).pack(side=tk.LEFT, padx=2) @@ -1558,18 +1557,6 @@ class DokoGenApp: "Версия 1.0\n" "Генератор документов из шаблонов") - - - def open_import_mapper(self): - """Открывает визуальный редактор маппинга импорта.""" - try: - from .import_mapper import ImportMapperWindow - ImportMapperWindow(self.root) - except ImportError as e: - messagebox.showerror("Ошибка", f"Модуль редактора импорта не загружен: {e}") - except Exception as e: - messagebox.showerror("Ошибка", str(e)) - def show_variables(self): text = "Переменные шаблонов:\n\n" for var, group, desc, _ in VARIABLE_DEFS: