feat(exporters): зеркалим permits_nearby (ГИСОГД 500м) в DOCX §6
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m57s
CI / backend-tests (pull_request) Successful in 14m41s
CI / changes (pull_request) Successful in 8s
CI Trade-In / backend-tests (pull_request) Has been skipped
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m57s
CI / backend-tests (pull_request) Successful in 14m41s
CI / changes (pull_request) Successful in 8s
CI Trade-In / backend-tests (pull_request) Has been skipped
DOCX полного отчёта не показывал блок «Разрешения на строительство рядом», который уже есть в PDF/HTML (#2367) — сознательное сужение границ в #2401. Портирован 1-в-1: та же честная плашка при total_count<=0, те же поля, тот же кап на 10 записей — через существующие DOCX-хелперы (_add_kv_table/_add_data_table/_add_caveat), без дублирования логики (общие PURE-форматтеры _fmt/_fmt_int_ru шарятся с HTML-версией). _build_section_6 расширена третьим параметром result (analyze-payload, уже нормализован в build_full_report_docx как _as_dict(analyze_result) — новый параметр функции не понадобился). Refs #2367
This commit is contained in:
parent
9c6afcc876
commit
e626a544aa
2 changed files with 199 additions and 4 deletions
|
|
@ -1001,8 +1001,55 @@ def _build_scenarios_honesty(doc: _DocxDocument, forecast: dict[str, Any]) -> No
|
||||||
_add_kv_table(doc, pairs)
|
_add_kv_table(doc, pairs)
|
||||||
|
|
||||||
|
|
||||||
def _build_section_6(doc: _DocxDocument, forecast: dict[str, Any]) -> None:
|
def _build_permits_nearby(doc: _DocxDocument, result: dict[str, Any]) -> None:
|
||||||
"""§6 «Риски и дефицит»: дефицит по горизонтам + давление предложения + риск-индексы."""
|
"""РНС/РВЭ в радиусе 500 м участка (ГИСОГД-66) — короткая сводка + список до 10.
|
||||||
|
|
||||||
|
`result` — analyze-payload (НЕ forecast): читает `permits_nearby` (см.
|
||||||
|
`permits_nearby.get_permits_nearby`). Пусто / total_count=0 → честная плашка-фраза
|
||||||
|
(полное предложение, не аббревиатура). Зеркало `_build_permits_nearby` в
|
||||||
|
full_report_html.
|
||||||
|
"""
|
||||||
|
nearby = _as_dict(result.get("permits_nearby"))
|
||||||
|
total = nearby.get("total_count")
|
||||||
|
if not isinstance(total, int) or total <= 0:
|
||||||
|
_add_caveat(
|
||||||
|
doc,
|
||||||
|
"В радиусе 500 м участка новых разрешений на строительство не найдено "
|
||||||
|
"(по данным ГИСОГД Свердловской области).",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
rs_count = nearby.get("rs_count") or 0
|
||||||
|
rv_count = nearby.get("rv_count") or 0
|
||||||
|
nearest = nearby.get("nearest_distance_m")
|
||||||
|
kv: list[tuple[str, Any]] = [
|
||||||
|
("Разрешений на строительство (РНС)", _fmt_int_ru(rs_count)),
|
||||||
|
("Разрешений на ввод (РВЭ)", _fmt_int_ru(rv_count)),
|
||||||
|
("Ближайшее, м", _fmt_int_ru(nearest) if nearest is not None else _DASH),
|
||||||
|
]
|
||||||
|
_add_kv_table(doc, kv)
|
||||||
|
|
||||||
|
rows: list[list[Any]] = []
|
||||||
|
for item in _as_list(nearby.get("items"))[:10]:
|
||||||
|
data = _as_dict(item)
|
||||||
|
rows.append(
|
||||||
|
[
|
||||||
|
data.get("doc_name"),
|
||||||
|
data.get("date_doc"),
|
||||||
|
data.get("approved_organization"),
|
||||||
|
data.get("distance_m"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
headers = ["Документ", "Дата", "Согласующий орган", "Дистанция, м"]
|
||||||
|
_add_data_table(doc, headers, rows)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_section_6(doc: _DocxDocument, forecast: dict[str, Any], result: dict[str, Any]) -> None:
|
||||||
|
"""§6 «Риски и дефицит»: дефицит по горизонтам + давление предложения + риск-индексы.
|
||||||
|
|
||||||
|
`result` — analyze-payload (для блока разрешений рядом, `permits_nearby`); `forecast` —
|
||||||
|
форсайт-ран (дефицит/сценарии). Разные источники — §6 читает оба (зеркало HTML-версии).
|
||||||
|
"""
|
||||||
_add_heading(doc, _TITLE_S6, level=1)
|
_add_heading(doc, _TITLE_S6, level=1)
|
||||||
future = _fc_as_dict(forecast.get("future_market"))
|
future = _fc_as_dict(forecast.get("future_market"))
|
||||||
summary = future.get("summary")
|
summary = future.get("summary")
|
||||||
|
|
@ -1016,6 +1063,8 @@ def _build_section_6(doc: _DocxDocument, forecast: dict[str, Any]) -> None:
|
||||||
_build_risk_indices(doc, forecast)
|
_build_risk_indices(doc, forecast)
|
||||||
_add_heading(doc, "Сценарии", level=2)
|
_add_heading(doc, "Сценарии", level=2)
|
||||||
_build_scenarios_honesty(doc, forecast)
|
_build_scenarios_honesty(doc, forecast)
|
||||||
|
_add_heading(doc, "Разрешения на строительство рядом (500 м)", level=2)
|
||||||
|
_build_permits_nearby(doc, result)
|
||||||
|
|
||||||
|
|
||||||
def _build_concept_program(doc: _DocxDocument, variant: dict[str, Any]) -> None:
|
def _build_concept_program(doc: _DocxDocument, variant: dict[str, Any]) -> None:
|
||||||
|
|
@ -1193,7 +1242,7 @@ def build_full_report_docx(
|
||||||
# ── §4–§7 (Part B) ──────────────────────────────────────────────────────────
|
# ── §4–§7 (Part B) ──────────────────────────────────────────────────────────
|
||||||
_build_section_4(doc, forecast)
|
_build_section_4(doc, forecast)
|
||||||
_build_section_5(doc, forecast, concept)
|
_build_section_5(doc, forecast, concept)
|
||||||
_build_section_6(doc, forecast)
|
_build_section_6(doc, forecast, result)
|
||||||
_build_section_7(doc, concept_result, concept_map_png)
|
_build_section_7(doc, concept_result, concept_map_png)
|
||||||
|
|
||||||
# ── Футер-дисклеймер ────────────────────────────────────────────────────────
|
# ── Футер-дисклеймер ────────────────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,11 @@ os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:
|
||||||
import pytest
|
import pytest
|
||||||
from docx import Document
|
from docx import Document
|
||||||
|
|
||||||
from app.services.exporters.full_report_docx import build_full_report_docx
|
from app.services.exporters.full_report_docx import (
|
||||||
|
_build_permits_nearby,
|
||||||
|
_build_section_6,
|
||||||
|
build_full_report_docx,
|
||||||
|
)
|
||||||
|
|
||||||
_CAD = "66:41:0204016:10"
|
_CAD = "66:41:0204016:10"
|
||||||
|
|
||||||
|
|
@ -402,3 +406,145 @@ def test_garbage_payload_does_not_crash() -> None:
|
||||||
concept_map_png=None,
|
concept_map_png=None,
|
||||||
)
|
)
|
||||||
assert out[:2] == b"PK"
|
assert out[:2] == b"PK"
|
||||||
|
|
||||||
|
|
||||||
|
# ── §6 «Разрешения на строительство рядом (500 м)» (ГИСОГД-66, зеркало HTML) ──────
|
||||||
|
|
||||||
|
|
||||||
|
def _permits_result() -> dict[str, Any]:
|
||||||
|
"""analyze-payload с непустым permits_nearby (2 документа: РНС + РВЭ)."""
|
||||||
|
return {
|
||||||
|
"permits_nearby": {
|
||||||
|
"total_count": 2,
|
||||||
|
"rs_count": 1,
|
||||||
|
"rv_count": 1,
|
||||||
|
"nearest_distance_m": 123.5,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"doc_group": "RS",
|
||||||
|
"doc_name": "Разрешение на строительство МКД",
|
||||||
|
"date_doc": "2025-03-14",
|
||||||
|
"approved_organization": "Администрация ЕКБ",
|
||||||
|
"distance_m": 123.5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"doc_group": "RV",
|
||||||
|
"doc_name": "Разрешение на ввод",
|
||||||
|
"date_doc": "2024-11-01",
|
||||||
|
"approved_organization": "Минстрой СО",
|
||||||
|
"distance_m": 340.0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _new_doc() -> Document:
|
||||||
|
"""Пустой python-docx Document для юнит-теста билдера в изоляции."""
|
||||||
|
return Document()
|
||||||
|
|
||||||
|
|
||||||
|
def test_permits_nearby_empty_honest_full_sentence() -> None:
|
||||||
|
"""Нет permits_nearby → честная плашка-параграф полным предложением, БЕЗ таблицы."""
|
||||||
|
doc = _new_doc()
|
||||||
|
_build_permits_nearby(doc, {})
|
||||||
|
text = _all_text(doc)
|
||||||
|
assert "новых разрешений на строительство не найдено" in text
|
||||||
|
assert "ГИСОГД Свердловской области" in text
|
||||||
|
assert len(doc.tables) == 0 # плашка — параграф, не таблица
|
||||||
|
|
||||||
|
|
||||||
|
def test_permits_nearby_zero_total_honest_note() -> None:
|
||||||
|
"""total_count=0 → та же честная плашка (не аббревиатура), таблицы нет."""
|
||||||
|
doc = _new_doc()
|
||||||
|
_build_permits_nearby(doc, {"permits_nearby": {"total_count": 0, "items": []}})
|
||||||
|
text = _all_text(doc)
|
||||||
|
assert "новых разрешений на строительство не найдено" in text
|
||||||
|
assert len(doc.tables) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_permits_nearby_non_empty_summary_and_list() -> None:
|
||||||
|
"""Есть данные → сводка РНС/РВЭ/ближайшее (kv-таблица) + таблица документов."""
|
||||||
|
doc = _new_doc()
|
||||||
|
_build_permits_nearby(doc, _permits_result())
|
||||||
|
text = _all_text(doc)
|
||||||
|
assert "не найдено" not in text
|
||||||
|
assert "Разрешений на строительство (РНС)" in text
|
||||||
|
assert "Разрешений на ввод (РВЭ)" in text
|
||||||
|
assert "Разрешение на строительство МКД" in text
|
||||||
|
assert "Администрация ЕКБ" in text
|
||||||
|
assert "123.5" in text
|
||||||
|
# kv-сводка + таблица items = две таблицы.
|
||||||
|
assert len(doc.tables) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_permits_nearby_items_capped_at_ten() -> None:
|
||||||
|
"""items[:10] — таблица документов не превышает шапку + 10 строк (зеркало HTML)."""
|
||||||
|
items = [
|
||||||
|
{
|
||||||
|
"doc_group": "RS",
|
||||||
|
"doc_name": f"РНС-{i}",
|
||||||
|
"date_doc": "2025-01-01",
|
||||||
|
"approved_organization": "Орган",
|
||||||
|
"distance_m": float(i),
|
||||||
|
}
|
||||||
|
for i in range(15)
|
||||||
|
]
|
||||||
|
result = {
|
||||||
|
"permits_nearby": {
|
||||||
|
"total_count": 15,
|
||||||
|
"rs_count": 15,
|
||||||
|
"rv_count": 0,
|
||||||
|
"nearest_distance_m": 0.0,
|
||||||
|
"items": items,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
doc = _new_doc()
|
||||||
|
_build_permits_nearby(doc, result)
|
||||||
|
# Вторая таблица (документы): шапка + не более 10 строк.
|
||||||
|
docs_table = doc.tables[1]
|
||||||
|
assert len(docs_table.rows) == 11
|
||||||
|
text = _all_text(doc)
|
||||||
|
assert "РНС-0" in text
|
||||||
|
assert "РНС-9" in text
|
||||||
|
assert "РНС-10" not in text
|
||||||
|
|
||||||
|
|
||||||
|
def test_section_6_wires_permits_nearby_from_analyze() -> None:
|
||||||
|
"""_build_section_6(doc, forecast, result) прокидывает result → блок разрешений виден."""
|
||||||
|
doc = _new_doc()
|
||||||
|
_build_section_6(doc, {}, _permits_result())
|
||||||
|
text = _all_text(doc)
|
||||||
|
headings = {p.text for p in doc.paragraphs}
|
||||||
|
assert "Разрешения на строительство рядом (500 м)" in headings
|
||||||
|
assert "Разрешение на строительство МКД" in text
|
||||||
|
|
||||||
|
|
||||||
|
def test_section_6_permits_nearby_absent_analyze_shows_honest_note() -> None:
|
||||||
|
"""§6 без permits_nearby в result всё равно рисует честную плашку (не падает)."""
|
||||||
|
doc = _new_doc()
|
||||||
|
_build_section_6(doc, {}, {})
|
||||||
|
text = _all_text(doc)
|
||||||
|
assert "Разрешения на строительство рядом (500 м)" in {p.text for p in doc.paragraphs}
|
||||||
|
assert "новых разрешений на строительство не найдено" in text
|
||||||
|
|
||||||
|
|
||||||
|
def test_full_report_wires_permits_nearby_end_to_end() -> None:
|
||||||
|
"""build_full_report_docx прокидывает analyze.permits_nearby в §6 (интеграция)."""
|
||||||
|
analyze = _analyze()
|
||||||
|
analyze["permits_nearby"] = _permits_result()["permits_nearby"]
|
||||||
|
out = _render(analyze_result=analyze)
|
||||||
|
doc = Document(io.BytesIO(out))
|
||||||
|
headings = {p.text for p in doc.paragraphs}
|
||||||
|
assert "Разрешения на строительство рядом (500 м)" in headings
|
||||||
|
assert "Разрешение на строительство МКД" in _all_text(doc)
|
||||||
|
|
||||||
|
|
||||||
|
def test_full_report_permits_nearby_honest_note_when_absent() -> None:
|
||||||
|
"""Дефолтный _analyze() без permits_nearby → честная плашка «не найдено» в §6."""
|
||||||
|
out = _render()
|
||||||
|
text = _all_text(Document(io.BytesIO(out)))
|
||||||
|
assert "Разрешения на строительство рядом (500 м)" in {
|
||||||
|
p.text for p in Document(io.BytesIO(out)).paragraphs
|
||||||
|
}
|
||||||
|
assert "новых разрешений на строительство не найдено" in text
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue