feat: переделка под ОКИИ (187-ФЗ) — объекты КИИ вместо ИС, категории 1-3, ОКВЭД, убраны ПДн/бумажные носители

- вкладка «Информационные системы» → «Объекты КИИ», убрана «Структура объекта»
- «Тип угроз»/«УЗ» → категория значимости 1/2/3/без категории
- удалены Категории ПДн, кол-во записей, перечень/субъекты/действия с ПДн + словари
- «Пользователи ИС» → «Пользователи ОКИИ», средства защиты оставлены
- вкладка «Бумажные документы» удалена полностью (GUI + логика + модели)
- ОКВЭД: поля во вкладке «Организация», словарь okved.json, выбор галочками, импорт
- нормализация регистра адреса DaData (КАПС → нормальный)
- importer: объекты КИИ (наименование объекта, ПО, СЗИ, пользователи)
This commit is contained in:
2026-08-04 13:58:23 +04:00
parent b115f9e2b4
commit ce1b56066a
11 changed files with 562 additions and 724 deletions
+158 -362
View File
@@ -11,7 +11,7 @@ from datetime import datetime
from typing import List, Optional
from . import (
Company, CommissionMember, InformationSystem, PaperDocument,
Company, CommissionMember, InformationSystem,
inflect_name,
build_replacements, VARIABLE_DEFS,
process_template,
@@ -157,7 +157,6 @@ class DokoGenApp:
self._build_is_tab()
self._build_commission_tab()
self._build_users_tab()
self._build_paper_tab()
self._build_generation_tab()
self._build_log_tab()
@@ -270,6 +269,33 @@ class DokoGenApp:
f2.columnconfigure(1, weight=1)
# --- ОКВЭД ---
f_okved = ttk.LabelFrame(left, text="ОКВЭД", padding=10)
f_okved.pack(fill=tk.X, pady=5)
# Основной ОКВЭД — один, из словаря или вручную
ttk.Label(f_okved, text="Основной ОКВЭД:").grid(row=0, column=0, sticky=tk.W, pady=2)
okved_entry = ttk.Entry(f_okved, width=50)
okved_entry.grid(row=0, column=1, padx=10, sticky=tk.EW, pady=1)
self._company_entries['okved'] = okved_entry
ttk.Button(f_okved, text="Выбрать из словаря",
command=self._select_main_okved).grid(row=0, column=2, padx=5)
# Дополнительные ОКВЭД — сколько угодно
ttk.Label(f_okved, text="Дополнительные ОКВЭД:").grid(row=1, column=0, sticky=tk.NW, pady=2)
self.okved_extra_listbox = tk.Listbox(f_okved, height=4, width=50)
self.okved_extra_listbox.grid(row=1, column=1, padx=10, sticky=tk.EW, pady=1)
okved_btn = ttk.Frame(f_okved)
okved_btn.grid(row=1, column=2, padx=5, sticky=tk.N)
ttk.Button(okved_btn, text="Выбрать из словаря",
command=self._select_extra_okved).pack(fill=tk.X, pady=1)
ttk.Button(okved_btn, text="+ Добавить",
command=self._add_okved_manual).pack(fill=tk.X, pady=1)
ttk.Button(okved_btn, text="- Удалить",
command=self._delete_okved_extra).pack(fill=tk.X, pady=1)
f_okved.columnconfigure(1, weight=1)
# --- Должностные лица ---
f3 = ttk.LabelFrame(left, text="Должностные лица", padding=10)
f3.pack(fill=tk.X, pady=5)
@@ -686,12 +712,12 @@ class DokoGenApp:
# ---------- ВКЛАДКА "ИНФОРМАЦИОННЫЕ СИСТЕМЫ" ----------
def _build_is_tab(self):
tab = ttk.Frame(self.notebook)
self.notebook.add(tab, text="Информационные системы")
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()
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)
@@ -703,7 +729,7 @@ class DokoGenApp:
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 = 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)
@@ -726,51 +752,19 @@ class DokoGenApp:
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,
# Категория значимости объекта КИИ
self.is_category_var = tk.StringVar(value="без категории")
ttk.Label(fields_frame, text="Категория значимости:").grid(row=3, column=0, sticky=tk.W, pady=2)
cat_frame = ttk.Frame(fields_frame)
cat_frame.grid(row=3, column=1, sticky=tk.W, padx=10)
for val in ["1", "2", "3", "без категории"]:
ttk.Radiobutton(cat_frame, text=val, variable=self.is_category_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())
# Структура ИС
ttk.Label(fields_frame, text="Структура ИС:").grid(row=7, column=0, sticky=tk.W, pady=2)
self.is_structure_entry = ttk.Entry(fields_frame, width=55)
self.is_structure_entry.grid(row=7, column=1, padx=10, sticky=tk.EW)
# Программное обеспечение
ttk.Label(fields_frame, text="Программное обеспечение:").grid(row=4, column=0, sticky=tk.W, pady=2)
self.is_software_entry = ttk.Entry(fields_frame, width=55)
self.is_software_entry.grid(row=4, column=1, padx=10, sticky=tk.EW)
fields_frame.columnconfigure(1, weight=1)
@@ -778,39 +772,6 @@ class DokoGenApp:
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="Средства защиты")
@@ -822,9 +783,9 @@ class DokoGenApp:
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="Пользователи ИС")
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)
@@ -886,7 +847,7 @@ class DokoGenApp:
frame = ttk.Frame(scrollable, padding=10)
frame.pack(fill=tk.BOTH, expand=True)
ttk.Label(frame, text="Сотрудники, имеющие доступ к ПДн",
ttk.Label(frame, text="Сотрудники, имеющие доступ к объектам КИИ",
font=('Arial', 11, 'bold')).pack(anchor=tk.W, pady=(0, 10))
# Заголовки
@@ -971,102 +932,6 @@ class DokoGenApp:
if emp['position'] or emp['fio']:
self.model.employees_access.append(emp)
# ---------- ВКЛАДКА "БУМАЖНЫЕ ДОКУМЕНТЫ" ----------
def _build_paper_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 [(45, "Название документа"), (45, "Место хранения")]:
ttk.Label(header, text=col, font=('', 9, 'bold'),
width=w, anchor=tk.W).pack(side=tk.LEFT, padx=2)
self._paper_frame = ttk.Frame(frame)
self._paper_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_paper_row_ui).pack(side=tk.LEFT, padx=2)
ttk.Button(btn_frame, text="- Удалить выбранный",
command=self._delete_paper_selected).pack(side=tk.LEFT, padx=2)
self._paper_count_label = ttk.Label(frame, text="Добавлено: 0")
self._paper_count_label.pack(anchor=tk.W, pady=5)
self._paper_entries = []
# Загрузка существующих
for p in self.model.paper_documents_list:
self._add_paper_row(p.name, p.storage)
self._update_paper_count()
def _add_paper_row(self, name="", storage=""):
"""Добавляет одну строку документа."""
row = ttk.Frame(self._paper_frame)
row.pack(fill=tk.X, pady=2)
name_entry = ttk.Entry(row, width=45)
name_entry.insert(0, name)
name_entry.pack(side=tk.LEFT, padx=2)
storage_entry = ttk.Entry(row, width=45)
storage_entry.insert(0, storage)
storage_entry.pack(side=tk.LEFT, padx=2)
del_btn = ttk.Button(row, text="", width=3,
command=lambda: self._delete_paper_row(row))
del_btn.pack(side=tk.LEFT, padx=5)
self._paper_entries.append((row, name_entry, storage_entry))
self._update_paper_count()
def _add_paper_row_ui(self):
self._add_paper_row()
def _delete_paper_row(self, row_frame):
self._paper_entries = [e for e in self._paper_entries if e[0] != row_frame]
row_frame.destroy()
self._update_paper_count()
def _delete_paper_selected(self):
if self._paper_entries:
row_frame = self._paper_entries[-1][0]
self._delete_paper_row(row_frame)
def _update_paper_count(self):
count = len(self._paper_entries)
if hasattr(self, '_paper_count_label'):
self._paper_count_label.config(text=f"Добавлено: {count}")
def _collect_paper_documents(self):
"""Собирает данные бумажных документов из GUI в модель."""
self.model.paper_documents_list.clear()
for row, name_e, storage_e in self._paper_entries:
name = name_e.get().strip()
storage = storage_e.get().strip()
if name or storage:
self.model.paper_documents_list.append(
PaperDocument(name=name, storage=storage)
)
# ---------- ВКЛАДКА "ГЕНЕРАЦИЯ" ----------
def _build_generation_tab(self):
tab = ttk.Frame(self.notebook)
@@ -1142,29 +1007,21 @@ class DokoGenApp:
setattr(c, key, entry.get().strip())
c.contract_number = self.doc_number_var.get()
c.contract_date = self.doc_date_var.get()
if hasattr(self, 'okved_extra_listbox'):
c.okved_extra = list(self.okved_extra_listbox.get(0, tk.END))
def _collect_is(self):
"""Собирает данные ИС из GUI в модель."""
"""Собирает данные объекта КИИ из 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()
if hasattr(self, 'is_structure_entry'):
isys.structure = self.is_structure_entry.get().strip()
if hasattr(self, 'is_software_entry'):
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
if hasattr(self, 'is_pd_count_entry'):
isys.pd_count = self.is_pd_count_entry.get().strip() or 'менее 100 000'
isys.category = self.is_category_var.get()
# Списки
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))
@@ -1199,6 +1056,12 @@ class DokoGenApp:
self.doc_number_var.set(c.contract_number or "")
self.doc_date_var.set(c.contract_date or "")
# Дополнительные ОКВЭД
if hasattr(self, 'okved_extra_listbox'):
self.okved_extra_listbox.delete(0, tk.END)
for item in (c.okved_extra or []):
self.okved_extra_listbox.insert(tk.END, item)
# Склонения
idx_map = {'nomn': 1, 'gent': 2, 'datv': 3, 'accs': 4, 'ablt': 5, 'loct': 6}
for code, entry in self.decl_entries.items():
@@ -1236,15 +1099,6 @@ class DokoGenApp:
)
self._update_emp_count()
# Бумажные документы
if hasattr(self, '_paper_entries'):
for row_frame, _, _ in self._paper_entries:
row_frame.destroy()
self._paper_entries.clear()
for p in c.paper_documents_list:
self._add_paper_row(p.name, p.storage)
self._update_paper_count()
# Шаблоны
self._on_templates_changed()
@@ -1296,64 +1150,6 @@ class DokoGenApp:
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)
@@ -1421,28 +1217,15 @@ class DokoGenApp:
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 "")
if hasattr(self, 'is_structure_entry'):
self.is_structure_entry.delete(0, tk.END)
self.is_structure_entry.insert(0, isys.structure or "")
if hasattr(self, 'is_software_entry'):
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)
# Заполняем кол-во записей ПДн
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')
self.is_category_var.set(isys.category or "без категории")
# Списки
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 []),
]:
@@ -1471,13 +1254,7 @@ class DokoGenApp:
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),
category=src.category,
defense_tools_list=list(src.defense_tools_list),
users_list=list(src.users_list),
)
@@ -1497,16 +1274,6 @@ class DokoGenApp:
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:
@@ -1533,7 +1300,7 @@ class DokoGenApp:
if not names:
messagebox.showinfo(
"Выбор пользователей",
"Список пуст.\nЗаполните вкладку «Пользователи» (сотрудники с доступом к ПДн) "
"Список пуст.\nЗаполните вкладку «Пользователи» (сотрудники с доступом к объектам КИИ) "
"или добавьте пользователей в другой ИС."
)
return
@@ -1607,44 +1374,6 @@ class DokoGenApp:
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
@@ -1656,6 +1385,84 @@ class DokoGenApp:
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось загрузить словарь: {e}")
# ==================== ОКВЭД ====================
def _load_okved_dict(self):
"""Загружает словарь ОКВЭД (dictionaries/okved.json)."""
import json
dict_path = os.path.join(os.path.dirname(__file__), 'dictionaries', 'okved.json')
try:
with open(dict_path, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось загрузить словарь ОКВЭД:\n{e}")
return []
def _select_main_okved(self):
"""Выбор основного ОКВЭД из словаря (всегда один)."""
items = self._load_okved_dict()
if not items:
return
current = self._company_entries.get('okved').get().strip() if 'okved' in self._company_entries else ''
dialog = tk.Toplevel(self.root)
dialog.title("Основной ОКВЭД")
dialog.geometry("500x500")
dialog.transient(self.root)
dialog.grab_set()
frame = ttk.Frame(dialog, padding=10)
frame.pack(fill=tk.BOTH, expand=True)
ttk.Label(frame, text="Выберите основной ОКВЭД (один):",
font=('Arial', 10, 'bold')).pack(anchor=tk.W, pady=(0, 5))
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)
# Радио-кнопки: основной всегда один
var = tk.StringVar(value=current)
for item in items:
ttk.Radiobutton(check_frame, text=item, variable=var,
value=item).pack(anchor=tk.W, pady=1)
btn_frame = ttk.Frame(dialog, padding=10)
btn_frame.pack(fill=tk.X)
def _apply():
val = var.get()
if val:
entry = self._company_entries.get('okved')
if entry:
entry.delete(0, tk.END)
entry.insert(0, val)
dialog.destroy()
ttk.Button(btn_frame, text="✅ Применить", command=_apply).pack(side=tk.RIGHT, padx=2)
ttk.Button(btn_frame, text="Отмена", command=dialog.destroy).pack(side=tk.RIGHT, padx=2)
dialog.wait_window()
def _select_extra_okved(self):
"""Выбор дополнительных ОКВЭД из словаря (можно несколько)."""
items = self._load_okved_dict()
if not items:
return
self._select_from_dialog("Выбор дополнительных ОКВЭД", items, self.okved_extra_listbox)
def _add_okved_manual(self):
"""Ручное добавление дополнительного ОКВЭД."""
item = simpledialog.askstring("Добавить ОКВЭД", "Введите код ОКВЭД:")
if item:
self.okved_extra_listbox.insert(tk.END, item.strip())
def _delete_okved_extra(self):
sel = self.okved_extra_listbox.curselection()
if sel:
self.okved_extra_listbox.delete(sel[0])
# ==================== ШАБЛОНЫ ====================
def select_templates(self):
folder = filedialog.askdirectory(title="Папка шаблонов")
@@ -1720,7 +1527,6 @@ class DokoGenApp:
self._collect_commission()
self._collect_is()
self._collect_employees()
self._collect_paper_documents()
# Проверка
if not self._validate_before_generate():
@@ -1939,7 +1745,24 @@ class DokoGenApp:
if data.get('ispdnDepartment'):
self.model.security_department = data['ispdnDepartment']
# Информационные системы
# ОКВЭД (основной и дополнительные)
if data.get('okved'):
entry = self._company_entries.get('okved')
if entry:
entry.delete(0, tk.END)
entry.insert(0, data['okved'])
if data.get('okvedExtra'):
if hasattr(self, 'okved_extra_listbox'):
self.okved_extra_listbox.delete(0, tk.END)
extra = data['okvedExtra']
if isinstance(extra, str):
parts = [p.strip() for p in extra.replace(';', '\n').split('\n') if p.strip()]
else:
parts = [str(p).strip() for p in extra if str(p).strip()]
for p in parts:
self.okved_extra_listbox.insert(tk.END, p)
# Объекты КИИ
if data.get('informationSystems'):
self.model.information_systems.clear()
for is_data in data['informationSystems']:
@@ -1948,17 +1771,12 @@ class DokoGenApp:
description=is_data.get('description', ''),
is_local_network=is_data.get('is_has_lan', is_data.get('is_local_network', False)),
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', []),
category=is_data.get('category', ''),
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', ''),
purpose=is_data.get('purpose', is_data.get('description', '')),
structure=is_data.get('structure', ''),
pd_actions_list=is_data.get('pd_actions', []),
software=is_data.get('software', ''),
)
self.model.information_systems.append(isys)
self._refresh_is_listbox()
@@ -2005,26 +1823,6 @@ class DokoGenApp:
)
self._update_emp_count()
# Бумажные документы (из «Названия документов» / «Место хранения»)
if data.get('paperDocuments'):
self.model.paper_documents_list.clear()
names = [n.strip() for n in data['paperDocuments'].replace('\n', ';').split(';') if n.strip()]
storages = [s.strip() for s in (data.get('paperStorage') or '').replace('\n', ';').split(';') if s.strip()]
for i, name in enumerate(names):
storage = storages[i] if i < len(storages) else (storages[0] if storages else '')
self.model.paper_documents_list.append(
PaperDocument(name=name, storage=storage)
)
# Обновляем GUI
if hasattr(self, '_paper_entries'):
for row_frame, _, _ in self._paper_entries:
row_frame.destroy()
self._paper_entries.clear()
for p in self.model.paper_documents_list:
self._add_paper_row(p.name, p.storage)
self._update_paper_count()
self.log(f" Бумажные документы: {len(self.model.paper_documents_list)} шт.")
self.log("✅ Импорт применён")
self.root.after(0, _apply)
@@ -2069,7 +1867,6 @@ class DokoGenApp:
self._collect_commission()
self._collect_is()
self._collect_employees()
self._collect_paper_documents()
try:
with open(filename, 'w', encoding='utf-8') as f:
json.dump(self.model.to_dict(), f, ensure_ascii=False, indent=2)
@@ -2095,7 +1892,6 @@ class DokoGenApp:
self._collect_commission()
self._collect_is()
self._collect_employees()
self._collect_paper_documents()
try:
os.makedirs(os.path.dirname(self._autosave_path), exist_ok=True)
with open(self._autosave_path, 'w', encoding='utf-8') as f: