diff --git a/dokogen/ui.py b/dokogen/ui.py index 1f1dfb3..bfd7930 100644 --- a/dokogen/ui.py +++ b/dokogen/ui.py @@ -2140,6 +2140,200 @@ class DokoGenApp: f"считает его применимым.", foreground='#1a6e1a', wraplength=920, justify=tk.LEFT).pack(anchor=tk.W, pady=(0, 8)) + def _open_calc_dialog(num, name, var, combo): + """Авторасчёт показателя 8 или 9 по «Рекомендациям по оценке показателей + критериев экономической значимости» ФСТЭК (2021). Показывает расчёт + и предлагает категорию как рекомендацию.""" + import re as _re2 + + def _num(s): + s = str(s or '').replace(' ', '').replace('\u00a0', '') + try: + return float(s) + except Exception: + return None + + # Данные организации (прибыль/налог) из модели + profit = getattr(self.model, 'profit', '') or '' + profit_tax = getattr(self.model, 'profit_tax', '') or '' + + w = tk.Toplevel(self.root) + w.title(f"🧮 Авторасчёт показателя {num} — {name[:60]}") + w.geometry("720x560") + w.transient(self.root) + frm = ttk.Frame(w, padding=10) + frm.pack(fill=tk.BOTH, expand=True) + + if num == '9': + ttk.Label(frm, text="Показатель 9 — ущерб бюджетам РФ (применим ко всем субъектам КИИ)", + font=('Arial', 10, 'bold')).pack(anchor=tk.W) + ttk.Label(frm, text="Формула: №9 = ΔБ / Бусредн × 100%, где ΔБ = ΔНП + ΔНДПИ + ΔДИВ + ΔВСО + ΔНС\n" + "ΔНП = ΔДоходов × (налог на прибыль / прибыль); если ΔДоходов > прибыли — весь налог.", + foreground='gray', wraplength=660, justify=tk.LEFT).pack(anchor=tk.W, pady=(2, 6)) + + def _row(label, key, default=''): + ttk.Label(frm, text=label).pack(anchor=tk.W, pady=(4, 0)) + e = ttk.Entry(frm, width=60) + e.insert(0, str(default or '')) + e.pack(fill=tk.X) + return e + + e_profit = _row("Прибыль субъекта КИИ за прошедший год (руб.):", 'profit', profit) + e_tax = _row("Налог на прибыль за прошедший год (руб.):", 'tax', profit_tax) + e_dincome = _row("Снижение уровня доходов ΔДоходов (руб., наихудший сценарий):", 'dincome', '') + e_budget = _row("Доход федерального бюджета, усреднённый за 3 года Бусредн (руб.):", 'budget', + '21228053357700') + ttk.Label(frm, text="Прочие составляющие ΔБ (если применимы, иначе 0):", + font=('', 9, 'bold')).pack(anchor=tk.W, pady=(8, 0)) + e_ndpi = _row("Снижение выплат от налога на добычу ПИ ΔНДПИ (руб.):", 'ndpi', '0') + e_div = _row("Снижение выплат от дивидендов ΔДИВ (руб.):", 'div', '0') + e_vso = _row("Снижение выплат сторонних организаций ΔВСО (руб.):", 'vso', '0') + e_ns = _row("Снижение сборов ΔНС (руб.):", 'ns', '0') + result = tk.Text(frm, height=10, wrap=tk.WORD, state=tk.NORMAL) + result.pack(fill=tk.BOTH, expand=True, pady=(8, 0)) + else: + ttk.Label(frm, text="Показатель 8 — ущерб субъекту КИИ", + font=('Arial', 10, 'bold')).pack(anchor=tk.W) + ttk.Label(frm, text="Применим только для: госкорпорации, ГУП, госкомпании, стратегического АО/предприятия " + "(Указ Президента № 1009).\nФормула: №8 = ΔДоходов / Дусредн × 100%.", + foreground='gray', wraplength=660, justify=tk.LEFT).pack(anchor=tk.W, pady=(2, 6)) + + def _row(label, key, default=''): + ttk.Label(frm, text=label).pack(anchor=tk.W, pady=(4, 0)) + e = ttk.Entry(frm, width=60) + e.insert(0, str(default or '')) + e.pack(fill=tk.X) + return e + + e_profit = e_tax = None + # Доходы за 5 лет — из данных организации (поля «Доходы за 5 лет») + _incs = [_num(getattr(self.model, f'income_year_{i}', '') or '') for i in range(1, 6)] + _incs = [x for x in _incs if x is not None] + _d_avg = (sum(_incs) / len(_incs)) if _incs else None + _d_loss = _num(getattr(self.model, 'income_loss', '') or '') + e_dincome = _row("Суммарный экономический ущерб от нарушения критических процессов (руб.):", + 'dincome', f"{_d_loss:,.0f}" if _d_loss else '') + e_budget = _row("Доходы субъекта КИИ, усреднённые за 5 лет (руб.):", + 'budget', f"{_d_avg:,.0f}" if _d_avg else '') + result = tk.Text(frm, height=10, wrap=tk.WORD, state=tk.NORMAL) + result.pack(fill=tk.BOTH, expand=True, pady=(8, 0)) + + def _calc(): + lines = [] + if num == '8': + d = _num(e_dincome.get()) + b = _num(e_budget.get()) + if d is None or b is None or b <= 0: + result.delete('1.0', tk.END) + result.insert('1.0', "Заполните оба поля (ΔДоходов и Дусредн).") + return + val = d / b * 100 + lines.append(f"Суммарный экономический ущерб от нарушения критических процессов: {d:,.0f} руб.") + lines.append(f"Доходы субъекта КИИ, усреднённые за 5 лет: {b:,.0f} руб.") + lines.append(f"№8 = ущерб / доходы за 5 лет × 100% = {d:,.0f} / {b:,.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% — ни одна категория не присваивается («—»).") + else: + p = _num(e_profit.get()) + t = _num(e_tax.get()) + d = _num(e_dincome.get()) + b = _num(e_budget.get()) + if d is None or b is None or b <= 0: + result.delete('1.0', tk.END) + result.insert('1.0', "Заполните ΔДоходов и Бусредн (прибыль/налог — из данных организации).") + return + if p and t is not None and p > 0: + knp = t / p + dnp = d * knp + if d > p: + dnp = t + lines.append(f"Коэффициент КНП = налог на прибыль / прибыль = {t:,.0f} / {p:,.0f} = {knp:.4f}") + lines.append(f"ΔНП = ΔДоходов × КНП = {d:,.0f} × {knp:.4f} = {dnp:,.1f} руб." + + (" (ΔДоходов > прибыли → весь налог)" if d > p else "")) + else: + dnp = 0 + lines.append("Прибыль/налог не заполнены — ΔНП принят равным 0 (расчёт неполный).") + ndpi = _num(e_ndpi.get()) or 0 + div = _num(e_div.get()) or 0 + vso = _num(e_vso.get()) or 0 + ns = _num(e_ns.get()) or 0 + db = dnp + ndpi + div + vso + ns + lines.append(f"ΔНДПИ = {ndpi:,.0f} руб., ΔДИВ = {div:,.0f} руб., ΔВСО = {vso:,.0f} руб., ΔНС = {ns:,.0f} руб.") + lines.append(f"ΔБ = ΔНП + ΔНДПИ + ΔДИВ + ΔВСО + ΔНС = {db:,.1f} руб.") + lines.append(f"Бусредн (доход федерального бюджета за 3 года): {b:,.0f} руб.") + val = db / b * 100 + lines.append(f"№9 = ΔБ / Бусредн × 100% = {db:,.1f} / {b:,.0f} × 100 = {val:.6f} %") + if val > 0.001: + cat = 'I' + elif val > 0.0006: + cat = 'II' + elif val > 0.0003: + cat = 'III' + else: + cat = None + if cat: + lines.append(f"Категория значимости: {cat} (более 0,001% → I; 0,0006-0,001% → II; 0,0003-0,0006% → III)") + else: + lines.append("Значение ≤ 0,0003% — ни одна категория не присваивается («—»).") + result.delete('1.0', tk.END) + result.insert('1.0', '\n'.join(lines)) + + def _apply(): + _calc() + txt = result.get('1.0', 'end-1c').strip() + lines = txt.split('\n') + cat_line = next((l for l in lines if l.startswith('Категория значимости:')), '') + cat = None + if 'Категория значимости: I' in cat_line: + cat = 'I' + elif 'Категория значимости: II' in cat_line: + cat = 'II' + elif 'Категория значимости: III' in cat_line: + cat = 'III' + if cat: + # Выбрать в комбобоксе соответствующую категорию + opts = list(combo['values']) + target = next((o for o in opts if o.startswith(f"{cat} категория")), None) + if target: + var.set(target) + # Обоснование с расчётом + prefix = f"Показатель {num}. {name} — {cat} категория." + full = prefix + "\n" + txt + cur = justify_txt.get('1.0', 'end-1c').strip() + if prefix not in cur: + if cur: + justify_txt.insert('end', '\n' + full) + else: + justify_txt.insert('1.0', full) + else: + var.set('—') + prefix = f"Показатель {num}. {name} — применим, категория не присвоена (—)." + full = prefix + "\n" + txt + cur = justify_txt.get('1.0', 'end-1c').strip() + if prefix not in cur: + if cur: + justify_txt.insert('end', '\n' + full) + else: + justify_txt.insert('1.0', full) + w.destroy() + + btns = ttk.Frame(frm) + btns.pack(fill=tk.X, pady=(8, 0)) + ttk.Button(btns, text="🧮 Рассчитать", command=_calc).pack(side=tk.LEFT, padx=2) + ttk.Button(btns, text="✔️ Применить категорию", command=_apply).pack(side=tk.LEFT, padx=2) + ttk.Button(btns, text="Отмена", command=w.destroy).pack(side=tk.LEFT, padx=2) + + rows = [] # (num, name, values{III:..,II:..,I:..}, var, applicable) _combos = {} # num -> комбобокс (для автозаполнения) @@ -2338,14 +2532,6 @@ class DokoGenApp: self.log(f"📋 Критерии значимости (ПП 127) сохранены: категория {cat}, показателей: {len(chosen)}") dialog.destroy() - btn_frame = ttk.Frame(body) - btn_frame.pack(fill=tk.X, pady=(8, 0)) - ttk.Button(btn_frame, text="⚡ Заполнить автоматически (по данным)", - command=_auto_fill_criteria).pack(side=tk.LEFT, padx=2) - ttk.Button(btn_frame, text="🧮 Рассчитать категорию", command=_calc).pack(side=tk.LEFT, padx=2) - ttk.Button(btn_frame, text="💾 Сохранить", command=_save).pack(side=tk.LEFT, padx=2) - ttk.Button(btn_frame, text="Отмена", command=dialog.destroy).pack(side=tk.LEFT, padx=2) - def _auto_fill_criteria(): """⚡ Автозаполнение показателей ПП 127 по данным организации/объекта (рекомендации). @@ -2457,198 +2643,14 @@ class DokoGenApp: if left else "\n\nВсе применимые показатели заполнены!") + "\n\nКатегория объекта пересчитана по наивысшей из выбранных.") - def _open_calc_dialog(num, name, var, combo): - """Авторасчёт показателя 8 или 9 по «Рекомендациям по оценке показателей - критериев экономической значимости» ФСТЭК (2021). Показывает расчёт - и предлагает категорию как рекомендацию.""" - import re as _re2 - def _num(s): - s = str(s or '').replace(' ', '').replace('\u00a0', '') - try: - return float(s) - except Exception: - return None - - # Данные организации (прибыль/налог) из модели - profit = getattr(self.model, 'profit', '') or '' - profit_tax = getattr(self.model, 'profit_tax', '') or '' - - w = tk.Toplevel(self.root) - w.title(f"🧮 Авторасчёт показателя {num} — {name[:60]}") - w.geometry("720x560") - w.transient(self.root) - frm = ttk.Frame(w, padding=10) - frm.pack(fill=tk.BOTH, expand=True) - - if num == '9': - ttk.Label(frm, text="Показатель 9 — ущерб бюджетам РФ (применим ко всем субъектам КИИ)", - font=('Arial', 10, 'bold')).pack(anchor=tk.W) - ttk.Label(frm, text="Формула: №9 = ΔБ / Бусредн × 100%, где ΔБ = ΔНП + ΔНДПИ + ΔДИВ + ΔВСО + ΔНС\n" - "ΔНП = ΔДоходов × (налог на прибыль / прибыль); если ΔДоходов > прибыли — весь налог.", - foreground='gray', wraplength=660, justify=tk.LEFT).pack(anchor=tk.W, pady=(2, 6)) - - def _row(label, key, default=''): - ttk.Label(frm, text=label).pack(anchor=tk.W, pady=(4, 0)) - e = ttk.Entry(frm, width=60) - e.insert(0, str(default or '')) - e.pack(fill=tk.X) - return e - - e_profit = _row("Прибыль субъекта КИИ за прошедший год (руб.):", 'profit', profit) - e_tax = _row("Налог на прибыль за прошедший год (руб.):", 'tax', profit_tax) - e_dincome = _row("Снижение уровня доходов ΔДоходов (руб., наихудший сценарий):", 'dincome', '') - e_budget = _row("Доход федерального бюджета, усреднённый за 3 года Бусредн (руб.):", 'budget', - '21228053357700') - ttk.Label(frm, text="Прочие составляющие ΔБ (если применимы, иначе 0):", - font=('', 9, 'bold')).pack(anchor=tk.W, pady=(8, 0)) - e_ndpi = _row("Снижение выплат от налога на добычу ПИ ΔНДПИ (руб.):", 'ndpi', '0') - e_div = _row("Снижение выплат от дивидендов ΔДИВ (руб.):", 'div', '0') - e_vso = _row("Снижение выплат сторонних организаций ΔВСО (руб.):", 'vso', '0') - e_ns = _row("Снижение сборов ΔНС (руб.):", 'ns', '0') - result = tk.Text(frm, height=10, wrap=tk.WORD, state=tk.NORMAL) - result.pack(fill=tk.BOTH, expand=True, pady=(8, 0)) - else: - ttk.Label(frm, text="Показатель 8 — ущерб субъекту КИИ", - font=('Arial', 10, 'bold')).pack(anchor=tk.W) - ttk.Label(frm, text="Применим только для: госкорпорации, ГУП, госкомпании, стратегического АО/предприятия " - "(Указ Президента № 1009).\nФормула: №8 = ΔДоходов / Дусредн × 100%.", - foreground='gray', wraplength=660, justify=tk.LEFT).pack(anchor=tk.W, pady=(2, 6)) - - def _row(label, key, default=''): - ttk.Label(frm, text=label).pack(anchor=tk.W, pady=(4, 0)) - e = ttk.Entry(frm, width=60) - e.insert(0, str(default or '')) - e.pack(fill=tk.X) - return e - - e_profit = e_tax = None - # Доходы за 5 лет — из данных организации (поля «Доходы за 5 лет») - _incs = [_num(getattr(self.model, f'income_year_{i}', '') or '') for i in range(1, 6)] - _incs = [x for x in _incs if x is not None] - _d_avg = (sum(_incs) / len(_incs)) if _incs else None - _d_loss = _num(getattr(self.model, 'income_loss', '') or '') - e_dincome = _row("Суммарный экономический ущерб от нарушения критических процессов (руб.):", - 'dincome', f"{_d_loss:,.0f}" if _d_loss else '') - e_budget = _row("Доходы субъекта КИИ, усреднённые за 5 лет (руб.):", - 'budget', f"{_d_avg:,.0f}" if _d_avg else '') - result = tk.Text(frm, height=10, wrap=tk.WORD, state=tk.NORMAL) - result.pack(fill=tk.BOTH, expand=True, pady=(8, 0)) - - def _calc(): - lines = [] - if num == '8': - d = _num(e_dincome.get()) - b = _num(e_budget.get()) - if d is None or b is None or b <= 0: - result.delete('1.0', tk.END) - result.insert('1.0', "Заполните оба поля (ΔДоходов и Дусредн).") - return - val = d / b * 100 - lines.append(f"Суммарный экономический ущерб от нарушения критических процессов: {d:,.0f} руб.") - lines.append(f"Доходы субъекта КИИ, усреднённые за 5 лет: {b:,.0f} руб.") - lines.append(f"№8 = ущерб / доходы за 5 лет × 100% = {d:,.0f} / {b:,.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% — ни одна категория не присваивается («—»).") - else: - p = _num(e_profit.get()) - t = _num(e_tax.get()) - d = _num(e_dincome.get()) - b = _num(e_budget.get()) - if d is None or b is None or b <= 0: - result.delete('1.0', tk.END) - result.insert('1.0', "Заполните ΔДоходов и Бусредн (прибыль/налог — из данных организации).") - return - if p and t is not None and p > 0: - knp = t / p - dnp = d * knp - if d > p: - dnp = t - lines.append(f"Коэффициент КНП = налог на прибыль / прибыль = {t:,.0f} / {p:,.0f} = {knp:.4f}") - lines.append(f"ΔНП = ΔДоходов × КНП = {d:,.0f} × {knp:.4f} = {dnp:,.1f} руб." - + (" (ΔДоходов > прибыли → весь налог)" if d > p else "")) - else: - dnp = 0 - lines.append("Прибыль/налог не заполнены — ΔНП принят равным 0 (расчёт неполный).") - ndpi = _num(e_ndpi.get()) or 0 - div = _num(e_div.get()) or 0 - vso = _num(e_vso.get()) or 0 - ns = _num(e_ns.get()) or 0 - db = dnp + ndpi + div + vso + ns - lines.append(f"ΔНДПИ = {ndpi:,.0f} руб., ΔДИВ = {div:,.0f} руб., ΔВСО = {vso:,.0f} руб., ΔНС = {ns:,.0f} руб.") - lines.append(f"ΔБ = ΔНП + ΔНДПИ + ΔДИВ + ΔВСО + ΔНС = {db:,.1f} руб.") - lines.append(f"Бусредн (доход федерального бюджета за 3 года): {b:,.0f} руб.") - val = db / b * 100 - lines.append(f"№9 = ΔБ / Бусредн × 100% = {db:,.1f} / {b:,.0f} × 100 = {val:.6f} %") - if val > 0.001: - cat = 'I' - elif val > 0.0006: - cat = 'II' - elif val > 0.0003: - cat = 'III' - else: - cat = None - if cat: - lines.append(f"Категория значимости: {cat} (более 0,001% → I; 0,0006-0,001% → II; 0,0003-0,0006% → III)") - else: - lines.append("Значение ≤ 0,0003% — ни одна категория не присваивается («—»).") - result.delete('1.0', tk.END) - result.insert('1.0', '\n'.join(lines)) - - def _apply(): - _calc() - txt = result.get('1.0', 'end-1c').strip() - lines = txt.split('\n') - cat_line = next((l for l in lines if l.startswith('Категория значимости:')), '') - cat = None - if 'Категория значимости: I' in cat_line: - cat = 'I' - elif 'Категория значимости: II' in cat_line: - cat = 'II' - elif 'Категория значимости: III' in cat_line: - cat = 'III' - if cat: - # Выбрать в комбобоксе соответствующую категорию - opts = list(combo['values']) - target = next((o for o in opts if o.startswith(f"{cat} категория")), None) - if target: - var.set(target) - # Обоснование с расчётом - prefix = f"Показатель {num}. {name} — {cat} категория." - full = prefix + "\n" + txt - cur = justify_txt.get('1.0', 'end-1c').strip() - if prefix not in cur: - if cur: - justify_txt.insert('end', '\n' + full) - else: - justify_txt.insert('1.0', full) - else: - var.set('—') - prefix = f"Показатель {num}. {name} — применим, категория не присвоена (—)." - full = prefix + "\n" + txt - cur = justify_txt.get('1.0', 'end-1c').strip() - if prefix not in cur: - if cur: - justify_txt.insert('end', '\n' + full) - else: - justify_txt.insert('1.0', full) - w.destroy() - - btns = ttk.Frame(frm) - btns.pack(fill=tk.X, pady=(8, 0)) - ttk.Button(btns, text="🧮 Рассчитать", command=_calc).pack(side=tk.LEFT, padx=2) - ttk.Button(btns, text="✔️ Применить категорию", command=_apply).pack(side=tk.LEFT, padx=2) - ttk.Button(btns, text="Отмена", command=w.destroy).pack(side=tk.LEFT, padx=2) + btn_frame = ttk.Frame(body) + btn_frame.pack(fill=tk.X, pady=(8, 0)) + ttk.Button(btn_frame, text="⚡ Заполнить автоматически (по данным)", + command=_auto_fill_criteria).pack(side=tk.LEFT, padx=2) + ttk.Button(btn_frame, text="🧮 Рассчитать категорию", command=_calc).pack(side=tk.LEFT, padx=2) + ttk.Button(btn_frame, text="💾 Сохранить", command=_save).pack(side=tk.LEFT, padx=2) + ttk.Button(btn_frame, text="Отмена", command=dialog.destroy).pack(side=tk.LEFT, padx=2) dialog.wait_window()