- models: income_year_1..5 (доход за 5 лет назад ... за прошлый год),
income_loss (суммарный экономический ущерб от нарушения критических
процессов) — понятные названия без ΔДоходов
- вкладка «Организация» → «Финансовые показатели»: 6 новых полей
- авторасчёт п. 8: берёт доходы за 5 лет и ущерб из полей,
считает №8 = ущерб / средний доход за 5 лет × 100% (проверено
на примере методички: 33% → I категория)
- импорт из опросника: incomeYear1..5 / incomeLoss
- переменные шаблонов: {{company_income_year_1..5}},
{{company_income_loss}}
574 lines
29 KiB
Python
574 lines
29 KiB
Python
# -*- 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_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 '—',
|
||
'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
|
||
|
||
return loops
|
||
|
||
|
||
# ============================================================
|
||
# РЕЗОЛВЕР ЗНАЧЕНИЙ
|
||
# ============================================================
|
||
|
||
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 """Для объекта КИИ, не являющегося значимым, необходимо:
|
||
— принятие мер по защите информации, предусмотренных законодательством РФ о безопасности КИИ."""
|
||
|