From a7878804e48379fd2d186d255f3a4015ec73b842 Mon Sep 17 00:00:00 2001 From: LocoAgent Date: Wed, 5 Aug 2026 16:57:49 +0400 Subject: [PATCH] =?UTF-8?q?=D0=9E=D1=82=D1=80=D0=B0=D1=81=D0=BB=D0=B5?= =?UTF-8?q?=D0=B2=D1=8B=D0=B5=20=D0=9F=D0=9F:=20=D0=BF=D1=80=D0=B8=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=BC=D0=BE=D1=81=D1=82=D1=8C=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=BA=D0=B0=D0=B7=D0=B0=D1=82=D0=B5=D0=BB=D0=B5=D0=B9=20+=20?= =?UTF-8?q?=D1=84=D0=BE=D1=80=D0=BC=D1=83=D0=BB=D1=8B=20=D0=9E=D0=9F=D0=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../dictionaries/criteria_applicability.json | 31 +++- dokogen/ui.py | 170 +++++++++++++++++- dokogen/variables.py | 130 +++++++++++++- 3 files changed, 319 insertions(+), 12 deletions(-) diff --git a/dokogen/dictionaries/criteria_applicability.json b/dokogen/dictionaries/criteria_applicability.json index 461ef2f..70af0c5 100644 --- a/dokogen/dictionaries/criteria_applicability.json +++ b/dokogen/dictionaries/criteria_applicability.json @@ -16,20 +16,27 @@ ], "Наука": [ "1", - "11", - "13", "7", "8", - "9" + "9", + "11", + "13" ], "Транспорт": [ "1", "2", "3", - "11" + "8", + "9", + "11", + "13", + "14" ], "Связь": [ - "4" + "4", + "6", + "8", + "9" ], "Энергетика": [ "1", @@ -85,20 +92,26 @@ "1", "2", "3", - "4", "5", + "6", "7", "8", "9", - "10", "11", "12", - "13", - "14" + "13" ], "Оборонная промышленность": [ + "1", + "2", + "3", + "4", + "6", + "7", "8", "9", + "11", + "12", "13" ], "Ракетно-космическая промышленность": [ diff --git a/dokogen/ui.py b/dokogen/ui.py index efdc732..5c8302c 100644 --- a/dokogen/ui.py +++ b/dokogen/ui.py @@ -2207,6 +2207,13 @@ class DokoGenApp: from .variables import (CRITERION_QUANT_THRESHOLDS, CRITERION_CHOICE_INDICATORS, CRITERION_QUANT_UNITS) + # Сфера ОПК — для показателей 8/13а/13б используются формулы ПП РФ № 796 + _sphere_opk = False + try: + _sphere_opk = (self.is_sphere_combo.get().strip() == 'Оборонная промышленность') + except Exception: + _sphere_opk = False + def _num(s): # «121 118 270 руб», «1 000», «906 779 руб», «2 212.5» → число s = str(s or '').strip() @@ -2247,7 +2254,54 @@ class DokoGenApp: canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) - if (num in CRITERION_QUANT_THRESHOLDS or num in CRITERION_CHOICE_INDICATORS): + if num in ('13а', '13б') and _sphere_opk: + # ОПК: 13а — V = (1−(V_max−V_инц)/V_ГОЗ)×100%, 13б — T = (1−(t_норм/t_ненорм))×100% + # (ПП РФ № 796 от 27.06.2026, п. 34, 38) + ttk.Label(frm, text=f"Показатель {num} — по отраслевым особенностям ОПК (ПП РФ № 796)", + font=('Arial', 10, 'bold')).pack(anchor=tk.W) + if num == '13а': + ttk.Label(frm, text="Формула: V = (1 − (V_max − V_инц) / V_ГОЗ) × 100%, где\n" + "V_max — максимальный объём продукции ГОЗ при максимальной скорости выпуска;\n" + "V_инц — объём, поставленный во время устранения компьютерного инцидента;\n" + "V_ГОЗ — полный объём продукции по ГОЗ.", + 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 + + _ci13a = dict(getattr(isys, 'criterion_input', None) or {}) if isys else {} + e_vmax = _row("V_max — максимальный объём продукции ГОЗ (ед.):", 'vmax', _ci13a.get('13a_vmax', '')) + e_vinc = _row("V_инц — объём продукции, поставленный во время устранения инцидента (ед.):", 'vinc', _ci13a.get('13a_vinc', '')) + e_vgoz = _row("V_ГОЗ — полный объём продукции по ГОЗ (ед.):", 'vgoz', _ci13a.get('13a_vgoz', '')) + else: + ttk.Label(frm, text="Формула: T = (1 − (t_норм / t_ненорм)) × 100%, где\n" + "t_норм — сумма времени операций по производству единицы продукции ГОЗ\n" + "при штатном функционировании объекта КИИ;\n" + "t_ненорм — сумма времени тех же операций при ненормальном функционировании.", + 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 + + _ci13b = dict(getattr(isys, 'criterion_input', None) or {}) if isys else {} + e_tnorm = _row("t_норм — время операций при штатном функционировании (час):", 'tnorm', _ci13b.get('13b_tnorm', '')) + e_tnon = _row("t_ненорм — время операций при ненормальном функционировании (час):", 'tnon', _ci13b.get('13b_tnonorm', '')) + 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 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, @@ -2384,6 +2438,60 @@ class DokoGenApp: 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 == '8' and _sphere_opk: + # ОПК: U_8 = U_у8 / R_год × 100%, U_у8 = C + A + X (ПП РФ № 796, п. 18–20) + ttk.Label(frm, text="Показатель 8 — ущерб субъекту КИИ (отраслевые особенности ОПК, ПП РФ № 796)", + font=('Arial', 10, 'bold')).pack(anchor=tk.W) + ttk.Label(frm, text="Формула: U_8 = U_у8 / R_год × 100%, где U_у8 = C + A + X\n" + "C — суммарные затраты на объект КИИ;\n" + "A — затраты на возмещение ущерба потерпевшим (если ущерба нет, A = 0);\n" + "X — ущерб от ранее произошедших компьютерных инцидентов (если не было, X = 0);\n" + "R_год — усреднённый объём годового дохода за прошедший 5-летний период.", + 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 + + _ci8 = dict(getattr(isys, 'criterion_input', None) or {}) if isys else {} + _bv8 = getattr(isys, 'balance_value', '') or '' + _bvn8 = _num(_bv8) + _c_def = _num(_ci8.get('8_c', '') or '') or _bvn8 + e_c = _row("C — суммарные затраты на объект КИИ (руб.):", 'c', + f"{_c_def:,.0f}" if _c_def else '') + if _bvn8 and not _num(_ci8.get('8_c', '') or ''): + ttk.Label(frm, text="⚠️ C — подставлена балансовая стоимость объекта КИИ " + f"({_bvn8:,.0f} руб.); можно поправить.", + foreground='#8a6d00', wraplength=900, justify=tk.LEFT).pack(anchor=tk.W) + e_a = _row("A — затраты на возмещение ущерба потерпевшим (руб., по умолчанию 0):", 'a', + _ci8.get('8_a', '')) + e_x = _row("X — ущерб от ранее произошедших компьютерных инцидентов (руб., по умолчанию 0):", 'x', + _ci8.get('8_x', '')) + _incs_raw8 = [getattr(self.model, f'income_year_{i}', '') or '' for i in range(1, 6)] + _incs8 = [_num(x) for x in _incs_raw8] + _incs8_f = [x for x in _incs8 if x is not None] + _inc58 = _num(getattr(self.model, 'income_5y', '') or '') + if _inc58: + _d_avg8 = _inc58 / 5.0 + elif _incs8_f: + _d_avg8 = sum(_incs8_f) / len(_incs8_f) + else: + _d_avg8 = None + e_r = _row("R_год — усреднённый годовой доход за 5 лет (руб.):", 'r', + f"{_d_avg8:,.0f}" if _d_avg8 else '') + ttk.Label(frm, text="⚠️ R_год — среднее по заполненным годам; ПП-127 требует усреднение " + "за прошедший пятилетний период.", + foreground='#8a6d00', wraplength=900, justify=tk.LEFT).pack(anchor=tk.W) + result_frame = ttk.Frame(frm) + result_frame.pack(fill=tk.BOTH, expand=True, pady=(8, 0)) + result = tk.Text(result_frame, height=12, 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) else: ttk.Label(frm, text="Показатель 8 — ущерб субъекту КИИ", font=('Arial', 10, 'bold')).pack(anchor=tk.W) @@ -2458,7 +2566,30 @@ class DokoGenApp: def _calc(): lines = [] - if (num in CRITERION_QUANT_THRESHOLDS or num in CRITERION_CHOICE_INDICATORS): + if num in ('13а', '13б') and _sphere_opk: + from .variables import _calc_criterion_13_opk + # Сохраняем введённые составляющие формулы в criterion_input (для шаблона) + if isys: + _ci = dict(getattr(isys, 'criterion_input', None) or {}) + if num == '13а': + _ci['13a_vmax'] = e_vmax.get().strip() + _ci['13a_vinc'] = e_vinc.get().strip() + _ci['13a_vgoz'] = e_vgoz.get().strip() + else: + _ci['13b_tnorm'] = e_tnorm.get().strip() + _ci['13b_tnonorm'] = e_tnon.get().strip() + isys.criterion_input = _ci + text, cat = _calc_criterion_13_opk(self.model, isys, num) + if text.startswith('Заполните'): + result.delete('1.0', tk.END) + result.insert('1.0', text) + return + lines.append(text) + if cat: + lines.append(f"Категория значимости: {cat} (по порогам ПП-127)") + else: + lines.append("Категория значимости: — (значение ниже порога III)") + elif (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 @@ -2508,6 +2639,41 @@ class DokoGenApp: lines.append(f"Категория значимости: {cat} (по порогам ПП-127)") else: lines.append("Категория значимости: — (значение ниже порога III)") + elif num == '8' and _sphere_opk: + c = _num(e_c.get()) + a = _num(e_a.get()) or 0 + x = _num(e_x.get()) or 0 + r = _num(e_r.get()) + if c is None or r is None or r <= 0: + result.delete('1.0', tk.END) + result.insert('1.0', "Заполните C (суммарные затраты на объект) и R_год (доходы за 5 лет).") + return + if isys: + _ci = dict(getattr(isys, 'criterion_input', None) or {}) + _ci['8_c'] = str(e_c.get().strip()) + _ci['8_a'] = str(e_a.get().strip()) + _ci['8_x'] = str(e_x.get().strip()) + isys.criterion_input = _ci + u_y8 = c + a + x + val = u_y8 / r * 100 + lines.append(f"Суммарные затраты на объект КИИ: C = {c:,.0f} руб.") + lines.append(f"Затраты на возмещение ущерба потерпевшим: A = {a:,.0f} руб.") + lines.append(f"Ущерб от ранее произошедших инцидентов: X = {x:,.0f} руб.") + lines.append(f"U_у8 = C + A + X = {c:,.0f} + {a:,.0f} + {x:,.0f} = {u_y8:,.0f} руб.") + lines.append(f"Усреднённый годовой доход за 5 лет: R_год = {r:,.0f} руб.") + lines.append(f"U_8 = U_у8 / R_год × 100% = {u_y8:,.0f} / {r:,.0f} × 100 = {val:.2f} %") + if val > 20: + cat = 'I' + elif val > 10: + cat = 'II' + elif val >= 1: + cat = 'III' + else: + cat = None + if cat: + lines.append(f"Категория значимости: {cat} (более 20% → I; 10-20% → II; 1-10% → III)") + else: + lines.append("Значение менее 1% — ни одна категория не присваивается («—»).") elif num == '8': d = _num(e_dincome.get()) b = _num(e_budget.get()) diff --git a/dokogen/variables.py b/dokogen/variables.py index 6940962..e04c36c 100644 --- a/dokogen/variables.py +++ b/dokogen/variables.py @@ -523,6 +523,113 @@ def _calc_criterion_8_9(company, isys, num): return text, cat +def _calc_criterion_8_opk(company, isys): + """Показатель 8 (ущерб субъекту КИИ) по отраслевым особенностям ОПК + (ПП РФ № 796 от 27.06.2026, п. 18–20): + + 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: (категория → (нижняя граница, верхняя граница)), границы «включительно» @@ -800,7 +907,11 @@ def _build_categorization_indicators(company, is_list): justification = (just.get(num) or just.get(grp_num) or f"Показатель не применим к объекту КИИ организации сферы «{sphere or '—'}».") elif grp_num in ('8', '9'): - text, cat = _calc_criterion_8_9(company, isys, grp_num) + if grp_num == '8' and sphere == 'Оборонная промышленность': + # ОПК: U_8 = (C+A+X)/R_год×100% (ПП РФ № 796, п. 18–20) + 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: @@ -815,6 +926,23 @@ def _build_categorization_indicators(company, is_list): category = f"{cat}-я категория" else: category = '—' + elif num in ('13а', '13б') and sphere == 'Оборонная промышленность': + # ОПК: V = (1−(V_max−V_инц)/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_max−V_инц)/V_ГОЗ)×100%, T = (1−(t_норм/t_ненорм))×100%).') else: # Применимый показатель без формульного авторасчёта (1–7, 11–14): # если значение введено (опросник/окно авторасчёта) — считаем категорию по порогам