Files
dokogen187/dokogen/variables.py
T
LocoAgent a7878804e4 Отраслевые ПП: применимость показателей + формулы ОПК
- criteria_applicability.json: Атомная (ПП-4: без 4, 10-10.7, 14; +6),
  Связь (ПП-402: +6, 8, 9), Оборонная (ПП-796: 1-4, 6-13.1, без 5/10/14),
  Транспорт (+8, 9, 13, 14), Наука (1, 7-9, 11, 13)
- variables.py: _calc_criterion_8_opk (U8=(C+A+X)/Rгод*100%, ПП-796 п.18-20),
  _calc_criterion_13_opk (13а: V=(1-(Vmax-Vинц)/VГОЗ)*100%, 13б: T=(1-tнорм/tненорм)*100%)
- ui.py: ОПК-поля в окне авторасчёта (C/A/X, Vmax/Vинц/VГОЗ, tнорм/tненорм),
  сохранение в criterion_input
2026-08-05 16:57:49 +04:00

1179 lines
67 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""DokoGen — Справочник переменных шаблонов и резолвер значений"""
from datetime import datetime
from typing import Dict, Any, List, Optional
import re
from . import declension as dc
from .models import Company, CommissionMember
def _norm_name(s):
"""Нормализация ФИО для сравнения: регистр, пробелы, точки, тире игнорируются."""
s = (s or '').lower().replace('ё', 'е')
return re.sub(r'[\s.,;:()«»"\'\-]+', '', s)
# ============================================================
# ОПИСАНИЕ ПЕРЕМЕННЫХ
# ============================================================
# Формат: (имя_переменной, группа, описание, ключ_резолвера)
# Группы: A — общие, B — 187-ФЗ
VARIABLE_DEFS = [
# === Полное название организации (6 падежей) ===
('{{company_full_name}}', 'A', 'Полное наименование организации (им.п.)', 'company:full_name'),
('{{company_full_name_genitive}}', 'A', 'Полное наименование (род.п.)', 'decl:full_name:gent'),
('{{company_full_name_dative}}', 'A', 'Полное наименование (дат.п.)', 'decl:full_name:datv'),
('{{company_full_name_accs}}', 'A', 'Полное наименование (вин.п.)', 'decl:full_name:accs'),
('{{company_full_name_ablt}}', 'A', 'Полное наименование (твор.п.)', 'decl:full_name:ablt'),
('{{company_full_name_loct}}', 'A', 'Полное наименование (пр.п.)', 'decl:full_name:loct'),
# === Краткое название организации (6 падежей) ===
('{{company_short_name}}', 'A', 'Краткое наименование (им.п.)', 'company:short_name'),
('{{company_short_name_genitive}}', 'A', 'Краткое наименование (род.п.)', 'decl:short_name:gent'),
('{{company_short_name_dative}}', 'A', 'Краткое наименование (дат.п.)', 'decl:short_name:datv'),
('{{company_short_name_accs}}', 'A', 'Краткое наименование (вин.п.)', 'decl:short_name:accs'),
('{{company_short_name_ablt}}', 'A', 'Краткое наименование (твор.п.)', 'decl:short_name:ablt'),
('{{company_short_name_loct}}', 'A', 'Краткое наименование (пр.п.)', 'decl:short_name:loct'),
# === Должностные лица ===
('{{chief_position}}', 'A', 'Должность руководителя', 'company:chief_position'),
('{{chief_fio_initials}}', 'A', 'И.О. Фамилия руководителя', 'initials:chief'),
('{{chief_fio_short}}', 'A', 'Фамилия И.О. руководителя', 'short_fio:chief'),
('{{responsible_position}}', 'A', 'Должность ответственного', 'company:responsible_position'),
('{{responsible_fio_initials}}', 'A', 'И.О. Фамилия ответственного', 'initials:responsible'),
('{{responsible_fio_short}}', 'A', 'Фамилия И.О. ответственного', 'short_fio:responsible'),
('{{admin_position}}', 'A', 'Должность администратора', 'company:admin_position'),
('{{admin_fio_initials}}', 'A', 'И.О. Фамилия администратора', 'initials:admin'),
('{{admin_fio_short}}', 'A', 'Фамилия И.О. администратора', 'short_fio:admin'),
# === Сведения об организации ===
('{{company_address}}', 'A', 'Адрес организации', 'company:address'),
('{{city_name}}', 'A', 'Город', 'company:city_name'),
('{{company_inn}}', 'A', 'ИНН', 'company:inn'),
('{{company_ogrn}}', 'A', 'ОГРН', 'company:ogrn'),
('{{company_ogrn_date}}', 'A', 'Дата ОГРН', 'company:ogrn_date'),
('{{company_kpp}}', 'A', 'КПП', 'company:kpp'),
('{{security_department}}', 'A', 'Наименование структурного подразделения по безопасности', 'company:security_department'),
('{{company_okved}}', 'A', 'Основной ОКВЭД', 'company:okved'),
('{{company_okved_extra}}', 'A', 'Дополнительные ОКВЭД', 'company:okved_extra'),
('{{company_profit}}', 'A', 'Сумма прибыли за предыдущий год (руб.)', 'company:profit'),
('{{company_profit_tax}}', 'A', 'Сумма налога с прибыли (руб.)', 'company:profit_tax'),
('{{company_income_5y}}', 'A', 'Доход за 5 лет суммарно (руб.) — показатель 8', 'company:income_5y'),
('{{company_income_year_1}}', 'A', 'Доход за 5 лет назад (руб.) — показатель 8 (обратная совместимость)', 'company:income_year_1'),
('{{company_income_year_2}}', 'A', 'Доход за 4 года назад (руб.) — показатель 8', 'company:income_year_2'),
('{{company_income_year_3}}', 'A', 'Доход за 3 года назад (руб.) — показатель 8', 'company:income_year_3'),
('{{company_income_year_4}}', 'A', 'Доход за 2 года назад (руб.) — показатель 8', 'company:income_year_4'),
('{{company_income_year_5}}', 'A', 'Доход за прошлый год (руб.) — показатель 8', 'company:income_year_5'),
('{{company_income_loss}}', 'A', 'Суммарный экономический ущерб от нарушения критических процессов (руб.) — показатель 8', 'company:income_loss'),
# === Комиссия ===
('{{commission_list}}', 'A', 'Список комиссии', 'commission_list'),
('{{fstek_district}}', 'A', 'Наименование управления ФСТЭК России (по адресу организации)', 'fstek_district'),
('{{fstek_recipient_address}}', 'A', 'Адрес управления ФСТЭК России для направления сведений', 'fstek_recipient_address'),
# === Специальные переменные ===
('{{date_year}}', 'A', 'Текущий год', 'date:year'),
('{{date}}', 'A', 'Текущая дата', 'date:full'),
('{{document_date}}', 'A', 'Дата документа', 'doc_date'),
('{{document_number}}', 'A', 'Номер документа', 'doc_number'),
# === 187-ФЗ ===
('{{is_security_requirements}}', 'B', 'Требования к защите объекта КИИ', 'security_reqs'),
('{{is_list}}', 'B', 'Список всех объектов КИИ (нумерованный)', 'is_list'),
('{{is_name}}', 'B', 'Наименование первого объекта КИИ', 'is:name'),
('{{name}}', 'B', 'Название объекта КИИ (алиас)', 'is:name'),
('{{description}}', 'B', 'Описание объекта КИИ (алиас)', 'is:description'),
('{{category}}', 'B', 'Категория значимости объекта КИИ (алиас)', 'is:category'),
('{{number}}', 'B', 'Номер (алиас)', 'doc_number'),
('{{users}}', 'B', 'Пользователи ОКИИ (алиас)', 'is:users'),
('{{full_name_genitive}}', 'A', 'Полное наименование (род.п., алиас)', 'decl:full_name:gent'),
('{{responsible_fio_accs}}', 'A', 'ФИО ответственного (вин.п.)', 'fio_acl:responsible'),
('{{commission1}}', 'A', 'Должность члена комиссии 1', 'commission_member:0:position'),
('{{commission1_fio}}', 'A', 'ФИО члена комиссии 1', 'commission_member:0:fio'),
('{{commission2}}', 'A', 'Должность члена комиссии 2', 'commission_member:1:position'),
('{{commission2_fio}}', 'A', 'ФИО члена комиссии 2', 'commission_member:1:fio'),
('{{commission3}}', 'A', 'Должность члена комиссии 3', 'commission_member:2:position'),
('{{commission3_fio}}', 'A', 'ФИО члена комиссии 3', 'commission_member:2:fio'),
]
# Маппинг старых переменных <...> → {{...}}
OLD_VAR_MAP = {
'<CompanyName1>': '{{company_full_name}}',
'<CompanyName2>': '{{company_short_name}}',
'<CompanyName1Gent>': '{{company_full_name_genitive}}',
'<CompanyName2Gent>': '{{company_short_name_genitive}}',
'<CompanyName1Datv>': '{{company_full_name_dative}}',
'<CompanyName2Datv>': '{{company_short_name_dative}}',
'<CompanyName1Accs>': '{{company_full_name_accs}}',
'<CompanyName2Accs>': '{{company_short_name_accs}}',
'<CompanyName1Ablt>': '{{company_full_name_ablt}}',
'<CompanyName2Ablt>': '{{company_short_name_ablt}}',
'<CompanyName1Loct>': '{{company_full_name_loct}}',
'<CompanyName2Loct>': '{{company_short_name_loct}}',
'<Address>': '{{company_address}}',
'<INN>': '{{company_inn}}',
'<OGRN>': '{{company_ogrn}}',
'<KPP>': '{{company_kpp}}',
'<ChiefFio>': '{{chief_fio_initials}}',
'<ChiefFioShort>': '{{chief_fio_short}}',
'<ChiefPosition>': '{{chief_position}}',
'<RspFio>': '{{responsible_fio_initials}}',
'<RspFioShort>': '{{responsible_fio_short}}',
'<RspPosition>': '{{responsible_position}}',
'<CommissionList>': '{{commission_list}}',
'<DocumentNumber>': '{{document_number}}',
'<DocumentDate>': '{{document_date}}',
'<Date>': '{{date}}',
'<SurveyDate>': '{{document_date}}',
}
VAR_PATTERN = re.compile(r'\{\{[^}]+\}\}')
OLD_VAR_PATTERN = re.compile(r'<[A-Za-z0-9_]+>')
# ============================================================
# ПОСТРОЕНИЕ СЛОВАРЯ ЗАМЕН
# ============================================================
def build_replacements(
company: Company,
is_list: List = None,
doc_number: str = "",
doc_date: str = "",
direction: str = "187fz",
) -> Dict[str, str]:
"""Строит полный словарь замен {{переменная}} → значение."""
if is_list is None:
is_list = []
# ---------- Комиссия ----------
commission_data = _build_commission_data(company)
# ---------- Контекст ----------
ctx = {
'company': company,
'is_list': is_list,
'commission_data': commission_data,
'doc_number': doc_number or '',
'doc_date': doc_date or '',
'direction': direction,
}
replacements = {}
for var_name, group, desc, source in VARIABLE_DEFS:
if direction == '187fz' and group not in ('A', 'B'):
continue
value = _resolve(source, ctx)
replacements[var_name] = str(value) if value else '—'
# Индексированные переменные ИС (is_name_1, is_pd_list_2 и т.д.)
for i, isys in enumerate(is_list, 1):
_add_is_indexed_vars(replacements, isys, i)
# Циклы
replacements['_loops'] = _build_loop_data(company, is_list)
return replacements
def _commission_role_display(members) -> List[str]:
"""Группирует роли комиссии для вывода:
- Председатель/Секретарь/Заместитель — отдельной строкой со своей ролью;
- обычные члены — первый получает «Члены комиссии», остальные — пустую роль
(чтобы не повторять «Член комиссии» в каждой строке)."""
result = []
member_used = False
for m in members:
role = (m.role or 'член комиссии').strip()
role_lower = role.lower()
if ('председател' in role_lower or 'секретар' in role_lower
or 'заместител' in role_lower or 'зам. председателя' in role_lower):
result.append(role)
continue
# обычный член комиссии
if not member_used:
result.append('Члены комиссии')
member_used = True
else:
result.append('')
return result
def _build_commission_data(company: Company) -> Dict[str, str]:
data = {}
members = company.commission or []
display_roles = _commission_role_display(members)
lines = []
for i, (m, role_disp) in enumerate(zip(members, display_roles), 1):
if role_disp:
line = (f"{i}. {role_disp}: {m.position}{m.fio}" if m.position
else f"{i}. {role_disp}: {m.fio}")
else:
line = (f"{i}. {m.position}{m.fio}" if m.position
else f"{i}. {m.fio}")
lines.append(line)
data['commission_list'] = '\n'.join(lines) if lines else '—'
return data
def _add_is_indexed_vars(replacements: Dict[str, str], isys, idx: int):
fields = {
'is_name': isys.name or '—',
'is_address': getattr(isys, 'address', '') or '—',
'is_description': isys.description or '—',
'is_has_internet': 'да' if isys.is_internet else 'нет',
'is_category': isys.category or '—',
'is_users': ', '.join(isys.users_list or []) or '—',
'is_purpose': isys.purpose or '—',
'is_structure': getattr(isys, 'structure', '') or '—',
'is_software': getattr(isys, 'software', '') or '—',
'is_sphere': getattr(isys, 'sphere', '') or '—',
'is_object_type': getattr(isys, 'object_type', '') or '—',
'is_architecture': getattr(isys, 'architecture', '') or '—',
'is_typical_object': getattr(isys, 'typical_object', '') or '—',
'is_net_category': getattr(isys, 'net_category', '') or '—',
'is_net_operator': getattr(isys, 'net_operator', '') or '—',
'is_net_purpose': getattr(isys, 'net_purpose', '') or '—',
'is_net_method': getattr(isys, 'net_method', '') or '—',
'is_hw_sw': getattr(isys, 'hw_sw', '') or '—',
'is_os_sw': getattr(isys, 'os_sw', '') or '—',
'is_app_sw': getattr(isys, 'app_sw', '') or '—',
}
for f, v in fields.items():
replacements[f'{{{{{f}_{idx}}}}}'] = str(v)
def _build_loop_data(company: Company, is_list: List) -> Dict[str, List[Dict]]:
loops = {}
# Комиссия
members = company.commission or []
if members:
display_roles = _commission_role_display(members)
items = []
for m, role_disp in zip(members, display_roles):
pos = m.position or '—'
fio = m.fio or '—'
if role_disp:
full = f"{role_disp} {pos}{fio}" if pos != '—' else f"{role_disp}{fio}"
else:
full = f"{pos}{fio}" if pos != '—' else fio
# Строка подписи: «___________ / » (только подчёркивание и слэш).
# Роль НЕ включаем — в шаблонах она выводится отдельной колонкой {{item.role}},
# иначе получается дубль «Председатель комиссии | Председатель комиссии: ___».
if 'Председатель' in role_disp:
sig = f"{'_' * 11} / "
sig_full = f"{role_disp}: {'_' * 11} / "
elif role_disp:
sig = f"{'_' * 17} / "
sig_full = f"{role_disp}: {'_' * 17} / "
else:
sig = f"{'_' * 17} / "
sig_full = f"{'_' * 17} / "
initials = dc.get_initials(fio) if fio != '—' else ''
items.append({
'role': role_disp,
'position': pos,
'fio': fio,
'fio_initials': dc.get_initials(fio) if fio != '—' else '',
'fio_short': dc.get_short_fio(fio) if fio != '—' else '',
'full': full,
# Рамка подписи без роли и ФИО: «___________ / » (ФИО добавится)
'signature': sig,
# Полная подпись с ролью и ФИО: «Роль: ___________ / И.О. Фамилия»
'signature_full': (sig_full + initials).strip(),
})
loops['commission'] = items
# ИС
if is_list:
items = []
for isys in is_list:
# Пользователи ИС для вложенного цикла <!-- loop:users_loop -->
# ВСЕ пользователи, включая администратора (без дублей по ФИО)
seen = set()
users_loop = []
for u in (isys.users_list or []):
key = _norm_name(u)
if key and key in seen:
continue
seen.add(key)
users_loop.append({'fio': u, 'position': ''})
items.append({
'name': isys.name or '—',
'description': isys.description or '—',
'address': getattr(isys, 'address', '') or '—',
'is_has_lan': 'Да' if isys.is_local_network else 'Нет',
'has_internet': 'да' if isys.is_internet else 'нет',
'category': isys.category or '—',
'users': ', '.join(isys.users_list or []) or '—',
'users_loop': users_loop,
'defense_tools_list': ', '.join(isys.defense_tools_list or []) or '—',
'is_purpose': isys.purpose or '—',
'structure': getattr(isys, 'structure', '') or '—',
'software': getattr(isys, 'software', '') or '—',
# 236 приказ
'purpose': isys.purpose or '—',
'sphere': getattr(isys, 'sphere', '') or '—',
'object_type': getattr(isys, 'object_type', '') or '—',
'architecture': getattr(isys, 'architecture', '') or '—',
'typical_object': getattr(isys, 'typical_object', '') or '—',
'subject_name': getattr(isys, 'subject_name', '') or '—',
'subject_address': getattr(isys, 'subject_address', '') or '—',
'subject_element': getattr(isys, 'subject_element', '') or '—',
'subject_inn': getattr(isys, 'subject_inn', '') or '—',
'subject_kpp': getattr(isys, 'subject_kpp', '') or '—',
'net_category': getattr(isys, 'net_category', '') or '—',
'net_operator': getattr(isys, 'net_operator', '') or '—',
'net_purpose': getattr(isys, 'net_purpose', '') or '—',
'net_method': getattr(isys, 'net_method', '') or '—',
'hw_sw': getattr(isys, 'hw_sw', '') or '—',
'os_sw': getattr(isys, 'os_sw', '') or '—',
'app_sw': getattr(isys, 'app_sw', '') or '—',
'balance_value': getattr(isys, 'balance_value', '') or '—',
'attacker_category': getattr(isys, 'attacker_category', '') or '—',
'attacker_level': getattr(isys, 'attacker_level', '') or '—',
'attacker_types': getattr(isys, 'attacker_types', '') or '—',
'threats': getattr(isys, 'threats', '') or '—',
# Алиасы под шаблон «Сведения о результатах категорирования» (ФСТЭК)
'objective': getattr(isys, 'purpose', '') or '—',
'standard_name': getattr(isys, 'typical_object', '') or '—',
'network_category': getattr(isys, 'net_category', '') or '—',
'provider': getattr(isys, 'net_operator', '') or '—',
'interaction_purpose': getattr(isys, 'net_purpose', '') or '—',
'interaction_method': getattr(isys, 'net_method', '') or '—',
'hardware': getattr(isys, 'hw_sw', '') or '—',
'system_software': getattr(isys, 'os_sw', '') or '—',
'defense_tools': ', '.join(isys.defense_tools_list or []) or '—',
'component': getattr(isys, 'subject_element', '') or '—',
'exploiting_org': getattr(isys, 'subject_name', '') or '—',
'exploiting_inn': getattr(isys, 'subject_inn', '') or '—',
'domain': getattr(isys, 'domain_name', '') or '—',
'threat_actor': getattr(isys, 'attacker_category', '') or '—',
'threat_justification': getattr(isys, 'threats', '') or '—',
'incidents': getattr(isys, 'incidents', '') or '—',
'criteria_values': getattr(isys, 'criteria_values', '') or '—',
'criteria_justification': getattr(isys, 'criteria_justification', '') or '—',
# Орг. меры: если не заполнены вручную — автогенерация по опроснику
# (с учётом ответов раздела «Физическая безопасность»).
'org_measures': (getattr(isys, 'org_measures', '') or '')
or _build_org_measures_text(company, isys),
'tech_measures': getattr(isys, 'tech_measures', '') or '—',
})
loops['information_systems'] = items
# Сотрудники с доступом
employees = getattr(company, 'employees_access', None) or []
if employees:
items = []
for e in employees:
items.append({
'position': e.get('position', ''),
'fio': e.get('fio', ''),
})
loops['employees_access'] = items
# --- Шаблон «Сведения о результатах категорирования» (ФСТЭК) ---
# Цикл объектов КИИ — тот же список, что information_systems.
if is_list:
loops['kii_objects'] = loops.get('information_systems', [])
# Цикл показателей критериев значимости (ПП-127): автоматически
# формируем строки таблицы с обоснованием «применим/не применим» по сфере
# и расчётом для показателей 8 и 9 (как в примере заказчика).
loops['categorization_indicators'] = _build_categorization_indicators(company, is_list)
return loops
# ============================================================
# ПОКАЗАТЕЛИ КРИТЕРИЕВ ЗНАЧИМОСТИ (ПП-127)
# ============================================================
def _num(s):
"""«121 118 270 руб», «1 000», «2 212.5» → число (float) или None."""
s = str(s or '').strip()
if not s:
return None
s = s.replace('\u00a0', ' ').replace(' ', '')\
.replace('руб', '').replace('₽', '').replace('р.', '').replace(',', '.')
try:
return float(s)
except Exception:
return None
def _load_json(name):
import json as _json
import os as _os
p = _os.path.join(_os.path.dirname(_os.path.abspath(__file__)), 'dictionaries', name)
try:
with open(p, encoding='utf-8') as f:
return _json.load(f)
except Exception:
return {}
def _calc_criterion_8_9(company, isys, num):
"""Расчёт показателя 8 (ущерб субъекту КИИ) или 9 (ущерб бюджету РФ).
Возвращает (текст_обоснования, категория или None).
Логика — как в авторасчёте программы (окно ПП-127), текст — по образцу
заполнения заказчика для ОПК.
"""
# ΔДоходов: ущерб из вкладки «Организация» или балансовая стоимость объекта
d = _num(getattr(company, 'income_loss', '') or '')
if d is None:
d = _num(getattr(isys, 'balance_value', '') or '')
# Доходы за 5 лет: основное поле income_5y (суммарно), иначе — обратная совместимость по годам
inc5 = _num(getattr(company, 'income_5y', '') or '')
if inc5:
r_avg = inc5 / 5.0
else:
incs = [_num(getattr(company, f'income_year_{i}', '') or '') for i in range(1, 6)]
incs = [x for x in incs if x is not None]
r_avg = (sum(incs) / len(incs)) if incs else None
if num == '8':
# Фолбэк: ущерб не заполнен → оценка «доход за год × 5» (наихудший сценарий)
d_estimated = False
if d is None and r_avg:
d = r_avg * 5
d_estimated = True
if d is None or not r_avg:
return ("Показатель применим. Для расчёта заполните данные организации: "
"«Снижение уровня дохода» и доходы за 5 лет, или балансовую стоимость объекта КИИ.", None)
val_pct = d / r_avg * 100
if val_pct > 20:
cat = 'I'
elif val_pct > 10:
cat = 'II'
elif val_pct >= 1:
cat = 'III'
else:
cat = None
text = (f"Реализация компьютерных атак на объект не приведет к снижению доходов субъекта КИИ. "
f"Объем среднегодового дохода субъекта КИИ (с учетом НДС), усредненный за прошедший "
f"5-летний период, составляет {r_avg:,.2f} руб. ")
if d_estimated:
text += (f"Снижение уровня дохода не заполнено, поэтому оценка ущерба принята по наихудшему "
f"сценарию: доход за год × 5 лет = {r_avg:,.2f} × 5 = {d:,.2f} руб. ")
else:
text += (f"В случае выхода из строя объекта КИИ потребуется замена, установка и настройка, "
f"стоимость которых составит около {d:,.2f} руб. ")
text += (f"Абсолютный ущерб субъекта КИИ: U={d:,.2f} руб. "
f"Показатель ущерба субъекту КИИ: U_8 = U / R_год = {d:,.2f} / {r_avg:,.2f} "
f"= {val_pct:.6f} %.")
if cat:
text += (f" Так как {val_pct:.6f} % более 1% (пороговое значение для 3-й категории "
f"значимости согласно Постановлению № 127), по данному показателю объекту "
f"присваивается {cat}-я категория значимости.")
else:
text += (f" Так как {val_pct:.6f} % менее 1% (пороговое значение для 3-й категории "
f"значимости согласно Постановлению № 127), по данному показателю объекту "
f"категория значимости не присваивается.")
return text, cat
# num == '9'
if d is None:
return ("Показатель применим. Для расчёта заполните данные организации: "
"«Снижение уровня дохода» или балансовую стоимость объекта КИИ.", None)
profit = _num(getattr(company, 'profit', '') or '')
tax = _num(getattr(company, 'profit_tax', '') or '')
if profit and tax is not None and profit > 0:
knp = tax / profit
dnp = d * knp
if d > profit:
dnp = tax
else:
knp = 0.0
dnp = 0.0
DEFAULT_BUDGET_AVG = 21228053357700.0 # Бусредн — прогноз годового дохода федбюджета, усреднённый за 3 года
b = DEFAULT_BUDGET_AVG
val_pct = dnp / b * 100
if val_pct > 0.001:
cat = 'I'
elif val_pct > 0.0006:
cat = 'II'
elif val_pct > 0.0003:
cat = 'III'
else:
cat = None
text = (f"Реализация компьютерных атак на объект не приведет к снижению доходов федерального бюджета. "
f"Объем прогнозируемого годового дохода федерального бюджета Российской Федерации, "
f"усредненный за планируемый трехлетний период, составляет {b:,.2f} руб. "
f"Коэффициент выплат (отчислений) от налога на прибыль субъекта КИИ = {knp:.4f}. "
f"В случае выхода из строя объекта КИИ потребуется замена, установка и настройка, "
f"стоимость которых составит около {d:,.2f} руб. "
f"Снижение выплат (отчислений) от налога на прибыль субъекта КИИ в бюджеты Российской Федерации "
f"Уабс = {d:,.2f} × {knp:.4f} = {dnp:,.2f} руб. (приближённо). "
f"Показатель ущерба бюджетам = {val_pct:.6f} %.")
if cat:
text += (f" Так как {val_pct:.6f} % более 0,0003% (пороговое значение для 3-й категории "
f"значимости согласно Постановлению № 127), по данному показателю объекту "
f"присваивается {cat}-я категория значимости.")
else:
text += (f" Так как {val_pct:.6f} % менее 0,0003% (пороговое значение для 3-й категории "
f"значимости согласно Постановлению № 127), по данному показателю объекту "
f"категория значимости не присваивается.")
return text, cat
def _calc_criterion_8_opk(company, isys):
"""Показатель 8 (ущерб субъекту КИИ) по отраслевым особенностям ОПК
(ПП РФ № 796 от 27.06.2026, п. 1820):
U_8 = U_у8 / R_год × 100%,
U_у8 = C + A + X,
где C — суммарные затраты на объект КИИ;
A — затраты на возмещение ущерба потерпевшим и их имуществу (если инцидент
не влечёт ущерба, A = 0);
X — ущерб от ранее произошедших компьютерных инцидентов (если инцидентов
не было или нет данных, X = 0);
R_год — усреднённый объём годового дохода за прошедший 5-летний период.
Значения C/A/X берутся из criterion_input (ключи 8_c, 8_a, 8_x);
если C не заполнен — используется балансовая стоимость объекта КИИ.
Возвращает (текст_обоснования, категория или None).
"""
crit = dict(getattr(isys, 'criterion_input', None) or {}) if isys else {}
c_val = _num(crit.get('8_c', '') or '') or _num(getattr(isys, 'balance_value', '') or '')
a_val = _num(crit.get('8_a', '') or '') or 0.0
x_val = _num(crit.get('8_x', '') or '') or 0.0
inc5 = _num(getattr(company, 'income_5y', '') or '')
if inc5:
r_avg = inc5 / 5.0
else:
incs = [_num(getattr(company, f'income_year_{i}', '') or '') for i in range(1, 6)]
incs = [x for x in incs if x is not None]
r_avg = (sum(incs) / len(incs)) if incs else None
if c_val is None or not r_avg:
return ("Показатель применим (отраслевые особенности ОПК, ПП РФ № 796). "
"Для расчёта заполните суммарные затраты на объект КИИ (C) и доходы за 5 лет "
"(или балансовую стоимость объекта КИИ).", None)
u_y8 = c_val + a_val + x_val
val_pct = u_y8 / r_avg * 100
if val_pct > 20:
cat = 'I'
elif val_pct > 10:
cat = 'II'
elif val_pct >= 1:
cat = 'III'
else:
cat = None
text = (f"Показатель 8 рассчитывается по отраслевым особенностям ОПК "
f"(ПП РФ № 796 от 27.06.2026): U_8 = U_у8 / R_год × 100%, U_у8 = C + A + X. "
f"Суммарные затраты на объект КИИ: C = {c_val:,.2f} руб. "
f"Затраты на возмещение ущерба потерпевшим: A = {a_val:,.2f} руб. "
f"Ущерб от ранее произошедших компьютерных инцидентов: X = {x_val:,.2f} руб. "
f"Ущерб субъекту КИИ: U_у8 = {c_val:,.2f} + {a_val:,.2f} + {x_val:,.2f} "
f"= {u_y8:,.2f} руб. "
f"Усреднённый годовой доход за 5-летний период: R_год = {r_avg:,.2f} руб. "
f"Показатель ущерба: U_8 = {u_y8:,.2f} / {r_avg:,.2f} × 100% = {val_pct:.6f} %.")
if cat:
text += (f" Так как {val_pct:.6f} % более 1% (пороговое значение для 3-й категории "
f"значимости), по данному показателю объекту присваивается {cat}-я категория значимости.")
else:
text += (f" Так как {val_pct:.6f} % менее 1% (пороговое значение для 3-й категории "
f"значимости), по данному показателю объекту категория значимости не присваивается.")
return text, cat
def _calc_criterion_13_opk(company, isys, num):
"""Показатели 13а/13б по отраслевым особенностям ОПК (ПП РФ № 796, п. 34, 38):
13а: V = (1 (V_max V_инц) / V_ГОЗ) × 100% (снижение объёма продукции ГОЗ)
13б: T = (1 (t_норм / t_ненорм)) × 100% (увеличение времени изготовления)
Значения берутся из criterion_input: 13a_vmax, 13a_vinc, 13a_vgoz;
13b_tnorm, 13b_tnonorm. Категория — по порогам ПП-127 (CRITERION_QUANT_THRESHOLDS).
Возвращает (текст_обоснования, категория или None).
"""
crit = dict(getattr(isys, 'criterion_input', None) or {}) if isys else {}
num = str(num)
if num == '13а':
vmax = _num(crit.get('13a_vmax', '') or '')
vinc = _num(crit.get('13a_vinc', '') or '')
vgoz = _num(crit.get('13a_vgoz', '') or '')
if vmax is None or vinc is None or not vgoz:
return ("Показатель 13а рассчитывается по отраслевым особенностям ОПК "
"(ПП РФ № 796): V = (1 (V_max V_инц) / V_ГОЗ) × 100%. "
"Заполните V_max, V_инц, V_ГОЗ (объёмы продукции ГОЗ).", None)
val = (1 - (vmax - vinc) / vgoz) * 100
formula = (f"V = (1 ({vmax:,.2f} {vinc:,.2f}) / {vgoz:,.2f}) × 100% = {val:.6f} %")
else: # 13б
tnorm = _num(crit.get('13b_tnorm', '') or '')
tnon = _num(crit.get('13b_tnonorm', '') or '')
if tnorm is None or tnon is None or tnon <= 0:
return ("Показатель 13б рассчитывается по отраслевым особенностям ОПК "
"(ПП РФ № 796): T = (1 − (t_норм / t_ненорм)) × 100%. "
"Заполните t_норм и t_ненорм (время операций по ГОЗ).", None)
val = (1 - tnorm / tnon) * 100
formula = (f"T = (1 ({tnorm:,.2f} / {tnon:,.2f})) × 100% = {val:.6f} %")
cat = _criterion_other_cat(num, val)
text = (f"Показатель {num} рассчитывается по отраслевым особенностям ОПК "
f"(ПП РФ № 796 от 27.06.2026): {formula}.")
if cat:
text += (f" По порогам ПП-127 это соответствует {cat}-й категории значимости.")
else:
text += " По порогам ПП-127 категория значимости не присваивается."
return text, cat
# Пороги показателя 10 и подпоказателей 10.1–10.7 (ПП-127, «Значение показателя»)
CRITERION_10_THRESHOLDS = {
# num: (категория → (нижняя граница, верхняя граница)), границы «включительно»
# Показатель 10 — среднедневное количество операций (млн ед.):
# III — менее или равно 70, II — более 70 но менее или равно 120, I — более 120.
'10': {'I': (120, None), 'II': (70, 120), 'III': (None, 70)},
'10.1': {'I': (10, None), 'II': (1, 10), 'III': (None, 1)}, # трлн руб.
'10.2': {'I': (25, None), 'II': (10, 25), 'III': (None, 10)}, # тыс. шт.
'10.3': {'I': (2000, None), 'II': (1000, 2000), 'III': (50, 1000)}, # млрд руб.
'10.4': {'I': (5000, None), 'II': (1500, 5000), 'III': (100, 1500)}, # млрд руб.
'10.5': {'I': (150, None), 'II': (100, 150), 'III': (25, 100)}, # договоров
'10.6': {'I': (5000, None), 'II': (1000, 5000), 'III': (100, 1000)}, # млрд руб.
'10.7': {'I': (500, None), 'II': (200, 500), 'III': (30, 200)}, # млн ед.
}
def _criterion_10_cat(num, value):
"""Категория значимости по показателю 10 / 10.x (ПП-127).
value — численное значение показателя. Возвращает 'I'/'II'/'III' или None.
"""
thr = CRITERION_10_THRESHOLDS.get(str(num))
if not thr or value is None:
return None
for cat in ('I', 'II', 'III'):
lo, hi = thr[cat]
if lo is not None and value < lo:
continue
if hi is not None and value > hi:
continue
return cat
return None
def _calc_criterion_10(company, isys, num):
"""Расчёт показателя 10 (прекращение финансовых операций) и подпоказателей 10.1–10.7.
Показатель 10: среднедневное количество операций = операции за год (млн ед.) / дни.
Подпоказатели 10.x: значение показателя (в своих единицах) сравнивается с порогами.
Возвращает (текст_обоснования, категория или None).
"""
num = str(num)
if num not in CRITERION_10_THRESHOLDS:
return None, None
if num == '10':
ops = _num(getattr(company, 'ops_count', '') or '')
days = _num(getattr(company, 'calendar_days', '') or '')
if days is None or days <= 0:
days = 365
if ops is None:
return ("Показатель применим. Для расчёта заполните данные организации: "
"«Количество операций за прошедший год (млн ед.)».", None)
val = ops / days
cat = _criterion_10_cat('10', val)
text = (f"Показатель прекращения финансовых операций оценивается среднедневным количеством "
f"операций, проведенных клиентами по банковским счетам и (или) без открытия банковского "
f"счета. Количество операций за прошедший год: {ops:,.2f} млн ед. Количество календарных "
f"дней в году: {days:.0f}. Среднедневное количество операций: {ops:,.2f} / {days:.0f} "
f"= {val:.6f} млн ед./день.")
else:
value = _num(getattr(company, 'criterion10_value', '') or '')
if value is None:
return (f"Показатель применим. Для расчёта заполните значение показателя "
f{num}» в данных организации.", None)
val = value
cat = _criterion_10_cat(num, val)
text = (f"Показатель {num} оценивается значением показателя: {val:,.6f} "
f"(в единицах, установленных ПП-127).")
if cat:
text += (f" Согласно Постановлению № 127 полученное значение соответствует "
f"{cat}-й категории значимости, по данному показателю объекту присваивается "
f"{cat}-я категория значимости.")
else:
text += (" Значение не достигает порога III категории — по данному показателю "
"категория значимости не присваивается.")
return text, cat
# Пороги количественных показателей 1–7, 11–14 (ПП-127), кроме 8/9/10:
# num → [(категория, lo, lo_incl, hi, hi_incl)] — значение соответствует, если
# (lo is None или value > lo при not lo_incl / value >= lo при lo_incl) и
# (hi is None или value < hi при not hi_incl / value <= hi при hi_incl).
CRITERION_QUANT_THRESHOLDS = {
'1': [('III', 1, True, 50, True), ('II', 50, False, 500, True), ('I', 500, False, None, False)], # человек
'2б': [('III', 2, True, 1000, False), ('II', 1000, True, 5000, False), ('I', 5000, True, None, False)], # тыс. чел.
'3б': [('III', 2, True, 1000, False), ('II', 1000, True, 5000, False), ('I', 5000, True, None, False)], # тыс. чел.
'4а': [('III', 3, True, 1000, False), ('II', 1000, True, 5000, False), ('I', 5000, True, None, False)], # тыс. абонентов
'5а': [('III', 12, False, 24, True), ('II', 6, False, 12, True), ('I', None, False, 6, True)], # часов
'5б': [('III', None, False, 30, True), ('II', 30, False, 70, True), ('I', 70, False, None, False)], # %
'11б': [('III', 2, True, 1000, False), ('II', 1000, True, 5000, False), ('I', 5000, True, None, False)], # тыс. чел.
'13а': [('III', 0, False, 10, True), ('II', 10, False, 15, True), ('I', 15, False, None, False)], # %
'13б': [('III', 0, False, 10, True), ('II', 10, False, 40, True), ('I', 40, False, None, False)], # %
'14': [('III', 2, False, 4, True), ('II', 1, False, 2, True), ('I', None, False, 1, True)], # часов
}
# Качественные показатели (выбор одного из вариантов III/II/I): 2а, 3а, 3в, 3г, 4б, 6, 7, 11а, 12, 13.1
CRITERION_CHOICE_INDICATORS = {'2а', '3а', '3в', '3г', '4б', '6', '7', '11а', '12', '13.1'}
# Единицы измерения количественных показателей (для подписи в окне авторасчёта)
CRITERION_QUANT_UNITS = {
'1': 'человек', '2б': 'тыс. человек', '3б': 'тыс. человек', '4а': 'тыс. абонентов',
'5а': 'часов', '5б': '%', '11б': 'тыс. человек', '13а': '%', '13б': '%', '14': 'часов',
}
def _criterion_other_cat(num, value):
"""Категория количественного показателя (кроме 8/9/10) по значению (ПП-127)."""
rules = CRITERION_QUANT_THRESHOLDS.get(str(num))
if not rules or value is None:
return None
for cat, lo, lo_incl, hi, hi_incl in rules:
ok = True
if lo is not None:
ok = ok and ((value >= lo) if lo_incl else (value > lo))
if hi is not None:
ok = ok and ((value <= hi) if hi_incl else (value < hi))
if ok:
return cat
return None
def _calc_criterion_other(company, isys, num, value=None, choice=None):
"""Расчёт остальных показателей критериев значимости (1–7, 11–14).
Количественные (1, 2б, 3б, 4а, 5а, 5б, 11б, 13а, 13б, 14): value — число,
категория по порогам ПП-127 (CRITERION_QUANT_THRESHOLDS).
Качественные (2а, 3а, 3в, 3г, 4б, 6, 7, 11а, 12, 13.1): choice — текст
выбранного варианта из criteria_127.json (values), категория — его ключ.
Возвращает (текст_обоснования, категория или None).
"""
num = str(num)
if num in CRITERION_CHOICE_INDICATORS:
# Качественный показатель: выбираем один из вариантов III/II/I
criteria = _load_json('criteria_127.json')
for grp in (criteria if isinstance(criteria, list) else []):
for ind in grp.get('indicators', []):
if str(ind.get('num', '')) == num:
vals = ind.get('values', {}) or {}
if not choice:
return (f"Показатель применим. Для расчёта выберите один из вариантов "
f"(III/II/I категория) в окне авторасчёта.", None)
cat = None
for c, txt in vals.items():
if txt and str(txt).strip() == str(choice).strip():
cat = c
break
if not cat:
return (f"Показатель применим. Выбранный вариант не найден в перечне значений.", None)
text = (f"Показатель {num}: {choice}. Согласно Постановлению № 127 данному значению "
f"соответствует {cat}-я категория значимости.")
return text, cat
return (f"Показатель применим. Варианты значений не найдены в справочнике.", None)
# Количественный показатель
if value is None:
unit = CRITERION_QUANT_UNITS.get(num, '')
return (f"Показатель применим. Для расчёта введите значение показателя"
+ (f" ({unit})" if unit else "") + ".", None)
cat = _criterion_other_cat(num, value)
unit = CRITERION_QUANT_UNITS.get(num, '')
text = (f"Показатель {num}: значение показателя — {value:,.6f} {unit}. "
f"Согласно Постановлению № 127 ")
if cat:
text += (f"данному значению соответствует {cat}-я категория значимости.")
else:
text += ("значение не достигает порога III категории — категория значимости не присваивается.")
return text, cat
def _build_org_measures_text(company, isys=None):
"""Формирует текст организационных мер из словаря org_measures.json
с учётом ответов раздела «Физическая безопасность» опросника.
Если в опроснике указано «нет» (охрана, сигнализация, тревожная кнопка,
видеонаблюдение, металлические двери и т.п.) — соответствующие пункты
из текста убираются. Пункты, не привязанные к средствам, остаются всегда.
"""
import os as _os
items = _load_json('org_measures.json')
if not isinstance(items, list):
items = []
ps = getattr(company, 'physical_security', None) or {}
def _no(*keys):
"""True, если хотя бы по одному ключу явно указано «нет»/«не имеется»."""
for k in keys:
v = str(ps.get(k) or '').strip().lower()
if v in ('нет', 'не имеется', 'нет,', '0', 'false'):
return True
return False
rules = [
# (подстрока пункта, ключи physical_security, при которых пункт УБИРАЕМ)
('Физическая охрана периметра', ('physicalGuard', 'physicalTurnstile', 'physicalVisitorLog')),
('Имеется система видеонаблюдения', ('physicalVideoSurveillance',)),
('Здания не имеют окон', ('physicalMetalDoors',)),
('охранно-пожарная сигнализация и тревожная кнопка', ('physicalFireAlarm', 'physicalPanicButton')),
('охранно-пожарная сигнализация', ('physicalFireAlarm',)),
('тревожная кнопка', ('physicalPanicButton',)),
('турникетом на проходной', ('physicalTurnstile',)),
('СКУД', ('physicalTurnstile',)),
('учет посетителей', ('physicalVisitorLog',)),
]
out = []
for line in items:
line = str(line).strip()
if not line:
continue
drop = False
for sub, keys in rules:
if sub in line and _no(*keys):
drop = True
break
if not drop:
out.append(line)
return '\n'.join(out) if out else '—'
def _build_categorization_indicators(company, is_list):
"""Строки таблицы показателей критериев значимости для шаблона
«Сведения о результатах категорирования» (цикл categorization_indicators).
Для каждого показателя ПП-127 (5 групп, показатели 1–14 с подпунктами):
section, name, number, value, justification, category.
- Если показатель не применим для сферы объекта — value = «Показатель не применим.»,
justification — текст из словаря обоснований по сфере (или заготовка).
- Если применим (8/9) — автоматический расчёт из данных организации.
"""
import re as _re
criteria = _load_json('criteria_127.json')
applicability = _load_json('criteria_applicability.json')
justifications = _load_json('criteria_justifications.json')
if not isinstance(criteria, list):
criteria = []
sphere = getattr(is_list[0], 'sphere', '') if is_list else ''
sphere = (sphere or '').strip()
appl = applicability.get(sphere) if isinstance(applicability, dict) else None
just = (justifications.get(sphere) or {}) if isinstance(justifications, dict) else {}
isys = is_list[0] if is_list else None
rows = []
first_cat_num = None # номер показателя, по которому объект отнесён к I категории (п. 6 ПП-127)
# Введённые значения показателей (из опросника/окна авторасчёта): {num: значение}
crit_in = dict(getattr(isys, 'criterion_input', None) or {}) if isys else {}
for grp in criteria:
section = grp.get('criteria', '')
for ind in grp.get('indicators', []):
num = str(ind.get('num', ''))
name = ind.get('name', '')
m = _re.match(r'(\d+)', num)
grp_num = m.group(1) if m else num
applicable = appl is None or grp_num in appl
value = '—'
justification = '—'
category = '—'
if first_cat_num is not None:
# Остановка расчёта: объект уже отнесён к I категории по одному из показателей
# (п. 6 Правил, утв. ПП РФ № 127: «расчет по остальным показателям не проводится»).
if applicable:
value = '—'
justification = (f"Расчёт по данному показателю не проводится: объект отнесён к "
f"I категории значимости по показателю {first_cat_num} "
f"(п. 6 Правил категорирования, утв. ПП РФ № 127).")
category = '—'
else:
value = 'Показатель не применим.'
justification = (just.get(num) or just.get(grp_num)
or f"Показатель не применим к объекту КИИ организации сферы «{sphere or '—'}».")
elif not applicable:
value = 'Показатель не применим.'
justification = (just.get(num) or just.get(grp_num)
or f"Показатель не применим к объекту КИИ организации сферы «{sphere or '—'}».")
elif grp_num in ('8', '9'):
if grp_num == '8' and sphere == 'Оборонная промышленность':
# ОПК: U_8 = (C+A+X)/R_год×100% (ПП РФ № 796, п. 1820)
text, cat = _calc_criterion_8_opk(company, isys)
else:
text, cat = _calc_criterion_8_9(company, isys, grp_num)
if text:
value = text
if cat:
category = f"{cat}-я категория"
else:
category = '—'
elif grp_num == '10':
text, cat = _calc_criterion_10(company, isys, num)
if text:
value = text
if cat:
category = f"{cat}-я категория"
else:
category = '—'
elif num in ('13а', '13б') and sphere == 'Оборонная промышленность':
# ОПК: V = (1(V_maxV_инц)/V_ГОЗ)×100%, T = (1(t_норм/t_ненорм))×100%
# (ПП РФ № 796, п. 34, 38) — при наличии введённых составляющих
_opk_crit = dict(getattr(isys, 'criterion_input', None) or {}) if isys else {}
_opk_ready = bool(_opk_crit.get('13a_vmax', '') or _opk_crit.get('13a_vinc', '') or _opk_crit.get('13a_vgoz', '') or _opk_crit.get('13b_tnorm', '') or _opk_crit.get('13b_tnonorm', ''))
if _opk_ready:
text, cat = _calc_criterion_13_opk(company, isys, num)
if text:
value = text
if cat:
category = f"{cat}-я категория"
else:
category = '—'
else:
value = '—'
justification = (just.get(num) or just.get(grp_num)
or 'Показатель применим к объекту КИИ; значение определяется по данным объекта (для ОПК — по формулам ПП РФ № 796: V = (1(V_maxV_инц)/V_ГОЗ)×100%, T = (1(t_норм/t_ненорм))×100%).')
else:
# Применимый показатель без формульного авторасчёта (1–7, 11–14):
# если значение введено (опросник/окно авторасчёта) — считаем категорию по порогам
if num in crit_in and str(crit_in.get(num, '')).strip():
raw = str(crit_in.get(num, '')).strip()
# Число или выбранный вариант (качественный)
try:
_v = float(raw.replace(' ', '').replace(',', '.').replace('\u00a0', ''))
text, cat = _calc_criterion_other(company, isys, num, value=_v)
except ValueError:
text, cat = _calc_criterion_other(company, isys, num, choice=raw)
if text:
value = text
if cat:
category = f"{cat}-я категория"
else:
category = '—'
else:
value = '—'
justification = (just.get(num) or just.get(grp_num)
or 'Показатель применим к объекту КИИ; значение определяется по данным объекта.')
# Если по показателю присвоена I категория — фиксируем для остановки расчёта
# (точное совпадение «I-я категория», а не startswith('I'), иначе ловится II/III)
if first_cat_num is None and category.startswith('I-я'):
first_cat_num = num
rows.append({
'section': section,
'name': name,
'number': num,
'value': value,
'justification': justification,
'category': category,
})
return rows
# ============================================================
# РЕЗОЛВЕР ЗНАЧЕНИЙ
# ============================================================
def _find_fstek_office(address_text: str):
"""Определяет управление ФСТЭК России по адресу организации.
Ищет город/регион из адреса в словаре fstek_offices.json.
Возвращает dict {'name': ..., 'address': ...} или None.
"""
import json as _json
import os as _os
if not address_text:
return None
dict_path = _os.path.join(_os.path.dirname(__file__), 'dictionaries', 'fstek_offices.json')
try:
with open(dict_path, encoding='utf-8') as _f:
offices = _json.load(_f)
except Exception:
return None
hay = address_text.lower()
# Нормализация сокращений: обл.→область, респ.→республика, г.о.→городской округ
hay = hay.replace('обл.', 'область').replace('обл,', 'область')
hay = hay.replace('респ.', 'республика').replace('респ,', 'республика')
hay = hay.replace('г.о.', 'городской округ').replace('г.о,', 'городской округ')
hay = hay.replace('ао', 'автономный округ')
best = None
best_len = 0
for off in offices:
# Ищем по городам
for city in off.get('cities', []):
if city in hay and len(city) > best_len:
best = off
best_len = len(city)
# Ищем по регионам (область/край/республика)
for region in off.get('regions', []):
if region in hay and len(region) > best_len:
best = off
best_len = len(region)
return best
def _resolve(source: str, ctx: Dict) -> str:
"""Преобразует ключ-источник в значение, подставляя из контекста."""
company = ctx.get('company')
is_list = ctx.get('is_list', [])
commission_data = ctx.get('commission_data', {})
doc_number = ctx.get('doc_number', '')
doc_date = ctx.get('doc_date', '')
# Прямые поля Company
if source.startswith('company:'):
field = source[8:]
return str(getattr(company, field, '') or '')
# Склонение названия
if source.startswith('decl:'):
_, field, case = source.split(':')
name = getattr(company, field, '') or ''
case_gr = dc._normalize_case(case)
# Сначала берём ГОТОВОЕ склонение из модели (пользователь мог
# подставить через Морфер/ИИ или ввести вручную), пересчитываем
# только если поле пустое
case_idx = {'nomn': 1, 'gent': 2, 'datv': 3, 'accs': 4, 'ablt': 5, 'loct': 6}.get(case_gr)
if case_idx:
if field == 'full_name':
ready = getattr(company, f'company_name_{case_idx}', '') or ''
if ready:
return ready
elif field == 'short_name':
ready = getattr(company, f'short_name_{case_idx}', '') or ''
if ready:
return ready
return dc.company_name_decline(name, case_gr) if name else '—'
# Форматы ФИО
if source.startswith('short_fio:'):
who = source.split(':', 1)[1]
fio = _get_fio(company, who)
return dc.get_short_fio(fio) if fio else '—'
if source.startswith('initials:'):
who = source.split(':', 1)[1]
fio = _get_fio(company, who)
return dc.get_initials(fio) if fio else '—'
# Комиссия
if source in commission_data:
return str(commission_data[source])
# Управление ФСТЭК (адресат отправки сведений о категорировании)
if source in ('fstek_recipient_address', 'fstek_district', 'fstek_office_name'):
addr = getattr(company, 'address', '') or ''
off = _find_fstek_office(addr)
if off:
if source == 'fstek_recipient_address':
return off.get('address', '') or ''
return off.get('name', '') or ''
return ''
# Даты
if source.startswith('date:'):
fmt = source.split(':', 1)[1]
now = datetime.now()
if fmt == 'year':
return str(now.year)
return now.strftime('%d.%m.%Y')
if source == 'doc_date':
return doc_date or datetime.now().strftime('%d.%m.%Y')
if source == 'doc_number':
num = str(doc_number).strip()
return re.sub(r'^[Nn]?[oо]?[№#]\s*', '', num) or '—'
# Требования защищённости
if source == 'security_reqs':
return _get_security_text(ctx)
# IS-алиасы (первая ИС)
if source.startswith('is:'):
field = source[3:]
if is_list:
first = is_list[0]
field_map = {
'name': 'name',
'description': 'description',
'category': 'category',
'users': lambda: ', '.join(first.users_list or []),
}
if field in field_map:
val = field_map[field]
if callable(val):
return val() or '—'
return str(getattr(first, val, '') or '—') if isinstance(val, str) else '—'
return '—'
# Список ИС
if source == 'is_list':
if is_list:
return '\n'.join(f"{i}. {isys.name}" for i, isys in enumerate(is_list, 1))
return '—'
if source == 'is_list_comma':
if is_list:
return ', '.join(isys.name for isys in is_list)
return '—'
# Члены комиссии
if source.startswith('commission_member:'):
parts = source.split(':')
idx, field = int(parts[1]), parts[2]
members = getattr(company, 'commission', None) or []
if idx < len(members):
return str(getattr(members[idx], field, '') or '—')
return '—'
# Склонение ФИО по падежам
if source.startswith('fio_acl:'):
who = source.split(':', 1)[1]
fio = _get_fio(company, who)
return dc.decline(fio, 'accs') if fio else '—'
return '—'
def _get_fio(company: Company, who: str) -> str:
mapping = {
'chief': 'chief_fio',
'responsible': 'responsible_fio',
'admin': 'admin_fio',
}
field = mapping.get(who, who + '_fio')
return getattr(company, field, '') or ''
def _get_security_text(ctx: Dict) -> str:
"""Возвращает текст требований к защите объекта КИИ по категории значимости."""
is_list = ctx.get('is_list', [])
category = ''
if is_list:
category = str(getattr(is_list[0], 'category', '') or '').strip()
TEXTS = {
'1': """Для значимого объекта КИИ 1-й категории значимости необходимо:
— создание системы безопасности значимого объекта КИИ;
— обеспечение защиты информации от компьютерных атак;
— категорирование объектов КИИ и контроль его результатов.""",
'2': """Для значимого объекта КИИ 2-й категории значимости необходимо:
— создание системы безопасности значимого объекта КИИ;
— обеспечение защиты информации от компьютерных атак.""",
'3': """Для значимого объекта КИИ 3-й категории значимости необходимо:
— обеспечение защиты информации от компьютерных атак.""",
}
if category in TEXTS:
return TEXTS[category]
return """Для объекта КИИ, не являющегося значимым, необходимо:
— принятие мер по защите информации, предусмотренных законодательством РФ о безопасности КИИ."""