diff --git a/backend/app/schemas/concept.py b/backend/app/schemas/concept.py index 31e3ed05..fd8044cd 100644 --- a/backend/app/schemas/concept.py +++ b/backend/app/schemas/concept.py @@ -29,15 +29,24 @@ class TEAP(BaseModel): class FinancialModel(BaseModel): - """Static developer P&L for a concept variant (PR-1, эпик #1881). + """Static developer P&L + monthly-DCF investment metrics (PR-1/PR-3, эпик #1881). - Full static cost cascade + VAT + profit tax → net profit + ROI. NOT a DCF — - timing/discounting lands in PR-3. VAT is a documented simplification (see - ``compute_financial`` docstring), not exact НК РФ mechanics. + Full static cost cascade + VAT + profit tax → net profit + ROI (PR-1). On top + of that static P&L, PR-3 lays the cascade onto a **monthly cashflow schedule** + (default phase norms — ПИР → СМР → продажи) and runs a real DCF: NPV, IRR + (bisection over the monthly cashflow) and *undiscounted* cumulative payback. + See ``compute_financial`` docstring for the schedule assumptions and the + налоги/рассрочка phasing choices. + + ``irr`` is a **real** annualised DCF IRR when ``irr_is_proxy is False``; for a + degenerate cashflow with no sign change (e.g. always-loss) the bisection cannot + bracket a root, so we fall back to the PR-1 annualised-ROI proxy and flag + ``irr_is_proxy=True``. VAT is a documented simplification (see docstring), not + exact НК РФ mechanics. The legacy summary fields (``revenue_rub`` / ``cost_rub`` / ``gross_margin_rub`` / ``irr``) are kept for backward-compat; the new fields - expose the full cascade and the net P&L. + expose the full cascade, the net P&L and the DCF metrics. """ # ── Legacy summary (backward-compat) ─────────────────────────────────────── @@ -68,7 +77,19 @@ class FinancialModel(BaseModel): # ── Metrics ──────────────────────────────────────────────────────────────── roi: float # net profit / total cost margin_pct: float # net profit / revenue - irr_is_proxy: bool = True # frontend caveat: not a real DCF IRR (см. PR-3) + irr_is_proxy: bool = True # frontend caveat: True → IRR is the ROI-proxy, not DCF + + # ── DCF / investment metrics (PR-3, эпик #1881) ──────────────────────────── + # NPV дисконтированного помесячного cashflow по ставке ``discount_rate_used``. + npv_rub: float = 0.0 + # Окупаемость (мес) по НЕдисконтированному накопительному cashflow, линейная + # интерполяция дробного месяца. ``None`` — проект не окупается на горизонте. + payback_months: float | None = None + # Годовая ставка дисконтирования, фактически применённая в NPV (норматив). + discount_rate_used: float = 0.0 + # График фаз/продаж — типовые нормативные ДОПУЩЕНИЯ (не график конкретного + # проекта). True → UI/PDF показывают caveat «график — типовое допущение». + schedule_is_default: bool = True # ── Price calibration (PR-2, эпик #1881) ─────────────────────────────────── # Цена продажи жилья, руб/кв.м, фактически использованная в выручке. Либо diff --git a/backend/app/services/generative/exporters/pdf.py b/backend/app/services/generative/exporters/pdf.py index 8c16f9a3..a6276e90 100644 --- a/backend/app/services/generative/exporters/pdf.py +++ b/backend/app/services/generative/exporters/pdf.py @@ -20,7 +20,7 @@ import html import logging from collections.abc import Sequence -from app.schemas.concept import ConceptVariant +from app.schemas.concept import ConceptVariant, FinancialModel logger = logging.getLogger(__name__) @@ -47,7 +47,7 @@ thead th { background: #ececec; } """ _TITLE = "Концепции застройки — сводка вариантов" -_SUBTITLE = "Generative Design · Stage 1c · детерминированный расчёт ТЭП и финмодели" +_SUBTITLE = "Generative Design · Stage 1c · детерминированный расчёт ТЭП, финмодели и DCF" def _fmt_int(value: float | int) -> str: @@ -92,8 +92,21 @@ def _teap_table(variants: Sequence[ConceptVariant]) -> str: ) +def _fmt_pbp(value: float | None) -> str: + """Срок окупаемости — мес (1 знак), либо «не окупается».""" + if value is None: + return "не окупается" + return f"{value:.1f} мес" + + +def _fmt_irr(financial: FinancialModel) -> str: + """IRR в % с пометкой «оценочный», если это proxy (вырожденный поток).""" + pct = f"{financial.irr * 100:.1f}%" + return f"{pct} (оценочный)" if financial.irr_is_proxy else pct + + def _financial_table(variants: Sequence[ConceptVariant]) -> str: - """HTML-таблица финмодели (деньги в млн руб; полный каскад + БДР; IRR proxy).""" + """HTML-таблица финмодели (деньги в млн руб; полный каскад + БДР + DCF).""" headers = "".join( f"
{html.escape(_SUBTITLE)}
" f"{_DASH} нет вариантов для отображения