gendesign/backend/tests/services/generative/test_exporters.py
bot-backend f3988add0a
Some checks failed
Deploy / changes (push) Has been cancelled
Deploy / build-backend (push) Has been cancelled
Deploy / build-worker (push) Has been cancelled
Deploy / build-frontend (push) Has been cancelled
Deploy / deploy (push) Has been cancelled
fix(generative): устаревший VAT/office caveat в concept PDF (#2445-C1) (#2455)
2026-07-05 19:40:30 +00:00

149 lines
5.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Stage 1c tests — DXF and PDF exporters.
DXF is asserted by a binary round-trip (re-read with ezdxf, check layers/entities).
The PDF render is skipped when WeasyPrint's native libraries are unavailable (dev
boxes без libgobject); the HTML-build step is always exercised.
"""
from __future__ import annotations
import importlib.util
import os
import tempfile
from collections import Counter
import ezdxf
import pytest
from app.schemas.concept import ConceptInput
from app.services.generative import geometry
from app.services.generative.exporters import dxf, pdf
_PARCEL_COORDS = [
[60.60, 56.830],
[60.6045, 56.830],
[60.6045, 56.8328],
[60.60, 56.8328],
[60.60, 56.830],
]
def _payload() -> ConceptInput:
return ConceptInput(
parcel_geojson={"type": "Polygon", "coordinates": [_PARCEL_COORDS]},
housing_class="comfort",
target_floors=9,
development_type="mid_rise",
land_cost_rub=150_000_000.0,
)
def _weasyprint_available() -> bool:
"""WeasyPrint импортируется только с нативными библиотеками (libgobject и т.д.)."""
if importlib.util.find_spec("weasyprint") is None:
return False
try:
import weasyprint # noqa: F401
except OSError:
return False
return True
def test_dxf_export_round_trips_with_layers() -> None:
payload = _payload()
parcel = geometry.parse_parcel(payload)
variant = geometry.generate(payload)[0]
data = dxf.export_concept_dxf(parcel, variant)
assert data.startswith(b"AutoCAD Binary DXF")
with tempfile.NamedTemporaryFile(suffix=".dxf", delete=False) as fh:
fh.write(data)
path = fh.name
try:
doc = ezdxf.readfile(path)
finally:
os.unlink(path)
msp = doc.modelspace()
by_layer = Counter(e.dxf.layer for e in msp)
# Участок и пятно застройки нарисованы.
assert by_layer["PARCEL"] == 1
assert by_layer["BUILDABLE"] == 1
# Секции нарисованы (по одному polyline на секцию + подписи).
n_sections = len(variant.buildings_geojson["features"])
assert n_sections > 0
assert by_layer["BUILDINGS"] >= n_sections
def test_dxf_building_footprints_have_metric_area() -> None:
from shapely.geometry import Polygon
payload = _payload()
parcel = geometry.parse_parcel(payload)
variant = geometry.generate(payload)[0]
data = dxf.export_concept_dxf(parcel, variant)
with tempfile.NamedTemporaryFile(suffix=".dxf", delete=False) as fh:
fh.write(data)
path = fh.name
try:
doc = ezdxf.readfile(path)
finally:
os.unlink(path)
areas = []
for e in doc.modelspace():
if e.dxftype() == "LWPOLYLINE" and e.dxf.layer == "BUILDINGS":
pts = [(p[0], p[1]) for p in e.get_points()]
areas.append(Polygon(pts).area)
assert areas, "no building polylines found"
# Площади секций — десятки/сотни кв.м (метры), не доли (градусы).
for area in areas:
assert 50.0 < area < 2000.0
def test_pdf_html_build_contains_tables() -> None:
variants = geometry.generate(_payload())
html = pdf._build_html(variants)
assert "Технико-экономические показатели" in html
assert "Финансовая модель" in html
# PR-3: DCF-строки заменили IRR-proxy в таблице финмодели.
assert "NPV (DCF), млн руб" in html
assert "IRR (DCF, годовой)" in html
assert "Окупаемость (PBP)" in html
def test_pdf_html_build_graceful_on_empty() -> None:
html = pdf._build_html([])
assert "нет вариантов" in html
def test_pdf_caveat_does_not_contradict_office_revenue_row() -> None:
"""Эпик #2445 C1: каскад включает нежилую выручку/НДС — caveat не должен это отрицать.
Payload — ``comfort`` (office_share=0.05 в teap.py), поэтому строка "Выручка —
нежилое (1-й этаж)" в таблице ненулевая: caveat ниже НЕ может утверждать, что
коммерция/офисы "не учитываются" или что НДС — "только машиноместа".
"""
variants = geometry.generate(_payload())
html = pdf._build_html(variants)
assert "Выручка — нежилое (1-й этаж), млн руб" in html
assert "НДС (нежилое: паркинг + коммерция), млн руб" in html
# Старые (стейл) формулировки, отрицающие модель нежилого, — не должны вернуться.
assert "не учитываются" not in html
assert "только на нежилую часть" not in html
assert "только машиноместа" not in html
# Caveat честно описывает нежилое как учтённое (не «нет в ТЭП»).
assert "нет в ТЭП" not in html
@pytest.mark.skipif(
not _weasyprint_available(),
reason="WeasyPrint native libs unavailable on this host",
)
def test_pdf_export_produces_pdf_bytes() -> None:
variants = geometry.generate(_payload())
data = pdf.export_concept_pdf(variants)
assert data.startswith(b"%PDF-")
assert len(data) > 1000