From ed13f0af319761c17d16001a0d3ddabb33f40ec6 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Thu, 20 Aug 2026 13:23:07 +0500 Subject: [PATCH] =?UTF-8?q?fix(ptica):=20HTML-=D1=8D=D0=BA=D1=81=D0=BF?= =?UTF-8?q?=D0=BE=D1=80=D1=82=D1=91=D1=80=20=D0=BF=D0=B5=D1=80=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D0=B0=D1=91=D1=82=20=D0=B2=D1=8B=D0=B1=D1=80=D0=B0=D1=81?= =?UTF-8?q?=D1=8B=D0=B2=D0=B0=D1=82=D1=8C=20=D0=BB=D0=B5=D0=B3=D0=B0=D1=81?= =?UTF-8?q?=D0=B8-=D0=B7=D0=BE=D0=BD=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5,=20=D0=BA=D0=BE=D1=82=D0=BE=D1=80=D0=BE=D0=B5=20?= =?UTF-8?q?=D1=81=D0=B0=D0=BC=20=D0=B6=D0=B5=20=D0=BF=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D0=B8=D0=BB=20(#2464)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _build_zoning при пустом nspd_zoning берёт легаси result["zoning"], проверяет, что там есть zone_code, — и дальше читает поля из так и оставшегося ПУСТЫМ nspd_zoning. Все пары выходят None, отбрасываются фильтром, и §1 печатает «нет данных» ПОВЕРХ имеющихся данных. То есть это не пустая таблица, а ложное утверждение об отсутствии. Соседний full_report_docx._build_zoning — та же структура слово в слово — закрывает эту ветку одной строкой `nspd_zoning = zoning`. В HTML её не было. Достижимость называю честно: сегодня ветка на проде не срабатывает. Легаси-зонирование заполняется из pzz_zones_ekb, а она пуста намеренно — задача pzz-sync отключена в beat-расписании, потому что Росреестр закрыл PKK6 (#233, #259). Дефект латентный. Чиню всё равно по двум причинам. HTML-сборка — основа полного PDF-отчёта (full_report_pdf импортирует build_full_report_html), так что в день, когда таблицу наполнят, зонирование молча пропадёт из главного документа. И правка — ровно одна строка, приводящая функцию к её же соседу. Тест против origin/main: код зоны Ж-4 не попал в таблицу — вывод '
нет данных
' заглушка «нет данных» рядом с данными — то же самое, отдельным утверждением приоритет nspd_zoning сохранён — контроль, зелёный на обеих сторонах отсутствие данных честно названо — контроль, зелёный на обеих сторонах Контроль «приоритет nspd_zoning» стоит не для симметрии: он ловит «починку», которая слила бы два источника вместо того, чтобы оставить НСПД главным. Прогоны: tests/services/exporters 224 passed rc=0; tests/services rc=0. Co-Authored-By: Claude Opus 5 --- .../services/exporters/full_report_html.py | 6 ++ .../test_2464_html_zoning_fallback.py | 90 +++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 backend/tests/services/exporters/test_2464_html_zoning_fallback.py diff --git a/backend/app/services/exporters/full_report_html.py b/backend/app/services/exporters/full_report_html.py index 32ab2368..e8d0f6ed 100644 --- a/backend/app/services/exporters/full_report_html.py +++ b/backend/app/services/exporters/full_report_html.py @@ -482,6 +482,12 @@ def _build_zoning(result: dict[str, Any]) -> str: note = zoning.get("note") note_html = f'

{_esc(note)}

' if note else "" return _no_data() + note_html + # Без этой строки годное легаси-зонирование признавалось пригодным выше и тут же + # терялось: ниже всё читается из nspd_zoning, а он в этой ветке пустой — все пары + # выходили None, отбрасывались фильтром, и §1 печатал «нет данных» ПОВЕРХ + # имеющихся данных. Соседний full_report_docx._build_zoning делает ровно это же + # присваивание (#2464). + nspd_zoning = zoning zone_code = nspd_zoning.get("zone_code") or nspd_zoning.get("regulation_zone_index") pairs: list[tuple[str, Any]] = [ diff --git a/backend/tests/services/exporters/test_2464_html_zoning_fallback.py b/backend/tests/services/exporters/test_2464_html_zoning_fallback.py new file mode 100644 index 00000000..9036e280 --- /dev/null +++ b/backend/tests/services/exporters/test_2464_html_zoning_fallback.py @@ -0,0 +1,90 @@ +"""HTML-экспортёр не должен выбрасывать легаси-зонирование, которое сам же проверил (#2464). + +`_build_zoning` при пустом `nspd_zoning` берёт легаси `result["zoning"]`, проверяет, что +там есть `zone_code`, — и дальше читает поля из так и оставшегося ПУСТЫМ `nspd_zoning`. +То есть данные признаны годными и тут же потеряны: все пары получаются None, отбрасываются +фильтром, и таблица уходит в отчёт без единой строки. + +Соседний экспортёр `full_report_docx._build_zoning` — та же структура слово в слово — эту +ветку закрывает одной строкой `nspd_zoning = zoning`. Здесь её не было. + +Достижимость. Сегодня ветка не срабатывает на проде: легаси-зонирование заполняется из +`pzz_zones_ekb`, а та пуста намеренно — задача `pzz-sync` отключена в beat-расписании, +потому что Росреестр закрыл PKK6 (#233, #259). Так что это латентный дефект, а не +наблюдаемый отказ. Чинится он всё равно: HTML-сборка — основа полного PDF-отчёта +(`full_report_pdf` импортирует `build_full_report_html`), и в день, когда таблицу +наполнят, зонирование молча пропадёт из главного документа. +""" + +from __future__ import annotations + +import os + +os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test") + +from typing import Any + +# Легаси-зонирование в той форме, в какой его собирает parcels.py из pzz_zones_ekb. +_LEGACY_ZONING: dict[str, Any] = { + "zone_code": "Ж-4", + "zone_name": "Зона многоэтажной жилой застройки", + "description": "Многоэтажные жилые дома", + "data_available": True, + "source": "rosreestr-pkk6-cached", +} + + +def _zoning_html(result: dict[str, Any]) -> str: + from app.services.exporters.full_report_html import _build_zoning + + return _build_zoning(result) + + +def test_legacy_zoning_reaches_the_table() -> None: + """Код зоны из легаси-источника обязан попасть в таблицу отчёта.""" + out = _zoning_html({"nspd_zoning": None, "zoning": dict(_LEGACY_ZONING)}) + + assert "Ж-4" in out, ( + "код зоны из легаси-зонирования не попал в таблицу: ветка признала данные " + "годными и продолжила читать из пустого nspd_zoning" + ) + assert "Зона многоэтажной жилой застройки" in out, "наименование зоны потеряно" + + +def test_legacy_zoning_does_not_render_as_no_data() -> None: + """Таблица с данными не должна выглядеть как «данных нет». + + Отдельно от предыдущего: даже если код зоны когда-нибудь попадёт в вывод другим + путём, заглушка «нет данных» рядом с реальными данными — сама по себе дефект. + """ + out = _zoning_html({"nspd_zoning": None, "zoning": dict(_LEGACY_ZONING)}) + + from app.services.exporters.full_report_html import _NO_DATA + + assert _NO_DATA not in out, f"в таблице с данными стоит заглушка {_NO_DATA!r}" + + +def test_nspd_zoning_still_wins_when_present() -> None: + """Контроль: при живом nspd_zoning легаси не подмешивается. + + Ловит «починку», которая слила бы два источника: приоритет НСПД должен остаться. + """ + out = _zoning_html( + { + "nspd_zoning": {"zone_code": "ЦС-1", "zone_name": "Зона делового назначения"}, + "zoning": dict(_LEGACY_ZONING), + } + ) + + assert "ЦС-1" in out + assert "Ж-4" not in out, "легаси-зона подмешалась поверх живого nspd_zoning" + + +def test_absent_zoning_still_says_no_data() -> None: + """Контроль: когда данных нет НИГДЕ, отчёт по-прежнему честно говорит об этом.""" + from app.services.exporters.full_report_html import _NO_DATA + + out = _zoning_html({"nspd_zoning": None, "zoning": {"zone_code": None, "note": "PKK6 закрыт"}}) + + assert _NO_DATA in out, "исчезла честная заглушка «данных нет»" + assert "PKK6 закрыт" in out, "потеряна пояснительная сноска к отсутствию данных" -- 2.45.3