mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Move the live plan, recommendation, recommendation_materials and (retiring) plan_recommendations tables into a new infrastructure/postgres/modelling/ subpackage as single SQLModel definitions (the epc_property pattern), absorbing the rebuild's partial PlanRow/RecommendationRow mirrors and carrying full legacy column parity plus recommendation.plan_id. Out-of-cluster references are plain indexed ints (mirror convention); the live FKs are owned by the Drizzle schema. backend/app/db/models/recommendations.py becomes a re-export shim (ScenarioModel/InstalledMeasure stay for a later slice). Fix the export conftest to create SQLModel-first (so Base funding_package's FK to the now-SQLModel plan resolves) and skip the redundant drop_all on its function-scoped throwaway DB (the epc enum type is now shared across both metadatas). Resolves the pre-existing dual-definition collision: the rebuild and legacy export suites are now co-runnable. No behaviour change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
106 lines
4 KiB
Python
106 lines
4 KiB
Python
from __future__ import annotations
|
|
|
|
import enum
|
|
from datetime import datetime
|
|
from typing import ClassVar, Optional
|
|
|
|
from sqlalchemy import Column, TIMESTAMP
|
|
from sqlalchemy import Enum as SAEnum
|
|
from sqlalchemy.sql import func
|
|
from sqlmodel import Field, SQLModel
|
|
|
|
from datatypes.epc.domain.epc import Epc
|
|
from domain.modelling.plan import Plan
|
|
|
|
# Calculator metrics are in kg CO₂/yr; the live ``plan`` columns are tonnes
|
|
# (legacy ``emissions_kg / 1000``). Convert on the way in.
|
|
_KG_PER_TONNE = 1000.0
|
|
|
|
|
|
class PlanType(enum.Enum):
|
|
SOLAR_ECO4 = "solar_eco4"
|
|
SOLAR_HHRSH_ECO4 = "solar_hhrsh_eco4"
|
|
EMPTY_CAVITY_ECO = "empty_cavity_eco"
|
|
PARTIAL_CAVITY_ECO = "partial_cavity_eco"
|
|
EXTRACTION_ECO = "extraction_eco"
|
|
|
|
|
|
class PlanRow(SQLModel, table=True):
|
|
"""The single SQLModel definition of the live ``plan`` table (ADR-0017
|
|
amendment). Full legacy column parity; out-of-cluster references
|
|
(``portfolio_id`` / ``property_id`` / ``scenario_id``) are plain indexed
|
|
ints, not FK constraints (mirror convention — the live FKs are owned by the
|
|
Drizzle schema)."""
|
|
|
|
__tablename__: ClassVar[str] = "plan" # pyright: ignore[reportIncompatibleVariableOverride]
|
|
|
|
id: Optional[int] = Field(default=None, primary_key=True)
|
|
name: Optional[str] = Field(default="")
|
|
portfolio_id: int
|
|
property_id: int = Field(index=True)
|
|
scenario_id: Optional[int] = Field(default=None)
|
|
created_at: Optional[datetime] = Field(
|
|
default=None,
|
|
sa_column=Column(TIMESTAMP, nullable=False, server_default=func.now()),
|
|
)
|
|
is_default: bool = False
|
|
|
|
valuation_increase_lower_bound: Optional[float] = Field(default=None)
|
|
valuation_increase_upper_bound: Optional[float] = Field(default=None)
|
|
valuation_increase_average: Optional[float] = Field(default=None)
|
|
|
|
plan_type: Optional[PlanType] = Field(
|
|
default=None,
|
|
sa_column=Column(
|
|
SAEnum(
|
|
PlanType,
|
|
name="plan_type",
|
|
values_callable=lambda cls: [m.value for m in cls], # pyright: ignore[reportUnknownLambdaType, reportUnknownMemberType, reportUnknownVariableType]
|
|
create_type=False,
|
|
),
|
|
nullable=True,
|
|
),
|
|
)
|
|
|
|
post_sap_points: Optional[float] = Field(default=None)
|
|
post_epc_rating: Optional[Epc] = Field(
|
|
default=None,
|
|
sa_column=Column(SAEnum(Epc, name="epc"), nullable=True),
|
|
)
|
|
post_co2_emissions: Optional[float] = Field(default=None) # tonnes/yr
|
|
co2_savings: Optional[float] = Field(default=None) # tonnes/yr
|
|
post_energy_bill: Optional[float] = Field(default=None) # £/yr
|
|
energy_bill_savings: Optional[float] = Field(default=None) # £/yr
|
|
post_energy_consumption: Optional[float] = Field(default=None) # kWh/yr
|
|
energy_consumption_savings: Optional[float] = Field(default=None) # kWh/yr
|
|
valuation_post_retrofit: Optional[float] = Field(default=None)
|
|
valuation_increase: Optional[float] = Field(default=None)
|
|
cost_of_works: Optional[float] = Field(default=None)
|
|
contingency_cost: Optional[float] = Field(default=None)
|
|
|
|
@classmethod
|
|
def from_domain(
|
|
cls,
|
|
plan: Plan,
|
|
*,
|
|
property_id: int,
|
|
scenario_id: int,
|
|
portfolio_id: int,
|
|
is_default: bool,
|
|
) -> "PlanRow":
|
|
return cls(
|
|
portfolio_id=portfolio_id,
|
|
property_id=property_id,
|
|
scenario_id=scenario_id,
|
|
is_default=is_default,
|
|
post_sap_points=plan.post_sap_continuous,
|
|
post_epc_rating=plan.post_epc_rating,
|
|
post_co2_emissions=plan.post_retrofit.co2_kg_per_yr / _KG_PER_TONNE,
|
|
co2_savings=plan.co2_savings_kg_per_yr / _KG_PER_TONNE,
|
|
cost_of_works=plan.cost_of_works,
|
|
contingency_cost=plan.contingency_cost,
|
|
post_energy_bill=plan.post_energy_bill,
|
|
energy_bill_savings=plan.energy_bill_savings,
|
|
post_energy_consumption=plan.post_energy_consumption,
|
|
energy_consumption_savings=plan.energy_consumption_savings,
|
|
)
|