fix: 8 багов — город, IS поля, ПО, авторасчёт, скролл, прогресc, комиссия, замены

This commit is contained in:
2026-07-30 14:38:47 +04:00
parent 7eb736bcbf
commit 30cb3f66bf
5 changed files with 210 additions and 38 deletions
+153 -21
View File
@@ -201,7 +201,7 @@ class DokoGenApp:
addr_fields = [
("Юридический адрес:", "address"),
("Город:", "city_name"),
("Город (заполняется автоматически):", "city_name"),
("ИНН:", "inn"),
("КПП:", "kpp"),
("ОГРН:", "ogrn"),
@@ -212,6 +212,12 @@ class DokoGenApp:
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)
# --- Должностные лица ---
@@ -278,13 +284,9 @@ class DokoGenApp:
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)
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()
@@ -292,33 +294,42 @@ class DokoGenApp:
# Тип угроз
self.is_threat_var = tk.StringVar(value="3")
ttk.Label(fields_frame, text="Тип угроз:").grid(row=4, column=0, sticky=tk.W, pady=2)
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=4, column=1, sticky=tk.W, padx=10)
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=5, column=0, sticky=tk.W, pady=2)
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=5, column=1, sticky=tk.W, padx=10)
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.Button(def_frame, text="Рассчитать автоматически",
command=self._auto_calc_defence_level).pack(side=tk.LEFT, padx=10)
# Категории ПДн
ttk.Label(fields_frame, text="Категории ПДн:").grid(row=6, column=0, sticky=tk.NW, pady=2)
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=6, column=1, sticky=tk.W, padx=10)
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_entry = ttk.Entry(fields_frame, width=30)
self.is_pd_count_entry.grid(row=6, column=1, padx=10, sticky=tk.W)
self.is_pd_count_entry.bind('<KeyRelease>', lambda e: self._on_categories_or_count_changed())
fields_frame.columnconfigure(1, weight=1)
# Вложенные списки
@@ -405,7 +416,17 @@ class DokoGenApp:
tab = ttk.Frame(self.notebook)
self.notebook.add(tab, text="Пользователи")
frame = ttk.Frame(tab, padding=10)
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="Сотрудники, имеющие доступ к ПДн",
@@ -572,13 +593,14 @@ class DokoGenApp:
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
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))
@@ -657,6 +679,92 @@ class DokoGenApp:
# Шаблоны
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)
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):
"""Автоматический расчёт уровня защищённости на основе категорий ПДн и кол-ва записей."""
if self._current_is_idx is None:
return
# Считываем категории
categories = [cat for cat, var in self.category_vars if var.get()]
pd_count_text = self.is_pd_count_entry.get().strip() if hasattr(self, 'is_pd_count_entry') else ''
# Определяем базовый уровень (4 — самый низкий, по умолчанию)
level = 4
# Специальные или биометрические → минимум 3 уровень
has_special = any(c in categories for c in ['специальные', 'биометрические'])
if has_special:
level = 3
# Больше 100 000 записей → повышаем уровень на 1 (уровень становится строже)
if pd_count_text:
# Парсим число
import re
nums = re.findall(r'\d+', pd_count_text.replace(' ', ''))
if nums:
count = int(nums[0])
if count > 100000:
level = max(1, level - 1) # Уровень повышается (число уменьшается)
# Также проверяем текстовые варианты
if 'более' in pd_count_text.lower() or 'свыше' in pd_count_text.lower():
level = max(1, level - 1)
self.is_defence_var.set(str(level))
self.log(f"🛡 Уровень защищённости автоматически рассчитан: {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)
@@ -665,7 +773,7 @@ class DokoGenApp:
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)
@@ -702,8 +810,6 @@ class DokoGenApp:
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)
@@ -726,14 +832,16 @@ 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 "")
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')
# Списки
for lb, items in [
@@ -765,7 +873,6 @@ class DokoGenApp:
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,
@@ -978,6 +1085,27 @@ class DokoGenApp:
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:
@@ -1006,12 +1134,14 @@ class DokoGenApp:
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()
@@ -1114,6 +1244,9 @@ class DokoGenApp:
entry.delete(0, tk.END)
entry.insert(0, val)
# Автоопределение города из адреса после импорта
self._on_address_changed()
# Должностные лица (маппинг)
officials_map = {
'chief_position': 'chiefPosition',
@@ -1152,7 +1285,6 @@ class DokoGenApp:
isys = InformationSystem(
name=is_data.get('name', ''),
description=is_data.get('description', ''),
software=is_data.get('software', ''),
is_internet=is_data.get('isInternet', False),
pd_count=is_data.get('personalDataCount', 'менее 100 000'),
personal_data_list=is_data.get('personalDataList', []),