fix(full-report): generic-маркеры областных админ-единиц в ЕКБ-фильтре тепла #2297

Merged
bot-backend merged 1 commit from fix/report-nonekb-generic into main 2026-07-03 19:59:59 +00:00
2 changed files with 47 additions and 8 deletions

View file

@ -84,6 +84,15 @@ _NON_EKB_MARKERS: tuple[str, ...] = (
"краснотурьинск", "краснотурьинск",
) )
_EKB_MARKER = "екатеринбург" _EKB_MARKER = "екатеринбург"
# Generic-маркеры областных админ-единиц: «СТ: Нижнетуринский муниципальный округ» и т.п.
# проходят мимо городского списка выше. Упоминание Екатеринбурга ПЕРЕВЕШИВАЕТ (см. _is_non_ekb).
_NON_EKB_GENERIC: tuple[str, ...] = (
"муниципальный округ",
"городской округ",
"муниципальный район",
"городское поселение",
"муниципальное образование",
)
# Кап видимых строк тепло/вода-таблиц (сверх — «и ещё K систем …»). # Кап видимых строк тепло/вода-таблиц (сверх — «и ещё K систем …»).
_HEAT_ROW_CAP = 15 _HEAT_ROW_CAP = 15
_WATER_ROW_CAP = 25 _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("ё", "е") haystack = " ".join(str(f) for f in fields if f).lower().replace("ё", "е")
if not haystack: if not haystack:
return False return False
return any(marker in haystack for marker in _NON_EKB_MARKERS) # Явное упоминание Екатеринбурга перевешивает любые маркеры — один предикат
# и для видимости, и для агрегата «Суммарно (ЕКБ)» (иначе они расходятся).
if _EKB_MARKER in haystack:
def _mentions_ekb(*fields: Any) -> bool: return False
"""True, если хотя бы одно поле явно упоминает Екатеринбург (ё-нормализовано). PURE.""" if any(marker in haystack for marker in _NON_EKB_MARKERS):
haystack = " ".join(str(f) for f in fields if f).lower().replace("ё", "е") return True
return _EKB_MARKER in haystack return any(marker in haystack for marker in _NON_EKB_GENERIC)
def _reserve_num(value: Any) -> float: def _reserve_num(value: Any) -> float:
@ -892,7 +901,7 @@ def _capacity_rows_capped(
if reserve < 0 and not _is_non_ekb(*names): if reserve < 0 and not _is_non_ekb(*names):
deficit.append(it) deficit.append(it)
continue continue
if _is_non_ekb(*names) and not _mentions_ekb(*names): if _is_non_ekb(*names):
hidden += 1 hidden += 1
continue continue
surplus_ekb.append(it) surplus_ekb.append(it)

View file

@ -477,6 +477,36 @@ def test_heat_table_aggregates_ekb_and_hides_province_noise() -> None:
assert "Ирбит" not in html 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: def test_heat_deficit_not_hidden_under_cap() -> None:
"""Честность: отрицательный резерв ЕКБ виден даже при переполнении капа (FIX 3).""" """Честность: отрицательный резерв ЕКБ виден даже при переполнении капа (FIX 3)."""
systems = [{"org": "ЕТК", "system_name": "СТ Екатеринбург дефицит", "reserve_gcal_h": -5.0}] systems = [{"org": "ЕТК", "system_name": "СТ Екатеринбург дефицит", "reserve_gcal_h": -5.0}]