fix: все замечания по UI, импортёр, словари, вкладка пользователей
- Добавлен dokogen/importer.py (импорт из Excel) - Созданы словари: ПДн, Субъекты, СЗИ (JSON) - Добавлена вкладка Пользователи (Должность, ФИО, доступ) - Исправлены: пустые поля номера/даты договора - Починен IS tab (названия не пропадают) - Кнопки Выбрать из списка для ПДн и СЗИ - Исправлен _apply_imported_data
This commit is contained in:
@@ -3,4 +3,3 @@ __pycache__/
|
|||||||
.autosave/
|
.autosave/
|
||||||
Сгенерированные_документы/
|
Сгенерированные_документы/
|
||||||
*.result.docx
|
*.result.docx
|
||||||
|
|
||||||
|
|||||||
@@ -6,5 +6,8 @@ from .declension import inflect_name, decline, company_name_decline, get_short_f
|
|||||||
from .variables import build_replacements, VARIABLE_DEFS
|
from .variables import build_replacements, VARIABLE_DEFS
|
||||||
from .generator import process_template
|
from .generator import process_template
|
||||||
|
|
||||||
|
# Импорт из Excel
|
||||||
|
from . import importer
|
||||||
|
|
||||||
__version__ = "1.0.0"
|
__version__ = "1.0.0"
|
||||||
__app_name__ = "ДоКоГеНеРаТоР"
|
__app_name__ = "ДоКоГеНеРаТоР"
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[
|
||||||
|
"Антивирусное ПО",
|
||||||
|
"СКЗИ (криптография)",
|
||||||
|
"Межсетевой экран",
|
||||||
|
"СЗИ от НСД",
|
||||||
|
"СОВ (обнаружение вторжений)",
|
||||||
|
"Доверенная загрузка",
|
||||||
|
"SIEM-система",
|
||||||
|
"DLP-система",
|
||||||
|
"VPN",
|
||||||
|
"Система парольной защиты",
|
||||||
|
"Антиспам",
|
||||||
|
"Средства резервного копирования"
|
||||||
|
]
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[
|
||||||
|
"работники оператора",
|
||||||
|
"клиенты",
|
||||||
|
"контрагенты",
|
||||||
|
"родственники работников",
|
||||||
|
"соискатели",
|
||||||
|
"иные лица"
|
||||||
|
]
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
[
|
||||||
|
"Фамилия, имя, отчество",
|
||||||
|
"Дата рождения",
|
||||||
|
"Место рождения",
|
||||||
|
"Паспортные данные",
|
||||||
|
"Адрес регистрации",
|
||||||
|
"Адрес проживания",
|
||||||
|
"ИНН",
|
||||||
|
"СНИЛС",
|
||||||
|
"Номер телефона",
|
||||||
|
"Адрес электронной почты",
|
||||||
|
"Образование",
|
||||||
|
"Профессия",
|
||||||
|
"Сведения о доходах",
|
||||||
|
"Семейное положение",
|
||||||
|
"Состав семьи",
|
||||||
|
"Состояние здоровья",
|
||||||
|
"Национальность",
|
||||||
|
"Гражданство",
|
||||||
|
"Сведения о судимости",
|
||||||
|
"Фотография",
|
||||||
|
"Биометрические данные",
|
||||||
|
"Рабочий e-mail",
|
||||||
|
"Должность",
|
||||||
|
"Табельный номер",
|
||||||
|
"Сведения о воинском учете",
|
||||||
|
"Сведения о командировках"
|
||||||
|
]
|
||||||
@@ -0,0 +1,436 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""DokoGen — Модуль импорта из Excel (опросный лист 152-ФЗ)"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import traceback
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Callable, Optional
|
||||||
|
|
||||||
|
try:
|
||||||
|
import openpyxl
|
||||||
|
except ImportError:
|
||||||
|
openpyxl = None
|
||||||
|
|
||||||
|
|
||||||
|
def clean_value(val):
|
||||||
|
"""Очистка и нормализация значения ячейки Excel."""
|
||||||
|
if val is None:
|
||||||
|
return ''
|
||||||
|
if isinstance(val, (int, float)):
|
||||||
|
# Попытка распознать дату (серийный номер Excel)
|
||||||
|
if 10000 < val < 80000:
|
||||||
|
serial = int(val)
|
||||||
|
if serial > 60:
|
||||||
|
serial -= 1
|
||||||
|
dt = datetime(1899, 12, 30) + timedelta(days=serial)
|
||||||
|
return dt.strftime('%d.%m.%Y')
|
||||||
|
if isinstance(val, float) and val == int(val):
|
||||||
|
return str(int(val))
|
||||||
|
return str(val)
|
||||||
|
text = str(val).strip()
|
||||||
|
text = text.replace('\n', ' ').replace('\r', ' ')
|
||||||
|
text = ' '.join(text.split())
|
||||||
|
# Приведение регистра: если текст содержит только заглавные буквы РУ
|
||||||
|
if len(text) > 4 and any('А' <= c <= 'Я' or c == 'Ё' for c in text):
|
||||||
|
has_lower = any('а' <= c <= 'я' or c == 'ё' for c in text)
|
||||||
|
if not has_lower:
|
||||||
|
words = text.split()
|
||||||
|
if len(words) >= 3:
|
||||||
|
text = text.title()
|
||||||
|
else:
|
||||||
|
text = text.capitalize()
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def import_152fz(filepath: str, log_fn: Optional[Callable] = None) -> dict:
|
||||||
|
"""Импорт данных из опросного листа Excel (152-ФЗ)."""
|
||||||
|
if openpyxl is None:
|
||||||
|
raise ImportError("openpyxl не установлен. Установите: pip install openpyxl")
|
||||||
|
|
||||||
|
if log_fn is None:
|
||||||
|
log_fn = lambda msg: None
|
||||||
|
|
||||||
|
log_fn(f"Загрузка опросного листа: {filepath}")
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'fullName': '', 'shortName': '', 'addressLegal': '', 'addressActual': '',
|
||||||
|
'email': '', 'phone': '', 'inn': '', 'ogrn': '', 'kpp': '',
|
||||||
|
'okved': '', 'chiefPosition': '', 'chiefFio': '',
|
||||||
|
'ispdnFio': '', 'ispdnPosition': '', 'paperDocuments': '', 'paperStorage': '',
|
||||||
|
'ispdnDepartment': '', 'ispdnEmail': '', 'ispdnPhone': '',
|
||||||
|
'informationSystems': [], 'commission': [], 'employeesAccess': [],
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
wb = openpyxl.load_workbook(filepath, data_only=True, read_only=False)
|
||||||
|
except Exception as e:
|
||||||
|
log_fn(f"❌ Ошибка открытия файла: {e}")
|
||||||
|
return data
|
||||||
|
|
||||||
|
try:
|
||||||
|
_import_152_common(wb, data, log_fn)
|
||||||
|
_import_152_is_list(wb, data, log_fn)
|
||||||
|
_import_152_employees(wb, data, log_fn)
|
||||||
|
except Exception as e:
|
||||||
|
log_fn(f"❌ Ошибка при импорте: {e}")
|
||||||
|
log_fn(traceback.format_exc())
|
||||||
|
finally:
|
||||||
|
wb.close()
|
||||||
|
|
||||||
|
# Копирование полей ИСПДн в админа, если админ не заполнен
|
||||||
|
for src, dst in [('ispdnFio', 'administratorFio'), ('ispdnFio', 'adminFio'),
|
||||||
|
('ispdnPosition', 'administratorPosition'), ('ispdnPosition', 'adminPosition'),
|
||||||
|
('ispdnPhone', 'phone'), ('ispdnEmail', 'email')]:
|
||||||
|
if data.get(src) and not data.get(dst):
|
||||||
|
data[dst] = data[src]
|
||||||
|
|
||||||
|
_validate_company_data(data, log_fn)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def _import_152_common(wb, data, log_fn):
|
||||||
|
"""Импорт общих сведений из листа 'Общие сведения'."""
|
||||||
|
sheet_name = None
|
||||||
|
for sn in wb.sheetnames:
|
||||||
|
if 'общие' in sn.lower():
|
||||||
|
sheet_name = sn
|
||||||
|
break
|
||||||
|
if not sheet_name:
|
||||||
|
log_fn("⚠ Лист 'Общие сведения' не найден")
|
||||||
|
return
|
||||||
|
|
||||||
|
ws = wb[sheet_name]
|
||||||
|
field_map = [
|
||||||
|
('полное наименование организации', 'fullName'),
|
||||||
|
('сокращенное наименование организации', 'shortName'),
|
||||||
|
('адрес организации \\(юридический\\)', 'addressLegal'),
|
||||||
|
('адрес организации \\(фактический\\)', 'addressActual'),
|
||||||
|
('инн', 'inn'),
|
||||||
|
('дата.*огрн', 'ogrnDate'),
|
||||||
|
('огрн', 'ogrn'),
|
||||||
|
('кпп', 'kpp'),
|
||||||
|
('основной оквэд', 'okved'),
|
||||||
|
('должность руководителя', 'chiefPosition'),
|
||||||
|
('фио руководителя', 'chiefFio'),
|
||||||
|
('должность.*защит.*информ|должность.*администрат', 'ispdnPosition'),
|
||||||
|
('фио.*защит.*информ|фио.*администрат', 'ispdnFio'),
|
||||||
|
('электронная почта.*защит|электронная почта.*ответств|электронная почта.*специалист', 'ispdnEmail'),
|
||||||
|
('телефон.*защит|телефон.*ответств|телефон.*специалист', 'ispdnPhone'),
|
||||||
|
('структур.*подраздел', 'ispdnDepartment'),
|
||||||
|
('номер.*договора|номер.*контракта', 'contractNumber'),
|
||||||
|
('дата.*договора|дата.*контракта', 'contractDate'),
|
||||||
|
('названи.*документ', 'paperDocuments'),
|
||||||
|
('место.*хранен', 'paperStorage'),
|
||||||
|
('должность.*ответственн', 'responsiblePosition'),
|
||||||
|
('фио.*ответственн', 'responsibleFio'),
|
||||||
|
('должность.*администрат.*(?:безопас|защит|пд)', 'administratorPosition'),
|
||||||
|
('фио.*администрат.*(?:безопас|защит|пд)', 'administratorFio'),
|
||||||
|
('сайт организации', 'site_name'),
|
||||||
|
('должность ответственного за сайт|должность.*отв.*сайт', 'site_responsible_position'),
|
||||||
|
('фио.*отв.*сайт|фио.*сайт', 'site_responsible_fio'),
|
||||||
|
('наименование организации.*разрабат.*сайт|наименование.*размещала.*сайт|разрабат.*сайт.*организац', 'site_service_provider'),
|
||||||
|
('инн.*разрабат.*сайт|инн.*размещала.*сайт', 'site_service_provider_inn'),
|
||||||
|
('размещение сайта|адрес расположения серверов|адрес.*сервер.*сайт', 'site_hosting'),
|
||||||
|
('адрес.*хостинг|адрес.*располож.*сервер', 'site_hosting_address'),
|
||||||
|
('электронная почта', 'email'),
|
||||||
|
('телефон', 'phone'),
|
||||||
|
]
|
||||||
|
|
||||||
|
for row in ws.iter_rows(min_row=1, values_only=True):
|
||||||
|
field_raw = clean_value(row[0]) if row[0] is not None else ''
|
||||||
|
field = field_raw.lower()
|
||||||
|
value = clean_value(row[1]) if len(row) > 1 else ''
|
||||||
|
if not field:
|
||||||
|
continue
|
||||||
|
for keyword, key in field_map:
|
||||||
|
if re.search(keyword, field):
|
||||||
|
if value:
|
||||||
|
data[key] = value
|
||||||
|
log_fn(f" {keyword}: {value}")
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
def _import_152_is_list(wb, data, log_fn):
|
||||||
|
"""Импорт информационных систем из остальных листов."""
|
||||||
|
is_sheets = [sn for sn in wb.sheetnames if sn not in ('Общие сведения', 'Сотрудники')]
|
||||||
|
if not is_sheets:
|
||||||
|
log_fn("⚠ Листы ИС не найдены")
|
||||||
|
return
|
||||||
|
|
||||||
|
is_list = []
|
||||||
|
for sheet_name in is_sheets:
|
||||||
|
ws = wb[sheet_name]
|
||||||
|
rows = []
|
||||||
|
for row in ws.iter_rows(min_row=1, values_only=True):
|
||||||
|
vals = [clean_value(c) for c in row]
|
||||||
|
field = (vals[0] or '').strip() if vals else ''
|
||||||
|
if not field:
|
||||||
|
continue
|
||||||
|
rows.append(vals)
|
||||||
|
|
||||||
|
if len(rows) < 2:
|
||||||
|
continue
|
||||||
|
|
||||||
|
is_obj = {
|
||||||
|
'name': '', 'description': '', 'room': '', 'employee': '', 'position': '',
|
||||||
|
'software': '', 'personalDataList': [], 'pd_subjects_list': [], 'pd_actions': [],
|
||||||
|
'usersList': [], 'isInternet': False, 'personalDataCount': 'менее 100 000',
|
||||||
|
'defense_tools': [], 'processing_mode': '',
|
||||||
|
}
|
||||||
|
|
||||||
|
current_section = 'header'
|
||||||
|
pd_items_raw = []
|
||||||
|
subjects_raw = []
|
||||||
|
actions_raw = []
|
||||||
|
|
||||||
|
for vals in rows:
|
||||||
|
field = (vals[0] or '').strip()
|
||||||
|
field_lower = field.lower()
|
||||||
|
val_b = clean_value(vals[1]) if len(vals) > 1 else ''
|
||||||
|
is_checked = val_b.upper() in ('ДА', 'YES', '✓', '✔', '1', 'TRUE', 'ЕСТЬ')
|
||||||
|
|
||||||
|
if 'цель обработки пд' in field_lower or ('цель обработки' in field_lower and ':' in field):
|
||||||
|
current_section = 'purpose'
|
||||||
|
continue
|
||||||
|
if 'перечисленные пдн принадлежат' in field_lower or 'принадлеж' in field_lower:
|
||||||
|
current_section = 'subjects'
|
||||||
|
continue
|
||||||
|
if 'количество записей' in field_lower or 'количество субъект' in field_lower:
|
||||||
|
if val_b:
|
||||||
|
is_obj['personalDataCount'] = val_b
|
||||||
|
continue
|
||||||
|
if 'перечень действий с пд' in field_lower or ('действи' in field_lower and 'пд' in field_lower):
|
||||||
|
current_section = 'actions'
|
||||||
|
continue
|
||||||
|
if 'способ обработки пд' in field_lower:
|
||||||
|
if val_b:
|
||||||
|
is_obj['processing_mode'] = val_b
|
||||||
|
continue
|
||||||
|
if 'интернет' in field_lower:
|
||||||
|
is_obj['isInternet'] = is_checked
|
||||||
|
continue
|
||||||
|
|
||||||
|
if current_section == 'header':
|
||||||
|
if 'перечень пд' in field_lower:
|
||||||
|
current_section = 'pd_items'
|
||||||
|
elif 'название ис' in field_lower or 'наименование ис' in field_lower or 'наименование информационной' in field_lower:
|
||||||
|
is_obj['name'] = val_b
|
||||||
|
log_fn(f" ИС: {val_b}")
|
||||||
|
elif 'сотрудник' in field_lower and 'должност' not in field_lower:
|
||||||
|
is_obj['employee'] = val_b
|
||||||
|
if val_b:
|
||||||
|
parts = [p.strip() for p in val_b.replace('\n', ';').split(';') if p.strip()]
|
||||||
|
for part in parts:
|
||||||
|
cleaned = part.strip()
|
||||||
|
if ':' in cleaned and len(cleaned.split(':')[0]) < 20:
|
||||||
|
cleaned = cleaned.split(':', 1)[1].strip()
|
||||||
|
if cleaned and cleaned not in is_obj['usersList']:
|
||||||
|
is_obj['usersList'].append(cleaned)
|
||||||
|
elif 'должность сотрудника' in field_lower:
|
||||||
|
is_obj['position'] = val_b
|
||||||
|
elif 'наименование отдела' in field_lower:
|
||||||
|
pass
|
||||||
|
elif 'номер кабинета' in field_lower:
|
||||||
|
is_obj['room'] = val_b
|
||||||
|
elif 'названия программ' in field_lower or ('программ' in field_lower and 'перечень' not in field_lower):
|
||||||
|
is_obj['software'] = val_b
|
||||||
|
elif 'значение' in field_lower or 'примечание' in field_lower:
|
||||||
|
continue
|
||||||
|
elif val_b and 'выбрать' not in field_lower:
|
||||||
|
if not is_obj['description']:
|
||||||
|
is_obj['description'] = f"{field}: {val_b}"
|
||||||
|
else:
|
||||||
|
is_obj['description'] += f"\n{field}: {val_b}"
|
||||||
|
elif current_section == 'pd_items':
|
||||||
|
if is_checked and 'выбрать' not in field_lower and field_lower != 'значение':
|
||||||
|
pd_items_raw.append(field)
|
||||||
|
if 'цель обработки' in field_lower:
|
||||||
|
current_section = 'purpose'
|
||||||
|
elif current_section == 'purpose':
|
||||||
|
if field and 'выбрать' not in field_lower and field_lower not in ('значение', 'примечание'):
|
||||||
|
is_obj['description'] = field
|
||||||
|
log_fn(f" Цель: {field[:80]}")
|
||||||
|
elif val_b and 'выбрать' not in field_lower:
|
||||||
|
is_obj['description'] = val_b
|
||||||
|
current_section = 'header'
|
||||||
|
elif current_section == 'subjects':
|
||||||
|
if is_checked and 'выбрать' not in field_lower:
|
||||||
|
subjects_raw.append(field)
|
||||||
|
if 'количество записей' in field_lower or 'действи' in field_lower:
|
||||||
|
continue
|
||||||
|
elif current_section == 'actions':
|
||||||
|
if is_checked and 'выбрать' not in field_lower and 'защита' not in field_lower[:20]:
|
||||||
|
actions_raw.append(field)
|
||||||
|
if 'защита' in field_lower and 'действи' not in field_lower:
|
||||||
|
current_section = 'defense'
|
||||||
|
elif current_section == 'defense':
|
||||||
|
defense_keywords = {
|
||||||
|
'антивирус': 'Антивирус',
|
||||||
|
'крипто': 'СКЗИ',
|
||||||
|
'межсетев': 'Межсетевой экран',
|
||||||
|
'сзи.*от несанкционирован': 'СЗИ от НСД',
|
||||||
|
'обнаружен.*вторжен': 'СОВ',
|
||||||
|
'программные.*средств.*модул': 'Доверенная загрузка',
|
||||||
|
'доверен.*загрузк': 'Доверенная загрузка',
|
||||||
|
'гарантирован': 'Доверенная загрузка',
|
||||||
|
}
|
||||||
|
matched_label = None
|
||||||
|
for kw, label in defense_keywords.items():
|
||||||
|
if re.search(kw, field_lower):
|
||||||
|
matched_label = label
|
||||||
|
break
|
||||||
|
if matched_label and val_b and 'выбрать' not in field_lower:
|
||||||
|
is_obj['defense_tools'].append(f"{matched_label}: {val_b}")
|
||||||
|
elif matched_label:
|
||||||
|
pass
|
||||||
|
elif 'куда' in field_lower or 'передаются' in field_lower:
|
||||||
|
current_section = 'header'
|
||||||
|
elif 'способ обработки' in field_lower or 'локальную сеть' in field_lower or 'интернет' in field_lower:
|
||||||
|
current_section = 'header'
|
||||||
|
|
||||||
|
if pd_items_raw:
|
||||||
|
is_obj['personalDataList'] = pd_items_raw
|
||||||
|
log_fn(f" ПДн: {len(pd_items_raw)}")
|
||||||
|
if subjects_raw:
|
||||||
|
is_obj['pd_subjects_list'] = subjects_raw
|
||||||
|
log_fn(f" Субъектов: {len(subjects_raw)}")
|
||||||
|
if actions_raw:
|
||||||
|
is_obj['pd_actions'] = actions_raw
|
||||||
|
log_fn(f" Действий: {len(actions_raw)}")
|
||||||
|
|
||||||
|
if not is_obj['name']:
|
||||||
|
is_obj['name'] = sheet_name.strip()
|
||||||
|
log_fn(f" ⚠ ИС без названия — использовано имя листа: {is_obj['name']}")
|
||||||
|
|
||||||
|
if is_obj['software'] and not is_obj['description']:
|
||||||
|
is_obj['description'] = f"Используемое ПО: {is_obj['software']}"
|
||||||
|
|
||||||
|
# Автоопределение категорий ПДн
|
||||||
|
pd_cats = []
|
||||||
|
all_pd_text = ' '.join(p.lower() for p in pd_items_raw)
|
||||||
|
special_kw = ['состоян.*здоров', 'национальн', 'политическ', 'религиозн', 'философ', 'судимост', 'интимн']
|
||||||
|
if any(re.search(kw, all_pd_text) for kw in special_kw):
|
||||||
|
pd_cats.append('специальные')
|
||||||
|
bio_kw = ['биометрическ', 'изображен.*лиц', 'голос.*человек', 'папилляр', 'дактилоскоп', 'фото.*изображен']
|
||||||
|
if any(re.search(kw, all_pd_text) for kw in bio_kw):
|
||||||
|
pd_cats.append('биометрические')
|
||||||
|
if pd_items_raw:
|
||||||
|
pd_cats.append('иные')
|
||||||
|
if pd_cats:
|
||||||
|
is_obj['personalDataCategory'] = pd_cats
|
||||||
|
log_fn(f" Категории ПДн: {', '.join(pd_cats)}")
|
||||||
|
|
||||||
|
is_list.append(is_obj)
|
||||||
|
log_fn(f" ✅ {is_obj['name']} — импортирована")
|
||||||
|
|
||||||
|
if is_list:
|
||||||
|
data['informationSystems'] = is_list
|
||||||
|
log_fn(f"\n✅ Всего импортировано ИС: {len(is_list)}")
|
||||||
|
|
||||||
|
|
||||||
|
def _import_152_employees(wb, data, log_fn):
|
||||||
|
"""Импорт комиссии и сотрудников из листа 'Сотрудники'."""
|
||||||
|
sheet_name = None
|
||||||
|
for sn in wb.sheetnames:
|
||||||
|
if 'сотруд' in sn.lower():
|
||||||
|
sheet_name = sn
|
||||||
|
break
|
||||||
|
if not sheet_name:
|
||||||
|
log_fn("⚠ Лист «Сотрудники» не найден")
|
||||||
|
return
|
||||||
|
|
||||||
|
ws = wb[sheet_name]
|
||||||
|
rows = []
|
||||||
|
for row in ws.iter_rows(min_row=1, values_only=True):
|
||||||
|
vals = [clean_value(c) for c in row]
|
||||||
|
if not vals or not vals[0]:
|
||||||
|
continue
|
||||||
|
rows.append(vals)
|
||||||
|
|
||||||
|
if not rows:
|
||||||
|
log_fn("⚠ Лист «Сотрудники» пуст")
|
||||||
|
return
|
||||||
|
|
||||||
|
commission = []
|
||||||
|
in_commission = False
|
||||||
|
COMMISSION_ROLES = {
|
||||||
|
'председатель': 'Председатель комиссии',
|
||||||
|
'секретарь': 'Секретарь комиссии',
|
||||||
|
'заместитель председателя': 'Заместитель председателя комиссии',
|
||||||
|
}
|
||||||
|
employees = []
|
||||||
|
in_employees = False
|
||||||
|
|
||||||
|
for vals in rows:
|
||||||
|
col_a = (vals[0] or '').strip()
|
||||||
|
col_b = clean_value(vals[1]) if len(vals) > 1 else ''
|
||||||
|
col_c = clean_value(vals[2]) if len(vals) > 2 else ''
|
||||||
|
col_d = clean_value(vals[3]) if len(vals) > 3 else ''
|
||||||
|
col_e = clean_value(vals[4]) if len(vals) > 4 else ''
|
||||||
|
a_lower = col_a.lower()
|
||||||
|
|
||||||
|
if 'состав комиссии' in a_lower:
|
||||||
|
in_commission = True
|
||||||
|
in_employees = False
|
||||||
|
log_fn(" Секция: состав комиссии")
|
||||||
|
continue
|
||||||
|
if 'перечень сотрудников' in a_lower:
|
||||||
|
in_commission = False
|
||||||
|
in_employees = True
|
||||||
|
log_fn(" Секция: сотрудники с доступом к ПДн")
|
||||||
|
continue
|
||||||
|
if a_lower in ('№ п/п', '№', 'номер', ''):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if in_commission:
|
||||||
|
role = 'Член комиссии'
|
||||||
|
if col_e:
|
||||||
|
e_lower = col_e.lower()
|
||||||
|
for keyword, role_name in COMMISSION_ROLES.items():
|
||||||
|
if keyword in e_lower:
|
||||||
|
role = role_name
|
||||||
|
break
|
||||||
|
if not col_b and not col_c:
|
||||||
|
continue
|
||||||
|
commission.append({
|
||||||
|
'role': role,
|
||||||
|
'position': col_b,
|
||||||
|
'fio': col_c.replace('\n', ' '),
|
||||||
|
})
|
||||||
|
|
||||||
|
if in_employees:
|
||||||
|
fio = col_c if col_c else col_d
|
||||||
|
if not fio:
|
||||||
|
continue
|
||||||
|
employee = {
|
||||||
|
'position': col_b,
|
||||||
|
'fio': fio.replace('\n', ' '),
|
||||||
|
}
|
||||||
|
if col_e:
|
||||||
|
employee['is_list'] = [s.strip() for s in col_e.split('\n') if s.strip()]
|
||||||
|
employees.append(employee)
|
||||||
|
|
||||||
|
if commission:
|
||||||
|
commission = [c for c in commission if c['position'] or c['fio']]
|
||||||
|
data['commission'] = commission
|
||||||
|
log_fn(f" Комиссия: {len(commission)} чел.")
|
||||||
|
for m in commission[:3]:
|
||||||
|
log_fn(f" {m['role']}: {m['position'][:40]} — {m['fio'][:40]}")
|
||||||
|
|
||||||
|
if employees:
|
||||||
|
data['employeesAccess'] = employees
|
||||||
|
log_fn(f" Сотрудников с доступом: {len(employees)}")
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_company_data(data, log_fn):
|
||||||
|
"""Проверка обязательных полей."""
|
||||||
|
required = ['fullName', 'inn']
|
||||||
|
missing = [f for f in required if not data.get(f)]
|
||||||
|
if missing:
|
||||||
|
log_fn(f"⚠️ ОБЯЗАТЕЛЬНЫЕ ПОЛЯ НЕ ЗАПОЛНЕНЫ: {', '.join(missing)}")
|
||||||
|
else:
|
||||||
|
log_fn("✅ Все обязательные поля заполнены")
|
||||||
|
|
||||||
|
inn = (data.get('inn') or '').replace(' ', '')
|
||||||
|
if inn and (not inn.isdigit() or len(inn) not in (10, 12)):
|
||||||
|
log_fn(f"⚠️ ИНН подозрительного формата: «{inn}»")
|
||||||
@@ -89,6 +89,7 @@ class Company:
|
|||||||
commission: List[CommissionMember] = field(default_factory=list)
|
commission: List[CommissionMember] = field(default_factory=list)
|
||||||
information_systems: List[InformationSystem] = field(default_factory=list)
|
information_systems: List[InformationSystem] = field(default_factory=list)
|
||||||
paper_documents_list: List[PaperDocument] = field(default_factory=list)
|
paper_documents_list: List[PaperDocument] = field(default_factory=list)
|
||||||
|
employees_access: List[Dict[str, str]] = field(default_factory=list)
|
||||||
|
|
||||||
def update_declensions(self, inflect_func):
|
def update_declensions(self, inflect_func):
|
||||||
"""Обновить склонения полного названия"""
|
"""Обновить склонения полного названия"""
|
||||||
@@ -133,6 +134,8 @@ class Company:
|
|||||||
PaperDocument(**p) if isinstance(p, dict) else p
|
PaperDocument(**p) if isinstance(p, dict) else p
|
||||||
for p in d["paper_documents_list"]
|
for p in d["paper_documents_list"]
|
||||||
]
|
]
|
||||||
|
if "employees_access" in d and not isinstance(d["employees_access"], list):
|
||||||
|
d["employees_access"] = []
|
||||||
valid = {f.name for f in cls.__dataclass_fields__.values()}
|
valid = {f.name for f in cls.__dataclass_fields__.values()}
|
||||||
return cls(**{k: v for k, v in d.items() if k in valid})
|
return cls(**{k: v for k, v in d.items() if k in valid})
|
||||||
|
|
||||||
|
|||||||
+338
-9
@@ -39,8 +39,8 @@ class DokoGenApp:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Переменные
|
# Переменные
|
||||||
self.doc_number_var = tk.StringVar(value="001")
|
self.doc_number_var = tk.StringVar(value="")
|
||||||
self.doc_date_var = tk.StringVar(value=datetime.now().strftime("%d.%m.%Y"))
|
self.doc_date_var = tk.StringVar(value="")
|
||||||
self.templates_var = tk.StringVar()
|
self.templates_var = tk.StringVar()
|
||||||
self.save_var = tk.StringVar()
|
self.save_var = tk.StringVar()
|
||||||
self.status_var = tk.StringVar(value="Готов")
|
self.status_var = tk.StringVar(value="Готов")
|
||||||
@@ -110,6 +110,7 @@ class DokoGenApp:
|
|||||||
self._build_company_tab()
|
self._build_company_tab()
|
||||||
self._build_is_tab()
|
self._build_is_tab()
|
||||||
self._build_commission_tab()
|
self._build_commission_tab()
|
||||||
|
self._build_users_tab()
|
||||||
self._build_generation_tab()
|
self._build_generation_tab()
|
||||||
self._build_log_tab()
|
self._build_log_tab()
|
||||||
|
|
||||||
@@ -327,23 +328,32 @@ class DokoGenApp:
|
|||||||
# Список ПДн
|
# Список ПДн
|
||||||
pd_tab = ttk.Frame(nb2)
|
pd_tab = ttk.Frame(nb2)
|
||||||
nb2.add(pd_tab, text="Перечень ПДн")
|
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=self._select_from_pd_list).pack(side=tk.LEFT, padx=2)
|
||||||
self.pd_listbox = tk.Listbox(pd_tab, height=6)
|
self.pd_listbox = tk.Listbox(pd_tab, height=6)
|
||||||
self.pd_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
self.pd_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||||
ttk.Button(pd_tab, text="+ Добавить", command=self._add_pd_item).pack(pady=3)
|
|
||||||
|
|
||||||
# Субъекты ПДн
|
# Субъекты ПДн
|
||||||
subj_tab = ttk.Frame(nb2)
|
subj_tab = ttk.Frame(nb2)
|
||||||
nb2.add(subj_tab, text="Субъекты ПДн")
|
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=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 = tk.Listbox(subj_tab, height=6)
|
||||||
self.pd_subjects_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
self.pd_subjects_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||||
ttk.Button(subj_tab, text="+ Добавить", command=self._add_pd_subjects_item).pack(pady=3)
|
|
||||||
|
|
||||||
# СЗИ
|
# СЗИ
|
||||||
def_tab = ttk.Frame(nb2)
|
def_tab = ttk.Frame(nb2)
|
||||||
nb2.add(def_tab, text="Средства защиты")
|
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=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 = tk.Listbox(def_tab, height=6)
|
||||||
self.defense_tools_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
self.defense_tools_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||||
ttk.Button(def_tab, text="+ Добавить", command=self._add_defense_item).pack(pady=3)
|
|
||||||
|
|
||||||
# Пользователи
|
# Пользователи
|
||||||
user_tab = ttk.Frame(nb2)
|
user_tab = ttk.Frame(nb2)
|
||||||
@@ -390,6 +400,103 @@ class DokoGenApp:
|
|||||||
ttk.Button(frame, text="+ Добавить члена комиссии",
|
ttk.Button(frame, text="+ Добавить члена комиссии",
|
||||||
command=self._add_commission_row_ui).pack(pady=10)
|
command=self._add_commission_row_ui).pack(pady=10)
|
||||||
|
|
||||||
|
# ---------- ВКЛАДКА "ПОЛЬЗОВАТЕЛИ" ----------
|
||||||
|
def _build_users_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))
|
||||||
|
|
||||||
|
# Заголовки
|
||||||
|
header = ttk.Frame(frame)
|
||||||
|
header.pack(fill=tk.X)
|
||||||
|
for w, col in [(25, "Должность"), (30, "ФИО"), (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', ''),
|
||||||
|
emp.get('is_list', ''),
|
||||||
|
)
|
||||||
|
self._update_emp_count()
|
||||||
|
|
||||||
|
def _add_employee_row(self, position="", fio="", is_list=""):
|
||||||
|
"""Добавляет одну строку сотрудника."""
|
||||||
|
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)
|
||||||
|
|
||||||
|
is_entry = ttk.Entry(row, width=30)
|
||||||
|
is_entry.insert(0, is_list if isinstance(is_list, str) else ', '.join(is_list))
|
||||||
|
is_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, is_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, is_e in self._employee_entries:
|
||||||
|
emp = {
|
||||||
|
'position': pos_e.get().strip(),
|
||||||
|
'fio': fio_e.get().strip(),
|
||||||
|
'is_list': [s.strip() for s in is_e.get().split(',') if s.strip()],
|
||||||
|
}
|
||||||
|
if emp['position'] or emp['fio']:
|
||||||
|
self.model.employees_access.append(emp)
|
||||||
|
|
||||||
# ---------- ВКЛАДКА "ГЕНЕРАЦИЯ" ----------
|
# ---------- ВКЛАДКА "ГЕНЕРАЦИЯ" ----------
|
||||||
def _build_generation_tab(self):
|
def _build_generation_tab(self):
|
||||||
tab = ttk.Frame(self.notebook)
|
tab = ttk.Frame(self.notebook)
|
||||||
@@ -534,6 +641,19 @@ class DokoGenApp:
|
|||||||
self.is_listbox.selection_set(0)
|
self.is_listbox.selection_set(0)
|
||||||
self._on_is_select()
|
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', ''),
|
||||||
|
emp.get('is_list', []),
|
||||||
|
)
|
||||||
|
self._update_emp_count()
|
||||||
|
|
||||||
# Шаблоны
|
# Шаблоны
|
||||||
self._on_templates_changed()
|
self._on_templates_changed()
|
||||||
|
|
||||||
@@ -627,6 +747,8 @@ class DokoGenApp:
|
|||||||
lb.insert(tk.END, item)
|
lb.insert(tk.END, item)
|
||||||
|
|
||||||
def _add_is(self):
|
def _add_is(self):
|
||||||
|
# Сохраняем текущую ИС перед добавлением новой
|
||||||
|
self._collect_is()
|
||||||
name = simpledialog.askstring("Добавить ИС", "Введите наименование ИС:")
|
name = simpledialog.askstring("Добавить ИС", "Введите наименование ИС:")
|
||||||
if name:
|
if name:
|
||||||
isys = InformationSystem(name=name)
|
isys = InformationSystem(name=name)
|
||||||
@@ -662,6 +784,7 @@ class DokoGenApp:
|
|||||||
def _delete_is(self):
|
def _delete_is(self):
|
||||||
if self._current_is_idx is None:
|
if self._current_is_idx is None:
|
||||||
return
|
return
|
||||||
|
self._collect_is()
|
||||||
del self.model.information_systems[self._current_is_idx]
|
del self.model.information_systems[self._current_is_idx]
|
||||||
self._current_is_idx = None
|
self._current_is_idx = None
|
||||||
self._refresh_is_listbox()
|
self._refresh_is_listbox()
|
||||||
@@ -694,6 +817,96 @@ class DokoGenApp:
|
|||||||
if sel:
|
if sel:
|
||||||
self.users_listbox.delete(sel[0])
|
self.users_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 _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):
|
def select_templates(self):
|
||||||
folder = filedialog.askdirectory(title="Папка шаблонов")
|
folder = filedialog.askdirectory(title="Папка шаблонов")
|
||||||
@@ -757,6 +970,7 @@ class DokoGenApp:
|
|||||||
self._collect_company()
|
self._collect_company()
|
||||||
self._collect_commission()
|
self._collect_commission()
|
||||||
self._collect_is()
|
self._collect_is()
|
||||||
|
self._collect_employees()
|
||||||
|
|
||||||
# Проверка
|
# Проверка
|
||||||
if not self._validate_before_generate():
|
if not self._validate_before_generate():
|
||||||
@@ -868,22 +1082,135 @@ class DokoGenApp:
|
|||||||
|
|
||||||
def _apply_imported_data(self, data: dict):
|
def _apply_imported_data(self, data: dict):
|
||||||
def _apply():
|
def _apply():
|
||||||
|
# Название организации
|
||||||
if data.get('fullName'):
|
if data.get('fullName'):
|
||||||
self.company_name_entry.delete(0, tk.END)
|
self.company_name_entry.delete(0, tk.END)
|
||||||
self.company_name_entry.insert(0, data['fullName'])
|
self.company_name_entry.insert(0, data['fullName'])
|
||||||
|
self.model.full_name = data['fullName']
|
||||||
if data.get('shortName'):
|
if data.get('shortName'):
|
||||||
self.short_name_entry.delete(0, tk.END)
|
self.short_name_entry.delete(0, tk.END)
|
||||||
self.short_name_entry.insert(0, data['shortName'])
|
self.short_name_entry.insert(0, data['shortName'])
|
||||||
for key, entry in self._company_entries.items():
|
self.model.short_name = data['shortName']
|
||||||
val = data.get(key) or data.get(key.replace('_', '')) or ''
|
|
||||||
|
# Адрес и реквизиты (маппинг 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:
|
if val:
|
||||||
entry.delete(0, tk.END)
|
entry.delete(0, tk.END)
|
||||||
entry.insert(0, val)
|
entry.insert(0, val)
|
||||||
for attr, entry in self._official_entries.items():
|
|
||||||
val = data.get(attr) or ''
|
# Должностные лица (маппинг)
|
||||||
|
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:
|
if val:
|
||||||
entry.delete(0, tk.END)
|
entry.delete(0, tk.END)
|
||||||
entry.insert(0, val)
|
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', ''),
|
||||||
|
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', []),
|
||||||
|
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', ''),
|
||||||
|
'is_list': emp.get('is_list', []),
|
||||||
|
})
|
||||||
|
self._add_employee_row(
|
||||||
|
emp.get('position', ''),
|
||||||
|
emp.get('fio', ''),
|
||||||
|
emp.get('is_list', []),
|
||||||
|
)
|
||||||
|
self._update_emp_count()
|
||||||
|
|
||||||
self.log("✅ Импорт применён")
|
self.log("✅ Импорт применён")
|
||||||
self.root.after(0, _apply)
|
self.root.after(0, _apply)
|
||||||
|
|
||||||
@@ -927,6 +1254,7 @@ class DokoGenApp:
|
|||||||
self._collect_company()
|
self._collect_company()
|
||||||
self._collect_commission()
|
self._collect_commission()
|
||||||
self._collect_is()
|
self._collect_is()
|
||||||
|
self._collect_employees()
|
||||||
try:
|
try:
|
||||||
with open(filename, 'w', encoding='utf-8') as f:
|
with open(filename, 'w', encoding='utf-8') as f:
|
||||||
json.dump(self.model.to_dict(), f, ensure_ascii=False, indent=2)
|
json.dump(self.model.to_dict(), f, ensure_ascii=False, indent=2)
|
||||||
@@ -951,6 +1279,7 @@ class DokoGenApp:
|
|||||||
self._collect_company()
|
self._collect_company()
|
||||||
self._collect_commission()
|
self._collect_commission()
|
||||||
self._collect_is()
|
self._collect_is()
|
||||||
|
self._collect_employees()
|
||||||
try:
|
try:
|
||||||
os.makedirs(os.path.dirname(self._autosave_path), exist_ok=True)
|
os.makedirs(os.path.dirname(self._autosave_path), exist_ok=True)
|
||||||
with open(self._autosave_path, 'w', encoding='utf-8') as f:
|
with open(self._autosave_path, 'w', encoding='utf-8') as f:
|
||||||
|
|||||||
Reference in New Issue
Block a user