fix: ОКВЭД импорт/DaData/словарь+интернет, доменное имя, вкладка ОКИИ → вложенные вкладки, словарики, СЗИ разбивка, адрес отдельно

- ОКВЭД: resolve_okved (словарь okved.json + DaData suggest okved2), применяется при импорте и DaData; галочки «ОКВЭД осн./доп.» в «что применять»
- DaData: показ и подстановка доп. ОКВЭД
- доменное имя: поле во вкладке «Организация» + импорт
- вкладка «Объекты КИИ»: сверху Наименование/Описание/Категория, остальное вкладками: Основное (назначение, сфера, тип, архитектура, типовой отраслевой объект — со словариками и выпадающими списками), Субъект, Сети и ПО, Безопасность (236), СЗИ, Пользователи ОКИИ
- новые словари: spheres.json, object_types.json, architectures.json, typical_objects.json
- адрес объекта — в отдельное поле (не в описание)
- СЗИ: разбивка слипшихся названий (Kaspersky...)
This commit is contained in:
2026-08-04 15:33:36 +04:00
parent 2037925986
commit ed71343d92
7 changed files with 237 additions and 68 deletions
+9
View File
@@ -0,0 +1,9 @@
[
"одноранговая сеть",
"клиент-серверная система",
"технология \"тонкий клиент\"",
"сеть передачи данных",
"система диспетчерского управления и контроля",
"распределенная система управления",
"иная архитектура"
]
+5
View File
@@ -0,0 +1,5 @@
[
"Информационная система",
"Автоматизированная система управления",
"Информационно-телекоммуникационная сеть"
]
+17
View File
@@ -0,0 +1,17 @@
[
"Здравоохранение",
"Наука",
"Транспорт",
"Связь",
"Энергетика",
"Государственная регистрация прав",
"Банковская сфера и финансовый рынок",
"Топливно-энергетический комплекс",
"Атомная энергетика",
"Оборонная промышленность",
"Ракетно-космическая промышленность",
"Горнодобывающая промышленность",
"Металлургическая промышленность",
"Химическая промышленность",
"Иная сфера деятельности"
]
+13
View File
@@ -0,0 +1,13 @@
[
"Информационная система управления",
"Автоматизированная система управления технологическим процессом",
"Информационно-телекоммуникационная сеть",
"Автоматизированная система управления производством",
"Система диспетчерского управления",
"Система управления базами данных",
"Автоматизированная система научных исследований",
"Система электронного документооборота",
"Информационная система расчетов",
"Система управления финансами",
"Иной типовой отраслевой объект КИИ"
]
+28 -2
View File
@@ -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:
+122 -66
View File
@@ -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'):
+43
View File
@@ -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: