"""Generative Design — Stage 1c: static developer financial model (PR-1, эпик #1881). 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. Cascade (in :func:`compute_financial`): * **Revenue (GDV)** = residential area × sale price + parking spaces × parking price. * **Cost cascade** = construction (residential GFA + parking) + ПИР (design) + networks (ТУ) + developer services + contingency + marketing/realtor + land. * **БДР / taxes** = gross margin − VAT − profit tax → net profit. * **Metrics** = net ROI on cost, net margin on revenue, and an IRR *proxy*. 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 import logging from typing import Literal from app.schemas.concept import TEAP, FinancialModel logger = logging.getLogger(__name__) HousingClass = Literal["econom", "comfort", "business"] # ── Цена продажи жилья, руб/кв.м (proxy рынка ЕКБ/региона, упрощённо) ────────── _SALE_PRICE_PER_SQM: dict[HousingClass, float] = { "econom": 110_000.0, "comfort": 145_000.0, "business": 210_000.0, } # ── Себестоимость СМР, руб/кв.м общей площади (выше класс -> дороже отделка/инж) ─ _CONSTRUCTION_COST_PER_SQM: dict[HousingClass, float] = { "econom": 72_000.0, "comfort": 88_000.0, "business": 120_000.0, } # ── Паркинг (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 def compute_financial( *, teap: TEAP, housing_class: HousingClass, land_cost_rub: float | None, ) -> FinancialModel: """Свести ТЭП + класс + стоимость земли в полный статический :class:`FinancialModel`. Полный девелоперский каскад: выручка (жильё + паркинг) → каскад затрат (СМР + ПИР + сети + услуги заказчика + резерв + маркетинг + земля) → БДР с НДС и налогом на прибыль → чистая прибыль + ROI. НЕ DCF (PR-3); НДС — упрощение (см. docstring модуля); офисов нет в TEAP — пропущены. Args: teap: Stage 1c ТЭП (residential_area_sqm, total_floor_area_sqm, parking_spaces). housing_class: задаёт цену продажи и себестоимость СМР. land_cost_rub: стоимость участка (опционально); None -> 0 в затратах. """ sale_price = _SALE_PRICE_PER_SQM[housing_class] construction_cost = _CONSTRUCTION_COST_PER_SQM[housing_class] # ── Выручка (GDV) ────────────────────────────────────────────────────────── revenue_residential = teap.residential_area_sqm * sale_price revenue_parking = teap.parking_spaces * _PARKING_PRICE_PER_SPOT revenue = revenue_residential + revenue_parking # ── Затраты (каскад) ─────────────────────────────────────────────────────── construction_resid = teap.total_floor_area_sqm * construction_cost construction_parking = teap.parking_spaces * _PARKING_COST_PER_SPOT construction = construction_resid + construction_parking pir = construction * _PIR_PCT_OF_CONSTRUCTION networks = teap.total_floor_area_sqm * _NETWORKS_COST_PER_SQM dev_services = construction * _DEVELOPER_SERVICES_PCT 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)) model = FinancialModel( # legacy summary revenue_rub=round(revenue, 2), cost_rub=round(cost, 2), gross_margin_rub=round(gross_margin, 2), 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( "financial: revenue=%.0f cost=%.0f gross=%.0f vat=%.0f tax=%.0f net=%.0f roi=%.3f", model.revenue_rub, model.cost_rub, model.gross_margin_rub, model.vat_rub, model.profit_tax_rub, model.net_profit_rub, model.roi, ) return model __all__ = ["HousingClass", "compute_financial"]