From 11bc503fefc106ecd590b28e5f202f097b049c03 Mon Sep 17 00:00:00 2001 From: Light1YT Date: Thu, 2 Jul 2026 23:19:32 +0500 Subject: [PATCH] =?UTF-8?q?fix(eias-heat):=20=D1=81=D1=83=D1=84=D1=84?= =?UTF-8?q?=D0=B8=D0=BA=D1=81=20=C2=AB=20(N)=C2=BB=20=D0=B4=D1=83=D0=B1?= =?UTF-8?q?=D0=BB=D1=8F=D0=BC=20=D0=B8=D0=BC=D1=91=D0=BD=20=D1=81=D0=B8?= =?UTF-8?q?=D1=81=D1=82=D0=B5=D0=BC=20=E2=80=94=20=D0=BD=D0=B5=20=D1=82?= =?UTF-8?q?=D0=B5=D1=80=D1=8F=D1=82=D1=8C=20=D1=80=D0=B5=D0=B7=D0=B5=D1=80?= =?UTF-8?q?=D0=B2=20=D0=BF=D1=80=D0=B8=20UPSERT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Живой кейс ЕЭ: две зоны «Локальные зоны … МУП "Екатеринбургэнерго"» в соседних колонках листа (62.198 и 0 Гкал/ч) — UPSERT по (org, sphere, system_name, period) схлопывал их, ноль перезаписывал 62.198. --- .../services/site_finder/eias_heat_loader.py | 8 +++ backend/tests/test_eias_heat_loader.py | 49 +++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/backend/app/services/site_finder/eias_heat_loader.py b/backend/app/services/site_finder/eias_heat_loader.py index 21eab3ab..155cd4a9 100644 --- a/backend/app/services/site_finder/eias_heat_loader.py +++ b/backend/app/services/site_finder/eias_heat_loader.py @@ -317,10 +317,18 @@ def parse_heat_sheet(xlsx_bytes: bytes) -> tuple[list[dict], list[list[object]]] return [], all_cells systems: list[dict] = [] + name_counts: dict[str, int] = {} for j, name in _system_columns(header_cells): reserve = parse_reserve_number(reserve_cells[j] if j < len(reserve_cells) else None) if reserve is None: continue # непарсибельная ячейка резерва (текст/пусто) → систему скипаем + # Разные системы могут носить БАЙТ-В-БАЙТ одинаковое имя в соседних колонках + # (живой пример ЕЭ: две «Локальные зоны … МУП "Екатеринбургэнерго"» с 62.198 + # и 0). UPSERT по (org, sphere, system_name, period) схлопнул бы их с потерей + # данных — вторым и далее вхождениям добавляем порядковый суффикс « (N)». + name_counts[name] = name_counts.get(name, 0) + 1 + if name_counts[name] > 1: + name = f"{name} ({name_counts[name]})" systems.append({"system_name": name, "reserve_gcal_h": reserve}) return systems, all_cells diff --git a/backend/tests/test_eias_heat_loader.py b/backend/tests/test_eias_heat_loader.py index ec232011..713f7ad1 100644 --- a/backend/tests/test_eias_heat_loader.py +++ b/backend/tests/test_eias_heat_loader.py @@ -327,6 +327,55 @@ def test_parse_heat_sheet_ignores_anketa_rubrics_flag_row_size() -> None: assert 52.0 not in reserves +def test_parse_heat_sheet_suffixes_duplicate_system_names() -> None: + """Разные системы с БАЙТ-В-БАЙТ одинаковым именем не схлопываются. + + Живой прод-кейс (Екатеринбургэнерго, guid fe0c8c57): col7 и col8 обе называются + «Локальные зоны теплоснабжения МУП "Екатеринбургэнерго"» с резервами 62.198 и 0 — + UPSERT по (org, sphere, system_name, period) перезаписывал 62.198 нулём. Вторым + и далее вхождениям добавляется суффикс « (N)». + """ + wb = openpyxl.Workbook() + ws = wb.active + ws.title = "ТП" + dup = 'Локальные зоны теплоснабжения МУП "Екатеринбургэнерго"' + ws.append( + _pad( + { + 4: "Централизованная система", + 7: dup, + 8: dup, + 9: 'Локальные зоны теплоснабжения ПАО "Т Плюс"', + _ROW_SIZE_COL: 15, + } + ) + ) + ws.append( + _pad( + { + 3: 5, + 4: "Резерв мощности источников тепловой энергии, в т.ч.:", + 5: "Гкал/час", + 7: 62.198, + 8: 0, + 9: 0, + _ROW_SIZE_COL: 57, + } + ) + ) + buf = io.BytesIO() + wb.save(buf) + + systems, _ = eh.parse_heat_sheet(buf.getvalue()) + + pairs = [(s["system_name"], s["reserve_gcal_h"]) for s in systems] + assert pairs == [ + (dup, 62.198), # первое вхождение — без суффикса, 62.198 НЕ теряется + (f"{dup} (2)", 0.0), + ('Локальные зоны теплоснабжения ПАО "Т Плюс"', 0.0), + ] + + def test_parse_heat_sheet_no_systems_header_returns_empty() -> None: """Нет строки «Централизованная система» → ([], all_cells) (graceful, не падаем).""" wb = openpyxl.Workbook()