From df677cb1b50c2bb792939fc79313cda30847bf4b Mon Sep 17 00:00:00 2001 From: bot-backend Date: Sat, 4 Jul 2026 00:44:00 +0500 Subject: [PATCH] =?UTF-8?q?fix(full-report):=20generic-=D0=BC=D0=B0=D1=80?= =?UTF-8?q?=D0=BA=D0=B5=D1=80=D1=8B=20=D0=BE=D0=B1=D0=BB=D0=B0=D1=81=D1=82?= =?UTF-8?q?=D0=BD=D1=8B=D1=85=20=D0=B0=D0=B4=D0=BC=D0=B8=D0=BD-=D0=B5?= =?UTF-8?q?=D0=B4=D0=B8=D0=BD=D0=B8=D1=86=20=D0=B2=20=D1=84=D0=B8=D0=BB?= =?UTF-8?q?=D1=8C=D1=82=D1=80=D0=B5=20=D0=95=D0=9A=D0=91-=D1=82=D0=B5?= =?UTF-8?q?=D0=BF=D0=BB=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit «СТ: Нижнетуринский муниципальный округ» и подобные проходили мимо городского маркер-списка → попадали в топ-15 и в агрегат «Суммарно (ЕКБ)» (лейбл врал). Добавлены generic-маркеры (муниципальный/городской округ, район, поселение, МО); упоминание Екатеринбурга перевешивает — теперь внутри самого _is_non_ekb, что заодно устраняет расхождение предикатов видимости и агрегата (нит ревью #2296). Мёртвый _mentions_ekb удалён. --- .../services/exporters/full_report_html.py | 25 +++++++++++----- .../exporters/test_full_report_html_part_a.py | 30 +++++++++++++++++++ 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/backend/app/services/exporters/full_report_html.py b/backend/app/services/exporters/full_report_html.py index 73fa9cae..543acc9e 100644 --- a/backend/app/services/exporters/full_report_html.py +++ b/backend/app/services/exporters/full_report_html.py @@ -84,6 +84,15 @@ _NON_EKB_MARKERS: tuple[str, ...] = ( "краснотурьинск", ) _EKB_MARKER = "екатеринбург" +# Generic-маркеры областных админ-единиц: «СТ: Нижнетуринский муниципальный округ» и т.п. +# проходят мимо городского списка выше. Упоминание Екатеринбурга ПЕРЕВЕШИВАЕТ (см. _is_non_ekb). +_NON_EKB_GENERIC: tuple[str, ...] = ( + "муниципальный округ", + "городской округ", + "муниципальный район", + "городское поселение", + "муниципальное образование", +) # Кап видимых строк тепло/вода-таблиц (сверх — «и ещё K систем …»). _HEAT_ROW_CAP = 15 _WATER_ROW_CAP = 25 @@ -838,13 +847,13 @@ def _is_non_ekb(*fields: Any) -> bool: haystack = " ".join(str(f) for f in fields if f).lower().replace("ё", "е") if not haystack: return False - return any(marker in haystack for marker in _NON_EKB_MARKERS) - - -def _mentions_ekb(*fields: Any) -> bool: - """True, если хотя бы одно поле явно упоминает Екатеринбург (ё-нормализовано). PURE.""" - haystack = " ".join(str(f) for f in fields if f).lower().replace("ё", "е") - return _EKB_MARKER in haystack + # Явное упоминание Екатеринбурга перевешивает любые маркеры — один предикат + # и для видимости, и для агрегата «Суммарно (ЕКБ)» (иначе они расходятся). + if _EKB_MARKER in haystack: + return False + if any(marker in haystack for marker in _NON_EKB_MARKERS): + return True + return any(marker in haystack for marker in _NON_EKB_GENERIC) def _reserve_num(value: Any) -> float: @@ -892,7 +901,7 @@ def _capacity_rows_capped( if reserve < 0 and not _is_non_ekb(*names): deficit.append(it) continue - if _is_non_ekb(*names) and not _mentions_ekb(*names): + if _is_non_ekb(*names): hidden += 1 continue surplus_ekb.append(it) diff --git a/backend/tests/services/exporters/test_full_report_html_part_a.py b/backend/tests/services/exporters/test_full_report_html_part_a.py index 2d8f9d2b..44c84426 100644 --- a/backend/tests/services/exporters/test_full_report_html_part_a.py +++ b/backend/tests/services/exporters/test_full_report_html_part_a.py @@ -477,6 +477,36 @@ def test_heat_table_aggregates_ekb_and_hides_province_noise() -> None: assert "Ирбит" not in html +def test_heat_generic_province_units_hidden_ekb_mention_wins() -> None: + """Generic-маркеры областных админ-единиц скрыты; упоминание ЕКБ перевешивает (FIX 3+).""" + systems = [ + { + "org": "Т Плюс", + "system_name": "СТ: Нижнетуринский муниципальный округ", + "reserve_gcal_h": 239.0, + }, + { + "org": "Регионгаз-инвест", + "system_name": "Невьянский муниципальный округ", + "reserve_gcal_h": 51.0, + }, + # Упоминание Екатеринбурга перевешивает generic-маркер — строка ЕКБ. + { + "org": "ЕТК", + "system_name": "муниципальное образование город Екатеринбург", + "reserve_gcal_h": 10.0, + }, + ] + html = build_full_report_html_part_a( + _synthetic_result(), cad="X", connection_capacity={"heat": {"systems": systems}} + ) + assert "Нижнетуринский" not in html + assert "Невьянский" not in html + assert "город Екатеринбург" in html + # Агрегат считает только ЕКБ-строку (10.0), а не областные 239+51. + assert "239" not in html + + def test_heat_deficit_not_hidden_under_cap() -> None: """Честность: отрицательный резерв ЕКБ виден даже при переполнении капа (FIX 3).""" systems = [{"org": "ЕТК", "system_name": "СТ Екатеринбург дефицит", "reserve_gcal_h": -5.0}] -- 2.45.3