1572 lines
71 KiB
Python
1572 lines
71 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="")
|
||
self.doc_date_var = tk.StringVar(value="")
|
||
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_users_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
|
||
|
||
# Привязка автозаполнения города из адреса
|
||
addr_entry = self._company_entries.get('address')
|
||
if addr_entry:
|
||
addr_entry.bind('<KeyRelease>', self._on_address_changed)
|
||
|
||
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)
|
||
|
||
# Чекбоксы
|
||
chk_frame = ttk.Frame(fields_frame)
|
||
chk_frame.grid(row=2, 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")
|
||
self.is_threat_var.trace_add("write", lambda *_: self._on_categories_or_count_changed())
|
||
ttk.Label(fields_frame, text="Тип угроз:").grid(row=3, column=0, sticky=tk.W, pady=2)
|
||
threat_frame = ttk.Frame(fields_frame)
|
||
threat_frame.grid(row=3, 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=4, column=0, sticky=tk.W, pady=2)
|
||
def_frame = ttk.Frame(fields_frame)
|
||
def_frame.grid(row=4, 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=5, column=0, sticky=tk.NW, pady=2)
|
||
cat_frame = ttk.Frame(fields_frame)
|
||
cat_frame.grid(row=5, column=1, sticky=tk.W, padx=10)
|
||
self.category_vars = []
|
||
for cat in ["специальные", "биометрические", "общедоступные", "иные"]:
|
||
var = tk.BooleanVar()
|
||
var.trace_add('write', lambda *_: self._on_categories_or_count_changed())
|
||
cb = ttk.Checkbutton(cat_frame, text=cat, variable=var)
|
||
cb.pack(anchor=tk.W)
|
||
self.category_vars.append((cat, var))
|
||
|
||
# Количество записей ПДн (выпадающий список)
|
||
ttk.Label(fields_frame, text="Кол-во записей:").grid(row=6, column=0, sticky=tk.W, pady=2)
|
||
self.is_pd_count_var = tk.StringVar(value="менее 100 000")
|
||
self.is_pd_count_combo = ttk.Combobox(fields_frame, textvariable=self.is_pd_count_var,
|
||
values=["менее 100 000", "более 100 000"],
|
||
state='readonly', width=28)
|
||
self.is_pd_count_combo.grid(row=6, column=1, padx=10, sticky=tk.W)
|
||
self.is_pd_count_combo.bind('<<ComboboxSelected>>', lambda e: self._on_categories_or_count_changed())
|
||
|
||
|
||
|
||
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="Перечень ПДн")
|
||
pd_btn_frame = ttk.Frame(pd_tab)
|
||
pd_btn_frame.pack(fill=tk.X)
|
||
ttk.Button(pd_btn_frame, text="+ Добавить", command=self._add_pd_item).pack(side=tk.LEFT, padx=2)
|
||
ttk.Button(pd_btn_frame, text="- Удалить", command=lambda: self._del_from_listbox(self.pd_listbox)).pack(side=tk.LEFT, padx=2)
|
||
ttk.Button(pd_btn_frame, text="Выбрать из списка", command=self._select_from_pd_list).pack(side=tk.LEFT, padx=2)
|
||
self.pd_listbox = tk.Listbox(pd_tab, height=6)
|
||
self.pd_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||
|
||
# Субъекты ПДн
|
||
subj_tab = ttk.Frame(nb2)
|
||
nb2.add(subj_tab, text="Субъекты ПДн")
|
||
subj_btn_frame = ttk.Frame(subj_tab)
|
||
subj_btn_frame.pack(fill=tk.X)
|
||
ttk.Button(subj_btn_frame, text="+ Добавить", command=self._add_pd_subjects_item).pack(side=tk.LEFT, padx=2)
|
||
ttk.Button(subj_btn_frame, text="- Удалить", command=lambda: self._del_from_listbox(self.pd_subjects_listbox)).pack(side=tk.LEFT, padx=2)
|
||
ttk.Button(subj_btn_frame, text="Выбрать из списка", command=self._select_from_subjects_list).pack(side=tk.LEFT, padx=2)
|
||
self.pd_subjects_listbox = tk.Listbox(subj_tab, height=6)
|
||
self.pd_subjects_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||
|
||
# Действия с ПДн
|
||
act_tab = ttk.Frame(nb2)
|
||
nb2.add(act_tab, text="Действия с ПДн")
|
||
act_btn_frame = ttk.Frame(act_tab)
|
||
act_btn_frame.pack(fill=tk.X)
|
||
ttk.Button(act_btn_frame, text="+ Добавить", command=self._add_pd_action_item).pack(side=tk.LEFT, padx=2)
|
||
ttk.Button(act_btn_frame, text="- Удалить", command=lambda: self._del_from_listbox(self.pd_actions_listbox)).pack(side=tk.LEFT, padx=2)
|
||
ttk.Button(act_btn_frame, text="Выбрать из списка", command=self._select_from_actions_list).pack(side=tk.LEFT, padx=2)
|
||
self.pd_actions_listbox = tk.Listbox(act_tab, height=6)
|
||
self.pd_actions_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||
|
||
# СЗИ
|
||
def_tab = ttk.Frame(nb2)
|
||
nb2.add(def_tab, text="Средства защиты")
|
||
def_btn_frame = ttk.Frame(def_tab)
|
||
def_btn_frame.pack(fill=tk.X)
|
||
ttk.Button(def_btn_frame, text="+ Добавить", command=self._add_defense_item).pack(side=tk.LEFT, padx=2)
|
||
ttk.Button(def_btn_frame, text="- Удалить", command=lambda: self._del_from_listbox(self.defense_tools_listbox)).pack(side=tk.LEFT, padx=2)
|
||
ttk.Button(def_btn_frame, text="Выбрать из списка", command=self._select_from_defense_list).pack(side=tk.LEFT, padx=2)
|
||
self.defense_tools_listbox = tk.Listbox(def_tab, height=6)
|
||
self.defense_tools_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||
|
||
# Пользователи
|
||
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_users_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)
|
||
|
||
frame = ttk.Frame(scrollable, padding=10)
|
||
frame.pack(fill=tk.BOTH, expand=True)
|
||
|
||
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 [(25, "Должность"), (30, "ФИО")]:
|
||
ttk.Label(header, text=col, font=('', 9, 'bold'),
|
||
width=w, anchor=tk.W).pack(side=tk.LEFT, padx=2)
|
||
|
||
self._employees_frame = ttk.Frame(frame)
|
||
self._employees_frame.pack(fill=tk.X, pady=5)
|
||
|
||
btn_frame = ttk.Frame(frame)
|
||
btn_frame.pack(fill=tk.X, pady=5)
|
||
ttk.Button(btn_frame, text="+ Добавить сотрудника",
|
||
command=self._add_employee_row_ui).pack(side=tk.LEFT, padx=2)
|
||
ttk.Button(btn_frame, text="- Удалить выбранного",
|
||
command=self._delete_employee_selected).pack(side=tk.LEFT, padx=2)
|
||
|
||
# Счётчик
|
||
self._emp_count_label = ttk.Label(frame, text="Добавлено: 0")
|
||
self._emp_count_label.pack(anchor=tk.W, pady=5)
|
||
|
||
self._employee_entries = []
|
||
# Загрузка существующих
|
||
for emp in self.model.employees_access:
|
||
self._add_employee_row(
|
||
emp.get('position', ''),
|
||
emp.get('fio', ''),
|
||
|
||
)
|
||
self._update_emp_count()
|
||
|
||
def _add_employee_row(self, position="", fio=""):
|
||
"""Добавляет одну строку сотрудника."""
|
||
row = ttk.Frame(self._employees_frame)
|
||
row.pack(fill=tk.X, pady=2)
|
||
|
||
pos_entry = ttk.Entry(row, width=25)
|
||
pos_entry.insert(0, position)
|
||
pos_entry.pack(side=tk.LEFT, padx=2)
|
||
|
||
fio_entry = ttk.Entry(row, width=30)
|
||
fio_entry.insert(0, fio)
|
||
fio_entry.pack(side=tk.LEFT, padx=2)
|
||
|
||
|
||
del_btn = ttk.Button(row, text="✖", width=3,
|
||
command=lambda: self._delete_employee_row(row))
|
||
del_btn.pack(side=tk.LEFT, padx=5)
|
||
|
||
self._employee_entries.append((row, pos_entry, fio_entry))
|
||
self._update_emp_count()
|
||
|
||
def _add_employee_row_ui(self):
|
||
self._add_employee_row()
|
||
|
||
def _delete_employee_row(self, row_frame):
|
||
self._employee_entries = [e for e in self._employee_entries if e[0] != row_frame]
|
||
row_frame.destroy()
|
||
self._update_emp_count()
|
||
|
||
def _delete_employee_selected(self):
|
||
# Удаляем последнюю строку, если есть
|
||
if self._employee_entries:
|
||
row_frame = self._employee_entries[-1][0]
|
||
self._delete_employee_row(row_frame)
|
||
|
||
def _update_emp_count(self):
|
||
count = len(self._employee_entries)
|
||
if hasattr(self, '_emp_count_label'):
|
||
self._emp_count_label.config(text=f"Добавлено: {count}")
|
||
|
||
def _collect_employees(self):
|
||
"""Собирает данные сотрудников из GUI в модель."""
|
||
self.model.employees_access.clear()
|
||
for row, pos_e, fio_e in self._employee_entries:
|
||
emp = {
|
||
'position': pos_e.get().strip(),
|
||
'fio': fio_e.get().strip(),
|
||
}
|
||
if emp['position'] or emp['fio']:
|
||
self.model.employees_access.append(emp)
|
||
|
||
# ---------- ВКЛАДКА "ГЕНЕРАЦИЯ" ----------
|
||
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.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
|
||
if hasattr(self, 'is_pd_count_entry'):
|
||
isys.pd_count = self.is_pd_count_entry.get().strip() or 'менее 100 000'
|
||
# Списки
|
||
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.pd_actions_list = list(self.pd_actions_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()
|
||
|
||
# Пользователи
|
||
if hasattr(self, '_employee_entries'):
|
||
for row_frame, _, _ in self._employee_entries:
|
||
row_frame.destroy()
|
||
self._employee_entries.clear()
|
||
for emp in c.employees_access:
|
||
self._add_employee_row(
|
||
emp.get('position', ''),
|
||
emp.get('fio', ''),
|
||
)
|
||
self._update_emp_count()
|
||
|
||
# Шаблоны
|
||
self._on_templates_changed()
|
||
|
||
# Автоопределение города из адреса после загрузки
|
||
self._on_address_changed()
|
||
|
||
# ==================== ГОРОД ИЗ АДРЕСА ====================
|
||
def _extract_city_from_address(self, address: str) -> str:
|
||
"""Извлекает название города из адреса (простая эвристика)."""
|
||
if not address:
|
||
return ''
|
||
# Пробуем найти "г. Город" или "Г Город" (без точки, заглавная)
|
||
import re
|
||
m = re.search(r'\b[гГ]\.?\s*([А-ЯЁ][а-яёA-Za-z-]+(?:\s+[А-ЯЁ][а-яёA-Za-z-]+)*)', address)
|
||
if m:
|
||
return m.group(1).strip()
|
||
# Пробуем найти "город Город"
|
||
m = re.search(r'\bгород\s+([А-ЯЁ][а-яёA-Za-z-]+(?:\s+[А-ЯЁ][а-яёA-Za-z-]+)*)', address, re.IGNORECASE)
|
||
if m:
|
||
return m.group(1).strip()
|
||
# Пробуем первую часть до запятой, если там известный город
|
||
parts = [p.strip() for p in address.split(',')]
|
||
if parts:
|
||
first = parts[0]
|
||
# Если первая часть — короткая и не похожа на улицу/дом
|
||
if len(first) <= 25 and first and not first.startswith(('ул', 'пр', 'пер', 'шоссе')):
|
||
# Проверяем, что это похоже на город
|
||
city_keywords = ['ск', 'цк', 'бург', 'град', 'полис', 'поль', 'стан', 'горск', 'чинск', 'янск', 'овск']
|
||
if any(first.lower().endswith(kw) for kw in city_keywords):
|
||
return first
|
||
return ''
|
||
|
||
def _on_address_changed(self, event=None):
|
||
"""Обработчик изменения адреса — автозаполняет город."""
|
||
addr_entry = self._company_entries.get('address')
|
||
city_entry = self._company_entries.get('city_name')
|
||
if not addr_entry or not city_entry:
|
||
return
|
||
address = addr_entry.get().strip()
|
||
if not address:
|
||
return
|
||
current_city = city_entry.get().strip()
|
||
if current_city:
|
||
return # Не перезаписываем, если уже заполнен
|
||
city = self._extract_city_from_address(address)
|
||
if city:
|
||
city_entry.delete(0, tk.END)
|
||
city_entry.insert(0, city)
|
||
self.log(f"🏙 Город определён: {city}")
|
||
|
||
# ==================== АВТОРАСЧЁТ УРОВНЯ ЗАЩИЩЁННОСТИ ====================
|
||
def _auto_calc_defence_level(self):
|
||
"""Автоматический расчёт УЗ по ПП №1119: категории ПДн, тип субъектов,
|
||
кол-во записей, тип угроз."""
|
||
if self._current_is_idx is None:
|
||
return
|
||
|
||
categories = [cat for cat, var in self.category_vars if var.get()]
|
||
pd_count = self.is_pd_count_var.get() if hasattr(self, 'is_pd_count_var') else 'менее 100 000'
|
||
threat_type = self.is_threat_var.get() if hasattr(self, 'is_threat_var') else '3'
|
||
is_large = 'более' in pd_count.lower()
|
||
# Автоопределение типа субъектов: если в Субъектах ПДн выбраны только "Сотрудники" → работники, иначе → иные
|
||
subj_items = self.pd_subjects_listbox.get(0, tk.END) if hasattr(self, 'pd_subjects_listbox') else []
|
||
has_only_employees = len(subj_items) > 0 and all('сотрудник' in s.lower() for s in subj_items)
|
||
is_employees = has_only_employees or len(subj_items) == 0
|
||
|
||
# Таблица ПП №1119: (категория, тип_субъектов, объём, тип_угроз) → УЗ
|
||
def lookup(cat_group, employees, large, threat):
|
||
# Общедоступные → всегда 4
|
||
if cat_group == 'public':
|
||
return 4
|
||
# Специальные/биометрические
|
||
if cat_group == 'special_bio':
|
||
tbl = {
|
||
('emp', False, '1'): 2, ('emp', True, '1'): 1,
|
||
('other', False, '1'): 1, ('other', True, '1'): 1,
|
||
('emp', False, '2'): 2, ('emp', True, '2'): 1,
|
||
('other', False, '2'): 2, ('other', True, '2'): 1,
|
||
('emp', False, '3'): 3, ('emp', True, '3'): 2,
|
||
('other', False, '3'): 3, ('other', True, '3'): 2,
|
||
}
|
||
else: # иные
|
||
tbl = {
|
||
('emp', False, '1'): 3, ('emp', True, '1'): 2,
|
||
('other', False, '1'): 2, ('other', True, '1'): 1,
|
||
('emp', False, '2'): 4, ('emp', True, '2'): 3,
|
||
('other', False, '2'): 3, ('other', True, '2'): 2,
|
||
('emp', False, '3'): 4, ('emp', True, '3'): 4,
|
||
('other', False, '3'): 4, ('other', True, '3'): 3,
|
||
}
|
||
key = ('emp' if employees else 'other', large, threat)
|
||
return tbl.get(key, 4)
|
||
|
||
# Определяем группу категорий
|
||
if 'общедоступные' in categories and not any(c in categories for c in ['специальные', 'биометрические', 'иные']):
|
||
cat_group = 'public'
|
||
elif any(c in categories for c in ['специальные', 'биометрические']):
|
||
cat_group = 'special_bio'
|
||
else:
|
||
cat_group = 'other'
|
||
|
||
level = lookup(cat_group, is_employees, is_large, threat_type)
|
||
self.is_defence_var.set(str(level))
|
||
|
||
def _on_categories_or_count_changed(self):
|
||
"""Обработчик изменения категорий ПДн или количества записей."""
|
||
if self._current_is_idx is not None:
|
||
self._auto_calc_defence_level()
|
||
|
||
# ==================== КОМИССИЯ ====================
|
||
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'
|
||
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_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)
|
||
# Заполняем кол-во записей ПДн
|
||
if hasattr(self, 'is_pd_count_entry'):
|
||
self.is_pd_count_entry.delete(0, tk.END)
|
||
self.is_pd_count_entry.insert(0, isys.pd_count or 'менее 100 000')
|
||
|
||
# Списки
|
||
for lb, items in [
|
||
(self.pd_listbox, isys.personal_data_list or []),
|
||
(self.pd_subjects_listbox, isys.pd_subjects_list or []),
|
||
(self.pd_actions_listbox, isys.pd_actions_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):
|
||
# Сохраняем текущую ИС перед добавлением новой
|
||
self._collect_is()
|
||
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,
|
||
is_local_network=src.is_local_network,
|
||
is_internet=src.is_internet,
|
||
defence_level=src.defence_level,
|
||
subject_type=src.subject_type,
|
||
threat_type=src.threat_type,
|
||
personal_data_category=list(src.personal_data_category),
|
||
personal_data_list=list(src.personal_data_list),
|
||
pd_actions_list=list(src.pd_actions_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
|
||
self._collect_is()
|
||
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 _del_from_listbox(self, listbox):
|
||
"""Удаление выбранного элемента из любого listbox."""
|
||
sel = listbox.curselection()
|
||
if sel:
|
||
listbox.delete(sel[0])
|
||
|
||
# ==================== ВЫБОР ИЗ СПИСКА (словари) ====================
|
||
def _select_from_dialog(self, title: str, items: list, listbox):
|
||
"""Открывает окно с чекбоксами для выбора элементов из списка."""
|
||
dialog = tk.Toplevel(self.root)
|
||
dialog.title(title)
|
||
dialog.geometry("400x500")
|
||
dialog.transient(self.root)
|
||
dialog.grab_set()
|
||
|
||
frame = ttk.Frame(dialog, padding=10)
|
||
frame.pack(fill=tk.BOTH, expand=True)
|
||
|
||
ttk.Label(frame, text=title, font=('Arial', 10, 'bold')).pack(anchor=tk.W, pady=(0, 5))
|
||
|
||
# Уже выбранные элементы
|
||
existing = set(listbox.get(0, tk.END))
|
||
|
||
canvas = tk.Canvas(frame, highlightthickness=0)
|
||
scrollbar = ttk.Scrollbar(frame, orient=tk.VERTICAL, command=canvas.yview)
|
||
check_frame = ttk.Frame(canvas)
|
||
check_frame.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
|
||
canvas.create_window((0, 0), window=check_frame, 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)
|
||
|
||
vars = {}
|
||
for item in items:
|
||
var = tk.BooleanVar(value=item in existing)
|
||
cb = ttk.Checkbutton(check_frame, text=item, variable=var)
|
||
cb.pack(anchor=tk.W, pady=1)
|
||
vars[item] = var
|
||
|
||
btn_frame = ttk.Frame(dialog, padding=10)
|
||
btn_frame.pack(fill=tk.X)
|
||
|
||
def _apply():
|
||
selected = [item for item, var in vars.items() if var.get()]
|
||
listbox.delete(0, tk.END)
|
||
for item in selected:
|
||
listbox.insert(tk.END, item)
|
||
dialog.destroy()
|
||
|
||
def _select_all():
|
||
for var in vars.values():
|
||
var.set(True)
|
||
|
||
def _clear_all():
|
||
for var in vars.values():
|
||
var.set(False)
|
||
|
||
ttk.Button(btn_frame, text="Выбрать всё", command=_select_all).pack(side=tk.LEFT, padx=2)
|
||
ttk.Button(btn_frame, text="Снять всё", command=_clear_all).pack(side=tk.LEFT, padx=2)
|
||
ttk.Button(btn_frame, text="✅ Применить", command=_apply).pack(side=tk.RIGHT, padx=2)
|
||
|
||
dialog.wait_window()
|
||
|
||
def _add_pd_action_item(self):
|
||
item = simpledialog.askstring("Добавить действие", "Введите действие с ПДн:")
|
||
if item:
|
||
self.pd_actions_listbox.insert(tk.END, item)
|
||
|
||
def _select_from_actions_list(self):
|
||
"""Выбор действий с ПДн из JSON словаря."""
|
||
import json
|
||
dict_path = os.path.join(os.path.dirname(__file__), 'dictionaries', 'pd_actions.json')
|
||
try:
|
||
with open(dict_path, 'r', encoding='utf-8') as f:
|
||
items = json.load(f)
|
||
self._select_from_dialog("Выбор действий с персональными данными", items, self.pd_actions_listbox)
|
||
except Exception as e:
|
||
messagebox.showerror("Ошибка", f"Не удалось загрузить словарь: {e}")
|
||
|
||
def _select_from_pd_list(self):
|
||
"""Выбор ПДн из JSON словаря."""
|
||
import json
|
||
dict_path = os.path.join(os.path.dirname(__file__), 'dictionaries', 'pd_items.json')
|
||
try:
|
||
with open(dict_path, 'r', encoding='utf-8') as f:
|
||
items = json.load(f)
|
||
self._select_from_dialog("Выбор персональных данных", items, self.pd_listbox)
|
||
except Exception as e:
|
||
messagebox.showerror("Ошибка", f"Не удалось загрузить словарь: {e}")
|
||
|
||
def _select_from_subjects_list(self):
|
||
"""Выбор категорий субъектов из JSON словаря."""
|
||
import json
|
||
dict_path = os.path.join(os.path.dirname(__file__), 'dictionaries', 'pd_categories.json')
|
||
try:
|
||
with open(dict_path, 'r', encoding='utf-8') as f:
|
||
items = json.load(f)
|
||
self._select_from_dialog("Выбор категорий субъектов ПДн", items, self.pd_subjects_listbox)
|
||
except Exception as e:
|
||
messagebox.showerror("Ошибка", f"Не удалось загрузить словарь: {e}")
|
||
|
||
def _select_from_defense_list(self):
|
||
"""Выбор СЗИ из JSON словаря."""
|
||
import json
|
||
dict_path = os.path.join(os.path.dirname(__file__), 'dictionaries', 'defense_tools.json')
|
||
try:
|
||
with open(dict_path, 'r', encoding='utf-8') as f:
|
||
items = json.load(f)
|
||
self._select_from_dialog("Выбор средств защиты информации", items, self.defense_tools_listbox)
|
||
except Exception as e:
|
||
messagebox.showerror("Ошибка", f"Не удалось загрузить словарь: {e}")
|
||
|
||
# ==================== ШАБЛОНЫ ====================
|
||
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()
|
||
self._collect_employees()
|
||
|
||
# Проверка
|
||
if not self._validate_before_generate():
|
||
return
|
||
|
||
os.makedirs(output_dir, exist_ok=True)
|
||
|
||
# Окно прогресса
|
||
progress_win = tk.Toplevel(self.root)
|
||
progress_win.title("Генерация документов")
|
||
progress_win.geometry("350x100")
|
||
progress_win.transient(self.root)
|
||
progress_win.grab_set()
|
||
progress_win.resizable(False, False)
|
||
ttk.Label(progress_win, text="Генерация документов...",
|
||
font=('Arial', 10)).pack(pady=(15, 5))
|
||
pb = ttk.Progressbar(progress_win, mode='indeterminate', length=300)
|
||
pb.pack(pady=5)
|
||
pb.start()
|
||
|
||
def _close_progress():
|
||
try:
|
||
pb.stop()
|
||
progress_win.grab_release()
|
||
progress_win.destroy()
|
||
except Exception:
|
||
pass
|
||
|
||
# Запускаем в отдельном потоке
|
||
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, _close_progress)
|
||
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, _close_progress)
|
||
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'])
|
||
self.model.full_name = data['fullName']
|
||
if data.get('shortName'):
|
||
self.short_name_entry.delete(0, tk.END)
|
||
self.short_name_entry.insert(0, data['shortName'])
|
||
self.model.short_name = data['shortName']
|
||
|
||
# Адрес и реквизиты (маппинг camelCase → snake_case)
|
||
addr_map = {
|
||
'address': 'addressLegal',
|
||
'city_name': None,
|
||
'inn': 'inn',
|
||
'kpp': 'kpp',
|
||
'ogrn': 'ogrn',
|
||
'ogrn_date': 'ogrnDate',
|
||
}
|
||
for entry_key, data_key in addr_map.items():
|
||
entry = self._company_entries.get(entry_key)
|
||
if not entry:
|
||
continue
|
||
val = ''
|
||
if data_key and data.get(data_key):
|
||
val = data[data_key]
|
||
elif data.get(entry_key):
|
||
val = data[entry_key]
|
||
if val:
|
||
entry.delete(0, tk.END)
|
||
entry.insert(0, val)
|
||
|
||
# Автоопределение города из адреса после импорта
|
||
self._on_address_changed()
|
||
|
||
# Должностные лица (маппинг)
|
||
officials_map = {
|
||
'chief_position': 'chiefPosition',
|
||
'chief_fio': 'chiefFio',
|
||
'responsible_position': 'responsiblePosition',
|
||
'responsible_fio': 'responsibleFio',
|
||
'admin_position': ('administratorPosition', 'ispdnPosition'),
|
||
'admin_fio': ('administratorFio', 'ispdnFio'),
|
||
}
|
||
for entry_key, data_keys in officials_map.items():
|
||
entry = self._official_entries.get(entry_key)
|
||
if not entry:
|
||
continue
|
||
val = ''
|
||
if isinstance(data_keys, tuple):
|
||
for dk in data_keys:
|
||
if data.get(dk):
|
||
val = data[dk]
|
||
break
|
||
elif data.get(data_keys):
|
||
val = data[data_keys]
|
||
if val:
|
||
entry.delete(0, tk.END)
|
||
entry.insert(0, val)
|
||
|
||
# Номер и дата документа
|
||
if data.get('contractNumber'):
|
||
self.doc_number_var.set(data['contractNumber'])
|
||
if data.get('contractDate'):
|
||
self.doc_date_var.set(data['contractDate'])
|
||
|
||
# Информационные системы
|
||
if data.get('informationSystems'):
|
||
self.model.information_systems.clear()
|
||
for is_data in data['informationSystems']:
|
||
isys = InformationSystem(
|
||
name=is_data.get('name', ''),
|
||
description=is_data.get('description', ''),
|
||
is_internet=is_data.get('isInternet', False),
|
||
pd_count=is_data.get('personalDataCount', 'менее 100 000'),
|
||
personal_data_list=is_data.get('personalDataList', []),
|
||
pd_subjects_list=is_data.get('pd_subjects_list', []),
|
||
defense_tools_list=is_data.get('defense_tools', []),
|
||
users_list=is_data.get('usersList', []),
|
||
personal_data_category=is_data.get('personalDataCategory', []),
|
||
processing_modes=is_data.get('processing_mode', ''),
|
||
purpose=is_data.get('description', ''),
|
||
room=is_data.get('room', ''),
|
||
)
|
||
self.model.information_systems.append(isys)
|
||
self._refresh_is_listbox()
|
||
if self.model.information_systems:
|
||
self.is_listbox.selection_set(0)
|
||
self._on_is_select()
|
||
|
||
# Комиссия
|
||
if data.get('commission'):
|
||
self.model.commission.clear()
|
||
# Очистим UI
|
||
for row_frame, _, _, _ in self.commission_entries:
|
||
row_frame.destroy()
|
||
self.commission_entries.clear()
|
||
for m in data['commission']:
|
||
self.model.commission.append(
|
||
CommissionMember(
|
||
role=m.get('role', 'Член комиссии'),
|
||
position=m.get('position', ''),
|
||
fio=m.get('fio', ''),
|
||
)
|
||
)
|
||
self._add_commission_row(
|
||
m.get('role', 'Член комиссии'),
|
||
m.get('position', ''),
|
||
m.get('fio', ''),
|
||
)
|
||
|
||
# Сотрудники с доступом
|
||
if data.get('employeesAccess'):
|
||
self.model.employees_access.clear()
|
||
if hasattr(self, '_employee_entries'):
|
||
for row_frame, _, _ in self._employee_entries:
|
||
row_frame.destroy()
|
||
self._employee_entries.clear()
|
||
for emp in data['employeesAccess']:
|
||
self.model.employees_access.append({
|
||
'position': emp.get('position', ''),
|
||
'fio': emp.get('fio', ''),
|
||
})
|
||
self._add_employee_row(
|
||
emp.get('position', ''),
|
||
emp.get('fio', ''),
|
||
)
|
||
self._update_emp_count()
|
||
|
||
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()
|
||
self._collect_employees()
|
||
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()
|
||
self._collect_employees()
|
||
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()
|