mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-27 23:35:01 +00:00
Pivot a Property's measure cost onto its scenario-sheet column 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
01e8b8c289
commit
1a0e87e593
4 changed files with 178 additions and 0 deletions
0
domain/scenario_export/__init__.py
Normal file
0
domain/scenario_export/__init__.py
Normal file
111
domain/scenario_export/scenario_sheet.py
Normal file
111
domain/scenario_export/scenario_sheet.py
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
"""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)."""
|
||||
raise NotImplementedError
|
||||
0
tests/domain/scenario_export/__init__.py
Normal file
0
tests/domain/scenario_export/__init__.py
Normal file
67
tests/domain/scenario_export/test_scenario_sheet.py
Normal file
67
tests/domain/scenario_export/test_scenario_sheet.py
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
"""The Scenario Export sheet shape (ADR-0065) — pure layout of one scenario
|
||||
sheet: measure costs pivoted onto a frozen column contract, savings summed per
|
||||
Property, and the Plan's post-works figures."""
|
||||
|
||||
from typing import Optional, Sequence
|
||||
|
||||
from domain.scenario_export.scenario_sheet import (
|
||||
ExportMeasure,
|
||||
PropertyScenarioData,
|
||||
shape_scenario_sheet,
|
||||
)
|
||||
|
||||
|
||||
def _measure(
|
||||
*,
|
||||
measure_type: str = "loft_insulation",
|
||||
estimated_cost: Optional[float] = 1000.0,
|
||||
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,
|
||||
) -> ExportMeasure:
|
||||
return ExportMeasure(
|
||||
measure_type=measure_type,
|
||||
estimated_cost=estimated_cost,
|
||||
sap_points=sap_points,
|
||||
co2_equivalent_savings=co2_equivalent_savings,
|
||||
kwh_savings=kwh_savings,
|
||||
energy_cost_savings=energy_cost_savings,
|
||||
includes_battery=includes_battery,
|
||||
)
|
||||
|
||||
|
||||
def _property(
|
||||
*,
|
||||
property_id: int = 1,
|
||||
landlord_property_id: Optional[str] = "LP1",
|
||||
measures: Sequence[ExportMeasure] = (),
|
||||
**fields: object,
|
||||
) -> PropertyScenarioData:
|
||||
return PropertyScenarioData(
|
||||
property_id=property_id,
|
||||
landlord_property_id=landlord_property_id,
|
||||
measures=measures,
|
||||
**fields, # pyright: ignore[reportArgumentType]
|
||||
)
|
||||
|
||||
|
||||
def test_a_measures_cost_lands_in_its_own_column_with_the_property_identity() -> None:
|
||||
# arrange — one Property with one loft-insulation measure priced at £1200.
|
||||
prop = _property(
|
||||
property_id=42,
|
||||
landlord_property_id="LP42",
|
||||
measures=[_measure(measure_type="loft_insulation", estimated_cost=1200.0)],
|
||||
)
|
||||
|
||||
# act
|
||||
sheet = shape_scenario_sheet([prop])
|
||||
|
||||
# assert — one row carrying the Property identity and the cost in the
|
||||
# loft_insulation column.
|
||||
assert len(sheet.rows) == 1
|
||||
row = sheet.rows[0]
|
||||
assert row["property_id"] == 42
|
||||
assert row["landlord_property_id"] == "LP42"
|
||||
assert row["loft_insulation"] == 1200.0
|
||||
Loading…
Add table
Reference in a new issue