diff --git a/dokogen/dictionaries/architectures.json b/dokogen/dictionaries/architectures.json new file mode 100644 index 0000000..2516137 --- /dev/null +++ b/dokogen/dictionaries/architectures.json @@ -0,0 +1,9 @@ +[ + "одноранговая сеть", + "клиент-серверная система", + "технология \"тонкий клиент\"", + "сеть передачи данных", + "система диспетчерского управления и контроля", + "распределенная система управления", + "иная архитектура" +] diff --git a/dokogen/dictionaries/object_types.json b/dokogen/dictionaries/object_types.json new file mode 100644 index 0000000..6102711 --- /dev/null +++ b/dokogen/dictionaries/object_types.json @@ -0,0 +1,5 @@ +[ + "Информационная система", + "Автоматизированная система управления", + "Информационно-телекоммуникационная сеть" +] diff --git a/dokogen/dictionaries/spheres.json b/dokogen/dictionaries/spheres.json new file mode 100644 index 0000000..864c36d --- /dev/null +++ b/dokogen/dictionaries/spheres.json @@ -0,0 +1,17 @@ +[ + "Здравоохранение", + "Наука", + "Транспорт", + "Связь", + "Энергетика", + "Государственная регистрация прав", + "Банковская сфера и финансовый рынок", + "Топливно-энергетический комплекс", + "Атомная энергетика", + "Оборонная промышленность", + "Ракетно-космическая промышленность", + "Горнодобывающая промышленность", + "Металлургическая промышленность", + "Химическая промышленность", + "Иная сфера деятельности" +] diff --git a/dokogen/dictionaries/typical_objects.json b/dokogen/dictionaries/typical_objects.json new file mode 100644 index 0000000..28f363f --- /dev/null +++ b/dokogen/dictionaries/typical_objects.json @@ -0,0 +1,13 @@ +[ + "Информационная система управления", + "Автоматизированная система управления технологическим процессом", + "Информационно-телекоммуникационная сеть", + "Автоматизированная система управления производством", + "Система диспетчерского управления", + "Система управления базами данных", + "Автоматизированная система научных исследований", + "Система электронного документооборота", + "Информационная система расчетов", + "Система управления финансами", + "Иной типовой отраслевой объект КИИ" +] diff --git a/dokogen/importer.py b/dokogen/importer.py index 67078e6..20122ef 100644 --- a/dokogen/importer.py +++ b/dokogen/importer.py @@ -279,7 +279,7 @@ def _import_187_is_list(wb, data, settings, log_fn): continue is_obj = { - 'name': '', 'description': '', 'usersList': [], + 'name': '', 'description': '', 'address': '', 'usersList': [], 'isInternet': False, 'defense_tools': [], 'software': '', # 236 приказ 'sphere': '', 'purpose': '', 'object_type': '', 'architecture': '', @@ -315,6 +315,25 @@ def _import_187_is_list(wb, data, settings, log_fn): return True return False + def _split_entries(text): + """Разбивает слипшиеся названия СЗИ: по переносам строк, ';' и по границе + 'цифра + заглавная буква' (напр. '15.1.0.22239Kaspersky' → два названия).""" + if not text: + return [] + parts = re.split(r'[\n;]+', text) + out = [] + for p in parts: + p = p.strip() + if not p: + continue + # граница: цифра/точка → заглавная буква без пробела + sub = re.split(r'(?<=[0-9.])(?=[А-ЯЁA-Z])', p) + for s in sub: + s = s.strip() + if s and s not in out: + out.append(s) + return out + for vals in rows: field = (vals[0] or '').strip() field_lower = field.lower() @@ -326,6 +345,11 @@ def _import_187_is_list(wb, data, settings, log_fn): is_obj['name'] = val_b log_fn(f" Объект КИИ: {val_b}") continue + # --- Адрес размещения объекта (в отдельное поле, не в описание) --- + if 'адрес размещения' in field_lower or 'адрес расположения' in field_lower or 'местонахождение объекта' in field_lower: + is_obj['address'] = val_b + log_fn(f" Адрес объекта: {val_b[:60]}") + continue # --- Описание --- if 'описание окки' in field_lower or 'описание объекта' in field_lower: if val_b: @@ -458,7 +482,9 @@ def _import_187_is_list(wb, data, settings, log_fn): for kw, label in defense_keywords.items(): if re.search(kw, field_lower): if val_b and 'выбрать' not in field_lower: - is_obj['defense_tools'].append(val_b) + for tool in _split_entries(val_b): + if tool not in is_obj['defense_tools']: + is_obj['defense_tools'].append(tool) matched = True break if matched: diff --git a/dokogen/ui.py b/dokogen/ui.py index 0a5283a..53f1ac6 100644 --- a/dokogen/ui.py +++ b/dokogen/ui.py @@ -359,6 +359,8 @@ class DokoGenApp: 'address': tk.BooleanVar(value=True), 'requisites': tk.BooleanVar(value=True), 'chief': tk.BooleanVar(value=True), + 'okved': tk.BooleanVar(value=True), + 'okved_extra': tk.BooleanVar(value=True), } apply_row = ttk.Frame(right) apply_row.pack(fill=tk.X, pady=(4, 0)) @@ -368,6 +370,11 @@ class DokoGenApp: ttk.Checkbutton(apply_row, text="адрес", variable=self._apply_flags['address']).pack(side=tk.LEFT) ttk.Checkbutton(apply_row, text="ИНН/КПП/ОГРН", variable=self._apply_flags['requisites']).pack(side=tk.LEFT) ttk.Checkbutton(apply_row, text="руковод.", variable=self._apply_flags['chief']).pack(side=tk.LEFT) + apply_row2 = ttk.Frame(right) + apply_row2.pack(fill=tk.X, pady=(2, 0)) + ttk.Label(apply_row2, text="", font=('', 8), width=10).pack(side=tk.LEFT) + ttk.Checkbutton(apply_row2, text="ОКВЭД осн.", variable=self._apply_flags['okved']).pack(side=tk.LEFT) + ttk.Checkbutton(apply_row2, text="ОКВЭД доп.", variable=self._apply_flags['okved_extra']).pack(side=tk.LEFT) self._verify_result = None # ---------- ПРОВЕРКА ДАННЫХ (Морфер / DaData / ИИ) ---------- @@ -704,15 +711,18 @@ class DokoGenApp: if comp.get(data_key) and ui_key in self._company_entries: self._company_entries[ui_key].delete(0, tk.END) self._company_entries[ui_key].insert(0, comp[data_key]) - # ОКВЭД из DaData - if comp.get('okved') and 'okved' in self._company_entries: + # ОКВЭД из DaData (с названием из словаря/интернета) + from .verify import resolve_okved + from .api_settings import load_api_settings as _las3 + _tok3 = (_las3().get('dadata_token') or '') + if want('okved') and comp.get('okved') and 'okved' in self._company_entries: self._company_entries['okved'].delete(0, tk.END) - self._company_entries['okved'].insert(0, comp['okved']) - if comp.get('okved_extra') and hasattr(self, 'okved_extra_listbox'): + self._company_entries['okved'].insert(0, resolve_okved(comp['okved'], _tok3)) + if want('okved_extra') and comp.get('okved_extra') and hasattr(self, 'okved_extra_listbox'): self.okved_extra_listbox.delete(0, tk.END) for code in comp['okved_extra']: if str(code).strip(): - self.okved_extra_listbox.insert(tk.END, str(code).strip()) + self.okved_extra_listbox.insert(tk.END, resolve_okved(str(code).strip(), _tok3)) if want('chief'): if comp.get('chief_position') and 'chief_position' in self._official_entries: self._official_entries['chief_position'].delete(0, tk.END) @@ -748,61 +758,87 @@ class DokoGenApp: 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 = ttk.Entry(fields_frame, width=60) 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 = ttk.Entry(fields_frame, width=60) 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_category_var = tk.StringVar(value="без категории") - ttk.Label(fields_frame, text="Категория значимости:").grid(row=3, column=0, sticky=tk.W, pady=2) + ttk.Label(fields_frame, text="Категория значимости:").grid(row=2, 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) + cat_frame.grid(row=2, 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) - # Программное обеспечение - 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) - # Вложенные списки + # ---- Вложенные вкладки ---- nb2 = ttk.Notebook(self._is_edit_panel) nb2.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) - # ===== Вкладка «Сведения (236 приказ)» ===== - info_tab = ttk.Frame(nb2) - nb2.add(info_tab, text="Сведения (236)") - info_frame = ttk.Frame(info_tab, padding=8) - info_frame.pack(fill=tk.BOTH, expand=True) - info_fields = [ - ("Назначение объекта:", "is_purpose_entry"), - ("Сфера деятельности:", "is_sphere_entry"), - ("Тип объекта:", "is_object_type_entry"), - ("Архитектура объекта:", "is_architecture_entry"), - ("Типовой отраслевой объект КИИ:", "is_typical_object_entry"), - ] - for i, (label, attr) in enumerate(info_fields): - ttk.Label(info_frame, text=label).grid(row=i, column=0, sticky=tk.W, pady=2) - entry = ttk.Entry(info_frame, width=70) - entry.grid(row=i, column=1, padx=8, sticky=tk.EW, pady=1) - setattr(self, attr, entry) - info_frame.columnconfigure(1, weight=1) + # ===== Вкладка «Основное» ===== + main_tab = ttk.Frame(nb2) + nb2.add(main_tab, text="Основное") + main_frame = ttk.Frame(main_tab, padding=8) + main_frame.pack(fill=tk.BOTH, expand=True) + + # Назначение объекта + ttk.Label(main_frame, text="Назначение объекта:").grid(row=0, column=0, sticky=tk.W, pady=2) + self.is_purpose_entry = ttk.Entry(main_frame, width=70) + self.is_purpose_entry.grid(row=0, column=1, padx=8, sticky=tk.EW, pady=1) + + # Сфера деятельности — словарик + выпадающий список + ttk.Label(main_frame, text="Сфера деятельности:").grid(row=1, column=0, sticky=tk.W, pady=2) + self.is_sphere_combo = ttk.Combobox(main_frame, width=68) + self.is_sphere_combo.grid(row=1, column=1, padx=8, sticky=tk.EW, pady=1) + try: + import json as _json + _sp = os.path.join(os.path.dirname(__file__), 'dictionaries', 'spheres.json') + with open(_sp, encoding='utf-8') as _f: + self.is_sphere_combo['values'] = _json.load(_f) + except Exception: + pass + + # Тип объекта — словарик + выпадающий список + ttk.Label(main_frame, text="Тип объекта:").grid(row=2, column=0, sticky=tk.W, pady=2) + self.is_object_type_combo = ttk.Combobox(main_frame, width=68) + self.is_object_type_combo.grid(row=2, column=1, padx=8, sticky=tk.EW, pady=1) + try: + _ot = os.path.join(os.path.dirname(__file__), 'dictionaries', 'object_types.json') + with open(_ot, encoding='utf-8') as _f: + self.is_object_type_combo['values'] = _json.load(_f) + except Exception: + pass + + # Архитектура объекта — словарик + выпадающий список + ttk.Label(main_frame, text="Архитектура объекта:").grid(row=3, column=0, sticky=tk.W, pady=2) + self.is_architecture_combo = ttk.Combobox(main_frame, width=68) + self.is_architecture_combo.grid(row=3, column=1, padx=8, sticky=tk.EW, pady=1) + try: + _ar = os.path.join(os.path.dirname(__file__), 'dictionaries', 'architectures.json') + with open(_ar, encoding='utf-8') as _f: + self.is_architecture_combo['values'] = _json.load(_f) + except Exception: + pass + + # Типовой отраслевой объект КИИ — словарик + выпадающий список + ttk.Label(main_frame, text="Типовой отраслевой объект КИИ:").grid(row=4, column=0, sticky=tk.W, pady=2) + self.is_typical_object_combo = ttk.Combobox(main_frame, width=68) + self.is_typical_object_combo.grid(row=4, column=1, padx=8, sticky=tk.EW, pady=1) + try: + _to = os.path.join(os.path.dirname(__file__), 'dictionaries', 'typical_objects.json') + with open(_to, encoding='utf-8') as _f: + self.is_typical_object_combo['values'] = _json.load(_f) + except Exception: + pass + + main_frame.columnconfigure(1, weight=1) # ===== Вкладка «Субъект» ===== subj_tab = ttk.Frame(nb2) @@ -835,19 +871,29 @@ class DokoGenApp: nb2.add(net_tab, text="Сети и ПО") net_frame = ttk.Frame(net_tab, padding=8) net_frame.pack(fill=tk.BOTH, expand=True) + + # ЛВС / Интернет + chk = ttk.Frame(net_frame) + chk.grid(row=0, column=0, columnspan=2, sticky=tk.W, pady=2) + self.is_lan_var = tk.BooleanVar() + ttk.Checkbutton(chk, text="ЛВС", variable=self.is_lan_var).pack(side=tk.LEFT, padx=5) + self.is_internet_var = tk.BooleanVar() + ttk.Checkbutton(chk, text="Интернет", variable=self.is_internet_var).pack(side=tk.LEFT, padx=5) + net_fields = [ - ("Категория сети электросвязи:", "is_net_category_entry"), - ("Оператор связи/провайдер хостинга:", "is_net_operator_entry"), - ("Цель взаимодействия с сетью:", "is_net_purpose_entry"), - ("Способ взаимодействия/тип доступа:", "is_net_method_entry"), - ("ПО и аппаратные средства:", "is_hw_sw_entry"), - ("Общесистемное ПО (ОС, виртуализация):", "is_os_sw_entry"), - ("Прикладные программы:", "is_app_sw_entry"), + ("Категория сети электросвязи:", "is_net_category_entry", 1), + ("Оператор связи/провайдер хостинга:", "is_net_operator_entry", 2), + ("Цель взаимодействия с сетью:", "is_net_purpose_entry", 3), + ("Способ взаимодействия/тип доступа:", "is_net_method_entry", 4), + ("ПО и аппаратные средства:", "is_hw_sw_entry", 5), + ("Общесистемное ПО (ОС, виртуализация):", "is_os_sw_entry", 6), + ("Прикладные программы:", "is_app_sw_entry", 7), + ("Программное обеспечение:", "is_software_entry", 8), ] - for i, (label, attr) in enumerate(net_fields): - ttk.Label(net_frame, text=label).grid(row=i, column=0, sticky=tk.W, pady=2) + for label, attr, row in net_fields: + ttk.Label(net_frame, text=label).grid(row=row, column=0, sticky=tk.W, pady=2) entry = ttk.Entry(net_frame, width=70) - entry.grid(row=i, column=1, padx=8, sticky=tk.EW, pady=1) + entry.grid(row=row, column=1, padx=8, sticky=tk.EW, pady=1) setattr(self, attr, entry) net_frame.columnconfigure(1, weight=1) @@ -872,7 +918,7 @@ class DokoGenApp: setattr(self, attr, entry) sec_frame.columnconfigure(1, weight=1) - # СЗИ + # ===== Вкладка «Средства защиты» ===== def_tab = ttk.Frame(nb2) nb2.add(def_tab, text="Средства защиты") def_btn_frame = ttk.Frame(def_tab) @@ -883,7 +929,7 @@ 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="Пользователи ОКИИ") self.users_listbox = tk.Listbox(user_tab, height=6) @@ -1124,10 +1170,10 @@ class DokoGenApp: # Поля 236 приказа for attr, model_field in [ ('is_purpose_entry', 'purpose'), - ('is_sphere_entry', 'sphere'), - ('is_object_type_entry', 'object_type'), - ('is_architecture_entry', 'architecture'), - ('is_typical_object_entry', 'typical_object'), + ('is_sphere_combo', 'sphere'), + ('is_object_type_combo', 'object_type'), + ('is_architecture_combo', 'architecture'), + ('is_typical_object_combo', 'typical_object'), ('is_subject_name_entry', 'subject_name'), ('is_subject_address_entry', 'subject_address'), ('is_subject_chief_position_entry', 'subject_chief_position'), @@ -1364,10 +1410,10 @@ class DokoGenApp: # Поля 236 приказа for attr, model_field in [ ('is_purpose_entry', 'purpose'), - ('is_sphere_entry', 'sphere'), - ('is_object_type_entry', 'object_type'), - ('is_architecture_entry', 'architecture'), - ('is_typical_object_entry', 'typical_object'), + ('is_sphere_combo', 'sphere'), + ('is_object_type_combo', 'object_type'), + ('is_architecture_combo', 'architecture'), + ('is_typical_object_combo', 'typical_object'), ('is_subject_name_entry', 'subject_name'), ('is_subject_address_entry', 'subject_address'), ('is_subject_chief_position_entry', 'subject_chief_position'), @@ -1397,8 +1443,12 @@ class DokoGenApp: ]: w = getattr(self, attr, None) if w is not None: - w.delete(0, tk.END) - w.insert(0, getattr(isys, model_field, '') or '') + val = getattr(isys, model_field, '') or '' + if isinstance(w, ttk.Combobox): + w.set(val) + else: + w.delete(0, tk.END) + w.insert(0, val) # Списки for lb, items in [ @@ -1970,10 +2020,16 @@ class DokoGenApp: if data.get('okved'): entry = self._company_entries.get('okved') if entry: + from .verify import resolve_okved + from .api_settings import load_api_settings as _las + _tok = (_las().get('dadata_token') or '') entry.delete(0, tk.END) - entry.insert(0, data['okved']) + entry.insert(0, resolve_okved(data['okved'], _tok)) if data.get('okvedExtra'): if hasattr(self, 'okved_extra_listbox'): + from .verify import resolve_okved + from .api_settings import load_api_settings as _las2 + _tok2 = (_las2().get('dadata_token') or '') self.okved_extra_listbox.delete(0, tk.END) extra = data['okvedExtra'] if isinstance(extra, str): @@ -1982,7 +2038,7 @@ class DokoGenApp: 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) + self.okved_extra_listbox.insert(tk.END, resolve_okved(p, _tok2)) # Объекты КИИ if data.get('informationSystems'): diff --git a/dokogen/verify.py b/dokogen/verify.py index b9e90bf..016cb93 100644 --- a/dokogen/verify.py +++ b/dokogen/verify.py @@ -148,6 +148,49 @@ def normalize_address(text): return ' '.join(fixed) +def resolve_okved(code, dadata_token=None): + """Код ОКВЭД → «код - название». + + 1) Ищем в словаре dictionaries/okved.json (строки вида «27.32.2 Название»). + 2) Если не нашли — запрашиваем DaData suggest okved2 (если есть токен). + 3) Иначе возвращаем код как есть. + """ + code = (code or '').strip() + if not code: + return '' + # 1) Словарь + try: + import os, json + dict_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'dictionaries', 'okved.json') + with open(dict_path, encoding='utf-8') as f: + items = json.load(f) + for item in items: + item = str(item).strip() + # совпадение по началу: «27.32.2» или «27.32.2 - ...» / «27.32.2 ...» + if item.split()[0].rstrip('.') == code.rstrip('.'): + # нормализуем: «код - название» + rest = item[len(item.split()[0]):].strip().lstrip('- ').strip() + if rest: + return f"{code} - {rest}" + return item + except Exception: + pass + # 2) DaData suggest okved2 + if dadata_token: + try: + js, err = _http_json('https://suggestions.dadata.ru/suggestions/api/4_1/rs/suggest/okved2', + data={'query': code, 'count': 1}, + headers={'Content-Type': 'application/json', + 'Authorization': f'Token {dadata_token}'}) + if not err and js: + sugg = (js.get('suggestions') or []) + if sugg and sugg[0].get('value'): + return sugg[0]['value'] + except Exception: + pass + return code + + def _fmt_date(value): """Дата: '2005-02-09' → '09.02.2005'; миллисекунды 1107907200000 → дата.""" if not value: