feat: import_mapper + кнопка в UI
This commit is contained in:
@@ -0,0 +1,467 @@
|
|||||||
|
# -*- 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))
|
||||||
@@ -100,6 +100,7 @@ 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)
|
||||||
|
|
||||||
@@ -1557,6 +1558,18 @@ 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:
|
||||||
|
|||||||
Reference in New Issue
Block a user