Revert "feat: import_mapper + кнопка в UI"

This reverts commit dd909792d4.
This commit is contained in:
2026-08-03 09:15:28 +04:00
parent dd909792d4
commit 12df5f106c
2 changed files with 0 additions and 480 deletions
-467
View File
@@ -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))
-13
View File
@@ -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.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.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.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.select_templates).pack(side=tk.LEFT, padx=2)
ttk.Button(toolbar, text="⚡ Генерация", command=self.generate_all).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" "Версия 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): def show_variables(self):
text = "Переменные шаблонов:\n\n" text = "Переменные шаблонов:\n\n"
for var, group, desc, _ in VARIABLE_DEFS: for var, group, desc, _ in VARIABLE_DEFS: