feat(generative): full static developer financial cascade (epic #1881 PR-1)
Объединяет наше (площади из реальной геометрии teap) + полноту девелоперской Excel-модели (каскад затрат + БДР с налогами). Расширяет financial.py из статического GDV−COST в полный статический расчёт. БЕЗ DCF (PR-3), БЕЗ новых вводных от пользователя — только нормативы + существующий TEAP. Каскад (financial.py): - Выручка: жильё (residential×price) + ПАРКИНГ (spaces×1.9М) - Затраты: СМР(резид+паркинг) + ПИР 2.5% + сети/ТУ 6000₽/м² + услуги заказчика 1.5% + резерв 3% + маркетинг/риэлтор 7% выручки + земля - БДР: валовая маржа → НДС (упрощённо) → прибыль до налога → налог 25% (ФЗ 176-ФЗ) → ЧИСТАЯ прибыль → ROI + чистая маржа% - Нормативы документированы (источник: Excel-эталон «Авангардная 13» + RU-рынок) FinancialModel (concept.py): +16 additive-полей (revenue breakdown, cost cascade, БДР/налоги, roi, margin_pct, irr_is_proxy). Старые 4 поля сохранены (backward-compat). IRR честно помечен irr_is_proxy=True (annualized net ROI, не DCF) — настоящий IRR/NPV/PBP в PR-3 (помесячный cashflow). НДС — документированное упрощение, не точный НК РФ. Офисы пропущены (нет площади в TEAP). PDF + concept UI: полный БДР-каскад + чистая прибыль/ROI + честные сноски. api-types.ts регенерён авторитетно (openapi-typescript + prettier, как CI). mypy strict clean (generative.*), +тесты (каскад/паркинг/налоги/backward-compat/ edge cost=0), 36 passed. Refs #1881
This commit is contained in:
parent
7ae96ad1af
commit
65bab902e1
8 changed files with 557 additions and 72 deletions
|
|
@ -29,11 +29,47 @@ class TEAP(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class FinancialModel(BaseModel):
|
class FinancialModel(BaseModel):
|
||||||
|
"""Static developer P&L for a concept variant (PR-1, эпик #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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ── Legacy summary (backward-compat) ───────────────────────────────────────
|
||||||
revenue_rub: float
|
revenue_rub: float
|
||||||
cost_rub: float
|
cost_rub: float
|
||||||
gross_margin_rub: float
|
gross_margin_rub: float
|
||||||
irr: float
|
irr: float
|
||||||
|
|
||||||
|
# ── Revenue breakdown ──────────────────────────────────────────────────────
|
||||||
|
revenue_residential_rub: float
|
||||||
|
revenue_parking_rub: float
|
||||||
|
|
||||||
|
# ── Cost cascade ───────────────────────────────────────────────────────────
|
||||||
|
construction_rub: float
|
||||||
|
pir_rub: float
|
||||||
|
networks_rub: float
|
||||||
|
developer_services_rub: float
|
||||||
|
contingency_rub: float
|
||||||
|
marketing_rub: float
|
||||||
|
land_rub: float
|
||||||
|
|
||||||
|
# ── БДР / taxes → net profit ───────────────────────────────────────────────
|
||||||
|
vat_rub: float
|
||||||
|
profit_before_tax_rub: float
|
||||||
|
profit_tax_rub: float
|
||||||
|
net_profit_rub: float
|
||||||
|
|
||||||
|
# ── 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)
|
||||||
|
|
||||||
|
|
||||||
class ConceptVariant(BaseModel):
|
class ConceptVariant(BaseModel):
|
||||||
strategy: Literal["max_area", "max_insolation", "balanced"]
|
strategy: Literal["max_area", "max_insolation", "balanced"]
|
||||||
|
|
|
||||||
|
|
@ -93,14 +93,41 @@ def _teap_table(variants: Sequence[ConceptVariant]) -> str:
|
||||||
|
|
||||||
|
|
||||||
def _financial_table(variants: Sequence[ConceptVariant]) -> str:
|
def _financial_table(variants: Sequence[ConceptVariant]) -> str:
|
||||||
"""HTML-таблица финмодели (деньги в млн руб; IRR — proxy, помечен)."""
|
"""HTML-таблица финмодели (деньги в млн руб; полный каскад + БДР; IRR proxy)."""
|
||||||
headers = "".join(
|
headers = "".join(
|
||||||
f"<th>{html.escape(_strategy_label(v.strategy))}</th>" for v in variants
|
f"<th>{html.escape(_strategy_label(v.strategy))}</th>" for v in variants
|
||||||
)
|
)
|
||||||
rows: list[tuple[str, list[str]]] = [
|
rows: list[tuple[str, list[str]]] = [
|
||||||
("Выручка, млн руб", [_fmt_money(v.financial.revenue_rub) for v in variants]),
|
(
|
||||||
("Затраты, млн руб", [_fmt_money(v.financial.cost_rub) for v in variants]),
|
"Выручка — жильё, млн руб",
|
||||||
|
[_fmt_money(v.financial.revenue_residential_rub) for v in variants],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Выручка — паркинг, млн руб",
|
||||||
|
[_fmt_money(v.financial.revenue_parking_rub) for v in variants],
|
||||||
|
),
|
||||||
|
("Выручка (GDV), млн руб", [_fmt_money(v.financial.revenue_rub) for v in variants]),
|
||||||
|
("СМР, млн руб", [_fmt_money(v.financial.construction_rub) for v in variants]),
|
||||||
|
("ПИР, млн руб", [_fmt_money(v.financial.pir_rub) for v in variants]),
|
||||||
|
("Сети (ТУ), млн руб", [_fmt_money(v.financial.networks_rub) for v in variants]),
|
||||||
|
(
|
||||||
|
"Услуги заказчика, млн руб",
|
||||||
|
[_fmt_money(v.financial.developer_services_rub) for v in variants],
|
||||||
|
),
|
||||||
|
("Резерв, млн руб", [_fmt_money(v.financial.contingency_rub) for v in variants]),
|
||||||
|
("Маркетинг + риэлтор, млн руб", [_fmt_money(v.financial.marketing_rub) for v in variants]),
|
||||||
|
("Земля, млн руб", [_fmt_money(v.financial.land_rub) for v in variants]),
|
||||||
|
("Итого затраты, млн руб", [_fmt_money(v.financial.cost_rub) for v in variants]),
|
||||||
("Валовая маржа, млн руб", [_fmt_money(v.financial.gross_margin_rub) for v in variants]),
|
("Валовая маржа, млн руб", [_fmt_money(v.financial.gross_margin_rub) for v in variants]),
|
||||||
|
("НДС (упрощ.), млн руб", [_fmt_money(v.financial.vat_rub) for v in variants]),
|
||||||
|
(
|
||||||
|
"Прибыль до налога, млн руб",
|
||||||
|
[_fmt_money(v.financial.profit_before_tax_rub) for v in variants],
|
||||||
|
),
|
||||||
|
("Налог на прибыль, млн руб", [_fmt_money(v.financial.profit_tax_rub) for v in variants]),
|
||||||
|
("Чистая прибыль, млн руб", [_fmt_money(v.financial.net_profit_rub) for v in variants]),
|
||||||
|
("ROI (на затраты)", [f"{v.financial.roi * 100:.1f}%" for v in variants]),
|
||||||
|
("Чистая маржа (на выручку)", [f"{v.financial.margin_pct * 100:.1f}%" for v in variants]),
|
||||||
("IRR-proxy", [f"{v.financial.irr * 100:.1f}%" for v in variants]),
|
("IRR-proxy", [f"{v.financial.irr * 100:.1f}%" for v in variants]),
|
||||||
]
|
]
|
||||||
body = "".join(
|
body = "".join(
|
||||||
|
|
@ -132,9 +159,12 @@ def _build_html(variants: Sequence[ConceptVariant]) -> str:
|
||||||
f"<p class='sub'>{html.escape(_SUBTITLE)}</p>"
|
f"<p class='sub'>{html.escape(_SUBTITLE)}</p>"
|
||||||
f"{_teap_table(variants)}"
|
f"{_teap_table(variants)}"
|
||||||
f"{_financial_table(variants)}"
|
f"{_financial_table(variants)}"
|
||||||
"<p class='sub'>IRR-proxy — аннуализированная маржа-на-затраты без "
|
"<p class='sub'>Статический девелоперский расчёт (без дисконтирования / DCF). "
|
||||||
"дисконтирования (не настоящий IRR). Цены и себестоимость — рыночные "
|
"НДС — упрощённая оценка (встроенный НДС в положительной марже, не полная "
|
||||||
"ориентиры, не калиброванная модель ценообразования.</p>"
|
"механика входного/выходного НДС НК РФ). Налог на прибыль — 25% (с 2025). "
|
||||||
|
"IRR-proxy — аннуализированный чистый ROI без дисконтирования (не настоящий "
|
||||||
|
"IRR). Цены и себестоимость — рыночные ориентиры, не калиброванная модель. "
|
||||||
|
"Коммерческие/офисные площади не учитываются (нет в ТЭП).</p>"
|
||||||
"</body></html>"
|
"</body></html>"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,46 @@
|
||||||
"""Generative Design — Stage 1c: simplified financial model.
|
"""Generative Design — Stage 1c: static developer financial model (PR-1, эпик #1881).
|
||||||
|
|
||||||
From the Stage 1c ``TEAP`` block we derive the ``FinancialModel`` contract:
|
We combine **our areas** (from real geometry → :class:`TEAP`) with the **cost
|
||||||
|
completeness** of a reference developer Excel model: a full static cost cascade,
|
||||||
|
VAT, profit tax → net profit + ROI. This is a *correct static* P&L — there is no
|
||||||
|
DCF / discounting yet (that lands in PR-3); no new user inputs are required, only
|
||||||
|
norms + the existing TEAP.
|
||||||
|
|
||||||
* ``revenue_rub`` = residential_area_sqm * sale price per sqm (by housing class).
|
Cascade (in :func:`compute_financial`):
|
||||||
* ``cost_rub`` = total_floor_area_sqm * construction cost per sqm + land cost.
|
|
||||||
* ``gross_margin_rub`` = revenue - cost.
|
|
||||||
* ``irr`` = simplified proxy (margin-on-cost / project years), NO time
|
|
||||||
discounting — this is a static stand-in until the Phase 1 cashflow model lands.
|
|
||||||
|
|
||||||
Prices/costs are coarse RU-market proxies for an MVP (см. константы ниже); they are
|
* **Revenue (GDV)** = residential area × sale price + parking spaces × parking price.
|
||||||
deliberately conservative round numbers, not a calibrated pricing engine. The IRR
|
* **Cost cascade** = construction (residential GFA + parking) + ПИР (design) +
|
||||||
field is a *proxy*: a real internal rate of return needs a dated cashflow series,
|
networks (ТУ) + developer services + contingency + marketing/realtor + land.
|
||||||
which is out of MVP scope — we return an annualised margin ratio so the field is
|
* **БДР / taxes** = gross margin − VAT − profit tax → net profit.
|
||||||
populated with a plausible, monotonic number rather than zero.
|
* **Metrics** = net ROI on cost, net margin on revenue, and an IRR *proxy*.
|
||||||
|
|
||||||
Детерминированно, без LLM / внешних API / БД.
|
VAT — DOCUMENTED SIMPLIFICATION
|
||||||
|
-------------------------------
|
||||||
|
Exact НК РФ VAT mechanics (output VAT on flats vs. input-VAT deductions on
|
||||||
|
construction/materials, эскроу specifics, residential-sale exemptions) are out of
|
||||||
|
PR-1 scope. We use a conservative, honest approximation on the project's
|
||||||
|
*value-added* (gross margin):
|
||||||
|
|
||||||
|
vat = max(0, gross_margin) × VAT_RATE / (1 + VAT_RATE)
|
||||||
|
|
||||||
|
i.e. the embedded VAT inside the positive margin. This deliberately *does not*
|
||||||
|
model the full input/output deduction chain — it is a stand-in that keeps the net
|
||||||
|
P&L plausible and conservative. A loss-making project pays no VAT here. Replace
|
||||||
|
with proper output/input modelling when calibrating (PR-2/PR-3).
|
||||||
|
|
||||||
|
IRR — PROXY UNTIL DCF
|
||||||
|
---------------------
|
||||||
|
A real IRR needs a dated cashflow series (PR-3). We expose ``irr`` as an
|
||||||
|
annualised net-ROI proxy (``roi / _PROJECT_YEARS``, clamped to ±1) and flag it via
|
||||||
|
``irr_is_proxy=True`` so the frontend can show the caveat.
|
||||||
|
|
||||||
|
OFFICES / COMMERCIAL — SKIPPED
|
||||||
|
------------------------------
|
||||||
|
TEAP carries no office/commercial area (only residential + parking), so commercial
|
||||||
|
revenue/cost is intentionally **not** modelled here. Add when TEAP gains the field.
|
||||||
|
|
||||||
|
Prices/costs of housing & СМР stay as the existing hardcoded per-class proxies
|
||||||
|
(calibration is PR-2). Детерминированно, без LLM / внешних API / БД.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
@ -42,7 +68,38 @@ _CONSTRUCTION_COST_PER_SQM: dict[HousingClass, float] = {
|
||||||
"business": 120_000.0,
|
"business": 120_000.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Условный горизонт проекта (лет) для аннуализации margin-on-cost в IRR-proxy.
|
# ── Паркинг (Excel-эталон девелоперской модели) ───────────────────────────────
|
||||||
|
# Цена продажи / себестоимость одного машиноместа, руб.
|
||||||
|
_PARKING_PRICE_PER_SPOT: float = 1_900_000.0 # Excel: цена продажи м/м
|
||||||
|
_PARKING_COST_PER_SPOT: float = 1_800_000.0 # Excel: себестоимость м/м
|
||||||
|
|
||||||
|
# ── Доли каскада затрат (источник — Excel-эталон + RU-рынок) ──────────────────
|
||||||
|
# ПИР (проектирование + экспертиза + РД) как доля от СМР.
|
||||||
|
# Excel: ПИР 147М при СМР 6123М ≈ 2.4% -> округлённо 2.5%.
|
||||||
|
_PIR_PCT_OF_CONSTRUCTION: float = 0.025
|
||||||
|
|
||||||
|
# Сети / подключение по ТУ, руб/кв.м GFA.
|
||||||
|
# Excel: ТУ 330М / 55042 кв.м ≈ 6000 руб/кв.м.
|
||||||
|
_NETWORKS_COST_PER_SQM: float = 6_000.0
|
||||||
|
|
||||||
|
# Услуги заказчика / управление как доля от СМР.
|
||||||
|
# Excel: 76.5М при СМР 6123М ≈ 1.25% -> округлённо 1.5%.
|
||||||
|
_DEVELOPER_SERVICES_PCT: float = 0.015
|
||||||
|
|
||||||
|
# Резерв / непредвиденные как доля от (СМР + ПИР + сети). Excel: 3%.
|
||||||
|
_CONTINGENCY_PCT: float = 0.03
|
||||||
|
|
||||||
|
# Реклама + риэлтор + маркетинг как доля от выручки. Excel: 7%.
|
||||||
|
_MARKETING_REALTOR_PCT: float = 0.07
|
||||||
|
|
||||||
|
# ── Налоги ────────────────────────────────────────────────────────────────────
|
||||||
|
# НДС, ставка. РФ стандартная 20%.
|
||||||
|
_VAT_RATE: float = 0.20
|
||||||
|
# Налог на прибыль организаций. С 01.01.2025 базовая ставка повышена 20% -> 25%
|
||||||
|
# (ФЗ № 176-ФЗ от 12.07.2024). Используем актуальные 25%.
|
||||||
|
_PROFIT_TAX_RATE: float = 0.25
|
||||||
|
|
||||||
|
# Условный горизонт проекта (лет) для аннуализации net-ROI в IRR-proxy.
|
||||||
_PROJECT_YEARS: float = 3.0
|
_PROJECT_YEARS: float = 3.0
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -52,44 +109,92 @@ def compute_financial(
|
||||||
housing_class: HousingClass,
|
housing_class: HousingClass,
|
||||||
land_cost_rub: float | None,
|
land_cost_rub: float | None,
|
||||||
) -> FinancialModel:
|
) -> FinancialModel:
|
||||||
"""Свести ТЭП + класс + стоимость земли в :class:`FinancialModel`.
|
"""Свести ТЭП + класс + стоимость земли в полный статический :class:`FinancialModel`.
|
||||||
|
|
||||||
|
Полный девелоперский каскад: выручка (жильё + паркинг) → каскад затрат (СМР +
|
||||||
|
ПИР + сети + услуги заказчика + резерв + маркетинг + земля) → БДР с НДС и
|
||||||
|
налогом на прибыль → чистая прибыль + ROI. НЕ DCF (PR-3); НДС — упрощение
|
||||||
|
(см. docstring модуля); офисов нет в TEAP — пропущены.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
teap: Stage 1c ТЭП (берём residential_area_sqm и total_floor_area_sqm).
|
teap: Stage 1c ТЭП (residential_area_sqm, total_floor_area_sqm, parking_spaces).
|
||||||
housing_class: задаёт цену продажи и себестоимость СМР.
|
housing_class: задаёт цену продажи и себестоимость СМР.
|
||||||
land_cost_rub: стоимость участка (опционально); None -> 0 в затратах.
|
land_cost_rub: стоимость участка (опционально); None -> 0 в затратах.
|
||||||
"""
|
"""
|
||||||
sale_price = _SALE_PRICE_PER_SQM[housing_class]
|
sale_price = _SALE_PRICE_PER_SQM[housing_class]
|
||||||
construction_cost = _CONSTRUCTION_COST_PER_SQM[housing_class]
|
construction_cost = _CONSTRUCTION_COST_PER_SQM[housing_class]
|
||||||
|
|
||||||
revenue = teap.residential_area_sqm * sale_price
|
# ── Выручка (GDV) ──────────────────────────────────────────────────────────
|
||||||
construction = teap.total_floor_area_sqm * construction_cost
|
revenue_residential = teap.residential_area_sqm * sale_price
|
||||||
land = land_cost_rub if land_cost_rub is not None else 0.0
|
revenue_parking = teap.parking_spaces * _PARKING_PRICE_PER_SPOT
|
||||||
cost = construction + land
|
revenue = revenue_residential + revenue_parking
|
||||||
gross_margin = revenue - cost
|
|
||||||
|
|
||||||
# IRR-proxy: аннуализированная маржа-на-затраты. НЕ настоящий IRR (нет дисконта/
|
# ── Затраты (каскад) ───────────────────────────────────────────────────────
|
||||||
# дат денежных потоков — отложено в Phase 1). Защита от деления на ноль и
|
construction_resid = teap.total_floor_area_sqm * construction_cost
|
||||||
# клампинг в разумный диапазон, чтобы поле было монотонным и читаемым.
|
construction_parking = teap.parking_spaces * _PARKING_COST_PER_SPOT
|
||||||
if cost > 0:
|
construction = construction_resid + construction_parking
|
||||||
margin_on_cost = gross_margin / cost
|
pir = construction * _PIR_PCT_OF_CONSTRUCTION
|
||||||
irr = margin_on_cost / _PROJECT_YEARS
|
networks = teap.total_floor_area_sqm * _NETWORKS_COST_PER_SQM
|
||||||
else:
|
dev_services = construction * _DEVELOPER_SERVICES_PCT
|
||||||
irr = 0.0
|
contingency = (construction + pir + networks) * _CONTINGENCY_PCT
|
||||||
|
marketing = revenue * _MARKETING_REALTOR_PCT
|
||||||
|
land = land_cost_rub if land_cost_rub is not None else 0.0
|
||||||
|
cost = construction + pir + networks + dev_services + contingency + marketing + land
|
||||||
|
|
||||||
|
# ── БДР / налоги → чистая прибыль ─────────────────────────────────────────
|
||||||
|
gross_margin = revenue - cost # валовая (как было)
|
||||||
|
# НДС: УПРОЩЕНО — встроенный НДС в положительной марже (value-added),
|
||||||
|
# не полная механика входного/выходного НДС НК РФ (см. docstring модуля).
|
||||||
|
vat = max(0.0, gross_margin) * _VAT_RATE / (1.0 + _VAT_RATE)
|
||||||
|
profit_before_tax = gross_margin - vat
|
||||||
|
profit_tax = max(0.0, profit_before_tax) * _PROFIT_TAX_RATE
|
||||||
|
net_profit = profit_before_tax - profit_tax
|
||||||
|
|
||||||
|
# ── Метрики ────────────────────────────────────────────────────────────────
|
||||||
|
roi = net_profit / cost if cost > 0 else 0.0 # чистый ROI на затраты
|
||||||
|
margin_pct = net_profit / revenue if revenue > 0 else 0.0 # чистая маржа на выручку
|
||||||
|
|
||||||
|
# IRR-proxy: аннуализированный net-ROI. НЕ настоящий IRR (нет дат/дисконта —
|
||||||
|
# PR-3). Клампинг ±1, чтобы поле было монотонным и читаемым; флаг proxy.
|
||||||
|
irr = roi / _PROJECT_YEARS
|
||||||
irr = max(-1.0, min(1.0, irr))
|
irr = max(-1.0, min(1.0, irr))
|
||||||
|
|
||||||
model = FinancialModel(
|
model = FinancialModel(
|
||||||
|
# legacy summary
|
||||||
revenue_rub=round(revenue, 2),
|
revenue_rub=round(revenue, 2),
|
||||||
cost_rub=round(cost, 2),
|
cost_rub=round(cost, 2),
|
||||||
gross_margin_rub=round(gross_margin, 2),
|
gross_margin_rub=round(gross_margin, 2),
|
||||||
irr=round(irr, 4),
|
irr=round(irr, 4),
|
||||||
|
# revenue breakdown
|
||||||
|
revenue_residential_rub=round(revenue_residential, 2),
|
||||||
|
revenue_parking_rub=round(revenue_parking, 2),
|
||||||
|
# cost cascade
|
||||||
|
construction_rub=round(construction, 2),
|
||||||
|
pir_rub=round(pir, 2),
|
||||||
|
networks_rub=round(networks, 2),
|
||||||
|
developer_services_rub=round(dev_services, 2),
|
||||||
|
contingency_rub=round(contingency, 2),
|
||||||
|
marketing_rub=round(marketing, 2),
|
||||||
|
land_rub=round(land, 2),
|
||||||
|
# БДР / taxes
|
||||||
|
vat_rub=round(vat, 2),
|
||||||
|
profit_before_tax_rub=round(profit_before_tax, 2),
|
||||||
|
profit_tax_rub=round(profit_tax, 2),
|
||||||
|
net_profit_rub=round(net_profit, 2),
|
||||||
|
# metrics
|
||||||
|
roi=round(roi, 4),
|
||||||
|
margin_pct=round(margin_pct, 4),
|
||||||
|
irr_is_proxy=True,
|
||||||
)
|
)
|
||||||
logger.info(
|
logger.info(
|
||||||
"financial: revenue=%.0f cost=%.0f margin=%.0f irr_proxy=%.3f",
|
"financial: revenue=%.0f cost=%.0f gross=%.0f vat=%.0f tax=%.0f net=%.0f roi=%.3f",
|
||||||
model.revenue_rub,
|
model.revenue_rub,
|
||||||
model.cost_rub,
|
model.cost_rub,
|
||||||
model.gross_margin_rub,
|
model.gross_margin_rub,
|
||||||
model.irr,
|
model.vat_rub,
|
||||||
|
model.profit_tax_rub,
|
||||||
|
model.net_profit_rub,
|
||||||
|
model.roi,
|
||||||
)
|
)
|
||||||
return model
|
return model
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -108,8 +108,29 @@ def test_concepts_response_matches_contract_keys() -> None:
|
||||||
"parking_spaces",
|
"parking_spaces",
|
||||||
}
|
}
|
||||||
assert set(variant["financial"].keys()) == {
|
assert set(variant["financial"].keys()) == {
|
||||||
|
# legacy summary (backward-compat)
|
||||||
"revenue_rub",
|
"revenue_rub",
|
||||||
"cost_rub",
|
"cost_rub",
|
||||||
"gross_margin_rub",
|
"gross_margin_rub",
|
||||||
"irr",
|
"irr",
|
||||||
|
# revenue breakdown
|
||||||
|
"revenue_residential_rub",
|
||||||
|
"revenue_parking_rub",
|
||||||
|
# cost cascade
|
||||||
|
"construction_rub",
|
||||||
|
"pir_rub",
|
||||||
|
"networks_rub",
|
||||||
|
"developer_services_rub",
|
||||||
|
"contingency_rub",
|
||||||
|
"marketing_rub",
|
||||||
|
"land_rub",
|
||||||
|
# БДР / taxes
|
||||||
|
"vat_rub",
|
||||||
|
"profit_before_tax_rub",
|
||||||
|
"profit_tax_rub",
|
||||||
|
"net_profit_rub",
|
||||||
|
# metrics
|
||||||
|
"roi",
|
||||||
|
"margin_pct",
|
||||||
|
"irr_is_proxy",
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -67,29 +67,88 @@ def test_teap_empty_placement_is_zeroed() -> None:
|
||||||
assert result.parking_spaces == 0
|
assert result.parking_spaces == 0
|
||||||
|
|
||||||
|
|
||||||
def _teap(residential: float, gfa: float) -> TEAP:
|
def _teap(residential: float, gfa: float, parking: int = 10) -> TEAP:
|
||||||
return TEAP(
|
return TEAP(
|
||||||
built_area_sqm=100.0,
|
built_area_sqm=100.0,
|
||||||
total_floor_area_sqm=gfa,
|
total_floor_area_sqm=gfa,
|
||||||
residential_area_sqm=residential,
|
residential_area_sqm=residential,
|
||||||
apartments_count=10,
|
apartments_count=10,
|
||||||
density=1.0,
|
density=1.0,
|
||||||
parking_spaces=10,
|
parking_spaces=parking,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_financial_revenue_cost_margin() -> None:
|
def test_financial_revenue_includes_parking() -> None:
|
||||||
t = _teap(residential=1000.0, gfa=1300.0)
|
t = _teap(residential=1000.0, gfa=1300.0, parking=10)
|
||||||
model = financial.compute_financial(
|
model = financial.compute_financial(
|
||||||
teap=t, housing_class="comfort", land_cost_rub=50_000_000.0
|
teap=t, housing_class="comfort", land_cost_rub=50_000_000.0
|
||||||
)
|
)
|
||||||
# revenue = 1000 * 145_000.
|
# revenue = жильё (1000 * 145_000) + паркинг (10 * 1_900_000).
|
||||||
assert model.revenue_rub == 1000.0 * 145_000.0
|
assert model.revenue_residential_rub == 1000.0 * 145_000.0
|
||||||
# cost = 1300 * 88_000 + land.
|
assert model.revenue_parking_rub == 10 * 1_900_000.0
|
||||||
assert model.cost_rub == 1300.0 * 88_000.0 + 50_000_000.0
|
assert model.revenue_rub == model.revenue_residential_rub + model.revenue_parking_rub
|
||||||
|
|
||||||
|
|
||||||
|
def test_financial_cost_cascade_includes_all_lines() -> None:
|
||||||
|
t = _teap(residential=1000.0, gfa=1300.0, parking=10)
|
||||||
|
model = financial.compute_financial(
|
||||||
|
teap=t, housing_class="comfort", land_cost_rub=50_000_000.0
|
||||||
|
)
|
||||||
|
# СМР = GFA*СМР + паркинг*себест.
|
||||||
|
assert model.construction_rub == 1300.0 * 88_000.0 + 10 * 1_800_000.0
|
||||||
|
# Каждая статья каскада > 0 при ненулевых вводных.
|
||||||
|
assert model.pir_rub > 0
|
||||||
|
assert model.networks_rub > 0
|
||||||
|
assert model.developer_services_rub > 0
|
||||||
|
assert model.contingency_rub > 0
|
||||||
|
assert model.marketing_rub > 0
|
||||||
|
assert model.land_rub == 50_000_000.0
|
||||||
|
# Итог затрат = сумма всех статей.
|
||||||
|
expected_cost = (
|
||||||
|
model.construction_rub
|
||||||
|
+ model.pir_rub
|
||||||
|
+ model.networks_rub
|
||||||
|
+ model.developer_services_rub
|
||||||
|
+ model.contingency_rub
|
||||||
|
+ model.marketing_rub
|
||||||
|
+ model.land_rub
|
||||||
|
)
|
||||||
|
assert abs(model.cost_rub - expected_cost) < 1.0
|
||||||
assert model.gross_margin_rub == model.revenue_rub - model.cost_rub
|
assert model.gross_margin_rub == model.revenue_rub - model.cost_rub
|
||||||
|
|
||||||
|
|
||||||
|
def test_financial_net_profit_below_gross_margin_when_profitable() -> None:
|
||||||
|
# Прибыльный сценарий: налоги срезают валовую маржу -> net < gross.
|
||||||
|
t = _teap(residential=10_000.0, gfa=11_000.0, parking=50)
|
||||||
|
model = financial.compute_financial(teap=t, housing_class="comfort", land_cost_rub=None)
|
||||||
|
assert model.gross_margin_rub > 0
|
||||||
|
assert model.vat_rub > 0
|
||||||
|
assert model.profit_tax_rub > 0
|
||||||
|
assert model.net_profit_rub < model.profit_before_tax_rub < model.gross_margin_rub
|
||||||
|
# ROI и маржа% согласованы с чистой прибылью (поля округлены до 4 знаков).
|
||||||
|
assert model.roi == round(model.net_profit_rub / model.cost_rub, 4)
|
||||||
|
assert model.margin_pct == round(model.net_profit_rub / model.revenue_rub, 4)
|
||||||
|
|
||||||
|
|
||||||
|
def test_financial_no_profit_tax_on_loss() -> None:
|
||||||
|
# Убыток: налог на прибыль = 0, НДС = 0; net == pbt == gross_margin.
|
||||||
|
t = _teap(residential=1.0, gfa=10_000.0, parking=0)
|
||||||
|
model = financial.compute_financial(teap=t, housing_class="business", land_cost_rub=None)
|
||||||
|
assert model.gross_margin_rub < 0
|
||||||
|
assert model.profit_tax_rub == 0.0
|
||||||
|
assert model.vat_rub == 0.0
|
||||||
|
assert model.profit_before_tax_rub == model.gross_margin_rub
|
||||||
|
assert model.net_profit_rub == model.gross_margin_rub
|
||||||
|
|
||||||
|
|
||||||
|
def test_financial_parking_zero_yields_zero_parking_lines() -> None:
|
||||||
|
t = _teap(residential=1000.0, gfa=1300.0, parking=0)
|
||||||
|
model = financial.compute_financial(teap=t, housing_class="comfort", land_cost_rub=None)
|
||||||
|
assert model.revenue_parking_rub == 0.0
|
||||||
|
# СМР без паркинговой составляющей = ровно GFA*СМР.
|
||||||
|
assert model.construction_rub == 1300.0 * 88_000.0
|
||||||
|
|
||||||
|
|
||||||
def test_financial_land_cost_optional() -> None:
|
def test_financial_land_cost_optional() -> None:
|
||||||
t = _teap(residential=1000.0, gfa=1300.0)
|
t = _teap(residential=1000.0, gfa=1300.0)
|
||||||
no_land = financial.compute_financial(teap=t, housing_class="comfort", land_cost_rub=None)
|
no_land = financial.compute_financial(teap=t, housing_class="comfort", land_cost_rub=None)
|
||||||
|
|
@ -98,17 +157,33 @@ def test_financial_land_cost_optional() -> None:
|
||||||
)
|
)
|
||||||
# Земля увеличивает затраты ровно на свою стоимость.
|
# Земля увеличивает затраты ровно на свою стоимость.
|
||||||
assert with_land.cost_rub - no_land.cost_rub == 10_000_000.0
|
assert with_land.cost_rub - no_land.cost_rub == 10_000_000.0
|
||||||
|
assert with_land.land_rub == 10_000_000.0
|
||||||
|
assert no_land.land_rub == 0.0
|
||||||
|
|
||||||
|
|
||||||
def test_financial_irr_proxy_clamped() -> None:
|
def test_financial_irr_proxy_clamped_and_flagged() -> None:
|
||||||
# Огромная маржа -> irr-proxy зажат в [-1, 1].
|
# Огромная маржа -> irr-proxy зажат в [-1, 1]; флаг proxy выставлен.
|
||||||
t = _teap(residential=100_000.0, gfa=1.0)
|
t = _teap(residential=100_000.0, gfa=1.0, parking=0)
|
||||||
model = financial.compute_financial(teap=t, housing_class="business", land_cost_rub=None)
|
model = financial.compute_financial(teap=t, housing_class="business", land_cost_rub=None)
|
||||||
assert -1.0 <= model.irr <= 1.0
|
assert -1.0 <= model.irr <= 1.0
|
||||||
|
assert model.irr_is_proxy is True
|
||||||
|
|
||||||
|
|
||||||
def test_financial_zero_cost_no_division_error() -> None:
|
def test_financial_zero_cost_no_division_error() -> None:
|
||||||
t = _teap(residential=0.0, gfa=0.0)
|
t = _teap(residential=0.0, gfa=0.0, parking=0)
|
||||||
model = financial.compute_financial(teap=t, housing_class="comfort", land_cost_rub=None)
|
model = financial.compute_financial(teap=t, housing_class="comfort", land_cost_rub=None)
|
||||||
assert model.irr == 0.0
|
|
||||||
assert model.cost_rub == 0.0
|
assert model.cost_rub == 0.0
|
||||||
|
assert model.revenue_rub == 0.0
|
||||||
|
assert model.roi == 0.0
|
||||||
|
assert model.margin_pct == 0.0
|
||||||
|
assert model.irr == 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_financial_backward_compat_fields_present() -> None:
|
||||||
|
t = _teap(residential=1000.0, gfa=1300.0)
|
||||||
|
model = financial.compute_financial(teap=t, housing_class="comfort", land_cost_rub=None)
|
||||||
|
# Старые summary-поля контракта остаются.
|
||||||
|
assert isinstance(model.revenue_rub, float)
|
||||||
|
assert isinstance(model.cost_rub, float)
|
||||||
|
assert isinstance(model.gross_margin_rub, float)
|
||||||
|
assert isinstance(model.irr, float)
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import {
|
||||||
STRATEGY_HINTS,
|
STRATEGY_HINTS,
|
||||||
STRATEGY_LABELS,
|
STRATEGY_LABELS,
|
||||||
type ConceptVariant,
|
type ConceptVariant,
|
||||||
|
type FinancialModel,
|
||||||
} from "@/lib/concept-api";
|
} from "@/lib/concept-api";
|
||||||
import type { Polygon } from "geojson";
|
import type { Polygon } from "geojson";
|
||||||
|
|
||||||
|
|
@ -75,10 +76,10 @@ interface PanelProps {
|
||||||
|
|
||||||
function VariantPanel({ parcel, variant }: PanelProps) {
|
function VariantPanel({ parcel, variant }: PanelProps) {
|
||||||
const { teap, financial } = variant;
|
const { teap, financial } = variant;
|
||||||
const marginPositive =
|
const netPositive =
|
||||||
financial.gross_margin_rub > 0
|
financial.net_profit_rub > 0
|
||||||
? true
|
? true
|
||||||
: financial.gross_margin_rub < 0
|
: financial.net_profit_rub < 0
|
||||||
? false
|
? false
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
|
@ -96,8 +97,9 @@ function VariantPanel({ parcel, variant }: PanelProps) {
|
||||||
<div style={{ fontSize: 15, fontWeight: 600 }}>
|
<div style={{ fontSize: 15, fontWeight: 600 }}>
|
||||||
{STRATEGY_LABELS[variant.strategy]}:{" "}
|
{STRATEGY_LABELS[variant.strategy]}:{" "}
|
||||||
{formatInt(teap.total_floor_area_sqm)} м² надземной площади ·{" "}
|
{formatInt(teap.total_floor_area_sqm)} м² надземной площади ·{" "}
|
||||||
{formatInt(teap.apartments_count)} квартир · валовая прибыль{" "}
|
{formatInt(teap.apartments_count)} квартир · чистая прибыль{" "}
|
||||||
{formatMoneyCompact(financial.gross_margin_rub)}
|
{formatMoneyCompact(financial.net_profit_rub)} · ROI{" "}
|
||||||
|
{formatPct(financial.roi)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -108,8 +110,9 @@ function VariantPanel({ parcel, variant }: PanelProps) {
|
||||||
color: "var(--fg-on-dark-muted)",
|
color: "var(--fg-on-dark-muted)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{STRATEGY_HINTS[variant.strategy]} IRR-proxy{" "}
|
{STRATEGY_HINTS[variant.strategy]} IRR {formatPct(financial.irr)}
|
||||||
{formatPct(financial.irr)} · плотность (FAR){" "}
|
{financial.irr_is_proxy ? " (оценочный, не DCF)" : ""} · плотность
|
||||||
|
(FAR){" "}
|
||||||
{teap.density.toLocaleString("ru-RU", {
|
{teap.density.toLocaleString("ru-RU", {
|
||||||
minimumFractionDigits: 2,
|
minimumFractionDigits: 2,
|
||||||
maximumFractionDigits: 2,
|
maximumFractionDigits: 2,
|
||||||
|
|
@ -180,7 +183,7 @@ function VariantPanel({ parcel, variant }: PanelProps) {
|
||||||
{/* Финмодель */}
|
{/* Финмодель */}
|
||||||
<Section
|
<Section
|
||||||
title="Финансовая модель"
|
title="Финансовая модель"
|
||||||
subtitle="Оценка выручки и доходности по ценам класса жилья и стоимости участка."
|
subtitle="Статический девелоперский расчёт: выручка, каскад затрат, НДС и налог на прибыль → чистая прибыль и ROI."
|
||||||
right={<ConceptExportButtons variant={variant} />}
|
right={<ConceptExportButtons variant={variant} />}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|
@ -191,40 +194,188 @@ function VariantPanel({ parcel, variant }: PanelProps) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<KpiCard
|
<KpiCard
|
||||||
label="Выручка"
|
label="Выручка (GDV)"
|
||||||
value={formatMoneyCompact(financial.revenue_rub)}
|
value={formatMoneyCompact(financial.revenue_rub)}
|
||||||
/>
|
/>
|
||||||
<KpiCard
|
<KpiCard
|
||||||
label="Затраты"
|
label="Итого затраты"
|
||||||
value={formatMoneyCompact(financial.cost_rub)}
|
value={formatMoneyCompact(financial.cost_rub)}
|
||||||
/>
|
/>
|
||||||
<KpiCard
|
<KpiCard
|
||||||
label="Валовая прибыль"
|
label="Чистая прибыль"
|
||||||
value={formatMoneyCompact(financial.gross_margin_rub)}
|
value={formatMoneyCompact(financial.net_profit_rub)}
|
||||||
delta={{
|
delta={{
|
||||||
value:
|
value:
|
||||||
marginPositive === true
|
netPositive === true
|
||||||
? "Положительная маржа"
|
? "Положительная (после НДС и налога на прибыль)"
|
||||||
: marginPositive === false
|
: netPositive === false
|
||||||
? "Отрицательная маржа"
|
? "Отрицательная (после НДС и налога на прибыль)"
|
||||||
: "Нулевая маржа",
|
: "Нулевая",
|
||||||
positive: marginPositive,
|
positive: netPositive,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<KpiCard
|
<KpiCard
|
||||||
label="IRR-proxy"
|
label="ROI на затраты"
|
||||||
value={formatPct(financial.irr)}
|
value={formatPct(financial.roi)}
|
||||||
delta={{
|
delta={{
|
||||||
value: "Упрощённая оценка: маржа на затраты, без дисконтирования",
|
value: `Чистая маржа на выручку ${formatPct(financial.margin_pct)}`,
|
||||||
positive: null,
|
positive: null,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Каскад затрат + БДР — под аккордеоном (плотность > выкладки) */}
|
||||||
|
<details style={{ marginTop: 12 }}>
|
||||||
|
<summary
|
||||||
|
style={{
|
||||||
|
cursor: "pointer",
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: 500,
|
||||||
|
color: "var(--fg-secondary)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Подробнее — каскад затрат и расчёт прибыли
|
||||||
|
</summary>
|
||||||
|
<div style={{ marginTop: 12 }}>
|
||||||
|
<FinancialCascadeTable financial={financial} />
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
marginTop: 12,
|
||||||
|
fontSize: 12,
|
||||||
|
color: "var(--fg-tertiary)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Статический расчёт без дисконтирования (DCF — следующий этап). НДС
|
||||||
|
— упрощённая оценка (встроенный НДС в положительной марже, не
|
||||||
|
полная механика входного/выходного НДС НК РФ). Налог на прибыль —
|
||||||
|
25% (с 2025 года). IRR{" "}
|
||||||
|
{financial.irr_is_proxy
|
||||||
|
? "— оценочный (аннуализированный чистый ROI), не настоящий IRR."
|
||||||
|
: "."}{" "}
|
||||||
|
Цены и себестоимость — рыночные ориентиры, не калиброванная
|
||||||
|
модель. Коммерческие и офисные площади не учитываются.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
</Section>
|
</Section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Cost cascade / P&L detail table ────────────────────────────────────────────
|
||||||
|
|
||||||
|
function CascadeRow({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
emphasis = false,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
emphasis?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
style={{
|
||||||
|
padding: "6px 8px",
|
||||||
|
borderBottom: "1px solid var(--border-soft)",
|
||||||
|
color: emphasis ? "var(--fg-primary)" : "var(--fg-secondary)",
|
||||||
|
fontWeight: emphasis ? 600 : 400,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
style={{
|
||||||
|
padding: "6px 8px",
|
||||||
|
borderBottom: "1px solid var(--border-soft)",
|
||||||
|
textAlign: "right",
|
||||||
|
fontWeight: emphasis ? 600 : 400,
|
||||||
|
fontVariantNumeric: "tabular-nums",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FinancialCascadeTable({ financial }: { financial: FinancialModel }) {
|
||||||
|
return (
|
||||||
|
<table style={{ width: "100%", borderCollapse: "collapse", fontSize: 13 }}>
|
||||||
|
<tbody>
|
||||||
|
<CascadeRow
|
||||||
|
label="Выручка — жильё"
|
||||||
|
value={formatMoneyCompact(financial.revenue_residential_rub)}
|
||||||
|
/>
|
||||||
|
<CascadeRow
|
||||||
|
label="Выручка — паркинг"
|
||||||
|
value={formatMoneyCompact(financial.revenue_parking_rub)}
|
||||||
|
/>
|
||||||
|
<CascadeRow
|
||||||
|
label="Выручка (GDV)"
|
||||||
|
value={formatMoneyCompact(financial.revenue_rub)}
|
||||||
|
emphasis
|
||||||
|
/>
|
||||||
|
<CascadeRow
|
||||||
|
label="СМР"
|
||||||
|
value={formatMoneyCompact(financial.construction_rub)}
|
||||||
|
/>
|
||||||
|
<CascadeRow
|
||||||
|
label="ПИР (проектирование)"
|
||||||
|
value={formatMoneyCompact(financial.pir_rub)}
|
||||||
|
/>
|
||||||
|
<CascadeRow
|
||||||
|
label="Сети (ТУ)"
|
||||||
|
value={formatMoneyCompact(financial.networks_rub)}
|
||||||
|
/>
|
||||||
|
<CascadeRow
|
||||||
|
label="Услуги заказчика"
|
||||||
|
value={formatMoneyCompact(financial.developer_services_rub)}
|
||||||
|
/>
|
||||||
|
<CascadeRow
|
||||||
|
label="Резерв"
|
||||||
|
value={formatMoneyCompact(financial.contingency_rub)}
|
||||||
|
/>
|
||||||
|
<CascadeRow
|
||||||
|
label="Маркетинг и риэлтор"
|
||||||
|
value={formatMoneyCompact(financial.marketing_rub)}
|
||||||
|
/>
|
||||||
|
<CascadeRow
|
||||||
|
label="Земля"
|
||||||
|
value={formatMoneyCompact(financial.land_rub)}
|
||||||
|
/>
|
||||||
|
<CascadeRow
|
||||||
|
label="Итого затраты"
|
||||||
|
value={formatMoneyCompact(financial.cost_rub)}
|
||||||
|
emphasis
|
||||||
|
/>
|
||||||
|
<CascadeRow
|
||||||
|
label="Валовая маржа"
|
||||||
|
value={formatMoneyCompact(financial.gross_margin_rub)}
|
||||||
|
/>
|
||||||
|
<CascadeRow
|
||||||
|
label="НДС (упрощённо)"
|
||||||
|
value={formatMoneyCompact(financial.vat_rub)}
|
||||||
|
/>
|
||||||
|
<CascadeRow
|
||||||
|
label="Прибыль до налога"
|
||||||
|
value={formatMoneyCompact(financial.profit_before_tax_rub)}
|
||||||
|
/>
|
||||||
|
<CascadeRow
|
||||||
|
label="Налог на прибыль (25%)"
|
||||||
|
value={formatMoneyCompact(financial.profit_tax_rub)}
|
||||||
|
/>
|
||||||
|
<CascadeRow
|
||||||
|
label="Чистая прибыль"
|
||||||
|
value={formatMoneyCompact(financial.net_profit_rub)}
|
||||||
|
emphasis
|
||||||
|
/>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Component ─────────────────────────────────────────────────────────────────
|
// ── Component ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
|
||||||
|
|
@ -3484,7 +3484,18 @@ export interface components {
|
||||||
*/
|
*/
|
||||||
rate_ms: number;
|
rate_ms: number;
|
||||||
};
|
};
|
||||||
/** FinancialModel */
|
/**
|
||||||
|
* FinancialModel
|
||||||
|
* @description Static developer P&L for a concept variant (PR-1, эпик #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.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
FinancialModel: {
|
FinancialModel: {
|
||||||
/** Revenue Rub */
|
/** Revenue Rub */
|
||||||
revenue_rub: number;
|
revenue_rub: number;
|
||||||
|
|
@ -3494,6 +3505,41 @@ export interface components {
|
||||||
gross_margin_rub: number;
|
gross_margin_rub: number;
|
||||||
/** Irr */
|
/** Irr */
|
||||||
irr: number;
|
irr: number;
|
||||||
|
/** Revenue Residential Rub */
|
||||||
|
revenue_residential_rub: number;
|
||||||
|
/** Revenue Parking Rub */
|
||||||
|
revenue_parking_rub: number;
|
||||||
|
/** Construction Rub */
|
||||||
|
construction_rub: number;
|
||||||
|
/** Pir Rub */
|
||||||
|
pir_rub: number;
|
||||||
|
/** Networks Rub */
|
||||||
|
networks_rub: number;
|
||||||
|
/** Developer Services Rub */
|
||||||
|
developer_services_rub: number;
|
||||||
|
/** Contingency Rub */
|
||||||
|
contingency_rub: number;
|
||||||
|
/** Marketing Rub */
|
||||||
|
marketing_rub: number;
|
||||||
|
/** Land Rub */
|
||||||
|
land_rub: number;
|
||||||
|
/** Vat Rub */
|
||||||
|
vat_rub: number;
|
||||||
|
/** Profit Before Tax Rub */
|
||||||
|
profit_before_tax_rub: number;
|
||||||
|
/** Profit Tax Rub */
|
||||||
|
profit_tax_rub: number;
|
||||||
|
/** Net Profit Rub */
|
||||||
|
net_profit_rub: number;
|
||||||
|
/** Roi */
|
||||||
|
roi: number;
|
||||||
|
/** Margin Pct */
|
||||||
|
margin_pct: number;
|
||||||
|
/**
|
||||||
|
* Irr Is Proxy
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
irr_is_proxy: boolean;
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* GroundedIn
|
* GroundedIn
|
||||||
|
|
|
||||||
|
|
@ -49,10 +49,31 @@ export interface Teap {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FinancialModel {
|
export interface FinancialModel {
|
||||||
|
// Legacy summary (backward-compat)
|
||||||
revenue_rub: number;
|
revenue_rub: number;
|
||||||
cost_rub: number;
|
cost_rub: number;
|
||||||
gross_margin_rub: number;
|
gross_margin_rub: number;
|
||||||
irr: number;
|
irr: number;
|
||||||
|
// Revenue breakdown
|
||||||
|
revenue_residential_rub: number;
|
||||||
|
revenue_parking_rub: number;
|
||||||
|
// Cost cascade
|
||||||
|
construction_rub: number;
|
||||||
|
pir_rub: number;
|
||||||
|
networks_rub: number;
|
||||||
|
developer_services_rub: number;
|
||||||
|
contingency_rub: number;
|
||||||
|
marketing_rub: number;
|
||||||
|
land_rub: number;
|
||||||
|
// БДР / taxes → net profit
|
||||||
|
vat_rub: number;
|
||||||
|
profit_before_tax_rub: number;
|
||||||
|
profit_tax_rub: number;
|
||||||
|
net_profit_rub: number;
|
||||||
|
// Metrics
|
||||||
|
roi: number;
|
||||||
|
margin_pct: number;
|
||||||
|
irr_is_proxy: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConceptVariant {
|
export interface ConceptVariant {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue