- Engine: Add ppa_revenue_cr, mcp_revenue_cr, tariff, units to PnLRow - Engine: Split PPA vs MCP revenue in P&L computation - Web: Collapsible rows for PPA/MCP Revenue and Opex - Web: Highlighted rows (Total Revenue, EBITDA, EBIT, PBT, PAT) - Web: Units above Tariff in breakdown, bg-blue-50 highlight - Fix sticky column z-index for horizontal scroll - CLAUDE.md: Add project documentation Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
149 lines
No EOL
4.7 KiB
Python
149 lines
No EOL
4.7 KiB
Python
"""25-year Profit & Loss statement.
|
|
|
|
Revenue
|
|
- OpEx (O&M, insurance, land lease, AM fee, misc)
|
|
= EBITDA
|
|
- Depreciation (book SLM)
|
|
= EBIT
|
|
- Interest expense (from debt schedule)
|
|
= PBT
|
|
- Current tax (115BAA)
|
|
= PAT
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from remodel_engine.schemas.financial import OpexConfig, PnLRow
|
|
|
|
|
|
def compute_ppa_units(
|
|
gen_mwh_by_year: list[float],
|
|
aux_pct: float,
|
|
tx_loss_pct: float,
|
|
dsm_loss_pct: float,
|
|
) -> list[float]:
|
|
"""Net PPA-billable units for each year in MWh."""
|
|
return [
|
|
round(mwh * (1 - aux_pct) * (1 - tx_loss_pct) * (1 - dsm_loss_pct), 2)
|
|
for mwh in gen_mwh_by_year
|
|
]
|
|
|
|
|
|
def compute_revenue(
|
|
ac_gen_mwh_by_year: list[float],
|
|
tariff_inr_per_kwh: float,
|
|
aux_pct: float,
|
|
tx_loss_pct: float,
|
|
dsm_loss_pct: float,
|
|
bad_debt_pct: float = 0.0,
|
|
) -> list[float]:
|
|
"""Net revenue for each year in INR Crore.
|
|
|
|
net_injection = generation * (1 - aux) * (1 - tx_loss) * (1 - dsm_loss)
|
|
revenue = net_injection * tariff / 1000 (kWh from MWh: already in kWh via MWh*1000)
|
|
"""
|
|
rev = []
|
|
for mwh in ac_gen_mwh_by_year:
|
|
net_kwh = mwh * 1000.0 * (1 - aux_pct) * (1 - tx_loss_pct) * (1 - dsm_loss_pct)
|
|
gross_rev = net_kwh * tariff_inr_per_kwh / 1e7 # INR → Cr (1 Cr = 1e7 INR)
|
|
rev.append(round(gross_rev * (1 - bad_debt_pct), 4))
|
|
return rev
|
|
|
|
|
|
def compute_opex(
|
|
revenue_by_year: list[float],
|
|
solar_mw: float,
|
|
wind_mw: float,
|
|
bess_mwh: float,
|
|
base_capex_cr: float,
|
|
config: OpexConfig,
|
|
) -> list[float]:
|
|
"""Total opex for each year in INR Crore (excluding depreciation and interest)."""
|
|
opex: list[float] = []
|
|
for y, rev in enumerate(revenue_by_year):
|
|
esc = (1.0 + config.om_escalation_pct) ** y
|
|
om = (
|
|
config.om_solar_cr_per_mw * solar_mw
|
|
+ config.om_wind_cr_per_mw * wind_mw
|
|
+ config.om_bess_cr_per_mwh * bess_mwh
|
|
) * esc
|
|
insurance = config.insurance_pct_of_capex * base_capex_cr
|
|
land = config.land_lease_cr * esc
|
|
am_fee = config.am_fee_pct_of_revenue * rev
|
|
misc = config.misc_cr * esc
|
|
opex.append(round(om + insurance + land + am_fee + misc, 4))
|
|
return opex
|
|
|
|
|
|
def build_pnl(
|
|
revenue_by_year: list[float],
|
|
gen_mwh_by_year: list[float],
|
|
tariff_inr_per_kwh: float,
|
|
mcp_revenue_by_year: list[float],
|
|
mcp_units_by_year: list[float],
|
|
opex_by_year: list[float],
|
|
book_depr_by_year: list[float],
|
|
interest_by_year: list[float],
|
|
current_tax_by_year: list[float],
|
|
deferred_tax_by_year: list[float],
|
|
solar_mw: float,
|
|
wind_mw: float,
|
|
bess_mwh: float,
|
|
base_capex_cr: float,
|
|
config: OpexConfig,
|
|
) -> list[PnLRow]:
|
|
"""Build 25-year P&L row list."""
|
|
n = len(revenue_by_year)
|
|
rows: list[PnLRow] = []
|
|
for y in range(n):
|
|
ppa_rev = revenue_by_year[y]
|
|
ppa_units = gen_mwh_by_year[y] if y < len(gen_mwh_by_year) else 0.0
|
|
mcp_rev = mcp_revenue_by_year[y] if y < len(mcp_revenue_by_year) else 0.0
|
|
mcp_units = mcp_units_by_year[y] if y < len(mcp_units_by_year) else 0.0
|
|
total_rev = ppa_rev + mcp_rev
|
|
|
|
esc = (1.0 + config.om_escalation_pct) ** y
|
|
om = (
|
|
config.om_solar_cr_per_mw * solar_mw
|
|
+ config.om_wind_cr_per_mw * wind_mw
|
|
+ config.om_bess_cr_per_mwh * bess_mwh
|
|
) * esc
|
|
ins = config.insurance_pct_of_capex * base_capex_cr
|
|
land = config.land_lease_cr * esc
|
|
am = config.am_fee_pct_of_revenue * total_rev
|
|
misc = config.misc_cr * esc
|
|
|
|
opex_total = om + ins + land + am + misc
|
|
ebitda = total_rev - opex_total
|
|
depr = book_depr_by_year[y]
|
|
ebit = ebitda - depr
|
|
interest = interest_by_year[y]
|
|
pbt = ebit - interest
|
|
ct = current_tax_by_year[y]
|
|
dt = deferred_tax_by_year[y]
|
|
pat = pbt - ct
|
|
|
|
rows.append(PnLRow(
|
|
year=y + 1,
|
|
revenue_cr=round(total_rev, 4),
|
|
ppa_revenue_cr=round(ppa_rev, 4),
|
|
mcp_revenue_cr=round(mcp_rev, 4),
|
|
ppa_tariff_inr_per_kwh=tariff_inr_per_kwh,
|
|
ppa_units_mwh=round(ppa_units, 2),
|
|
mcp_units_mwh=round(mcp_units, 2),
|
|
opex_total_cr=round(opex_total, 4),
|
|
om_cr=round(om, 4),
|
|
insurance_cr=round(ins, 4),
|
|
land_lease_cr=round(land, 4),
|
|
am_fee_cr=round(am, 4),
|
|
misc_opex_cr=round(misc, 4),
|
|
ebitda_cr=round(ebitda, 4),
|
|
depreciation_book_cr=round(depr, 4),
|
|
ebit_cr=round(ebit, 4),
|
|
interest_cr=round(interest, 4),
|
|
pbt_cr=round(pbt, 4),
|
|
tax_cr=round(ct, 4),
|
|
pat_cr=round(pat, 4),
|
|
deferred_tax_cr=round(dt, 4),
|
|
))
|
|
return rows |