mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-27 23:35:01 +00:00
121 lines
4.5 KiB
Python
121 lines
4.5 KiB
Python
"""The Scenario Export sheet shape (ADR-0065).
|
|
|
|
Pure logic: given the persisted, override-resolved data for the Properties in one
|
|
Scenario, lay out the wide export sheet — property identity + Effective-EPC
|
|
descriptive fields, each measure's cost pivoted onto its own column against a
|
|
frozen column contract, savings summed per Property, and the Plan's post-works
|
|
figures. No I/O: the repository reads the rows and the infrastructure layer
|
|
renders the workbook; this module only shapes one sheet's rows.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Any, Optional, Sequence
|
|
|
|
# The measure cost columns always present on every sheet, in a stable order, so
|
|
# every scenario sheet in the workbook shares one column contract regardless of
|
|
# which measures a given selection happens to contain (ADR-0065). A Property that
|
|
# lacks a measure carries a blank in that column, never a missing column.
|
|
MEASURE_COLUMNS: tuple[str, ...] = (
|
|
"suspended_floor_insulation",
|
|
"solid_floor_insulation",
|
|
"external_wall_insulation",
|
|
"internal_wall_insulation",
|
|
"cavity_wall_insulation",
|
|
"loft_insulation",
|
|
"flat_roof_insulation",
|
|
"room_roof_insulation",
|
|
"secondary_glazing",
|
|
"double_glazing",
|
|
"solar_pv",
|
|
"high_heat_retention_storage_heaters",
|
|
"air_source_heat_pump",
|
|
"boiler_upgrade",
|
|
"gas_boiler_upgrade",
|
|
"roomstat_programmer_trvs",
|
|
"time_temperature_zone_control",
|
|
"low_energy_lighting",
|
|
"mechanical_ventilation",
|
|
"system_tune_up",
|
|
"system_tune_up_zoned",
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ExportMeasure:
|
|
"""One selected measure on a Property's default Plan for the Scenario: the
|
|
measure type (the pivot column), its installed cost, and the SAP / carbon /
|
|
energy / bill savings attributed to it. ``includes_battery`` distinguishes a
|
|
solar-with-battery Option so it pivots to its own column (ADR-0065)."""
|
|
|
|
measure_type: str
|
|
estimated_cost: Optional[float] = None
|
|
sap_points: Optional[float] = None
|
|
co2_equivalent_savings: Optional[float] = None
|
|
kwh_savings: Optional[float] = None
|
|
energy_cost_savings: Optional[float] = None
|
|
includes_battery: bool = False
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class PropertyScenarioData:
|
|
"""One Property's persisted data for one Scenario: identity, the
|
|
Effective-EPC descriptive fields (already override-resolved by the
|
|
repository), the default Plan's post-works figures, and the selected
|
|
measures. The input row the shaper turns into one wide sheet row."""
|
|
|
|
property_id: int
|
|
landlord_property_id: Optional[str] = None
|
|
uprn: Optional[str] = None
|
|
address: Optional[str] = None
|
|
postcode: Optional[str] = None
|
|
property_type: Optional[str] = None
|
|
walls: Optional[str] = None
|
|
roof: Optional[str] = None
|
|
floor: Optional[str] = None
|
|
windows: Optional[str] = None
|
|
heating: Optional[str] = None
|
|
hot_water: Optional[str] = None
|
|
lighting: Optional[str] = None
|
|
total_floor_area: Optional[float] = None
|
|
number_of_rooms: Optional[int] = None
|
|
lodgement_date: Optional[str] = None
|
|
is_expired: Optional[bool] = None
|
|
current_epc_rating: Optional[str] = None
|
|
current_sap_points: Optional[float] = None
|
|
post_sap_points: Optional[float] = None
|
|
post_epc_rating: Optional[str] = None
|
|
cost_of_works: Optional[float] = None
|
|
contingency_cost: Optional[float] = None
|
|
co2_savings: Optional[float] = None
|
|
energy_bill_savings: Optional[float] = None
|
|
energy_consumption_savings: Optional[float] = None
|
|
valuation_increase: Optional[float] = None
|
|
measures: Sequence[ExportMeasure] = ()
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ExportSheet:
|
|
"""One laid-out scenario sheet: the ordered column contract and the rows,
|
|
each a column-keyed mapping ready for the workbook renderer."""
|
|
|
|
columns: tuple[str, ...]
|
|
rows: tuple[dict[str, Any], ...]
|
|
|
|
|
|
def shape_scenario_sheet(
|
|
properties: Sequence[PropertyScenarioData],
|
|
) -> ExportSheet:
|
|
"""Shape one Scenario's Properties into a wide export sheet (ADR-0065)."""
|
|
columns: tuple[str, ...] = ("property_id", "landlord_property_id")
|
|
rows: list[dict[str, Any]] = []
|
|
for prop in properties:
|
|
row: dict[str, Any] = {
|
|
"property_id": prop.property_id,
|
|
"landlord_property_id": prop.landlord_property_id,
|
|
}
|
|
for measure in prop.measures:
|
|
row[measure.measure_type] = measure.estimated_cost
|
|
rows.append(row)
|
|
return ExportSheet(columns=columns, rows=tuple(rows))
|