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'
' 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, "потеряна пояснительная сноска к отсутствию данных"