diff --git a/backend/documents_parser/elmhurst_extractor.py b/backend/documents_parser/elmhurst_extractor.py index 7bd1dba6..0f440f4a 100644 --- a/backend/documents_parser/elmhurst_extractor.py +++ b/backend/documents_parser/elmhurst_extractor.py @@ -670,12 +670,24 @@ class ElmhurstSiteNotesExtractor: # even when the main wall fields are inherited; merge # them into the inherited WallDetails so the bp carries # them through to its SapBuildingPart. + # + # "As Main Wall: Yes" inherits the EXTERNAL wall + # construction only — the PARTY WALL TYPE is lodged + # separately in the extension's §7 block and may differ + # (cert 001431: Main "CU Cavity masonry unfilled" U=0.5, + # 1st Extension "U Unable to determine" → RdSAP default + # U=0.25). Read the extension's own party wall type when + # present; fall back to the main's only when absent. + ext_party_wall_type = ( + self._local_str(wall_lines, "Party Wall Type") + or main_walls.party_wall_type + ) walls = WallDetails( wall_type=main_walls.wall_type, insulation=main_walls.insulation, thickness_unknown=main_walls.thickness_unknown, u_value_known=main_walls.u_value_known, - party_wall_type=main_walls.party_wall_type, + party_wall_type=ext_party_wall_type, thickness_mm=main_walls.thickness_mm, insulation_thickness_mm=main_walls.insulation_thickness_mm, alternative_walls=self._alternative_walls_from_lines(wall_lines), diff --git a/backend/documents_parser/tests/fixtures/Summary_001431_double_glazing.pdf b/backend/documents_parser/tests/fixtures/Summary_001431_double_glazing.pdf new file mode 100644 index 00000000..f58a0217 Binary files /dev/null and b/backend/documents_parser/tests/fixtures/Summary_001431_double_glazing.pdf differ diff --git a/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py b/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py index b1966d40..d3ed87d6 100644 --- a/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py +++ b/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py @@ -1502,6 +1502,37 @@ def test_elmhurst_glazing_label_full_coverage_per_sap10_table_6b() -> None: ) +def test_extension_party_wall_type_read_independently_of_as_main_wall() -> None: + # Arrange — RdSAP 10 §3.3: "As Main Wall: Yes" inherits only the + # external wall CONSTRUCTION; the party wall type is lodged + # separately per building part and may differ. The double_glazing + # fixture (Summary_001431) lodges Main party "CU Cavity masonry + # unfilled" (SAP10 wall_construction 4 → u_party_wall 0.5) but the + # 1st Extension party "U Unable to determine" (→ wall_construction 0 + # → RdSAP default u_party_wall 0.25), even though the extension is + # "As Main Wall: Yes". Pre-fix the extension inherited the Main's + # party type (both 0.5), inflating worksheet (32) party heat loss. + pages = _summary_pdf_to_textract_style_pages( + _FIXTURES / "Summary_001431_double_glazing.pdf" + ) + site_notes = ElmhurstSiteNotesExtractor(pages).extract() + + # Act + epc = EpcPropertyDataMapper.from_elmhurst_site_notes(site_notes) + + # Assert — Main BP keeps cavity-unfilled (4); the extension BP gets + # the "Unable to determine" sentinel (0), a distinct party wall U. + party_codes = [ + bp.party_wall_construction for bp in epc.sap_building_parts + ] + assert party_codes == [4, 0], ( + f"expected Main=4 (CU, U=0.5) + Ext=0 (Unable, U=0.25), got {party_codes}" + ) + # The two map to different SAP party-wall U-values. + assert abs(u_party_wall(4) - 0.5) <= 1e-9 + assert abs(u_party_wall(0) - 0.25) <= 1e-9 + + def test_summary_mapper_raises_on_unmapped_glazing_type_label() -> None: # Arrange — same strict-coverage gate as the cylinder-size helper # (Slice S0380.15 + S0380.16): silently routing an unknown glazing