diff --git a/infrastructure/postgres/epc_property_table.py b/infrastructure/postgres/epc_property_table.py index f77128799..c5f20269f 100644 --- a/infrastructure/postgres/epc_property_table.py +++ b/infrastructure/postgres/epc_property_table.py @@ -686,6 +686,12 @@ class EpcBuildingPartModel(SQLModel, table=True): alt_wall_1_thickness_measured: Optional[str] = Field(default=None) alt_wall_1_insulation_thickness: Optional[str] = Field(default=None) alt_wall_1_is_sheltered: Optional[bool] = Field(default=None) + # #1665: calculator-read alt-wall fields (heat_transmission.py) with no column. + alt_wall_1_u_value: Optional[float] = Field(default=None) + alt_wall_1_thickness_mm: Optional[int] = Field(default=None) + # Nullable is load-bearing: None ("not stated") != False ("not a basement"). + # A NOT NULL DEFAULT false re-enables the code-6 basement heuristic (cf #1661). + alt_wall_1_is_basement: Optional[bool] = Field(default=None) alt_wall_2_area: Optional[float] = Field(default=None) alt_wall_2_dry_lined: Optional[str] = Field(default=None) alt_wall_2_construction: Optional[int] = Field(default=None) @@ -693,6 +699,9 @@ class EpcBuildingPartModel(SQLModel, table=True): alt_wall_2_thickness_measured: Optional[str] = Field(default=None) alt_wall_2_insulation_thickness: Optional[str] = Field(default=None) alt_wall_2_is_sheltered: Optional[bool] = Field(default=None) + alt_wall_2_u_value: Optional[float] = Field(default=None) + alt_wall_2_thickness_mm: Optional[int] = Field(default=None) + alt_wall_2_is_basement: Optional[bool] = Field(default=None) @classmethod def from_domain( @@ -741,6 +750,9 @@ class EpcBuildingPartModel(SQLModel, table=True): aw1.wall_insulation_thickness if aw1 else None ), alt_wall_1_is_sheltered=aw1.is_sheltered if aw1 else None, + alt_wall_1_u_value=aw1.u_value if aw1 else None, + alt_wall_1_thickness_mm=aw1.wall_thickness_mm if aw1 else None, + alt_wall_1_is_basement=aw1.is_basement if aw1 else None, alt_wall_2_area=aw2.wall_area if aw2 else None, alt_wall_2_dry_lined=aw2.wall_dry_lined if aw2 else None, alt_wall_2_construction=aw2.wall_construction if aw2 else None, @@ -750,6 +762,9 @@ class EpcBuildingPartModel(SQLModel, table=True): aw2.wall_insulation_thickness if aw2 else None ), alt_wall_2_is_sheltered=aw2.is_sheltered if aw2 else None, + alt_wall_2_u_value=aw2.u_value if aw2 else None, + alt_wall_2_thickness_mm=aw2.wall_thickness_mm if aw2 else None, + alt_wall_2_is_basement=aw2.is_basement if aw2 else None, ) diff --git a/repositories/epc/epc_postgres_repository.py b/repositories/epc/epc_postgres_repository.py index 81620b069..c633cc542 100644 --- a/repositories/epc/epc_postgres_repository.py +++ b/repositories/epc/epc_postgres_repository.py @@ -1044,6 +1044,13 @@ class EpcPostgresRepository(EpcRepository): else bp.alt_wall_2_insulation_thickness ) sheltered = bp.alt_wall_1_is_sheltered if n == 1 else bp.alt_wall_2_is_sheltered + u_value = bp.alt_wall_1_u_value if n == 1 else bp.alt_wall_2_u_value + thickness_mm = ( + bp.alt_wall_1_thickness_mm if n == 1 else bp.alt_wall_2_thickness_mm + ) + is_basement = ( + bp.alt_wall_1_is_basement if n == 1 else bp.alt_wall_2_is_basement + ) return SapAlternativeWall( wall_area=area, wall_dry_lined=_require(dry_lined, f"alt_wall_{n}_dry_lined"), @@ -1058,6 +1065,9 @@ class EpcPostgresRepository(EpcRepository): # Nullable column (added later than the alt wall itself); a row from # before the column existed reads None → the domain default False. is_sheltered=sheltered if sheltered is not None else False, + u_value=u_value, + wall_thickness_mm=thickness_mm, + is_basement=is_basement, ) @private diff --git a/tests/repositories/epc/test_epc_persistence_field_coverage.py b/tests/repositories/epc/test_epc_persistence_field_coverage.py index af416983a..631d69707 100644 --- a/tests/repositories/epc/test_epc_persistence_field_coverage.py +++ b/tests/repositories/epc/test_epc_persistence_field_coverage.py @@ -45,8 +45,6 @@ _UNPERSISTED_ALLOWLIST: dict[str, str] = { "EpcPropertyData.solar_hw_collector_orientation": "FE column pending — tracked round-trip gap", "EpcPropertyData.solar_hw_collector_pitch_deg": "FE column pending — tracked round-trip gap", "EpcPropertyData.solar_hw_overshading": "FE column pending — tracked round-trip gap", - "SapAlternativeWall.u_value": "FE column pending — tracked round-trip gap", - "SapAlternativeWall.wall_thickness_mm": "FE column pending — tracked round-trip gap", # Room-in-roof detail: only floor_area + age_band reconstruct from the # inlined columns; the geometry / surface detail awaits an FE child table # (tracked round-trip gap, docs/migrations §2). @@ -58,8 +56,6 @@ _UNPERSISTED_ALLOWLIST: dict[str, str] = { "SapRoomInRoof.gable_2_height_m": "FE child table pending — room-in-roof detail", "SapRoomInRoof.gable_2_length_m": "FE child table pending — room-in-roof detail", "SapRoomInRoofSurface": "FE child table pending — room-in-roof surface detail", - # --- Nested gaps the calculator does NOT read (dormant); no FE column. - "SapAlternativeWall.is_basement": "dormant — not read by the calculator; no FE column", # --- DELIBERATELY not persisted (#1656). These four ARE read by the # calculator (`heat_transmission.py:994` wall U, `:1068` roof U, `:1118` # floor U, `:955` basement wall) — do NOT re-label them "not read by the