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()