Files
dokogen152/ui.py
T
prog1764 72bbb117b9 🎉 Первый коммит — DokoGen v1.0
Чистый генератор документов 152-ФЗ.
Только нужные переменные, без мусора от 187-ФЗ/117-ФСТЭК.
Модели, склонение, генератор DOCX, tkinter GUI, импорт из Excel.
2026-07-30 13:47:02 +04:00

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()