- Пакет dokogen/ в подпапке - main.py и requirements.txt в корне - Импорт: from dokogen.ui import main
1053 lines
46 KiB
Python
1053 lines
46 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""DokoGen — Пользовательский интерфейс (tkinter)"""
|
|
|
|
import tkinter as tk
|
|
from tkinter import ttk, messagebox, filedialog, simpledialog, scrolledtext
|
|
import os
|
|
import json
|
|
import threading
|
|
from datetime import datetime
|
|
from typing import List, Optional
|
|
|
|
from . import (
|
|
Company, CommissionMember, InformationSystem, PaperDocument,
|
|
inflect_name,
|
|
build_replacements, VARIABLE_DEFS,
|
|
process_template,
|
|
get_short_fio, get_initials,
|
|
)
|
|
|
|
|
|
class DokoGenApp:
|
|
"""Главное окно приложения."""
|
|
|
|
def __init__(self, root: tk.Tk):
|
|
self.root = root
|
|
root.title("ДоКоГеНеРаТоР — 152-ФЗ (Персональные данные)")
|
|
root.geometry("1200x800")
|
|
root.minsize(900, 600)
|
|
|
|
self.model = Company()
|
|
self.current_file: Optional[str] = None
|
|
self._current_is_idx: Optional[int] = None
|
|
self._template_files: List[str] = []
|
|
self._selected_templates: set = set()
|
|
self._last_template_dir: str = ""
|
|
self._autosave_path = os.path.join(
|
|
os.path.dirname(os.path.abspath(__file__)),
|
|
'.autosave', 'autosave_152.json'
|
|
)
|
|
|
|
# Переменные
|
|
self.doc_number_var = tk.StringVar(value="001")
|
|
self.doc_date_var = tk.StringVar(value=datetime.now().strftime("%d.%m.%Y"))
|
|
self.templates_var = tk.StringVar()
|
|
self.save_var = tk.StringVar()
|
|
self.status_var = tk.StringVar(value="Готов")
|
|
|
|
# Словари для хранения виджетов
|
|
self._company_entries: dict = {}
|
|
self._official_entries: dict = {}
|
|
self.commission_entries: list = []
|
|
|
|
self._build_menu()
|
|
self._build_ui()
|
|
self._enable_context_menu()
|
|
self._init_company()
|
|
|
|
# Привязка закрытия
|
|
root.protocol("WM_DELETE_WINDOW", self._on_close)
|
|
|
|
# Автосохранение
|
|
self._load_autosave()
|
|
self._start_autosave_timer()
|
|
|
|
# ==================== МЕНЮ ====================
|
|
def _build_menu(self):
|
|
menubar = tk.Menu(self.root)
|
|
self.root.config(menu=menubar)
|
|
|
|
file_menu = tk.Menu(menubar, tearoff=0)
|
|
menubar.add_cascade(label="Файл", menu=file_menu)
|
|
file_menu.add_command(label="Новое", command=self.new_data, accelerator="Ctrl+N")
|
|
file_menu.add_command(label="Открыть", command=self.open_data, accelerator="Ctrl+O")
|
|
file_menu.add_command(label="Сохранить", command=self.save_data, accelerator="Ctrl+S")
|
|
file_menu.add_command(label="Сохранить как", command=self.save_as_data, accelerator="Ctrl+Shift+S")
|
|
file_menu.add_separator()
|
|
|
|
self.recent_menu = tk.Menu(file_menu, tearoff=0)
|
|
file_menu.add_cascade(label="Последние файлы", menu=self.recent_menu)
|
|
file_menu.add_separator()
|
|
file_menu.add_command(label="Выход", command=self._on_close)
|
|
|
|
gen_menu = tk.Menu(menubar, tearoff=0)
|
|
menubar.add_cascade(label="Генерация", menu=gen_menu)
|
|
gen_menu.add_command(label="Импорт из опросника", command=self.import_from_survey)
|
|
gen_menu.add_separator()
|
|
gen_menu.add_command(label="Сгенерировать всё", command=self.generate_all)
|
|
|
|
help_menu = tk.Menu(menubar, tearoff=0)
|
|
menubar.add_cascade(label="Справка", menu=help_menu)
|
|
help_menu.add_command(label="О программе", command=self.show_about)
|
|
help_menu.add_command(label="Переменные шаблонов", command=self.show_variables)
|
|
|
|
# ==================== ИНТЕРФЕЙС ====================
|
|
def _build_ui(self):
|
|
# Панель инструментов
|
|
toolbar = ttk.Frame(self.root)
|
|
toolbar.pack(side=tk.TOP, fill=tk.X, padx=5, pady=3)
|
|
|
|
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.select_templates).pack(side=tk.LEFT, padx=2)
|
|
ttk.Button(toolbar, text="⚡ Генерация", command=self.generate_all).pack(side=tk.LEFT, padx=2)
|
|
|
|
# Ноутбук (вкладки)
|
|
self.notebook = ttk.Notebook(self.root)
|
|
self.notebook.pack(fill=tk.BOTH, expand=True, padx=5, pady=2)
|
|
|
|
self._build_company_tab()
|
|
self._build_is_tab()
|
|
self._build_commission_tab()
|
|
self._build_generation_tab()
|
|
self._build_log_tab()
|
|
|
|
# Статус-бар
|
|
statusbar = ttk.Label(self.root, textvariable=self.status_var,
|
|
relief=tk.SUNKEN, anchor=tk.W)
|
|
statusbar.pack(side=tk.BOTTOM, fill=tk.X)
|
|
|
|
# Горячие клавиши
|
|
self.root.bind("<Control-n>", lambda e: self.new_data())
|
|
self.root.bind("<Control-o>", lambda e: self.open_data())
|
|
self.root.bind("<Control-s>", lambda e: self.save_data())
|
|
self.root.bind("<Control-S>", lambda e: self.save_as_data())
|
|
|
|
# ---------- ВКЛАДКА "ОРГАНИЗАЦИЯ" ----------
|
|
def _build_company_tab(self):
|
|
tab = ttk.Frame(self.notebook)
|
|
self.notebook.add(tab, text="Организация")
|
|
|
|
canvas = tk.Canvas(tab, highlightthickness=0)
|
|
scrollbar = ttk.Scrollbar(tab, orient=tk.VERTICAL, command=canvas.yview)
|
|
scrollable = ttk.Frame(canvas)
|
|
scrollable.bind("<Configure>",
|
|
lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
|
|
canvas.create_window((0, 0), window=scrollable, anchor="nw")
|
|
canvas.configure(yscrollcommand=scrollbar.set)
|
|
canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
|
|
|
main = ttk.Frame(scrollable, padding=15)
|
|
main.pack(fill=tk.BOTH, expand=True)
|
|
|
|
# --- Название организации ---
|
|
f1 = ttk.LabelFrame(main, text="Наименование организации", padding=10)
|
|
f1.pack(fill=tk.X, pady=5)
|
|
|
|
ttk.Label(f1, text="Полное название:").grid(row=0, column=0, sticky=tk.W, pady=2)
|
|
self.company_name_entry = ttk.Entry(f1, width=60)
|
|
self.company_name_entry.grid(row=0, column=1, padx=10, sticky=tk.EW)
|
|
ttk.Button(f1, text="Склонять", command=self._update_company_declensions).grid(
|
|
row=0, column=2, padx=5)
|
|
|
|
ttk.Label(f1, text="Краткое название:").grid(row=1, column=0, sticky=tk.W, pady=2)
|
|
self.short_name_entry = ttk.Entry(f1, width=60)
|
|
self.short_name_entry.grid(row=1, column=1, padx=10, sticky=tk.EW)
|
|
ttk.Button(f1, text="Склонять", command=self._update_short_declensions).grid(
|
|
row=1, column=2, padx=5)
|
|
f1.columnconfigure(1, weight=1)
|
|
|
|
# Склонения полного названия
|
|
decl_frame = ttk.Frame(f1)
|
|
decl_frame.grid(row=2, column=0, columnspan=3, sticky=tk.EW, pady=(5, 0))
|
|
f1.columnconfigure(0, weight=0)
|
|
f1.columnconfigure(1, weight=1)
|
|
f1.columnconfigure(2, weight=0)
|
|
|
|
self.decl_entries = {}
|
|
cases = [
|
|
("Именительный (Кто? Что?)", "nomn"),
|
|
("Родительный (Кого? Чего?)", "gent"),
|
|
("Дательный (Кому? Чему?)", "datv"),
|
|
("Винительный (Кого? Что?)", "accs"),
|
|
("Творительный (Кем? Чем?)", "ablt"),
|
|
("Предложный (О ком? О чём?)", "loct"),
|
|
]
|
|
for i, (label, code) in enumerate(cases):
|
|
ttk.Label(decl_frame, text=label, font=('', 8)).grid(row=i, column=0, sticky=tk.W, padx=(0, 5))
|
|
entry = ttk.Entry(decl_frame, width=55, font=('', 8))
|
|
entry.grid(row=i, column=1, sticky=tk.EW, pady=1)
|
|
self.decl_entries[code] = entry
|
|
decl_frame.columnconfigure(1, weight=1)
|
|
|
|
# Склонения краткого названия
|
|
ttk.Label(decl_frame, text="", font=('', 5)).grid(row=6, column=0)
|
|
self.short_decl_entries = {}
|
|
for i, (label, code) in enumerate(cases):
|
|
row = i + 7
|
|
ttk.Label(decl_frame, text=f"Кратк. {label.lower()}",
|
|
font=('', 8)).grid(row=row, column=0, sticky=tk.W, padx=(0, 5))
|
|
entry = ttk.Entry(decl_frame, width=55, font=('', 8))
|
|
entry.grid(row=row, column=1, sticky=tk.EW, pady=1)
|
|
self.short_decl_entries[code] = entry
|
|
decl_frame.columnconfigure(1, weight=1)
|
|
|
|
# --- Адрес и реквизиты ---
|
|
f2 = ttk.LabelFrame(main, text="Адрес и реквизиты", padding=10)
|
|
f2.pack(fill=tk.X, pady=5)
|
|
|
|
addr_fields = [
|
|
("Юридический адрес:", "address"),
|
|
("Город:", "city_name"),
|
|
("ИНН:", "inn"),
|
|
("КПП:", "kpp"),
|
|
("ОГРН:", "ogrn"),
|
|
("Дата ОГРН:", "ogrn_date"),
|
|
]
|
|
for i, (label, key) in enumerate(addr_fields):
|
|
ttk.Label(f2, text=label).grid(row=i, column=0, sticky=tk.W, pady=2)
|
|
entry = ttk.Entry(f2, width=50)
|
|
entry.grid(row=i, column=1, padx=10, sticky=tk.EW, pady=1)
|
|
self._company_entries[key] = entry
|
|
f2.columnconfigure(1, weight=1)
|
|
|
|
# --- Должностные лица ---
|
|
f3 = ttk.LabelFrame(main, text="Должностные лица", padding=10)
|
|
f3.pack(fill=tk.X, pady=5)
|
|
|
|
of_fields = [
|
|
("Руководитель (должность):", "chief_position"),
|
|
("Руководитель (ФИО):", "chief_fio"),
|
|
("Ответственный (должность):", "responsible_position"),
|
|
("Ответственный (ФИО):", "responsible_fio"),
|
|
("Администратор (должность):", "admin_position"),
|
|
("Администратор (ФИО):", "admin_fio"),
|
|
]
|
|
for i, (label, key) in enumerate(of_fields):
|
|
ttk.Label(f3, text=label).grid(row=i, column=0, sticky=tk.W, pady=2)
|
|
entry = ttk.Entry(f3, width=50)
|
|
entry.grid(row=i, column=1, padx=10, sticky=tk.EW, pady=1)
|
|
self._official_entries[key] = entry
|
|
f3.columnconfigure(1, weight=1)
|
|
|
|
# --- Документ ---
|
|
f4 = ttk.LabelFrame(main, text="Документ", padding=10)
|
|
f4.pack(fill=tk.X, pady=5)
|
|
ttk.Label(f4, text="Номер договора:").grid(row=0, column=0, sticky=tk.W)
|
|
ttk.Entry(f4, textvariable=self.doc_number_var, width=25).grid(
|
|
row=0, column=1, padx=10, sticky=tk.W)
|
|
ttk.Label(f4, text="Дата договора:").grid(row=1, column=0, sticky=tk.W)
|
|
ttk.Entry(f4, textvariable=self.doc_date_var, width=25).grid(
|
|
row=1, column=1, padx=10, sticky=tk.W)
|
|
|
|
# ---------- ВКЛАДКА "ИНФОРМАЦИОННЫЕ СИСТЕМЫ" ----------
|
|
def _build_is_tab(self):
|
|
tab = ttk.Frame(self.notebook)
|
|
self.notebook.add(tab, text="Информационные системы")
|
|
|
|
# Левая панель — список ИС
|
|
left = ttk.Frame(tab)
|
|
left.pack(side=tk.LEFT, fill=tk.Y, padx=5, pady=5)
|
|
ttk.Label(left, text="Информационные системы:").pack()
|
|
self.is_listbox = tk.Listbox(left, width=35, height=18, exportselection=False)
|
|
self.is_listbox.pack(fill=tk.BOTH, expand=True, pady=5)
|
|
self.is_listbox.bind('<<ListboxSelect>>', self._on_is_select)
|
|
|
|
btn_frame = ttk.Frame(left)
|
|
btn_frame.pack(fill=tk.X)
|
|
ttk.Button(btn_frame, text="+ Добавить", command=self._add_is).pack(side=tk.LEFT, padx=2)
|
|
ttk.Button(btn_frame, text="Копировать", command=self._copy_is).pack(side=tk.LEFT, padx=2)
|
|
ttk.Button(btn_frame, text="- Удалить", command=self._delete_is).pack(side=tk.LEFT, padx=2)
|
|
|
|
# Правая панель — редактирование
|
|
self._is_edit_panel = ttk.LabelFrame(tab, text="Редактирование ИС")
|
|
self._is_edit_panel.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
|
|
fields_frame = ttk.Frame(self._is_edit_panel, padding=10)
|
|
fields_frame.pack(fill=tk.X)
|
|
|
|
# Основные поля
|
|
ttk.Label(fields_frame, text="Наименование:").grid(row=0, column=0, sticky=tk.W, pady=2)
|
|
self.is_name_entry = ttk.Entry(fields_frame, width=55)
|
|
self.is_name_entry.grid(row=0, column=1, padx=10, sticky=tk.EW)
|
|
|
|
ttk.Label(fields_frame, text="Описание:").grid(row=1, column=0, sticky=tk.W, pady=2)
|
|
self.is_description_entry = ttk.Entry(fields_frame, width=55)
|
|
self.is_description_entry.grid(row=1, column=1, padx=10, sticky=tk.EW)
|
|
|
|
ttk.Label(fields_frame, text="ПО:").grid(row=2, column=0, sticky=tk.W, pady=2)
|
|
self.is_software_entry = ttk.Entry(fields_frame, width=55)
|
|
self.is_software_entry.grid(row=2, column=1, padx=10, sticky=tk.EW)
|
|
|
|
# Чекбоксы
|
|
chk_frame = ttk.Frame(fields_frame)
|
|
chk_frame.grid(row=3, column=1, sticky=tk.W, pady=5)
|
|
self.is_lan_var = tk.BooleanVar()
|
|
ttk.Checkbutton(chk_frame, text="ЛВС", variable=self.is_lan_var).pack(side=tk.LEFT, padx=5)
|
|
self.is_internet_var = tk.BooleanVar()
|
|
ttk.Checkbutton(chk_frame, text="Интернет", variable=self.is_internet_var).pack(side=tk.LEFT, padx=5)
|
|
|
|
# Тип угроз
|
|
self.is_threat_var = tk.StringVar(value="3")
|
|
ttk.Label(fields_frame, text="Тип угроз:").grid(row=4, column=0, sticky=tk.W, pady=2)
|
|
threat_frame = ttk.Frame(fields_frame)
|
|
threat_frame.grid(row=4, column=1, sticky=tk.W, padx=10)
|
|
for val, text in [("1", "1 (НДВ в СПО)"), ("2", "2 (НДВ в ППО)"), ("3", "3 (НДВ нет)")]:
|
|
ttk.Radiobutton(threat_frame, text=text, variable=self.is_threat_var,
|
|
value=val).pack(side=tk.LEFT, padx=3)
|
|
|
|
# Уровень защищённости
|
|
self.is_defence_var = tk.StringVar(value="4")
|
|
ttk.Label(fields_frame, text="Ур. защищённости:").grid(row=5, column=0, sticky=tk.W, pady=2)
|
|
def_frame = ttk.Frame(fields_frame)
|
|
def_frame.grid(row=5, column=1, sticky=tk.W, padx=10)
|
|
for lvl in ["1", "2", "3", "4"]:
|
|
ttk.Radiobutton(def_frame, text=f"{lvl} ур.",
|
|
variable=self.is_defence_var, value=lvl).pack(side=tk.LEFT, padx=3)
|
|
|
|
# Категории ПДн
|
|
ttk.Label(fields_frame, text="Категории ПДн:").grid(row=6, column=0, sticky=tk.NW, pady=2)
|
|
cat_frame = ttk.Frame(fields_frame)
|
|
cat_frame.grid(row=6, column=1, sticky=tk.W, padx=10)
|
|
self.category_vars = []
|
|
for cat in ["специальные", "биометрические", "общедоступные", "иные"]:
|
|
var = tk.BooleanVar()
|
|
cb = ttk.Checkbutton(cat_frame, text=cat, variable=var)
|
|
cb.pack(anchor=tk.W)
|
|
self.category_vars.append((cat, var))
|
|
|
|
fields_frame.columnconfigure(1, weight=1)
|
|
|
|
# Вложенные списки
|
|
nb2 = ttk.Notebook(self._is_edit_panel)
|
|
nb2.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
|
|
|
# Список ПДн
|
|
pd_tab = ttk.Frame(nb2)
|
|
nb2.add(pd_tab, text="Перечень ПДн")
|
|
self.pd_listbox = tk.Listbox(pd_tab, height=6)
|
|
self.pd_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
ttk.Button(pd_tab, text="+ Добавить", command=self._add_pd_item).pack(pady=3)
|
|
|
|
# Субъекты ПДн
|
|
subj_tab = ttk.Frame(nb2)
|
|
nb2.add(subj_tab, text="Субъекты ПДн")
|
|
self.pd_subjects_listbox = tk.Listbox(subj_tab, height=6)
|
|
self.pd_subjects_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
ttk.Button(subj_tab, text="+ Добавить", command=self._add_pd_subjects_item).pack(pady=3)
|
|
|
|
# СЗИ
|
|
def_tab = ttk.Frame(nb2)
|
|
nb2.add(def_tab, text="Средства защиты")
|
|
self.defense_tools_listbox = tk.Listbox(def_tab, height=6)
|
|
self.defense_tools_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
ttk.Button(def_tab, text="+ Добавить", command=self._add_defense_item).pack(pady=3)
|
|
|
|
# Пользователи
|
|
user_tab = ttk.Frame(nb2)
|
|
nb2.add(user_tab, text="Пользователи ИС")
|
|
self.users_listbox = tk.Listbox(user_tab, height=6)
|
|
self.users_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
btnf = ttk.Frame(user_tab)
|
|
btnf.pack(pady=3)
|
|
ttk.Button(btnf, text="+ Добавить", command=self._add_user_to_is).pack(side=tk.LEFT, padx=2)
|
|
ttk.Button(btnf, text="- Удалить", command=self._del_user_from_is).pack(side=tk.LEFT, padx=2)
|
|
|
|
self._set_is_edit_state(False)
|
|
|
|
# ---------- ВКЛАДКА "КОМИССИЯ" ----------
|
|
def _build_commission_tab(self):
|
|
tab = ttk.Frame(self.notebook)
|
|
self.notebook.add(tab, text="Комиссия")
|
|
|
|
canvas = tk.Canvas(tab, highlightthickness=0)
|
|
scrollbar = ttk.Scrollbar(tab, orient=tk.VERTICAL, command=canvas.yview)
|
|
inner = ttk.Frame(canvas)
|
|
inner.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
|
|
canvas.create_window((0, 0), window=inner, anchor="nw")
|
|
canvas.configure(yscrollcommand=scrollbar.set)
|
|
canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
|
|
|
frame = ttk.Frame(inner, padding=10)
|
|
frame.pack(fill=tk.X)
|
|
|
|
ttk.Label(frame, text="Состав комиссии", font=('Arial', 11, 'bold')).pack(
|
|
anchor=tk.W, pady=(0, 10))
|
|
|
|
# Заголовки
|
|
header = ttk.Frame(frame)
|
|
header.pack(fill=tk.X)
|
|
for w, col in [(20, "Роль"), (30, "Должность"), (25, "ФИО")]:
|
|
ttk.Label(header, text=col, font=('', 9, 'bold'),
|
|
width=w, anchor=tk.W).pack(side=tk.LEFT, padx=2)
|
|
|
|
self._comm_members_frame = ttk.Frame(frame)
|
|
self._comm_members_frame.pack(fill=tk.X)
|
|
|
|
ttk.Button(frame, text="+ Добавить члена комиссии",
|
|
command=self._add_commission_row_ui).pack(pady=10)
|
|
|
|
# ---------- ВКЛАДКА "ГЕНЕРАЦИЯ" ----------
|
|
def _build_generation_tab(self):
|
|
tab = ttk.Frame(self.notebook)
|
|
self.notebook.add(tab, text="Генерация")
|
|
|
|
frame = ttk.Frame(tab, padding=10)
|
|
frame.pack(fill=tk.BOTH, expand=True)
|
|
|
|
ttk.Label(frame, text="Параметры генерации", font=('Arial', 11, 'bold')).pack(
|
|
anchor=tk.W, pady=(0, 10))
|
|
|
|
ttk.Label(frame, text="Папка шаблонов:").pack(anchor=tk.W)
|
|
tf = ttk.Frame(frame)
|
|
tf.pack(fill=tk.X, pady=(0, 5))
|
|
ttk.Entry(tf, textvariable=self.templates_var).pack(side=tk.LEFT, fill=tk.X, expand=True)
|
|
ttk.Button(tf, text="Обзор", command=self.select_templates).pack(side=tk.RIGHT, padx=5)
|
|
|
|
ttk.Label(frame, text="Папка результатов:").pack(anchor=tk.W)
|
|
sf = ttk.Frame(frame)
|
|
sf.pack(fill=tk.X, pady=(0, 5))
|
|
ttk.Entry(sf, textvariable=self.save_var).pack(side=tk.LEFT, fill=tk.X, expand=True)
|
|
ttk.Button(sf, text="Обзор", command=self._select_save_dir).pack(side=tk.RIGHT, padx=5)
|
|
|
|
ttk.Label(frame, text="Шаблоны для генерации:", font=('Arial', 10, 'bold')).pack(
|
|
anchor=tk.W, pady=(10, 0))
|
|
|
|
btn_frame = ttk.Frame(frame)
|
|
btn_frame.pack(fill=tk.X, pady=(0, 5))
|
|
ttk.Button(btn_frame, text="Выделить всё",
|
|
command=lambda: self._select_templates_all(True)).pack(side=tk.LEFT, padx=(0, 5))
|
|
ttk.Button(btn_frame, text="Снять всё",
|
|
command=lambda: self._select_templates_all(False)).pack(side=tk.LEFT)
|
|
|
|
self.template_listbox = tk.Listbox(frame, height=8, selectmode=tk.EXTENDED)
|
|
self.template_listbox.pack(fill=tk.BOTH, expand=True, pady=5)
|
|
self.template_listbox.bind('<<ListboxSelect>>', self._on_template_select)
|
|
|
|
ttk.Button(frame, text="⚡ Сгенерировать выбранные документы",
|
|
command=self.generate_all).pack(fill=tk.X, pady=10)
|
|
|
|
# ---------- ВКЛАДКА "ЛОГ" ----------
|
|
def _build_log_tab(self):
|
|
tab = ttk.Frame(self.notebook)
|
|
self.notebook.add(tab, text="Лог")
|
|
self.log_text = scrolledtext.ScrolledText(tab, height=10)
|
|
self.log_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
self.log_text.insert(tk.END, "Лог готов\n")
|
|
self.log_text.configure(state='disabled')
|
|
|
|
# ==================== МЕТОДЫ РАБОТЫ С ДАННЫМИ ====================
|
|
def _init_company(self):
|
|
"""Инициализация модели."""
|
|
self.model = Company()
|
|
# 3 члена комиссии по умолчанию
|
|
for _ in range(3):
|
|
self.model.commission.append(CommissionMember())
|
|
|
|
def _collect_company(self):
|
|
"""Собирает данные из GUI в модель."""
|
|
c = self.model
|
|
c.full_name = self.company_name_entry.get().strip()
|
|
c.short_name = self.short_name_entry.get().strip()
|
|
for key, entry in self._company_entries.items():
|
|
setattr(c, key, entry.get().strip())
|
|
for key, entry in self._official_entries.items():
|
|
setattr(c, key, entry.get().strip())
|
|
c.contract_number = self.doc_number_var.get()
|
|
c.contract_date = self.doc_date_var.get()
|
|
|
|
def _collect_is(self):
|
|
"""Собирает данные ИС из GUI в модель."""
|
|
if self._current_is_idx is not None and self._current_is_idx < len(self.model.information_systems):
|
|
isys = self.model.information_systems[self._current_is_idx]
|
|
isys.name = self.is_name_entry.get().strip()
|
|
isys.description = self.is_description_entry.get().strip()
|
|
isys.software = self.is_software_entry.get().strip()
|
|
isys.is_local_network = self.is_lan_var.get()
|
|
isys.is_internet = self.is_internet_var.get()
|
|
isys.threat_type = self.is_threat_var.get()
|
|
isys.defence_level = self.is_defence_var.get()
|
|
cats = [c for c, v in self.category_vars if v.get()]
|
|
isys.personal_data_category = cats
|
|
# Списки
|
|
isys.personal_data_list = list(self.pd_listbox.get(0, tk.END))
|
|
isys.pd_subjects_list = list(self.pd_subjects_listbox.get(0, tk.END))
|
|
isys.defense_tools_list = list(self.defense_tools_listbox.get(0, tk.END))
|
|
isys.users_list = list(self.users_listbox.get(0, tk.END))
|
|
|
|
def _collect_commission(self):
|
|
"""Собирает комиссию из GUI."""
|
|
self.model.commission.clear()
|
|
for row_frame, role_var, pos_entry, fio_entry in self.commission_entries:
|
|
self.model.commission.append(
|
|
CommissionMember(
|
|
role=role_var.get(),
|
|
position=pos_entry.get().strip(),
|
|
fio=fio_entry.get().strip(),
|
|
)
|
|
)
|
|
|
|
def _company_to_gui(self):
|
|
"""Заполняет GUI из модели."""
|
|
c = self.model
|
|
self.company_name_entry.delete(0, tk.END)
|
|
self.company_name_entry.insert(0, c.full_name or "")
|
|
self.short_name_entry.delete(0, tk.END)
|
|
self.short_name_entry.insert(0, c.short_name or "")
|
|
|
|
for key, entry in self._company_entries.items():
|
|
entry.delete(0, tk.END)
|
|
entry.insert(0, getattr(c, key, "") or "")
|
|
|
|
for key, entry in self._official_entries.items():
|
|
entry.delete(0, tk.END)
|
|
entry.insert(0, getattr(c, key, "") or "")
|
|
|
|
self.doc_number_var.set(c.contract_number or "")
|
|
self.doc_date_var.set(c.contract_date or "")
|
|
|
|
# Склонения
|
|
idx_map = {'nomn': 1, 'gent': 2, 'datv': 3, 'accs': 4, 'ablt': 5, 'loct': 6}
|
|
for code, entry in self.decl_entries.items():
|
|
entry.delete(0, tk.END)
|
|
entry.insert(0, getattr(c, f"company_name_{idx_map[code]}", "") or "")
|
|
for code, entry in self.short_decl_entries.items():
|
|
entry.delete(0, tk.END)
|
|
entry.insert(0, getattr(c, f"short_name_{idx_map[code]}", "") or "")
|
|
|
|
# Комиссия
|
|
for frame in list(self._comm_members_frame.winfo_children()):
|
|
frame.destroy()
|
|
self.commission_entries.clear()
|
|
for member in c.commission:
|
|
self._add_commission_row(member.role, member.position, member.fio)
|
|
if not self.commission_entries:
|
|
for _ in range(3):
|
|
self._add_commission_row()
|
|
|
|
# ИС
|
|
self._refresh_is_listbox()
|
|
if self.model.information_systems:
|
|
self.is_listbox.selection_set(0)
|
|
self._on_is_select()
|
|
|
|
# Шаблоны
|
|
self._on_templates_changed()
|
|
|
|
# ==================== КОМИССИЯ ====================
|
|
def _add_commission_row(self, role="Член комиссии", position="", fio=""):
|
|
row = ttk.Frame(self._comm_members_frame)
|
|
row.pack(fill=tk.X, pady=2)
|
|
|
|
role_var = tk.StringVar(value=role)
|
|
role_cb = ttk.Combobox(row, textvariable=role_var, state='readonly', width=22,
|
|
values=["Председатель комиссии", "Секретарь комиссии",
|
|
"Член комиссии", "Заместитель председателя"])
|
|
role_cb.pack(side=tk.LEFT, padx=2)
|
|
|
|
pos_entry = ttk.Entry(row, width=30)
|
|
pos_entry.insert(0, position)
|
|
pos_entry.pack(side=tk.LEFT, padx=2)
|
|
|
|
fio_entry = ttk.Entry(row, width=25)
|
|
fio_entry.insert(0, fio)
|
|
fio_entry.pack(side=tk.LEFT, padx=2)
|
|
|
|
ttk.Button(row, text="✖", width=3,
|
|
command=lambda: self._delete_commission_row(row)).pack(side=tk.LEFT, padx=5)
|
|
|
|
self.commission_entries.append((row, role_var, pos_entry, fio_entry))
|
|
|
|
def _add_commission_row_ui(self):
|
|
self._add_commission_row()
|
|
|
|
def _delete_commission_row(self, row_frame):
|
|
self.commission_entries = [e for e in self.commission_entries if e[0] != row_frame]
|
|
row_frame.destroy()
|
|
|
|
# ==================== ИНФОРМАЦИОННЫЕ СИСТЕМЫ ====================
|
|
def _refresh_is_listbox(self):
|
|
self.is_listbox.delete(0, tk.END)
|
|
for isys in self.model.information_systems:
|
|
self.is_listbox.insert(tk.END, isys.name or "(без названия)")
|
|
if not self.model.information_systems:
|
|
self._is_edit_panel.pack_forget()
|
|
self._is_edit_panel.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
self._set_is_edit_state(False)
|
|
else:
|
|
self._is_edit_panel.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
|
|
def _set_is_edit_state(self, enabled):
|
|
state = 'normal' if enabled else 'disabled'
|
|
for w in [self.is_name_entry, self.is_description_entry, self.is_software_entry]:
|
|
w.config(state=state)
|
|
self.is_lan_var.set(False)
|
|
self.is_internet_var.set(False)
|
|
|
|
def _on_is_select(self, event=None):
|
|
sel = self.is_listbox.curselection()
|
|
if not sel:
|
|
self._current_is_idx = None
|
|
return
|
|
|
|
# Сохраняем текущую ИС
|
|
self._collect_is()
|
|
|
|
idx = sel[0]
|
|
if idx >= len(self.model.information_systems):
|
|
return
|
|
self._current_is_idx = idx
|
|
isys = self.model.information_systems[idx]
|
|
|
|
self.is_name_entry.delete(0, tk.END)
|
|
self.is_name_entry.insert(0, isys.name or "")
|
|
self.is_description_entry.delete(0, tk.END)
|
|
self.is_description_entry.insert(0, isys.description or "")
|
|
self.is_software_entry.delete(0, tk.END)
|
|
self.is_software_entry.insert(0, isys.software or "")
|
|
self.is_lan_var.set(isys.is_local_network)
|
|
self.is_internet_var.set(isys.is_internet)
|
|
self.is_threat_var.set(isys.threat_type or "3")
|
|
self.is_defence_var.set(isys.defence_level or "4")
|
|
for cat, var in self.category_vars:
|
|
var.set(cat in isys.personal_data_category)
|
|
|
|
# Списки
|
|
for lb, items in [
|
|
(self.pd_listbox, isys.personal_data_list or []),
|
|
(self.pd_subjects_listbox, isys.pd_subjects_list or []),
|
|
(self.defense_tools_listbox, isys.defense_tools_list or []),
|
|
(self.users_listbox, isys.users_list or []),
|
|
]:
|
|
lb.delete(0, tk.END)
|
|
for item in items:
|
|
lb.insert(tk.END, item)
|
|
|
|
def _add_is(self):
|
|
name = simpledialog.askstring("Добавить ИС", "Введите наименование ИС:")
|
|
if name:
|
|
isys = InformationSystem(name=name)
|
|
self.model.information_systems.append(isys)
|
|
self._refresh_is_listbox()
|
|
self.is_listbox.selection_set(tk.END)
|
|
self._on_is_select()
|
|
|
|
def _copy_is(self):
|
|
if self._current_is_idx is None:
|
|
return
|
|
self._collect_is()
|
|
src = self.model.information_systems[self._current_is_idx]
|
|
copy = InformationSystem(
|
|
name=src.name + " (копия)",
|
|
description=src.description,
|
|
software=src.software,
|
|
is_local_network=src.is_local_network,
|
|
is_internet=src.is_internet,
|
|
defence_level=src.defence_level,
|
|
threat_type=src.threat_type,
|
|
personal_data_category=list(src.personal_data_category),
|
|
personal_data_list=list(src.personal_data_list),
|
|
pd_subjects_list=list(src.pd_subjects_list),
|
|
defense_tools_list=list(src.defense_tools_list),
|
|
users_list=list(src.users_list),
|
|
)
|
|
self.model.information_systems.append(copy)
|
|
self._refresh_is_listbox()
|
|
self.is_listbox.selection_set(tk.END)
|
|
self._on_is_select()
|
|
|
|
def _delete_is(self):
|
|
if self._current_is_idx is None:
|
|
return
|
|
del self.model.information_systems[self._current_is_idx]
|
|
self._current_is_idx = None
|
|
self._refresh_is_listbox()
|
|
if self.model.information_systems:
|
|
self.is_listbox.selection_set(0)
|
|
self._on_is_select()
|
|
|
|
def _add_pd_item(self):
|
|
item = simpledialog.askstring("Добавить ПДн", "Введите персональные данные:")
|
|
if item:
|
|
self.pd_listbox.insert(tk.END, item)
|
|
|
|
def _add_pd_subjects_item(self):
|
|
item = simpledialog.askstring("Добавить категорию", "Введите категорию субъектов:")
|
|
if item:
|
|
self.pd_subjects_listbox.insert(tk.END, item)
|
|
|
|
def _add_defense_item(self):
|
|
item = simpledialog.askstring("Добавить СЗИ", "Введите средство защиты:")
|
|
if item:
|
|
self.defense_tools_listbox.insert(tk.END, item)
|
|
|
|
def _add_user_to_is(self):
|
|
user = simpledialog.askstring("Добавить пользователя", "Введите ФИО или должность:")
|
|
if user:
|
|
self.users_listbox.insert(tk.END, user)
|
|
|
|
def _del_user_from_is(self):
|
|
sel = self.users_listbox.curselection()
|
|
if sel:
|
|
self.users_listbox.delete(sel[0])
|
|
|
|
# ==================== ШАБЛОНЫ ====================
|
|
def select_templates(self):
|
|
folder = filedialog.askdirectory(title="Папка шаблонов")
|
|
if folder:
|
|
self.templates_var.set(folder)
|
|
self._on_templates_changed()
|
|
|
|
def _select_save_dir(self):
|
|
folder = filedialog.askdirectory(title="Папка для результатов")
|
|
if folder:
|
|
self.save_var.set(folder)
|
|
|
|
def _on_templates_changed(self, *_):
|
|
"""Обновить список шаблонов при смене папки."""
|
|
folder = self.templates_var.get()
|
|
self.template_listbox.delete(0, tk.END)
|
|
self._template_files = []
|
|
if folder and os.path.isdir(folder):
|
|
for f in sorted(os.listdir(folder)):
|
|
if f.endswith('.docx') and not f.endswith('_result.docx'):
|
|
self.template_listbox.insert(tk.END, f)
|
|
self._template_files.append(f)
|
|
# Выделяем все по умолчанию
|
|
self.template_listbox.selection_set(0, tk.END)
|
|
self._selected_templates = set(self._template_files)
|
|
self.log(f"Найдено шаблонов: {len(self._template_files)}")
|
|
|
|
def _on_template_select(self, event=None):
|
|
sel = self.template_listbox.curselection()
|
|
self._selected_templates = {self._template_files[i] for i in sel}
|
|
|
|
def _select_templates_all(self, select: bool):
|
|
if select:
|
|
self.template_listbox.selection_set(0, tk.END)
|
|
else:
|
|
self.template_listbox.selection_clear(0, tk.END)
|
|
self._on_template_select()
|
|
|
|
# ==================== ГЕНЕРАЦИЯ ====================
|
|
def generate_all(self):
|
|
"""Генерация всех выбранных документов."""
|
|
templates_dir = self.templates_var.get()
|
|
output_dir = self.save_var.get()
|
|
|
|
if not templates_dir or not os.path.isdir(templates_dir):
|
|
messagebox.showerror("Ошибка", "Выберите папку с шаблонами")
|
|
return
|
|
if not output_dir:
|
|
output_dir = os.path.join(
|
|
os.path.dirname(os.path.abspath(__file__)),
|
|
"Сгенерированные_документы"
|
|
)
|
|
self.save_var.set(output_dir)
|
|
|
|
selected = self._selected_templates
|
|
if not selected:
|
|
messagebox.showwarning("Предупреждение", "Не выбран ни один шаблон")
|
|
return
|
|
|
|
# Собираем данные
|
|
self._collect_company()
|
|
self._collect_commission()
|
|
self._collect_is()
|
|
|
|
# Проверка
|
|
if not self._validate_before_generate():
|
|
return
|
|
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
|
|
# Запускаем в отдельном потоке
|
|
def _run():
|
|
try:
|
|
replacements = build_replacements(
|
|
company=self.model,
|
|
is_list=self.model.information_systems,
|
|
doc_number=self.doc_number_var.get(),
|
|
doc_date=self.doc_date_var.get(),
|
|
direction='152fz',
|
|
)
|
|
|
|
results = []
|
|
for fname in selected:
|
|
template_path = os.path.join(templates_dir, fname)
|
|
if not os.path.isfile(template_path):
|
|
self.log(f"❌ Файл не найден: {fname}")
|
|
continue
|
|
|
|
base = os.path.splitext(fname)[0]
|
|
output_path = os.path.join(output_dir, f"{base}.docx")
|
|
|
|
try:
|
|
process_template(template_path, replacements, output_path, self.log)
|
|
results.append(output_path)
|
|
except Exception as e:
|
|
self.log(f"❌ {fname}: {e}")
|
|
|
|
self.log(f"✅ Готово: {len(results)} из {len(selected)}")
|
|
self.root.after(0, lambda: messagebox.showinfo(
|
|
"Готово", f"Сгенерировано {len(results)} документов\n"
|
|
f"Папка: {output_dir}"
|
|
))
|
|
except Exception as e:
|
|
self.log(f"❌ Ошибка: {e}")
|
|
self.root.after(0, lambda: messagebox.showerror("Ошибка", str(e)))
|
|
|
|
threading.Thread(target=_run, daemon=True).start()
|
|
|
|
def _validate_before_generate(self):
|
|
warnings = []
|
|
if not self.model.full_name:
|
|
warnings.append("Полное наименование организации")
|
|
if not self.model.inn:
|
|
warnings.append("ИНН")
|
|
if not self.model.chief_fio:
|
|
warnings.append("ФИО руководителя")
|
|
if not self.model.information_systems:
|
|
warnings.append("Нет информационных систем")
|
|
if warnings:
|
|
msg = "Не заполнены:\n" + "\n".join(warnings)
|
|
return messagebox.askyesno("Внимание", msg + "\n\nПродолжить?")
|
|
return True
|
|
|
|
# ==================== СКЛОНЕНИЕ ====================
|
|
def _update_company_declensions(self):
|
|
name = self.company_name_entry.get().strip()
|
|
if not name:
|
|
return
|
|
self.model.full_name = name
|
|
self.model.update_declensions(inflect_name)
|
|
idx_map = {'nomn': 1, 'gent': 2, 'datv': 3, 'accs': 4, 'ablt': 5, 'loct': 6}
|
|
for code, entry in self.decl_entries.items():
|
|
entry.delete(0, tk.END)
|
|
entry.insert(0, getattr(self.model, f"company_name_{idx_map[code]}", ""))
|
|
self.log("✅ Склонения полного названия обновлены")
|
|
|
|
def _update_short_declensions(self):
|
|
name = self.short_name_entry.get().strip()
|
|
if not name:
|
|
return
|
|
self.model.short_name = name
|
|
self.model.update_short_declensions(inflect_name)
|
|
idx_map = {'nomn': 1, 'gent': 2, 'datv': 3, 'accs': 4, 'ablt': 5, 'loct': 6}
|
|
for code, entry in self.short_decl_entries.items():
|
|
entry.delete(0, tk.END)
|
|
entry.insert(0, getattr(self.model, f"short_name_{idx_map[code]}", ""))
|
|
self.log("✅ Склонения краткого названия обновлены")
|
|
|
|
# ==================== ИМПОРТ ====================
|
|
def import_from_survey(self):
|
|
path = filedialog.askopenfilename(
|
|
title="Выберите опросный лист (152-ФЗ)",
|
|
filetypes=[("Excel", "*.xlsx *.xls")]
|
|
)
|
|
if not path:
|
|
return
|
|
self.log(f"Импорт: {path}")
|
|
|
|
# Импорт в отдельном потоке
|
|
def _run():
|
|
try:
|
|
from .importer import import_152fz
|
|
data = import_152fz(path, self.log)
|
|
if data:
|
|
self._apply_imported_data(data)
|
|
except ImportError:
|
|
self.log("❌ Модуль импорта не найден")
|
|
except Exception as e:
|
|
self.log(f"❌ Ошибка импорта: {e}")
|
|
|
|
threading.Thread(target=_run, daemon=True).start()
|
|
|
|
def _apply_imported_data(self, data: dict):
|
|
def _apply():
|
|
if data.get('fullName'):
|
|
self.company_name_entry.delete(0, tk.END)
|
|
self.company_name_entry.insert(0, data['fullName'])
|
|
if data.get('shortName'):
|
|
self.short_name_entry.delete(0, tk.END)
|
|
self.short_name_entry.insert(0, data['shortName'])
|
|
for key, entry in self._company_entries.items():
|
|
val = data.get(key) or data.get(key.replace('_', '')) or ''
|
|
if val:
|
|
entry.delete(0, tk.END)
|
|
entry.insert(0, val)
|
|
for attr, entry in self._official_entries.items():
|
|
val = data.get(attr) or ''
|
|
if val:
|
|
entry.delete(0, tk.END)
|
|
entry.insert(0, val)
|
|
self.log("✅ Импорт применён")
|
|
self.root.after(0, _apply)
|
|
|
|
# ==================== ФАЙЛОВЫЕ ОПЕРАЦИИ ====================
|
|
def new_data(self):
|
|
if self.model.full_name:
|
|
if not messagebox.askyesno("Новое", "Текущие данные будут потеряны. Продолжить?"):
|
|
return
|
|
self._init_company()
|
|
self.current_file = None
|
|
self._company_to_gui()
|
|
self.log("Новые данные")
|
|
|
|
def open_data(self):
|
|
filename = filedialog.askopenfilename(filetypes=[("JSON", "*.json")])
|
|
if filename:
|
|
try:
|
|
with open(filename, 'r', encoding='utf-8') as f:
|
|
data = json.load(f)
|
|
self.model = Company.from_dict(data)
|
|
self.current_file = filename
|
|
self._company_to_gui()
|
|
self.log(f"Загружено: {filename}")
|
|
except Exception as e:
|
|
messagebox.showerror("Ошибка", str(e))
|
|
|
|
def save_data(self):
|
|
if self.current_file:
|
|
self._save_to(self.current_file)
|
|
else:
|
|
self.save_as_data()
|
|
|
|
def save_as_data(self):
|
|
filename = filedialog.asksaveasfilename(
|
|
defaultextension=".json", filetypes=[("JSON", "*.json")]
|
|
)
|
|
if filename:
|
|
self._save_to(filename)
|
|
|
|
def _save_to(self, filename):
|
|
self._collect_company()
|
|
self._collect_commission()
|
|
self._collect_is()
|
|
try:
|
|
with open(filename, 'w', encoding='utf-8') as f:
|
|
json.dump(self.model.to_dict(), f, ensure_ascii=False, indent=2)
|
|
self.current_file = filename
|
|
self.log(f"Сохранено: {filename}")
|
|
except Exception as e:
|
|
messagebox.showerror("Ошибка", str(e))
|
|
|
|
# ==================== АВТОСОХРАНЕНИЕ ====================
|
|
def _start_autosave_timer(self):
|
|
self._autosave()
|
|
self._autosave_timer_id = self.root.after(120000, self._start_autosave_timer)
|
|
|
|
def _stop_autosave_timer(self):
|
|
if hasattr(self, '_autosave_timer_id') and self._autosave_timer_id:
|
|
try:
|
|
self.root.after_cancel(self._autosave_timer_id)
|
|
except:
|
|
pass
|
|
|
|
def _autosave(self):
|
|
self._collect_company()
|
|
self._collect_commission()
|
|
self._collect_is()
|
|
try:
|
|
os.makedirs(os.path.dirname(self._autosave_path), exist_ok=True)
|
|
with open(self._autosave_path, 'w', encoding='utf-8') as f:
|
|
json.dump(self.model.to_dict(), f, ensure_ascii=False, indent=2)
|
|
except Exception:
|
|
pass
|
|
|
|
def _load_autosave(self):
|
|
try:
|
|
if not os.path.isfile(self._autosave_path):
|
|
return
|
|
mtime = os.path.getmtime(self._autosave_path)
|
|
if datetime.now().timestamp() - mtime > 86400:
|
|
return
|
|
with open(self._autosave_path, 'r', encoding='utf-8') as f:
|
|
data = json.load(f)
|
|
self.model = Company.from_dict(data)
|
|
self._company_to_gui()
|
|
self.log("📂 Данные восстановлены из автосохранения")
|
|
except Exception:
|
|
pass
|
|
|
|
def _on_close(self):
|
|
self._stop_autosave_timer()
|
|
self._autosave()
|
|
self.root.destroy()
|
|
|
|
# ==================== КОНТЕКСТНОЕ МЕНЮ ====================
|
|
def _enable_context_menu(self):
|
|
def _show_menu(event):
|
|
widget = event.widget
|
|
if not isinstance(widget, (tk.Entry, ttk.Entry, tk.Text, tk.Listbox)):
|
|
return
|
|
menu = tk.Menu(self.root, tearoff=0)
|
|
def _copy():
|
|
try:
|
|
text = ""
|
|
if isinstance(widget, (tk.Entry, ttk.Entry)):
|
|
text = widget.selection_get() if widget.selection_present() else ""
|
|
elif isinstance(widget, tk.Text):
|
|
text = widget.selection_get() if widget.tag_ranges(tk.SEL) else ""
|
|
if text:
|
|
self.root.clipboard_clear()
|
|
self.root.clipboard_append(text)
|
|
except:
|
|
pass
|
|
def _paste():
|
|
try:
|
|
text = self.root.clipboard_get()
|
|
if not text: return
|
|
if isinstance(widget, (tk.Entry, ttk.Entry)):
|
|
try: widget.delete(tk.SEL_FIRST, tk.SEL_LAST)
|
|
except: pass
|
|
widget.insert(tk.INSERT, text)
|
|
elif isinstance(widget, tk.Text):
|
|
try:
|
|
if widget.tag_ranges(tk.SEL):
|
|
widget.delete(tk.SEL_FIRST, tk.SEL_LAST)
|
|
except: pass
|
|
widget.insert(tk.INSERT, text)
|
|
except:
|
|
pass
|
|
menu.add_command(label="Копировать", command=_copy)
|
|
menu.add_command(label="Вставить", command=_paste)
|
|
menu.add_separator()
|
|
menu.add_command(label="Выделить всё", command=lambda: widget.select_range(0, tk.END) if hasattr(widget, 'select_range') else None)
|
|
menu.tk_popup(event.x_root, event.y_root)
|
|
self.root.bind_all('<Button-3>', _show_menu, add='+')
|
|
|
|
# ==================== ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ ====================
|
|
def log(self, message: str):
|
|
ts = datetime.now().strftime("%H:%M:%S")
|
|
try:
|
|
self.log_text.configure(state='normal')
|
|
self.log_text.insert(tk.END, f"[{ts}] {message}\n")
|
|
self.log_text.see(tk.END)
|
|
self.log_text.configure(state='disabled')
|
|
except Exception:
|
|
pass
|
|
self.status_var.set(message)
|
|
|
|
def show_about(self):
|
|
messagebox.showinfo("О программе",
|
|
"ДоКоГеНеРаТоР — 152-ФЗ\n"
|
|
"Версия 1.0\n"
|
|
"Генератор документов из шаблонов")
|
|
|
|
def show_variables(self):
|
|
text = "Переменные шаблонов:\n\n"
|
|
for var, group, desc, _ in VARIABLE_DEFS:
|
|
text += f"{var} — {desc} ({'общ.' if group == 'A' else '152-ФЗ'})\n"
|
|
messagebox.showinfo("Переменные шаблонов", text)
|
|
|
|
|
|
def main():
|
|
"""Точка входа в приложение."""
|
|
root = tk.Tk()
|
|
app = DokoGenApp(root)
|
|
root.mainloop()
|