diff --git a/dokogen/ui.py b/dokogen/ui.py index 1b14c10..0a55f7f 100644 --- a/dokogen/ui.py +++ b/dokogen/ui.py @@ -2204,6 +2204,8 @@ class DokoGenApp: критериев экономической значимости» ФСТЭК (2021). Показывает расчёт и предлагает категорию как рекомендацию.""" import re as _re2 + from .variables import (CRITERION_QUANT_THRESHOLDS, CRITERION_CHOICE_INDICATORS, + CRITERION_QUANT_UNITS) def _num(s): # «121 118 270 руб», «1 000», «906 779 руб», «2 212.5» → число @@ -2245,7 +2247,51 @@ class DokoGenApp: canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) - if num == '10' or str(num).startswith('10.'): + if (num in CRITERION_QUANT_THRESHOLDS or num in CRITERION_CHOICE_INDICATORS): + # Остальные показатели (1–7, 11–14): количественные — ввод значения, + # качественные — выбор варианта (III/II/I) из справочника ПП-127 + from .variables import (CRITERION_QUANT_THRESHOLDS, CRITERION_CHOICE_INDICATORS, + CRITERION_QUANT_UNITS) + is_choice = num in CRITERION_CHOICE_INDICATORS + ttk.Label(frm, text=f"Показатель {num} — по порогам ПП-127", + font=('Arial', 10, 'bold')).pack(anchor=tk.W) + if is_choice: + ttk.Label(frm, text="Выберите один из вариантов значений (соответствует III/II/I категории):", + foreground='gray', wraplength=760, justify=tk.LEFT).pack(anchor=tk.W, pady=(2, 6)) + else: + _unit = CRITERION_QUANT_UNITS.get(num, '') + ttk.Label(frm, text=f"Введите значение показателя ({_unit}): категория определяется " + "по пороговым значениям ПП-127.", + foreground='gray', wraplength=760, justify=tk.LEFT).pack(anchor=tk.W, pady=(2, 6)) + + def _row(label, key, default=''): + ttk.Label(frm, text=label, wraplength=900, justify=tk.LEFT).pack(anchor=tk.W, pady=(4, 0)) + e = ttk.Entry(frm, width=80) + e.insert(0, str(default or '')) + e.pack(fill=tk.X) + return e + + if is_choice: + # Варианты значений для качественного показателя (из criteria_127.json) + _opts = [] + for _g in (criteria if isinstance(criteria, list) else []): + for _ind in _g.get('indicators', []): + if str(_ind.get('num', '')) == num: + _vals = _ind.get('values', {}) or {} + _opts = [str(v) for v in _vals.values() if str(v).strip()] + break + e_choice = ttk.Combobox(frm, width=90, state='readonly', values=_opts) + e_choice.pack(fill=tk.X, pady=4) + else: + e_value = _row(f"Значение показателя ({_unit}):", 'value', '') + result_frame = ttk.Frame(frm) + result_frame.pack(fill=tk.BOTH, expand=True, pady=(8, 0)) + result = tk.Text(result_frame, height=10, wrap=tk.WORD, state=tk.NORMAL) + result_scroll = ttk.Scrollbar(result_frame, orient=tk.VERTICAL, command=result.yview) + result.configure(yscrollcommand=result_scroll.set) + result.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + result_scroll.pack(side=tk.RIGHT, fill=tk.Y) + elif num == '10' or str(num).startswith('10.'): # Показатель 10 — прекращение финансовых операций (банковская сфера) is_base = str(num) == '10' if is_base: @@ -2412,7 +2458,30 @@ class DokoGenApp: def _calc(): lines = [] - if num == '10' or str(num).startswith('10.'): + if (num in CRITERION_QUANT_THRESHOLDS or num in CRITERION_CHOICE_INDICATORS): + from .variables import (CRITERION_QUANT_THRESHOLDS, CRITERION_CHOICE_INDICATORS, + CRITERION_QUANT_UNITS, _calc_criterion_other) + is_choice = num in CRITERION_CHOICE_INDICATORS + if is_choice: + choice = e_choice.get().strip() + if not choice: + result.delete('1.0', tk.END) + result.insert('1.0', "Выберите вариант значения показателя.") + return + text, cat = _calc_criterion_other(self.model, isys, num, choice=choice) + else: + val = _num(e_value.get()) + if val is None: + result.delete('1.0', tk.END) + result.insert('1.0', "Введите числовое значение показателя.") + return + text, cat = _calc_criterion_other(self.model, isys, num, value=val) + lines.append(text) + if cat: + lines.append(f"Категория значимости: {cat} (по порогам ПП-127)") + else: + lines.append("Категория значимости: — (значение ниже порога III)") + elif num == '10' or str(num).startswith('10.'): from .variables import _criterion_10_cat if str(num) == '10': ops = _num(e_ops.get()) @@ -2591,9 +2660,12 @@ class DokoGenApp: combo.pack(side=tk.RIGHT, padx=6) _combos[num] = combo - # Кнопка «🧮» — авторасчёт показателя (п. 8 «ущерб субъекту КИИ», - # п. 9 «ущерб бюджетам», п. 10 «прекращение финансовых операций») - if grp_num in ('8', '9', '10'): + # Кнопка «🧮» — авторасчёт показателя: + # 8/9/10 — экономические формулы; остальные (1–7, 11–14) — по порогам ПП-127 + from .variables import (CRITERION_QUANT_THRESHOLDS, CRITERION_CHOICE_INDICATORS) + if (grp_num in ('8', '9', '10') + or num in CRITERION_QUANT_THRESHOLDS + or num in CRITERION_CHOICE_INDICATORS): ttk.Button(row, text='🧮', width=3, command=lambda n=num, nm=name, v=var, c=combo: _open_calc_dialog(n, nm, v, c) diff --git a/dokogen/variables.py b/dokogen/variables.py index ff1fcac..2f9420e 100644 --- a/dokogen/variables.py +++ b/dokogen/variables.py @@ -601,6 +601,97 @@ def _calc_criterion_10(company, isys, num): 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 с учётом ответов раздела «Физическая безопасность» опросника.