From 694bb0b569d806485d98531f97afd709369dadaf Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 20 Jan 2026 15:57:56 +0000 Subject: [PATCH 01/50] Define classes --- backend/condition/domain/asset_condition.py | 11 +++++++++++ backend/condition/domain/element.py | 4 ++++ backend/condition/domain/mapping/lbwf_mapper.py | 9 +++++++++ backend/condition/domain/mapping/mapper.py | 11 +++++++++++ .../parsing/records/lbwf/lbwf_asset_condition.py | 15 ++++++++------- .../condition/parsing/records/lbwf/lbwf_house.py | 4 ++-- 6 files changed, 45 insertions(+), 9 deletions(-) create mode 100644 backend/condition/domain/asset_condition.py create mode 100644 backend/condition/domain/element.py create mode 100644 backend/condition/domain/mapping/lbwf_mapper.py create mode 100644 backend/condition/domain/mapping/mapper.py diff --git a/backend/condition/domain/asset_condition.py b/backend/condition/domain/asset_condition.py new file mode 100644 index 00000000..df87fd9f --- /dev/null +++ b/backend/condition/domain/asset_condition.py @@ -0,0 +1,11 @@ +from dataclasses import dataclass +from typing import Optional + +from backend.condition.domain.element import Element + +@dataclass +class AssetCondition: + uprn: int + element: Element + condition_description: str + renewal_year: Optional[int] = None \ No newline at end of file diff --git a/backend/condition/domain/element.py b/backend/condition/domain/element.py new file mode 100644 index 00000000..89246f9c --- /dev/null +++ b/backend/condition/domain/element.py @@ -0,0 +1,4 @@ +from enum import Enum + +class Element(Enum): + pass \ No newline at end of file diff --git a/backend/condition/domain/mapping/lbwf_mapper.py b/backend/condition/domain/mapping/lbwf_mapper.py new file mode 100644 index 00000000..154f3db4 --- /dev/null +++ b/backend/condition/domain/mapping/lbwf_mapper.py @@ -0,0 +1,9 @@ +from typing import Any, List +from backend.condition.domain.asset_condition import AssetCondition +from backend.condition.domain.mapping.mapper import Mapper + + +class LbwfMapper(Mapper): + + def map_asset_conditions(self, client_data: List[Any]) -> List[AssetCondition]: + raise NotImplementedError \ No newline at end of file diff --git a/backend/condition/domain/mapping/mapper.py b/backend/condition/domain/mapping/mapper.py new file mode 100644 index 00000000..b314e01c --- /dev/null +++ b/backend/condition/domain/mapping/mapper.py @@ -0,0 +1,11 @@ +from abc import ABC, abstractmethod +from typing import Any, List + +from backend.condition.domain.asset_condition import AssetCondition + +class Mapper(ABC): + + @abstractmethod + def map_asset_conditions(self, client_data: List[Any]) -> List[AssetCondition]: + #TODO: client_data should be properly typed + pass \ No newline at end of file diff --git a/backend/condition/parsing/records/lbwf/lbwf_asset_condition.py b/backend/condition/parsing/records/lbwf/lbwf_asset_condition.py index dffd1e53..2b4c4992 100644 --- a/backend/condition/parsing/records/lbwf/lbwf_asset_condition.py +++ b/backend/condition/parsing/records/lbwf/lbwf_asset_condition.py @@ -1,5 +1,6 @@ from dataclasses import dataclass from datetime import date +from typing import Optional @dataclass @@ -16,11 +17,11 @@ class LbwfAssetCondition: element_code_description: str attribute_code: str attribute_code_description: str - element_date_value: str | None = None - element_numerical_value: int | None = None - element_text_value: str | None = None - quantity: int | None = None - install_date: date | None = None - remaining_life: int | None = None - element_comments: str | None = None + element_date_value: Optional[str] = None + element_numerical_value: Optional[int] = None + element_text_value: Optional[str] = None + quantity: Optional[int] = None + install_date: Optional[date] = None + remaining_life: Optional[int] = None + element_comments: Optional[str] = None diff --git a/backend/condition/parsing/records/lbwf/lbwf_house.py b/backend/condition/parsing/records/lbwf/lbwf_house.py index 6db16862..3b472fbe 100644 --- a/backend/condition/parsing/records/lbwf/lbwf_house.py +++ b/backend/condition/parsing/records/lbwf/lbwf_house.py @@ -8,8 +8,8 @@ class LbwfHouse: uprn: int reference: int address: str - epc: str # TODO: make enum - shdf: bool + epc: str # TODO: make enum? + shdf: str house: str fail_decency: int assets: List[LbwfAssetCondition] \ No newline at end of file From c8abc19e599440fbe1faed11b37014e3a6e8b187 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 20 Jan 2026 17:18:51 +0000 Subject: [PATCH 02/50] =?UTF-8?q?Map=20LbwfHouse=20to=20AssetCondition=20l?= =?UTF-8?q?ist=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/condition/domain/asset_condition.py | 9 +- backend/condition/domain/element.py | 125 ++++++++- .../condition/domain/mapping/lbwf_mapper.py | 8 +- .../tests/mapping/test_lbwf_mapper.py | 239 ++++++++++++++++++ .../tests/parsing/test_lbwf_parser.py | 2 +- 5 files changed, 375 insertions(+), 8 deletions(-) create mode 100644 backend/condition/tests/mapping/test_lbwf_mapper.py diff --git a/backend/condition/domain/asset_condition.py b/backend/condition/domain/asset_condition.py index df87fd9f..2b7946c2 100644 --- a/backend/condition/domain/asset_condition.py +++ b/backend/condition/domain/asset_condition.py @@ -6,6 +6,9 @@ from backend.condition.domain.element import Element @dataclass class AssetCondition: uprn: int - element: Element - condition_description: str - renewal_year: Optional[int] = None \ No newline at end of file + element: Element # TODO: should HHSRS elements be handled differently? + condition_description: str # TODO: this probably needs to be some sort of enum so it's searchable/filterable on the frontend + quantity: int + renewal_year: Optional[int] = None + source: Optional[str] = None + # TODO: add install_date diff --git a/backend/condition/domain/element.py b/backend/condition/domain/element.py index 89246f9c..021c8492 100644 --- a/backend/condition/domain/element.py +++ b/backend/condition/domain/element.py @@ -1,4 +1,123 @@ -from enum import Enum +from enum import StrEnum -class Element(Enum): - pass \ No newline at end of file + +class Element(StrEnum): + AHR_CAT = "Accessible Housing Register Category" + ASBESTOS = "Asbestos Present" + ASSETSAREA = "Assets Area for Decent Homes and Investment" + DECNTHMINC = "Include for Decent Homes Reporting - LBWF Stock" + EICINSFREQ = "EICR - Elec Install Conditions Report Inspection Frequency" + EXTBALCONY = "Private Balconies in External Area" + EXTBKSDDR1 = "Back and Side Doors 1 in External Area" + EXTBKSDDR2 = "Back and Side Doors 2 in External Area" + EXTBPOINTG = "Brickwork Pointing in External Area" + EXTCHIMNEY = "Chimneys in External Area" + EXTDWNPTYP = "Downpipes in External Area" + EXTDRPKERB = "Drop Kerb in External Area" + EXTEXTDECS = "External Decorations in External Area" + EXTFASOFBR = "Fascia / Soffit / Bargeboard in External Area" + EXTGARDOOR = "Garage Door in External Area" + EXTGARROOF = "Garage Roof in External Area" + EXTGARSTDR = "Garage and Store Doors in External Area" + EXTGARSTRF = "Garage and Store Roofs in External Area" + EXTGARSTWD = "Garage and Store Windows in External Area" + EXTGARWDWS = "Garage Windows in External Area" + EXTGUTRTYP = "Gutters in External Area" + EXTHARDSTD = "Hardstanding in External Area" + EXTINTDWNP = "Internal Downpipes in External Area" + EXTLINTELS = "Lintels in External Area" + EXTOUTBOH = "Overhaul of Outbuilding in External Area" + EXTPARKING = "Parking in External Area" + EXTPCHCNPY = "Porch and / or Canopy in External Area" + EXTPTFRDR1 = "Patio and French Doors 1 in External Area" + EXTROOF1 = "Roof Covering 1 in External Area" + EXTROOF2 = "Roof Covering 2 in External Area" + EXTROOF3 = "Roof Covering 3 in External Area" + EXTRFSTR1 = "Roof Structure 1 in External Area" + EXTRFSTR2 = "Roof Structure 2 in External Area" + EXTRFSTR3 = "Roof Structure 3 in External Area" + EXTSTOREY = "Number of Storeys within the Property or Block" + EXTSTRDOOR = "Store Door in External Area" + EXTSTRINSP = "Structural Defects in External Area" + EXTSTRROOF = "Store Roof in External Area" + EXTSTRWDWS = "Store Windows in External Area" + EXTWALLFN1 = "Wall Finish 1 in External Area" + EXTWALLFN2 = "Wall Finish 2 in External Area" + EXTWALLINS = "Wall Insulation Improvement in External Area" + EXTWALLSPL = "Wall Spalling in External Area" + EXTWALLSTR = "Wall Structure in External Area" + EXTWNDWS1 = "Windows 1 in External Area" + EXTWNDWS2 = "Windows 2 in External Area" + FFHHDAMP = "Fitness for Human Habitation - Serious problem with damp" + FFHHDRNWC = "Fitness for Human Habitation - Problems with the drainage or the lavatories" + FFHHHCWAT = "Fitness for Human Habitation - Problem with the supply of hot and cold water" + FFHHNEGLC = "Fitness for Human Habitation - Building neglected and is in a bad condition" + FFHHNONAT = "Fitness for Human Habitation - Not enough natural light" + FFHHNOVEN = "Fitness for Human Habitation - Not enough ventilation" + FFHHPRPCK = "Fitness for Human Habitation - Difficult to prepare and cook food or wash up" + FFHHUNLAY = "Fitness for Human Habitation - Unsafe layout" + FFHHUNSTA = "Fitness for Human Habitation - Building is unstable" + FRARISKRTG = "Fire Risk Assessment Rating" + FRAEVACSTR = "Fire Risk Assessment Evacuation Strategy" + FRATYPE = "Fire Risk Assessment Type" + FLVL = "Floor Level of Front Door" + HHSRSASB = "Asbestos (and MMF)" + HHSRSBIOC = "Biocides" + HHSRSCO = "Carbon monoxide" + HHSRSCOLD = "Excess cold" + HHSRSCLOW = "Collision hazards and low headroom" + HHSRSCROWD = "Crowding and space" + HHSRSDAMP = "Damp and mould growth" + HHSRSDOMES = "Domestic hygeine, Pests and Refuse" + HHSRSELEC = "Electrical hazards" + HHSRSENTRP = "Collision and entrapment" + HHSRSENTRY = "Entry by intruders" + HHSRSEXPLO = "Explosions" + HHSRSFBATH = "Falls associated with baths etc" + HHSRSFBETW = "Falling between levels" + HHSRSFIRE = "Fire" + HHSRSFLAME = "Flames, hot surfaces etc" + HHSRSFLEVE = "Falling on level surfaces etc" + HHSRSFOOD = "Food safety" + HHSRSFSTAI = "Falling on stairs etc" + HHSRSFUEL = "Uncombusted fuel gas" + HHSRSHEAT = "Excess heat" + HHSRSLEAD = "Lead" + HHSRSLIGHT = "Lighting" + HHSRSNO2 = "Nitrogen dioxide" + HHSRSNOISE = "Noise" + HHSRSORGAN = "Volatile organic compounds" + HHSRSPERS = "Personal hygeine, Sanitation and Drainage" + HHSRSPOSI = "Position and operability of amenities etc" + HHSRSRADIA = "Radiation" + HHSRSSO2 = "Sulphur dioxide and smoke" + HHSRSSTRUC = "Structural collapse and falling elements" + HHSRSWATER = "Water supply" + INTACCRAMP = "Access Ramp 1:12 Gradient to Property" + INTADDWCW = "Additional WCs and / or WHBs in Property" + INTBTHADEQ = "Adequacy of Bathroom Location in Property" + INTBTHREML = "Source of Bathroom Remaining Life in Property" + INTBTHRLOC = "Location of Bathroom in Property" + INTBOILERF = "Boiler Fuel in Property" + INTCHEXTNT = "Extent of Central Heating in Property" + INTCKRLOC = "Adequacy of Cooker Location in Property" + INTCOMHTG = "Community Heating in Property" + INTELECTRC = "Electrics Required in Property" + INTFLRLVL = "Floor Level Location for Property" + INTFRDOOR = "Type and Location of Front Door in Property" + INTFRDRFRR = "Front Door Fire Rating in Property" + INTGASAVAI = "Gas Available in Property" + INTHEATREC = "Heat Recovery Units in Property" + INTHTDISYS = "Heating Distribution System in Property" + INTHTIMP = "Heating Improvement Required in Property" + INTKITADEQ = "Adequacy of Kitchen and Type in Property" + INTKITREML = "Source of Kitchen Remaining Life in Property" + INTLOFTINS = "Size in mm of Loft Insulation Thickness in Property" + INTNSEINSL = "Adequacy of Noise Insulation in Property" + INTPROGHTG = "Programmable Heating in Property" + INTSMKDET = "Smoke Detectors in Property" + INTSTEPSFD = "Number of Steps to Front Door for Property" + INTTNTINST = "Tenant Installed Kitchen in Property" + INTWDWTYPE = "Windows in Property" + INTWTRHTNG = "Type of Water Heating in Property" + QUALITYSTD = "Quality standard" diff --git a/backend/condition/domain/mapping/lbwf_mapper.py b/backend/condition/domain/mapping/lbwf_mapper.py index 154f3db4..0bbdc0d6 100644 --- a/backend/condition/domain/mapping/lbwf_mapper.py +++ b/backend/condition/domain/mapping/lbwf_mapper.py @@ -1,9 +1,15 @@ -from typing import Any, List +from typing import Any, List, Optional from backend.condition.domain.asset_condition import AssetCondition +from backend.condition.domain.element import Element from backend.condition.domain.mapping.mapper import Mapper +from backend.condition.parsing.records.lbwf.lbwf_asset_condition import LbwfAssetCondition class LbwfMapper(Mapper): def map_asset_conditions(self, client_data: List[Any]) -> List[AssetCondition]: + raise NotImplementedError + + @staticmethod + def _map_element(lbwf_asset: LbwfAssetCondition) -> Optional[Element]: raise NotImplementedError \ No newline at end of file diff --git a/backend/condition/tests/mapping/test_lbwf_mapper.py b/backend/condition/tests/mapping/test_lbwf_mapper.py new file mode 100644 index 00000000..e26b9928 --- /dev/null +++ b/backend/condition/tests/mapping/test_lbwf_mapper.py @@ -0,0 +1,239 @@ +from typing import List +import pytest +from datetime import date + +from backend.condition.domain.mapping.lbwf_mapper import LbwfMapper +from backend.condition.parsing.records.lbwf.lbwf_house import LbwfHouse +from backend.condition.parsing.records.lbwf.lbwf_asset_condition import LbwfAssetCondition +from backend.condition.domain.element import Element +from backend.condition.domain.asset_condition import AssetCondition + +def test_lbwf_mapper_maps_house(): + # arrange + lbwf_house = LbwfHouse( + uprn=1, + reference=100, + address="123 Fake Street, London, A10 1AB", + epc="F", + shdf="NO", + house="HOUSE", + fail_decency=2025, + assets=[ + LbwfAssetCondition( + prop_ref=100, + domna=100, + address="123 Fake Street, London, A10 1AB", + ownership="LBWF_OWNED", + prop_status="OCCP", + prop_type="HOU", + prop_sub_type="TERRACED", + element_group="ASSETS", + element_code="AHR_CAT", + element_code_description="Accessible Housing Register Category", + attribute_code="F", + attribute_code_description="General Needs", + element_date_value=None, + element_numerical_value=None, + element_text_value=None, + quantity=1, + install_date=None, + remaining_life=None, + element_comments=None, + ), + LbwfAssetCondition( + prop_ref=100, + domna=100, + address="123 Fake Street, London, A10 1AB", + ownership="LBWF_OWNED", + prop_status="OCCP", + prop_type="HOU", + prop_sub_type="TERRACED", + element_group="ASSETS", + element_code="FLVL", + element_code_description="Floor Level of Front Door", + attribute_code="0G", + attribute_code_description="Ground Floor", + element_date_value=None, + element_numerical_value=None, + element_text_value=None, + quantity=1, + install_date=None, + remaining_life=None, + element_comments=None, + ), + LbwfAssetCondition( + prop_ref=100, + domna=100, + address="123 Fake Street, London, A10 1AB", + ownership="LBWF_OWNED", + prop_status="OCCP", + prop_type="HOU", + prop_sub_type="TERRACED", + element_group="ASSETS", + element_code="ASBESTOS", + element_code_description="Asbestos Present", + attribute_code="YES", + attribute_code_description="Yes", + element_date_value=None, + element_numerical_value=None, + element_text_value=None, + quantity=None, + install_date=None, + remaining_life=None, + element_comments="Source of Data = ACT", + ), + LbwfAssetCondition( + prop_ref=100, + domna=100, + address="123 Fake Street, London, A10 1AB", + ownership="LBWF_OWNED", + prop_status="OCCP", + prop_type="HOU", + prop_sub_type="TERRACED", + element_group="ASSETS", + element_code="INTBTHRLOC", + element_code_description="Location of Bathroom in Property", + attribute_code="ENTRANCE", + attribute_code_description="Bathroom on Entrance Level in Property", + element_date_value=None, + element_numerical_value=None, + element_text_value=None, + quantity=1, + install_date=None, + remaining_life=None, + element_comments="Source of Data = Codeman", + ), + LbwfAssetCondition( + prop_ref=100, + domna=100, + address="123 Fake Street, London, A10 1AB", + ownership="LBWF_OWNED", + prop_status="OCCP", + prop_type="HOU", + prop_sub_type="TERRACED", + element_group="ASSETS", + element_code="INTCHEXTNT", + element_code_description="Extent of Central Heating in Property", + attribute_code="NONE", + attribute_code_description="No Central Heating in Property", + element_date_value=None, + element_numerical_value=None, + element_text_value=None, + quantity=1, + install_date=None, + remaining_life=None, + element_comments="Source of Data = Codeman", + ), + LbwfAssetCondition( + prop_ref=100, + domna=100, + address="123 Fake Street, London, A10 1AB", + ownership="LBWF_OWNED", + prop_status="OCCP", + prop_type="HOU", + prop_sub_type="TERRACED", + element_group="ASSETS", + element_code="HHSRSFIRE", + element_code_description="Fire", + attribute_code="TYPRISK", + attribute_code_description="Category 4 - Typical Risk", + element_date_value=None, + element_numerical_value=None, + element_text_value=None, + quantity=1, + install_date=None, + remaining_life=None, + element_comments="Source of Data = Morgan Sindall", + ), + LbwfAssetCondition( + prop_ref=100, + domna=100, + address="123 Fake Street, London, A10 1AB", + ownership="LBWF_OWNED", + prop_status="OCCP", + prop_type="HOU", + prop_sub_type="TERRACED", + element_group="ASSETS", + element_code="EXTWALLFN1", + element_code_description="Wall Finish 1 in External Area", + attribute_code="RENDERPBBL", + attribute_code_description="Render or Pebbledash Wall Finish 1 in External Area", + element_date_value=None, + element_numerical_value=None, + element_text_value=None, + quantity=1, + install_date=date(2009,4,1), + remaining_life=26, + element_comments="Source of Data = Codeman", + ), + ] + ) + mapper = LbwfMapper() + + current_year = 2026 + + expected_assets: List[AssetCondition] = [ + AssetCondition( + uprn=1, + element=Element.AHR_CAT, + condition_description="General Needs", + quantity=1, + renewal_year=None, + source=None + ), + AssetCondition( + uprn=1, + element=Element.FLVL, + condition_description="Ground Floor", + quantity=1, + renewal_year=None, + source=None + ), + AssetCondition( + uprn=1, + element=Element.ASBESTOS, + condition_description="Yes", + quantity=None, + renewal_year=None, + source="Source of Data = ACT" + ), + AssetCondition( + uprn=1, + element=Element.INTBTHRLOC, + condition_description="Bathroom on Entrance Level in Property", + quantity=1, + renewal_year=None, + source="Source of Data = Codeman" + ), + AssetCondition( + uprn=1, + element=Element.INTCHEXTNT, + condition_description="No Central Heating in Property", + quantity=1, + renewal_year=None, + source="Source of Data = Codeman" + ), + AssetCondition( + uprn=1, + element=Element.HHSRSFIRE, + condition_description="Category 4 - Typical Risk", + quantity=1, + renewal_year=None, + source="Source of Data = Morgan Sindall" + ), + AssetCondition( + uprn=1, + element=Element.EXTWALLFN1, + condition_description="Render or Pebbledash Wall Finish 1 in External Area", + quantity=1, + renewal_year=2052, + source="Source of Data = Codeman" + ), + + ] + + # act + actual_assets: List[AssetCondition] = mapper.map_asset_conditions(lbwf_house) + + # assert + assert actual_assets == expected_assets \ No newline at end of file diff --git a/backend/condition/tests/parsing/test_lbwf_parser.py b/backend/condition/tests/parsing/test_lbwf_parser.py index 7556b845..beb81a03 100644 --- a/backend/condition/tests/parsing/test_lbwf_parser.py +++ b/backend/condition/tests/parsing/test_lbwf_parser.py @@ -112,7 +112,7 @@ def lbwf_homes_xlsx_bytes() -> BytesIO: return stream -def test_lbwf_parser_passes_houses(lbwf_homes_xlsx_bytes): +def test_lbwf_parser_parses_houses(lbwf_homes_xlsx_bytes): # arrange parser = LbwfParser() From fc08a7df4ff64dcc2c64c9ad1de692c588adf66d Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Wed, 21 Jan 2026 11:01:25 +0000 Subject: [PATCH 03/50] =?UTF-8?q?Map=20LbwfHouse=20to=20AssetCondition=20l?= =?UTF-8?q?ist=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../condition/domain/mapping/lbwf_mapper.py | 52 +++++++++++++++++-- backend/condition/domain/mapping/mapper.py | 2 +- .../tests/mapping/test_lbwf_mapper.py | 2 +- 3 files changed, 50 insertions(+), 6 deletions(-) diff --git a/backend/condition/domain/mapping/lbwf_mapper.py b/backend/condition/domain/mapping/lbwf_mapper.py index 0bbdc0d6..63434240 100644 --- a/backend/condition/domain/mapping/lbwf_mapper.py +++ b/backend/condition/domain/mapping/lbwf_mapper.py @@ -1,15 +1,59 @@ from typing import Any, List, Optional +from datetime import datetime, date + from backend.condition.domain.asset_condition import AssetCondition from backend.condition.domain.element import Element from backend.condition.domain.mapping.mapper import Mapper from backend.condition.parsing.records.lbwf.lbwf_asset_condition import LbwfAssetCondition +from backend.condition.parsing.records.lbwf.lbwf_house import LbwfHouse +from utils.logger import setup_logger +logger = setup_logger() class LbwfMapper(Mapper): - def map_asset_conditions(self, client_data: List[Any]) -> List[AssetCondition]: - raise NotImplementedError + def map_asset_conditions_for_property(self, client_data: Any) -> List[AssetCondition]: + assert isinstance(client_data, LbwfHouse) # TODO: think of a better way to do this + + mapped_assets: List[AssetCondition] = [] + + uprn: int = client_data.uprn + for raw_asset in client_data.assets: + try: + element: Element = LbwfMapper._map_element(raw_asset.element_code) + except: + logger.warning(f"Unrecognised LBWF Asset Element Code: {raw_asset.element_code}. Skipping record") + continue + + + mapped_assets.append( + AssetCondition( + uprn=uprn, + element=element, + condition_description=raw_asset.attribute_code_description, + quantity=raw_asset.quantity, + renewal_year=LbwfMapper._calculate_renewal_year(raw_asset), + source=raw_asset.element_comments, + ) + ) + + return mapped_assets + + @staticmethod - def _map_element(lbwf_asset: LbwfAssetCondition) -> Optional[Element]: - raise NotImplementedError \ No newline at end of file + def _map_element(lbwf_element_code: LbwfAssetCondition) -> Element: + return Element[lbwf_element_code] + + @staticmethod + def _calculate_renewal_year(lbwf_asset: LbwfAssetCondition) -> Optional[int]: + remaining_life_years: Optional[int] = lbwf_asset.remaining_life + if not remaining_life_years: + return None + + try: + survey_year: int = datetime.now().year # TODO: get survey year from filename or elsewhere + return survey_year + remaining_life_years + except: + logger.debug(f"Unable to map LBWF Asset remaining life {remaining_life_years} to renewal year, returning None") + return None \ No newline at end of file diff --git a/backend/condition/domain/mapping/mapper.py b/backend/condition/domain/mapping/mapper.py index b314e01c..f08fa4e1 100644 --- a/backend/condition/domain/mapping/mapper.py +++ b/backend/condition/domain/mapping/mapper.py @@ -6,6 +6,6 @@ from backend.condition.domain.asset_condition import AssetCondition class Mapper(ABC): @abstractmethod - def map_asset_conditions(self, client_data: List[Any]) -> List[AssetCondition]: + def map_asset_conditions_for_property(self, client_data: Any) -> List[AssetCondition]: #TODO: client_data should be properly typed pass \ No newline at end of file diff --git a/backend/condition/tests/mapping/test_lbwf_mapper.py b/backend/condition/tests/mapping/test_lbwf_mapper.py index e26b9928..3e066d27 100644 --- a/backend/condition/tests/mapping/test_lbwf_mapper.py +++ b/backend/condition/tests/mapping/test_lbwf_mapper.py @@ -233,7 +233,7 @@ def test_lbwf_mapper_maps_house(): ] # act - actual_assets: List[AssetCondition] = mapper.map_asset_conditions(lbwf_house) + actual_assets: List[AssetCondition] = mapper.map_asset_conditions_for_property(lbwf_house) # assert assert actual_assets == expected_assets \ No newline at end of file From 25923cbc9fb0f9c9f7f6bfa72ffb46480df2e8c0 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Wed, 21 Jan 2026 12:09:02 +0000 Subject: [PATCH 04/50] add mapping to processor --- backend/condition/parsing/factory.py | 8 ++++++++ backend/condition/processor.py | 13 ++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/backend/condition/parsing/factory.py b/backend/condition/parsing/factory.py index 01dce75d..ea54d3e0 100644 --- a/backend/condition/parsing/factory.py +++ b/backend/condition/parsing/factory.py @@ -1,3 +1,5 @@ +from backend.condition.domain.mapping.lbwf_mapper import LbwfMapper +from backend.condition.domain.mapping.mapper import Mapper from backend.condition.file_type import FileType from backend.condition.parsing.parser import Parser from backend.condition.parsing.lbwf_parser import LbwfParser @@ -7,3 +9,9 @@ def select_parser(file_type: FileType) -> Parser: return LbwfParser() raise ValueError("Unrecognised file type, unable to instantiate Parser") + +def select_mapper(file_type: FileType) -> Mapper: + if file_type is FileType.LBWF: + return LbwfMapper() + + raise ValueError("Unrecognised file type, unable to instantiate Mapper") diff --git a/backend/condition/processor.py b/backend/condition/processor.py index fb06c888..4f379b23 100644 --- a/backend/condition/processor.py +++ b/backend/condition/processor.py @@ -1,9 +1,11 @@ from typing import Any, BinaryIO, List +from backend.condition.domain.asset_condition import AssetCondition +from backend.condition.domain.mapping.mapper import Mapper from backend.condition.parsing.parser import Parser from utils.logger import setup_logger from backend.condition.file_type import FileType, detect_file_type -from backend.condition.parsing.factory import select_parser +from backend.condition.parsing.factory import select_parser, select_mapper def process_file(file_stream: BinaryIO, source_key: str) -> None: print(f"[processor] Received file: {source_key}") @@ -11,8 +13,13 @@ def process_file(file_stream: BinaryIO, source_key: str) -> None: # Instantiation file_type: FileType = detect_file_type(source_key) parser: Parser = select_parser(file_type) + mapper: Mapper = select_mapper(file_type) # Orchestration - records: List[Any] = parser.parse(file_stream) + raw_properties: List[Any] = parser.parse(file_stream) - print(records) # temp \ No newline at end of file + assets: List[AssetCondition] = [] + for p in raw_properties: + assets.extend(mapper.map_asset_conditions_for_property(p)) + + print(assets) # temp \ No newline at end of file From dfe04601551d94f3b831a405a3095a0d62f34cb0 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Wed, 21 Jan 2026 12:14:16 +0000 Subject: [PATCH 05/50] Pass survey year to mapper rather than using today's year --- backend/condition/domain/mapping/lbwf_mapper.py | 10 ++++++---- backend/condition/domain/mapping/mapper.py | 4 ++-- backend/condition/processor.py | 5 ++++- backend/condition/tests/mapping/test_lbwf_mapper.py | 4 ++-- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/backend/condition/domain/mapping/lbwf_mapper.py b/backend/condition/domain/mapping/lbwf_mapper.py index 63434240..bc44e4c3 100644 --- a/backend/condition/domain/mapping/lbwf_mapper.py +++ b/backend/condition/domain/mapping/lbwf_mapper.py @@ -12,7 +12,7 @@ logger = setup_logger() class LbwfMapper(Mapper): - def map_asset_conditions_for_property(self, client_data: Any) -> List[AssetCondition]: + def map_asset_conditions_for_property(self, client_data: Any, survey_year: Optional[int]) -> List[AssetCondition]: assert isinstance(client_data, LbwfHouse) # TODO: think of a better way to do this mapped_assets: List[AssetCondition] = [] @@ -32,7 +32,7 @@ class LbwfMapper(Mapper): element=element, condition_description=raw_asset.attribute_code_description, quantity=raw_asset.quantity, - renewal_year=LbwfMapper._calculate_renewal_year(raw_asset), + renewal_year=LbwfMapper._calculate_renewal_year(raw_asset, survey_year), source=raw_asset.element_comments, ) ) @@ -46,13 +46,15 @@ class LbwfMapper(Mapper): return Element[lbwf_element_code] @staticmethod - def _calculate_renewal_year(lbwf_asset: LbwfAssetCondition) -> Optional[int]: + def _calculate_renewal_year(lbwf_asset: LbwfAssetCondition, survey_year: Optional[int]) -> Optional[int]: remaining_life_years: Optional[int] = lbwf_asset.remaining_life if not remaining_life_years: return None + if not survey_year: + return None + try: - survey_year: int = datetime.now().year # TODO: get survey year from filename or elsewhere return survey_year + remaining_life_years except: logger.debug(f"Unable to map LBWF Asset remaining life {remaining_life_years} to renewal year, returning None") diff --git a/backend/condition/domain/mapping/mapper.py b/backend/condition/domain/mapping/mapper.py index f08fa4e1..4e51d46b 100644 --- a/backend/condition/domain/mapping/mapper.py +++ b/backend/condition/domain/mapping/mapper.py @@ -1,11 +1,11 @@ from abc import ABC, abstractmethod -from typing import Any, List +from typing import Any, List, Optional from backend.condition.domain.asset_condition import AssetCondition class Mapper(ABC): @abstractmethod - def map_asset_conditions_for_property(self, client_data: Any) -> List[AssetCondition]: + def map_asset_conditions_for_property(self, client_data: Any, survey_year: Optional[int]) -> List[AssetCondition]: #TODO: client_data should be properly typed pass \ No newline at end of file diff --git a/backend/condition/processor.py b/backend/condition/processor.py index 4f379b23..cc44e38a 100644 --- a/backend/condition/processor.py +++ b/backend/condition/processor.py @@ -1,4 +1,5 @@ from typing import Any, BinaryIO, List +from datetime import datetime from backend.condition.domain.asset_condition import AssetCondition from backend.condition.domain.mapping.mapper import Mapper @@ -18,8 +19,10 @@ def process_file(file_stream: BinaryIO, source_key: str) -> None: # Orchestration raw_properties: List[Any] = parser.parse(file_stream) + survey_year = datetime.now().year # TODO: get this from filepath or elsewhere + assets: List[AssetCondition] = [] for p in raw_properties: - assets.extend(mapper.map_asset_conditions_for_property(p)) + assets.extend(mapper.map_asset_conditions_for_property(p, survey_year)) print(assets) # temp \ No newline at end of file diff --git a/backend/condition/tests/mapping/test_lbwf_mapper.py b/backend/condition/tests/mapping/test_lbwf_mapper.py index 3e066d27..926e34c1 100644 --- a/backend/condition/tests/mapping/test_lbwf_mapper.py +++ b/backend/condition/tests/mapping/test_lbwf_mapper.py @@ -170,7 +170,7 @@ def test_lbwf_mapper_maps_house(): ) mapper = LbwfMapper() - current_year = 2026 + survey_year = 2026 expected_assets: List[AssetCondition] = [ AssetCondition( @@ -233,7 +233,7 @@ def test_lbwf_mapper_maps_house(): ] # act - actual_assets: List[AssetCondition] = mapper.map_asset_conditions_for_property(lbwf_house) + actual_assets: List[AssetCondition] = mapper.map_asset_conditions_for_property(lbwf_house, survey_year) # assert assert actual_assets == expected_assets \ No newline at end of file From 7bd70ae001c63acaf58e34bd069af0d31326c129 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Wed, 21 Jan 2026 12:36:35 +0000 Subject: [PATCH 06/50] =?UTF-8?q?include=20install=5Fdate=20on=20AssetCond?= =?UTF-8?q?ition=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/condition/domain/asset_condition.py | 5 +++-- .../tests/mapping/test_lbwf_mapper.py | 21 ++++++++++++------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/backend/condition/domain/asset_condition.py b/backend/condition/domain/asset_condition.py index 2b7946c2..dffbdf88 100644 --- a/backend/condition/domain/asset_condition.py +++ b/backend/condition/domain/asset_condition.py @@ -1,5 +1,6 @@ from dataclasses import dataclass from typing import Optional +from datetime import date from backend.condition.domain.element import Element @@ -7,8 +8,8 @@ from backend.condition.domain.element import Element class AssetCondition: uprn: int element: Element # TODO: should HHSRS elements be handled differently? - condition_description: str # TODO: this probably needs to be some sort of enum so it's searchable/filterable on the frontend + condition_description: str # TODO: this probably needs to be some sort of enum so it's searchable/filterable on the frontend. Could be hard to map from string though quantity: int renewal_year: Optional[int] = None source: Optional[str] = None - # TODO: add install_date + install_date: Optional[date] = None diff --git a/backend/condition/tests/mapping/test_lbwf_mapper.py b/backend/condition/tests/mapping/test_lbwf_mapper.py index 926e34c1..151e5d19 100644 --- a/backend/condition/tests/mapping/test_lbwf_mapper.py +++ b/backend/condition/tests/mapping/test_lbwf_mapper.py @@ -179,7 +179,8 @@ def test_lbwf_mapper_maps_house(): condition_description="General Needs", quantity=1, renewal_year=None, - source=None + source=None, + install_date=None, ), AssetCondition( uprn=1, @@ -187,7 +188,8 @@ def test_lbwf_mapper_maps_house(): condition_description="Ground Floor", quantity=1, renewal_year=None, - source=None + source=None, + install_date=None, ), AssetCondition( uprn=1, @@ -195,7 +197,8 @@ def test_lbwf_mapper_maps_house(): condition_description="Yes", quantity=None, renewal_year=None, - source="Source of Data = ACT" + source="Source of Data = ACT", + install_date=None, ), AssetCondition( uprn=1, @@ -203,7 +206,8 @@ def test_lbwf_mapper_maps_house(): condition_description="Bathroom on Entrance Level in Property", quantity=1, renewal_year=None, - source="Source of Data = Codeman" + source="Source of Data = Codeman", + install_date=None, ), AssetCondition( uprn=1, @@ -211,7 +215,8 @@ def test_lbwf_mapper_maps_house(): condition_description="No Central Heating in Property", quantity=1, renewal_year=None, - source="Source of Data = Codeman" + source="Source of Data = Codeman", + install_date=None, ), AssetCondition( uprn=1, @@ -219,7 +224,8 @@ def test_lbwf_mapper_maps_house(): condition_description="Category 4 - Typical Risk", quantity=1, renewal_year=None, - source="Source of Data = Morgan Sindall" + source="Source of Data = Morgan Sindall", + install_date=None, ), AssetCondition( uprn=1, @@ -227,7 +233,8 @@ def test_lbwf_mapper_maps_house(): condition_description="Render or Pebbledash Wall Finish 1 in External Area", quantity=1, renewal_year=2052, - source="Source of Data = Codeman" + source="Source of Data = Codeman", + install_date=date(2009,4,1), ), ] From a8ff74c2ea1c71e0a822152ff3bee6129806d435 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Wed, 21 Jan 2026 12:37:23 +0000 Subject: [PATCH 07/50] =?UTF-8?q?include=20install=5Fdate=20on=20AssetCond?= =?UTF-8?q?ition=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/condition/domain/mapping/lbwf_mapper.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/condition/domain/mapping/lbwf_mapper.py b/backend/condition/domain/mapping/lbwf_mapper.py index bc44e4c3..0af21b7a 100644 --- a/backend/condition/domain/mapping/lbwf_mapper.py +++ b/backend/condition/domain/mapping/lbwf_mapper.py @@ -34,6 +34,7 @@ class LbwfMapper(Mapper): quantity=raw_asset.quantity, renewal_year=LbwfMapper._calculate_renewal_year(raw_asset, survey_year), source=raw_asset.element_comments, + install_date=raw_asset.install_date, ) ) From d43d9d9069d2610943df7cb99a83603cb4699f64 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Wed, 21 Jan 2026 15:13:17 +0000 Subject: [PATCH 08/50] =?UTF-8?q?Parse=20Peabody=20condition=20data=20xlsx?= =?UTF-8?q?=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/condition/file_type.py | 4 + backend/condition/parsing/factory.py | 4 + backend/condition/parsing/peabody_parser.py | 7 + .../peabody/peabody_asset_condition.py | 22 ++++ .../records/peabody/peabody_property.py | 9 ++ .../tests/parsing/test_parsing_factory.py | 11 ++ .../tests/parsing/test_peabody_parser.py | 124 ++++++++++++++++++ 7 files changed, 181 insertions(+) create mode 100644 backend/condition/parsing/peabody_parser.py create mode 100644 backend/condition/parsing/records/peabody/peabody_asset_condition.py create mode 100644 backend/condition/parsing/records/peabody/peabody_property.py create mode 100644 backend/condition/tests/parsing/test_peabody_parser.py diff --git a/backend/condition/file_type.py b/backend/condition/file_type.py index b9a4357f..07a0669c 100644 --- a/backend/condition/file_type.py +++ b/backend/condition/file_type.py @@ -2,6 +2,7 @@ from enum import Enum class FileType(Enum): LBWF = "lbwf" + Peabody = "peabody" def detect_file_type(filepath: str) -> FileType: path = filepath.lower() @@ -9,4 +10,7 @@ def detect_file_type(filepath: str) -> FileType: if "lbwf" in path: return FileType.LBWF + if "peadbody" in path: + return FileType.Peabody + raise ValueError("Unrecognised file path") \ No newline at end of file diff --git a/backend/condition/parsing/factory.py b/backend/condition/parsing/factory.py index ea54d3e0..3a28df78 100644 --- a/backend/condition/parsing/factory.py +++ b/backend/condition/parsing/factory.py @@ -3,10 +3,14 @@ from backend.condition.domain.mapping.mapper import Mapper from backend.condition.file_type import FileType from backend.condition.parsing.parser import Parser from backend.condition.parsing.lbwf_parser import LbwfParser +from backend.condition.parsing.peabody_parser import PeabodyParser def select_parser(file_type: FileType) -> Parser: if file_type is FileType.LBWF: return LbwfParser() + + if file_type is FileType.Peabody: + return PeabodyParser() raise ValueError("Unrecognised file type, unable to instantiate Parser") diff --git a/backend/condition/parsing/peabody_parser.py b/backend/condition/parsing/peabody_parser.py new file mode 100644 index 00000000..e276e48e --- /dev/null +++ b/backend/condition/parsing/peabody_parser.py @@ -0,0 +1,7 @@ +from typing import Any, BinaryIO +from backend.condition.parsing.parser import Parser + + +class PeabodyParser(Parser): + def parse(self, file_stream: BinaryIO) -> Any: + raise NotImplementedError \ No newline at end of file diff --git a/backend/condition/parsing/records/peabody/peabody_asset_condition.py b/backend/condition/parsing/records/peabody/peabody_asset_condition.py new file mode 100644 index 00000000..5682d13a --- /dev/null +++ b/backend/condition/parsing/records/peabody/peabody_asset_condition.py @@ -0,0 +1,22 @@ +from dataclasses import dataclass +from datetime import datetime +from typing import Optional + +@dataclass +class PeabodyAssetCondition: + lo_reference: str + full_address: str + location_type_code: int + parent_lo_reference: str + element_code: int + element: int + sub_element_code: int + sub_element: str + material_code: int + material_or_answer: str + renewal_quantity: int + renewal: int + cloned: str + lo_type_code: int + renewal_cost: Optional[float] = None + condition_survey_date: Optional[datetime] = None \ No newline at end of file diff --git a/backend/condition/parsing/records/peabody/peabody_property.py b/backend/condition/parsing/records/peabody/peabody_property.py new file mode 100644 index 00000000..1bff1b55 --- /dev/null +++ b/backend/condition/parsing/records/peabody/peabody_property.py @@ -0,0 +1,9 @@ +from dataclasses import dataclass +from typing import List + +from backend.condition.parsing.records.peabody.peabody_asset_condition import PeabodyAssetCondition + +@dataclass +class PeabodyProperty: + uprn: int + assets: List[PeabodyAssetCondition] \ No newline at end of file diff --git a/backend/condition/tests/parsing/test_parsing_factory.py b/backend/condition/tests/parsing/test_parsing_factory.py index 481418d7..e2b478ff 100644 --- a/backend/condition/tests/parsing/test_parsing_factory.py +++ b/backend/condition/tests/parsing/test_parsing_factory.py @@ -11,5 +11,16 @@ def test_selects_lbwf_parser(): # act actual_class_name = select_parser(file_type).__class__.__name__ + # assert + assert expected_class_name == actual_class_name + +def test_selects_peabody_parser(): + # arrange + file_type = FileType.Peabody + expected_class_name = "PeabodyParser" + + # act + actual_class_name = select_parser(file_type).__class__.__name__ + # assert assert expected_class_name == actual_class_name \ No newline at end of file diff --git a/backend/condition/tests/parsing/test_peabody_parser.py b/backend/condition/tests/parsing/test_peabody_parser.py new file mode 100644 index 00000000..5196e65d --- /dev/null +++ b/backend/condition/tests/parsing/test_peabody_parser.py @@ -0,0 +1,124 @@ +from typing import Any +import pytest +from io import BytesIO +from openpyxl import Workbook +from datetime import datetime + +from backend.condition.parsing.peabody_parser import PeabodyParser +from backend.condition.parsing.records.peabody.peabody_asset_condition import PeabodyAssetCondition +from backend.condition.parsing.records.peabody.peabody_property import PeabodyProperty + +@pytest.fixture +def peabody_assets_xlsx_bytes() -> BytesIO: + wb = Workbook() + survey_records_d_and_lower = wb.active + survey_records_d_and_lower.title = "Survey Records - D & Lower" + survey_records_d_and_lower.append([ + "Lo_Reference", + "full_address", + "location_type_code", + "Parent_Lo_Reference", + "Element_Code", + "Element", + "Sub_Element_Code", + "Sub_Element", + "Material_Code", + "material_or_answer", + "Renewal_Quantity", + "Renewal_Year", + "Renewal_Cost", + "cloned", + "lo_type_code", + "condition_survey_date", + ]) + survey_records_d_and_lower.append([ + "B000RAND", + "1-11 RANDOM HOUSE LONDON", + 3, + "RAND2EST", + 110, + "ROOFS", + 1, + "Primary Roof", + 9, + "Other", + 3, + 2054, + 330, + "N", + 3, + datetime(2025,12,4,9,17,0) + ]) + survey_records_d_and_lower.append([ + "B000FAKE", + "3-10 FAKE CLOSE LONDON", + 3, + "FAKEEST", + 100, + "GENERAL", + 15, + "External Decoration", + 2, + "Normal", + 1, + 2035, + 1500.7, + "N", + 3, + datetime(2025,7,5,0,0,0) + ]) + survey_records_d_and_lower.append([ + "B000MIS", + "99 MISC ROAD LONDON", + 3, + "300828", + 54, + "HHSRS", + 29, + "HHSRS Structural Collapse & Falling Elements", + 4, + "HHSRS Moderate", + 2, + 2027, + None, + "N", + 3, + None + ]) + survey_records_d_and_lower.append([ + "B000MIS", + "99 MISC ROAD LONDON", + 3, + "300828", + 53, + "External", + 2, + "Chimney", + 2, + "Present", + 33, + 2053, + 3531, + "N", + 3, + None + ]) + + + stream = BytesIO() + wb.save(stream) + stream.seek(0) + + return stream + +def test_peabody_parser_parses_conditions(peabody_assets_xlsx_bytes): + # arrange + parser = PeabodyParser() + + # act + result: Any = parser.parse(peabody_assets_xlsx_bytes) + + # assert + assert len(result) == 3 + + assert all(isinstance(item, PeabodyProperty) for item in result) From 4e190328cc93481a025a35291c73ac3ea43259dd Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Wed, 21 Jan 2026 16:25:58 +0000 Subject: [PATCH 09/50] =?UTF-8?q?Parse=20Peabody=20condition=20data=20xlsx?= =?UTF-8?q?=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/condition/parsing/lbwf_parser.py | 8 +- backend/condition/parsing/peabody_parser.py | 142 +++++++++++++++++- .../peabody/peabody_asset_condition.py | 2 +- .../records/peabody/peabody_property.py | 2 + 4 files changed, 147 insertions(+), 7 deletions(-) diff --git a/backend/condition/parsing/lbwf_parser.py b/backend/condition/parsing/lbwf_parser.py index 8d52f6d5..63512c41 100644 --- a/backend/condition/parsing/lbwf_parser.py +++ b/backend/condition/parsing/lbwf_parser.py @@ -8,13 +8,13 @@ from backend.condition.parsing.records.lbwf.lbwf_house import LbwfHouse from backend.condition.utils.date_utils import normalise_date from utils.logger import setup_logger -logger = setup_logger +logger = setup_logger() class LbwfParser(Parser): def parse(self, file_stream: BinaryIO) -> Any: wb: Workbook = load_workbook(file_stream) - address_to_uprn_map: Dict[str, int] = self._generate_address_to_uprn_dict(wb) + address_to_uprn_map: Dict[str, int] = LbwfParser._generate_address_to_uprn_dict(wb) assets = self._parse_assets(wb) houses = self._parse_houses(wb, address_to_uprn_map) @@ -132,7 +132,7 @@ class LbwfParser(Parser): @staticmethod def _generate_address_to_uprn_dict(wb: Workbook) -> Dict[str, int | None]: - sheet: Workbook = wb["All Energy Breakdown "] + sheet = wb["All Energy Breakdown "] rows: Iterator[Tuple[object | None, ...]] = sheet.iter_rows(values_only=True) @@ -159,6 +159,7 @@ class LbwfParser(Parser): return mapping + @staticmethod def _get_column_indexes_by_name( headers: Tuple[object | None, ...] ) -> Dict[str, int]: @@ -170,6 +171,7 @@ class LbwfParser(Parser): return index + @staticmethod def _get_uprn_from_address(address: str, address_to_uprn_map: Dict[str, int]) -> int | None: pseudo_name = address.split(",")[0] diff --git a/backend/condition/parsing/peabody_parser.py b/backend/condition/parsing/peabody_parser.py index e276e48e..d2229e1c 100644 --- a/backend/condition/parsing/peabody_parser.py +++ b/backend/condition/parsing/peabody_parser.py @@ -1,7 +1,143 @@ -from typing import Any, BinaryIO -from backend.condition.parsing.parser import Parser +from typing import Any, BinaryIO, Dict, Iterator, List, Tuple, DefaultDict +from openpyxl import Workbook, load_workbook +from collections import defaultdict +from backend.condition.parsing.parser import Parser +from backend.condition.parsing.records.peabody.peabody_asset_condition import PeabodyAssetCondition +from backend.condition.parsing.records.peabody.peabody_property import PeabodyProperty +from utils.logger import setup_logger + +logger = setup_logger() class PeabodyParser(Parser): def parse(self, file_stream: BinaryIO) -> Any: - raise NotImplementedError \ No newline at end of file + wb: Workbook = load_workbook(file_stream) + address_to_uprn_map: Dict[str, int] = PeabodyParser._generate_address_to_uprn_dict(wb) + + assets = self._parse_assets(wb) + + return self._group_assets_into_properties( + assets=assets, + address_to_uprn_map=address_to_uprn_map, + ) + + + @staticmethod + def _parse_assets(wb: Workbook) -> List[PeabodyAssetCondition]: + assets_sheet = wb["Survey Records - D & Lower"] + asset_rows = assets_sheet.iter_rows(values_only=True) + + asset_headers = next(asset_rows) + asset_header_indexes = PeabodyParser._get_column_indexes_by_name(asset_headers) + + assets: List[PeabodyAssetCondition] = [] + for row in asset_rows: + try: + assets.append( + PeabodyParser._map_row_to_asset_record(row, asset_header_indexes) + ) + except Exception as e: + logger.error(f"Error mapping Peabody row to asset record: {e}") + continue + + return assets + + @staticmethod + def _group_assets_into_properties( + assets: List[PeabodyAssetCondition], + address_to_uprn_map: Dict[str, int], + ) -> List[PeabodyProperty]: + assets_by_address: DefaultDict[str, List[PeabodyAssetCondition]] = defaultdict(list) + + for asset in assets: + if asset.full_address is None: + continue + + address = asset.full_address + assets_by_address[address].append(asset) + + properties: List[PeabodyProperty] = [] + + for address, grouped_assets in assets_by_address.items(): + uprn = address_to_uprn_map.get(address) + + if uprn is None: + logger.warning(f"No UPRN found for address: {address}") + continue + + properties.append( + PeabodyProperty( + uprn=uprn, + assets=grouped_assets, + ) + ) + + return properties + + + @staticmethod + def _map_row_to_asset_record( + row: Any | Tuple[object | None, ...], + header_indexes: Dict[str, int], + ) -> PeabodyAssetCondition: + return PeabodyAssetCondition( + lo_reference=row[header_indexes["Lo_Reference"]], + full_address=row[header_indexes["full_address"]], + location_type_code=row[header_indexes["location_type_code"]], + parent_lo_reference=row[header_indexes["Parent_Lo_Reference"]], + element_code=row[header_indexes["Element_Code"]], + element=row[header_indexes["Element"]], + sub_element_code=row[header_indexes["Sub_Element_Code"]], + sub_element=row[header_indexes["Sub_Element"]], + material_code=row[header_indexes["Material_Code"]], + material_or_answer=row[header_indexes["material_or_answer"]], + renewal_quantity=row[header_indexes["Renewal_Quantity"]], + renewal_year=row[header_indexes["Renewal_Year"]], + renewal_cost=row[header_indexes["Renewal_Cost"]], + cloned=row[header_indexes["cloned"]], + lo_type_code=row[header_indexes["lo_type_code"]], + condition_survey_date=row[header_indexes["condition_survey_date"]], + ) + + @staticmethod + def _generate_address_to_uprn_dict(wb: Workbook) -> Dict[str, int | None]: + sheet = wb["Survey Records - D & Lower"] + rows: Iterator[Tuple[object | None, ...]] = sheet.iter_rows(values_only=True) + + headers = next(rows) + header_indexes: Dict[str, int] = PeabodyParser._get_column_indexes_by_name(headers) + + address_idx = header_indexes["full_address"] + + + address_to_uprn: Dict[str, int] = {} + # Generate random UPRNs for now + next_uprn = 1 # TODO: get real UPRNs + + for row in rows: + address = row[address_idx] + + if address is None: + continue + + # Optional normalization + address = address.strip() + + if address not in address_to_uprn: + address_to_uprn[address] = next_uprn + next_uprn += 1 + + return address_to_uprn + + + @staticmethod + def _get_column_indexes_by_name( + headers: Tuple[object | None, ...] + ) -> Dict[str, int]: + index: Dict[str, int] = {} + + for i, header in enumerate(headers): + if isinstance(header, str): + index[header] = i + + return index \ No newline at end of file diff --git a/backend/condition/parsing/records/peabody/peabody_asset_condition.py b/backend/condition/parsing/records/peabody/peabody_asset_condition.py index 5682d13a..a82e87f1 100644 --- a/backend/condition/parsing/records/peabody/peabody_asset_condition.py +++ b/backend/condition/parsing/records/peabody/peabody_asset_condition.py @@ -15,7 +15,7 @@ class PeabodyAssetCondition: material_code: int material_or_answer: str renewal_quantity: int - renewal: int + renewal_year: int cloned: str lo_type_code: int renewal_cost: Optional[float] = None diff --git a/backend/condition/parsing/records/peabody/peabody_property.py b/backend/condition/parsing/records/peabody/peabody_property.py index 1bff1b55..bfa6b65b 100644 --- a/backend/condition/parsing/records/peabody/peabody_property.py +++ b/backend/condition/parsing/records/peabody/peabody_property.py @@ -5,5 +5,7 @@ from backend.condition.parsing.records.peabody.peabody_asset_condition import Pe @dataclass class PeabodyProperty: + # This could just be a uprn:assets dict, but making it a dataclass for consistency with + # other client models, might change in future uprn: int assets: List[PeabodyAssetCondition] \ No newline at end of file From ad03d11bc9443b02adb9fc363e5302a257d282e3 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Wed, 21 Jan 2026 17:26:15 +0000 Subject: [PATCH 10/50] fix typo in file type detector --- backend/condition/file_type.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/condition/file_type.py b/backend/condition/file_type.py index 07a0669c..e0736814 100644 --- a/backend/condition/file_type.py +++ b/backend/condition/file_type.py @@ -10,7 +10,7 @@ def detect_file_type(filepath: str) -> FileType: if "lbwf" in path: return FileType.LBWF - if "peadbody" in path: + if "peabody" in path: return FileType.Peabody raise ValueError("Unrecognised file path") \ No newline at end of file From a07020d085d2517737624dd24d42bba37316e1f3 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Wed, 21 Jan 2026 17:33:14 +0000 Subject: [PATCH 11/50] =?UTF-8?q?Detect=20block-level=20asset=20conditions?= =?UTF-8?q?=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peabody/peabody_asset_condition.py | 6 +++- .../tests/parsing/test_peabody_parser.py | 31 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/backend/condition/parsing/records/peabody/peabody_asset_condition.py b/backend/condition/parsing/records/peabody/peabody_asset_condition.py index a82e87f1..5451d570 100644 --- a/backend/condition/parsing/records/peabody/peabody_asset_condition.py +++ b/backend/condition/parsing/records/peabody/peabody_asset_condition.py @@ -19,4 +19,8 @@ class PeabodyAssetCondition: cloned: str lo_type_code: int renewal_cost: Optional[float] = None - condition_survey_date: Optional[datetime] = None \ No newline at end of file + condition_survey_date: Optional[datetime] = None + + @property + def is_block_level(self) -> bool: + raise NotImplementedError \ No newline at end of file diff --git a/backend/condition/tests/parsing/test_peabody_parser.py b/backend/condition/tests/parsing/test_peabody_parser.py index 5196e65d..830e8f2c 100644 --- a/backend/condition/tests/parsing/test_peabody_parser.py +++ b/backend/condition/tests/parsing/test_peabody_parser.py @@ -122,3 +122,34 @@ def test_peabody_parser_parses_conditions(peabody_assets_xlsx_bytes): assert len(result) == 3 assert all(isinstance(item, PeabodyProperty) for item in result) + +def test_peabody_asset_is_block_level(): + # arrange + asset_condition = PeabodyAssetCondition( + lo_reference="", + full_address="1-80 PRINCESS ALICE HOUSE LONDON", + location_type_code=0, + parent_lo_reference="", + element_code=0, + element="", + sub_element_code=0, + sub_element="", + material_code=0, + material_or_answer="", + renewal_quantity=0, + renewal_year=2026, + cloned="", + lo_type_code=0, + renewal_cost=None, + condition_survey_date=None + ) + + expected_block_level = True + + # act + actual_block_level = asset_condition.is_block_level + + # assert + assert expected_block_level == actual_block_level + + \ No newline at end of file From 187d7fbadd2284f58c31e89f30e91f13352ceb7e Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 22 Jan 2026 09:06:05 +0000 Subject: [PATCH 12/50] =?UTF-8?q?Detect=20block-level=20asset=20conditions?= =?UTF-8?q?=20-=20additional=20test=20cases=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tests/parsing/test_peabody_parser.py | 69 +++++++++++-------- 1 file changed, 41 insertions(+), 28 deletions(-) diff --git a/backend/condition/tests/parsing/test_peabody_parser.py b/backend/condition/tests/parsing/test_peabody_parser.py index 830e8f2c..63ed0799 100644 --- a/backend/condition/tests/parsing/test_peabody_parser.py +++ b/backend/condition/tests/parsing/test_peabody_parser.py @@ -1,5 +1,5 @@ -from typing import Any import pytest +from typing import Any from io import BytesIO from openpyxl import Workbook from datetime import datetime @@ -123,33 +123,46 @@ def test_peabody_parser_parses_conditions(peabody_assets_xlsx_bytes): assert all(isinstance(item, PeabodyProperty) for item in result) -def test_peabody_asset_is_block_level(): - # arrange - asset_condition = PeabodyAssetCondition( - lo_reference="", - full_address="1-80 PRINCESS ALICE HOUSE LONDON", - location_type_code=0, - parent_lo_reference="", - element_code=0, - element="", - sub_element_code=0, - sub_element="", - material_code=0, - material_or_answer="", - renewal_quantity=0, - renewal_year=2026, - cloned="", - lo_type_code=0, - renewal_cost=None, - condition_survey_date=None - ) +@pytest.fixture +def asset_condition_factory(): + def _factory(full_address: str) -> PeabodyAssetCondition: + return PeabodyAssetCondition( + lo_reference="", + full_address=full_address, + location_type_code=0, + parent_lo_reference="", + element_code=0, + element="", + sub_element_code=0, + sub_element="", + material_code=0, + material_or_answer="", + renewal_quantity=0, + renewal_year=2026, + cloned="", + lo_type_code=0, + renewal_cost=None, + condition_survey_date=None, + ) - expected_block_level = True + return _factory - # act - actual_block_level = asset_condition.is_block_level +@pytest.mark.parametrize( + "full_address, expected_block_level", + [ + ("1-80 PRINCESS ALICE HOUSE LONDON", True), + ("FLATS A-D 7 ST CHARLES SQUARE LONDON", True), + ("9A-9H HEDGEGATE COURT LONDON", True), + ("BLOCK MILNE HOUSE LONDON", True), + ("25 HAVERSHAM COURT GREENFORD", False), + ("FLAT 10 SPARROW COURT SOUTHMERE DRIVE LONDON SE2 9ES", False) + ], +) +def test_peabody_asset_is_block_level( + asset_condition_factory, + full_address, + expected_block_level, +): + asset_condition = asset_condition_factory(full_address) - # assert - assert expected_block_level == actual_block_level - - \ No newline at end of file + assert asset_condition.is_block_level == expected_block_level \ No newline at end of file From 80f3325cf07c7a5820f3efaa1bc1e34f628cead2 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 22 Jan 2026 09:08:15 +0000 Subject: [PATCH 13/50] =?UTF-8?q?Detect=20block-level=20asset=20conditions?= =?UTF-8?q?=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../records/peabody/peabody_asset_condition.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/backend/condition/parsing/records/peabody/peabody_asset_condition.py b/backend/condition/parsing/records/peabody/peabody_asset_condition.py index 5451d570..71fa1e9d 100644 --- a/backend/condition/parsing/records/peabody/peabody_asset_condition.py +++ b/backend/condition/parsing/records/peabody/peabody_asset_condition.py @@ -1,3 +1,5 @@ +import re + from dataclasses import dataclass from datetime import datetime from typing import Optional @@ -23,4 +25,15 @@ class PeabodyAssetCondition: @property def is_block_level(self) -> bool: - raise NotImplementedError \ No newline at end of file + if not self.full_address: + return False + + address = self.full_address.upper() + + block_level_patterns = [ + r"\bBLOCK\b", # "BLOCK MILNE HOUSE" + r"\bFLATS\b", # "FLATS A-D ..." + r"\b\d+[A-Z]?-\d+[A-Z]?\b", # "1-80", "9A-9H" + ] + + return any(re.search(pattern, address) for pattern in block_level_patterns) \ No newline at end of file From f8db0cadbafa33fee75419c8fb38c55cadf04f75 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 22 Jan 2026 09:15:37 +0000 Subject: [PATCH 14/50] Ignore block level assets during parsing --- backend/condition/parsing/peabody_parser.py | 7 +++--- .../peabody/peabody_asset_condition.py | 2 +- .../tests/parsing/test_peabody_parser.py | 24 +++++++++++++++++-- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/backend/condition/parsing/peabody_parser.py b/backend/condition/parsing/peabody_parser.py index d2229e1c..b053f2ea 100644 --- a/backend/condition/parsing/peabody_parser.py +++ b/backend/condition/parsing/peabody_parser.py @@ -33,9 +33,10 @@ class PeabodyParser(Parser): assets: List[PeabodyAssetCondition] = [] for row in asset_rows: try: - assets.append( - PeabodyParser._map_row_to_asset_record(row, asset_header_indexes) - ) + asset = PeabodyParser._map_row_to_asset_record(row, asset_header_indexes) + if not asset.is_block_level: + assets.append(asset) + except Exception as e: logger.error(f"Error mapping Peabody row to asset record: {e}") continue diff --git a/backend/condition/parsing/records/peabody/peabody_asset_condition.py b/backend/condition/parsing/records/peabody/peabody_asset_condition.py index 71fa1e9d..b1624999 100644 --- a/backend/condition/parsing/records/peabody/peabody_asset_condition.py +++ b/backend/condition/parsing/records/peabody/peabody_asset_condition.py @@ -32,7 +32,7 @@ class PeabodyAssetCondition: block_level_patterns = [ r"\bBLOCK\b", # "BLOCK MILNE HOUSE" - r"\bFLATS\b", # "FLATS A-D ..." + r"\bFLATS\b", # "FLATS A-D ..." r"\b\d+[A-Z]?-\d+[A-Z]?\b", # "1-80", "9A-9H" ] diff --git a/backend/condition/tests/parsing/test_peabody_parser.py b/backend/condition/tests/parsing/test_peabody_parser.py index 63ed0799..fb0e9d51 100644 --- a/backend/condition/tests/parsing/test_peabody_parser.py +++ b/backend/condition/tests/parsing/test_peabody_parser.py @@ -33,7 +33,25 @@ def peabody_assets_xlsx_bytes() -> BytesIO: ]) survey_records_d_and_lower.append([ "B000RAND", - "1-11 RANDOM HOUSE LONDON", + "1 RANDOM HOUSE LONDON", + 3, + "RAND2EST", + 110, + "ROOFS", + 1, + "Primary Roof", + 9, + "Other", + 3, + 2054, + 330, + "N", + 3, + datetime(2025,12,4,9,17,0) + ]) + survey_records_d_and_lower.append([ + "B000BLOCK", + "1100 BLOCK", 3, "RAND2EST", 110, @@ -51,7 +69,7 @@ def peabody_assets_xlsx_bytes() -> BytesIO: ]) survey_records_d_and_lower.append([ "B000FAKE", - "3-10 FAKE CLOSE LONDON", + "3 FAKE CLOSE LONDON", 3, "FAKEEST", 100, @@ -163,6 +181,8 @@ def test_peabody_asset_is_block_level( full_address, expected_block_level, ): + # arrange asset_condition = asset_condition_factory(full_address) + # act + assert assert asset_condition.is_block_level == expected_block_level \ No newline at end of file From 3cdc871aaea0575be8d97f0ea5a391681b8e8962 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 22 Jan 2026 09:23:01 +0000 Subject: [PATCH 15/50] =?UTF-8?q?Detect=20block-level=20asset=20conditions?= =?UTF-8?q?=20-=20additional=20test=20cases=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/condition/tests/parsing/test_peabody_parser.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/condition/tests/parsing/test_peabody_parser.py b/backend/condition/tests/parsing/test_peabody_parser.py index fb0e9d51..32ff79d8 100644 --- a/backend/condition/tests/parsing/test_peabody_parser.py +++ b/backend/condition/tests/parsing/test_peabody_parser.py @@ -172,6 +172,8 @@ def asset_condition_factory(): ("FLATS A-D 7 ST CHARLES SQUARE LONDON", True), ("9A-9H HEDGEGATE COURT LONDON", True), ("BLOCK MILNE HOUSE LONDON", True), + ("81A-B GORE ROAD LONDON", True), + ("73 & 74 HARVEST COURT ST. ALBANS", True), ("25 HAVERSHAM COURT GREENFORD", False), ("FLAT 10 SPARROW COURT SOUTHMERE DRIVE LONDON SE2 9ES", False) ], From 9ccbbafb29817d64f0e4c68262b663f04bc80331 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 22 Jan 2026 09:24:23 +0000 Subject: [PATCH 16/50] =?UTF-8?q?Detect=20block-level=20asset=20conditions?= =?UTF-8?q?=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../parsing/records/peabody/peabody_asset_condition.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/backend/condition/parsing/records/peabody/peabody_asset_condition.py b/backend/condition/parsing/records/peabody/peabody_asset_condition.py index b1624999..01215a26 100644 --- a/backend/condition/parsing/records/peabody/peabody_asset_condition.py +++ b/backend/condition/parsing/records/peabody/peabody_asset_condition.py @@ -31,9 +31,11 @@ class PeabodyAssetCondition: address = self.full_address.upper() block_level_patterns = [ - r"\bBLOCK\b", # "BLOCK MILNE HOUSE" - r"\bFLATS\b", # "FLATS A-D ..." - r"\b\d+[A-Z]?-\d+[A-Z]?\b", # "1-80", "9A-9H" + r"\bBLOCK\b", # BLOCK MILNE HOUSE + r"\bFLATS\b", # FLATS A-D + r"\b\d+[A-Z]?-\d+[A-Z]?\b", # 1-80, 9A-9H + r"\b\d+[A-Z]-[A-Z]\b", # 81A-B + r"\b\d+\s*&\s*\d+\b", # 73 & 74 ] return any(re.search(pattern, address) for pattern in block_level_patterns) \ No newline at end of file From 1634e04dda32bc4159b56a98970d68cd1779b5f4 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 22 Jan 2026 09:30:50 +0000 Subject: [PATCH 17/50] consistent address trimming when assmebling property objects --- backend/condition/parsing/peabody_parser.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/condition/parsing/peabody_parser.py b/backend/condition/parsing/peabody_parser.py index b053f2ea..17a955d7 100644 --- a/backend/condition/parsing/peabody_parser.py +++ b/backend/condition/parsing/peabody_parser.py @@ -54,7 +54,7 @@ class PeabodyParser(Parser): if asset.full_address is None: continue - address = asset.full_address + address = asset.full_address.strip() assets_by_address[address].append(asset) properties: List[PeabodyProperty] = [] @@ -121,7 +121,6 @@ class PeabodyParser(Parser): if address is None: continue - # Optional normalization address = address.strip() if address not in address_to_uprn: From a7201b0dc406a50f43b8307ed765b57458da0523 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 22 Jan 2026 12:20:17 +0000 Subject: [PATCH 18/50] Start writing peabody mapper. Now rethink model before continuing --- .../condition/domain/mapping/lbwf_mapper.py | 1 - .../domain/mapping/peabody_mapper.py | 14 +++++++ backend/condition/parsing/peabody_parser.py | 4 +- .../tests/mapping/test_peabody_mapper.py | 38 +++++++++++++++++++ 4 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 backend/condition/domain/mapping/peabody_mapper.py create mode 100644 backend/condition/tests/mapping/test_peabody_mapper.py diff --git a/backend/condition/domain/mapping/lbwf_mapper.py b/backend/condition/domain/mapping/lbwf_mapper.py index 0af21b7a..8b556284 100644 --- a/backend/condition/domain/mapping/lbwf_mapper.py +++ b/backend/condition/domain/mapping/lbwf_mapper.py @@ -1,5 +1,4 @@ from typing import Any, List, Optional -from datetime import datetime, date from backend.condition.domain.asset_condition import AssetCondition from backend.condition.domain.element import Element diff --git a/backend/condition/domain/mapping/peabody_mapper.py b/backend/condition/domain/mapping/peabody_mapper.py new file mode 100644 index 00000000..88d0a626 --- /dev/null +++ b/backend/condition/domain/mapping/peabody_mapper.py @@ -0,0 +1,14 @@ +from typing import Any, List, Optional + +from backend.condition.domain.asset_condition import AssetCondition +from backend.condition.domain.element import Element +from backend.condition.domain.mapping.mapper import Mapper +from backend.condition.parsing.records.peabody.peabody_asset_condition import PeabodyAssetCondition +from backend.condition.parsing.records.peabody.peabody_property import PeabodyProperty +from utils.logger import setup_logger + +logger = setup_logger() + +class PeabodyMapper(Mapper): + def map_asset_conditions_for_property(self, client_data: Any, survey_year: Optional[int]) -> List[AssetCondition]: + raise NotImplementedError \ No newline at end of file diff --git a/backend/condition/parsing/peabody_parser.py b/backend/condition/parsing/peabody_parser.py index 17a955d7..b8a548a7 100644 --- a/backend/condition/parsing/peabody_parser.py +++ b/backend/condition/parsing/peabody_parser.py @@ -35,7 +35,9 @@ class PeabodyParser(Parser): try: asset = PeabodyParser._map_row_to_asset_record(row, asset_header_indexes) if not asset.is_block_level: - assets.append(asset) + # Block-level condition surveys are out of scope for now + # until we have a wider think on how to handle block + assets.append(asset) # TODO: handle block-level assets except Exception as e: logger.error(f"Error mapping Peabody row to asset record: {e}") diff --git a/backend/condition/tests/mapping/test_peabody_mapper.py b/backend/condition/tests/mapping/test_peabody_mapper.py new file mode 100644 index 00000000..fc70b015 --- /dev/null +++ b/backend/condition/tests/mapping/test_peabody_mapper.py @@ -0,0 +1,38 @@ +from datetime import datetime + +from backend.condition.domain.mapping.peabody_mapper import PeabodyMapper +from backend.condition.domain.element import Element +from backend.condition.parsing.records.peabody.peabody_asset_condition import PeabodyAssetCondition +from backend.condition.parsing.records.peabody.peabody_property import PeabodyProperty +from backend.condition.domain.asset_condition import AssetCondition + +def test_peabody_mapper_maps_property(): + # arrange + peabody_property = PeabodyProperty( + uprn=1, + assets=[ + PeabodyAssetCondition( + lo_reference="1000RAND0000", + full_address="FLAT 1 RANDOM SQUARE FAKE STREET LONDON E1 1EE", + location_type_code=1, + parent_lo_reference="RAND1000", + element_code=50, + element="Internal", + sub_element_code=3, + sub_element="CCU", + material_code=2, + material_or_answer="RCD/MCB CCU", + renewal_quantity=1, + renewal_year=2038, + renewal_cost=500, + cloned="N", + lo_type_code=1, + condition_survey_date=datetime(2024,2,15,0,0,0), + ) + ] + ) + + # act + + # assert + assert False #temp \ No newline at end of file From 3289dc226dbbcbf17f5bce14339b329515c53664 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 22 Jan 2026 13:28:51 +0000 Subject: [PATCH 19/50] Rename Element to LbwfElement --- backend/condition/domain/asset_condition.py | 7 +-- .../domain/{element.py => lbwf_element.py} | 18 +++++-- .../condition/domain/mapping/lbwf_mapper.py | 50 ++++++++++++------- .../domain/mapping/peabody_mapper.py | 13 +++-- .../tests/mapping/test_lbwf_mapper.py | 34 +++++++------ .../tests/mapping/test_peabody_mapper.py | 13 +++-- 6 files changed, 84 insertions(+), 51 deletions(-) rename backend/condition/domain/{element.py => lbwf_element.py} (92%) diff --git a/backend/condition/domain/asset_condition.py b/backend/condition/domain/asset_condition.py index dffbdf88..a489090f 100644 --- a/backend/condition/domain/asset_condition.py +++ b/backend/condition/domain/asset_condition.py @@ -2,13 +2,14 @@ from dataclasses import dataclass from typing import Optional from datetime import date -from backend.condition.domain.element import Element +from backend.condition.domain.lbwf_element import LbwfElement + @dataclass class AssetCondition: uprn: int - element: Element # TODO: should HHSRS elements be handled differently? - condition_description: str # TODO: this probably needs to be some sort of enum so it's searchable/filterable on the frontend. Could be hard to map from string though + element: LbwfElement # TODO: should HHSRS elements be handled differently? + condition_description: str # TODO: this probably needs to be some sort of enum so it's searchable/filterable on the frontend. Could be hard to map from string though quantity: int renewal_year: Optional[int] = None source: Optional[str] = None diff --git a/backend/condition/domain/element.py b/backend/condition/domain/lbwf_element.py similarity index 92% rename from backend/condition/domain/element.py rename to backend/condition/domain/lbwf_element.py index 021c8492..52928c72 100644 --- a/backend/condition/domain/element.py +++ b/backend/condition/domain/lbwf_element.py @@ -1,7 +1,7 @@ from enum import StrEnum -class Element(StrEnum): +class LbwfElement(StrEnum): AHR_CAT = "Accessible Housing Register Category" ASBESTOS = "Asbestos Present" ASSETSAREA = "Assets Area for Decent Homes and Investment" @@ -49,12 +49,20 @@ class Element(StrEnum): EXTWNDWS1 = "Windows 1 in External Area" EXTWNDWS2 = "Windows 2 in External Area" FFHHDAMP = "Fitness for Human Habitation - Serious problem with damp" - FFHHDRNWC = "Fitness for Human Habitation - Problems with the drainage or the lavatories" - FFHHHCWAT = "Fitness for Human Habitation - Problem with the supply of hot and cold water" - FFHHNEGLC = "Fitness for Human Habitation - Building neglected and is in a bad condition" + FFHHDRNWC = ( + "Fitness for Human Habitation - Problems with the drainage or the lavatories" + ) + FFHHHCWAT = ( + "Fitness for Human Habitation - Problem with the supply of hot and cold water" + ) + FFHHNEGLC = ( + "Fitness for Human Habitation - Building neglected and is in a bad condition" + ) FFHHNONAT = "Fitness for Human Habitation - Not enough natural light" FFHHNOVEN = "Fitness for Human Habitation - Not enough ventilation" - FFHHPRPCK = "Fitness for Human Habitation - Difficult to prepare and cook food or wash up" + FFHHPRPCK = ( + "Fitness for Human Habitation - Difficult to prepare and cook food or wash up" + ) FFHHUNLAY = "Fitness for Human Habitation - Unsafe layout" FFHHUNSTA = "Fitness for Human Habitation - Building is unstable" FRARISKRTG = "Fire Risk Assessment Rating" diff --git a/backend/condition/domain/mapping/lbwf_mapper.py b/backend/condition/domain/mapping/lbwf_mapper.py index 8b556284..dcd1d748 100644 --- a/backend/condition/domain/mapping/lbwf_mapper.py +++ b/backend/condition/domain/mapping/lbwf_mapper.py @@ -1,29 +1,37 @@ from typing import Any, List, Optional from backend.condition.domain.asset_condition import AssetCondition -from backend.condition.domain.element import Element +from backend.condition.domain.lbwf_element import LbwfElement from backend.condition.domain.mapping.mapper import Mapper -from backend.condition.parsing.records.lbwf.lbwf_asset_condition import LbwfAssetCondition +from backend.condition.parsing.records.lbwf.lbwf_asset_condition import ( + LbwfAssetCondition, +) from backend.condition.parsing.records.lbwf.lbwf_house import LbwfHouse from utils.logger import setup_logger logger = setup_logger() + class LbwfMapper(Mapper): - - def map_asset_conditions_for_property(self, client_data: Any, survey_year: Optional[int]) -> List[AssetCondition]: - assert isinstance(client_data, LbwfHouse) # TODO: think of a better way to do this + + def map_asset_conditions_for_property( + self, client_data: Any, survey_year: Optional[int] + ) -> List[AssetCondition]: + assert isinstance( + client_data, LbwfHouse + ) # TODO: think of a better way to do this mapped_assets: List[AssetCondition] = [] uprn: int = client_data.uprn for raw_asset in client_data.assets: try: - element: Element = LbwfMapper._map_element(raw_asset.element_code) + element: LbwfElement = LbwfMapper._map_element(raw_asset.element_code) except: - logger.warning(f"Unrecognised LBWF Asset Element Code: {raw_asset.element_code}. Skipping record") + logger.warning( + f"Unrecognised LBWF Asset Element Code: {raw_asset.element_code}. Skipping record" + ) continue - mapped_assets.append( AssetCondition( @@ -31,7 +39,9 @@ class LbwfMapper(Mapper): element=element, condition_description=raw_asset.attribute_code_description, quantity=raw_asset.quantity, - renewal_year=LbwfMapper._calculate_renewal_year(raw_asset, survey_year), + renewal_year=LbwfMapper._calculate_renewal_year( + raw_asset, survey_year + ), source=raw_asset.element_comments, install_date=raw_asset.install_date, ) @@ -39,23 +49,25 @@ class LbwfMapper(Mapper): return mapped_assets + @staticmethod + def _map_element(lbwf_element_code: LbwfAssetCondition) -> LbwfElement: + return LbwfElement[lbwf_element_code] - @staticmethod - def _map_element(lbwf_element_code: LbwfAssetCondition) -> Element: - return Element[lbwf_element_code] - - @staticmethod - def _calculate_renewal_year(lbwf_asset: LbwfAssetCondition, survey_year: Optional[int]) -> Optional[int]: + def _calculate_renewal_year( + lbwf_asset: LbwfAssetCondition, survey_year: Optional[int] + ) -> Optional[int]: remaining_life_years: Optional[int] = lbwf_asset.remaining_life if not remaining_life_years: return None - + if not survey_year: return None - + try: return survey_year + remaining_life_years except: - logger.debug(f"Unable to map LBWF Asset remaining life {remaining_life_years} to renewal year, returning None") - return None \ No newline at end of file + logger.debug( + f"Unable to map LBWF Asset remaining life {remaining_life_years} to renewal year, returning None" + ) + return None diff --git a/backend/condition/domain/mapping/peabody_mapper.py b/backend/condition/domain/mapping/peabody_mapper.py index 88d0a626..4c647380 100644 --- a/backend/condition/domain/mapping/peabody_mapper.py +++ b/backend/condition/domain/mapping/peabody_mapper.py @@ -1,14 +1,19 @@ from typing import Any, List, Optional from backend.condition.domain.asset_condition import AssetCondition -from backend.condition.domain.element import Element +from backend.condition.domain.lbwf_element import LbwfElement from backend.condition.domain.mapping.mapper import Mapper -from backend.condition.parsing.records.peabody.peabody_asset_condition import PeabodyAssetCondition +from backend.condition.parsing.records.peabody.peabody_asset_condition import ( + PeabodyAssetCondition, +) from backend.condition.parsing.records.peabody.peabody_property import PeabodyProperty from utils.logger import setup_logger logger = setup_logger() + class PeabodyMapper(Mapper): - def map_asset_conditions_for_property(self, client_data: Any, survey_year: Optional[int]) -> List[AssetCondition]: - raise NotImplementedError \ No newline at end of file + def map_asset_conditions_for_property( + self, client_data: Any, survey_year: Optional[int] + ) -> List[AssetCondition]: + raise NotImplementedError diff --git a/backend/condition/tests/mapping/test_lbwf_mapper.py b/backend/condition/tests/mapping/test_lbwf_mapper.py index 151e5d19..c007b575 100644 --- a/backend/condition/tests/mapping/test_lbwf_mapper.py +++ b/backend/condition/tests/mapping/test_lbwf_mapper.py @@ -4,10 +4,13 @@ from datetime import date from backend.condition.domain.mapping.lbwf_mapper import LbwfMapper from backend.condition.parsing.records.lbwf.lbwf_house import LbwfHouse -from backend.condition.parsing.records.lbwf.lbwf_asset_condition import LbwfAssetCondition -from backend.condition.domain.element import Element +from backend.condition.parsing.records.lbwf.lbwf_asset_condition import ( + LbwfAssetCondition, +) +from backend.condition.domain.lbwf_element import LbwfElement from backend.condition.domain.asset_condition import AssetCondition + def test_lbwf_mapper_maps_house(): # arrange lbwf_house = LbwfHouse( @@ -162,11 +165,11 @@ def test_lbwf_mapper_maps_house(): element_numerical_value=None, element_text_value=None, quantity=1, - install_date=date(2009,4,1), + install_date=date(2009, 4, 1), remaining_life=26, element_comments="Source of Data = Codeman", ), - ] + ], ) mapper = LbwfMapper() @@ -175,7 +178,7 @@ def test_lbwf_mapper_maps_house(): expected_assets: List[AssetCondition] = [ AssetCondition( uprn=1, - element=Element.AHR_CAT, + element=LbwfElement.AHR_CAT, condition_description="General Needs", quantity=1, renewal_year=None, @@ -184,7 +187,7 @@ def test_lbwf_mapper_maps_house(): ), AssetCondition( uprn=1, - element=Element.FLVL, + element=LbwfElement.FLVL, condition_description="Ground Floor", quantity=1, renewal_year=None, @@ -193,7 +196,7 @@ def test_lbwf_mapper_maps_house(): ), AssetCondition( uprn=1, - element=Element.ASBESTOS, + element=LbwfElement.ASBESTOS, condition_description="Yes", quantity=None, renewal_year=None, @@ -202,7 +205,7 @@ def test_lbwf_mapper_maps_house(): ), AssetCondition( uprn=1, - element=Element.INTBTHRLOC, + element=LbwfElement.INTBTHRLOC, condition_description="Bathroom on Entrance Level in Property", quantity=1, renewal_year=None, @@ -211,7 +214,7 @@ def test_lbwf_mapper_maps_house(): ), AssetCondition( uprn=1, - element=Element.INTCHEXTNT, + element=LbwfElement.INTCHEXTNT, condition_description="No Central Heating in Property", quantity=1, renewal_year=None, @@ -220,7 +223,7 @@ def test_lbwf_mapper_maps_house(): ), AssetCondition( uprn=1, - element=Element.HHSRSFIRE, + element=LbwfElement.HHSRSFIRE, condition_description="Category 4 - Typical Risk", quantity=1, renewal_year=None, @@ -229,18 +232,19 @@ def test_lbwf_mapper_maps_house(): ), AssetCondition( uprn=1, - element=Element.EXTWALLFN1, + element=LbwfElement.EXTWALLFN1, condition_description="Render or Pebbledash Wall Finish 1 in External Area", quantity=1, renewal_year=2052, source="Source of Data = Codeman", - install_date=date(2009,4,1), + install_date=date(2009, 4, 1), ), - ] # act - actual_assets: List[AssetCondition] = mapper.map_asset_conditions_for_property(lbwf_house, survey_year) + actual_assets: List[AssetCondition] = mapper.map_asset_conditions_for_property( + lbwf_house, survey_year + ) # assert - assert actual_assets == expected_assets \ No newline at end of file + assert actual_assets == expected_assets diff --git a/backend/condition/tests/mapping/test_peabody_mapper.py b/backend/condition/tests/mapping/test_peabody_mapper.py index fc70b015..2d2446e5 100644 --- a/backend/condition/tests/mapping/test_peabody_mapper.py +++ b/backend/condition/tests/mapping/test_peabody_mapper.py @@ -1,11 +1,14 @@ from datetime import datetime from backend.condition.domain.mapping.peabody_mapper import PeabodyMapper -from backend.condition.domain.element import Element -from backend.condition.parsing.records.peabody.peabody_asset_condition import PeabodyAssetCondition +from backend.condition.domain.lbwf_element import LbwfElement +from backend.condition.parsing.records.peabody.peabody_asset_condition import ( + PeabodyAssetCondition, +) from backend.condition.parsing.records.peabody.peabody_property import PeabodyProperty from backend.condition.domain.asset_condition import AssetCondition + def test_peabody_mapper_maps_property(): # arrange peabody_property = PeabodyProperty( @@ -27,12 +30,12 @@ def test_peabody_mapper_maps_property(): renewal_cost=500, cloned="N", lo_type_code=1, - condition_survey_date=datetime(2024,2,15,0,0,0), + condition_survey_date=datetime(2024, 2, 15, 0, 0, 0), ) - ] + ], ) # act # assert - assert False #temp \ No newline at end of file + assert False # temp From 03fb727994672c9d389c7c44a129ab2679c4705d Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 22 Jan 2026 15:00:58 +0000 Subject: [PATCH 20/50] =?UTF-8?q?Remodel=20dataclasses=20map=20from=20LBWF?= =?UTF-8?q?=20objects=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/condition/domain/aspect_type.py | 20 +++ backend/condition/domain/asset_condition.py | 25 ++- backend/condition/domain/element.py | 152 ++++++++++++++++++ .../tests/mapping/test_lbwf_mapper.py | 108 ++++++++++--- 4 files changed, 274 insertions(+), 31 deletions(-) create mode 100644 backend/condition/domain/aspect_type.py create mode 100644 backend/condition/domain/element.py diff --git a/backend/condition/domain/aspect_type.py b/backend/condition/domain/aspect_type.py new file mode 100644 index 00000000..45d0f24b --- /dev/null +++ b/backend/condition/domain/aspect_type.py @@ -0,0 +1,20 @@ +from enum import Enum + + +class AspectType(str, Enum): + MATERIAL = "material" + CONDITION = "condition" + TYPE = "type" + CONFIGURATION = "configuration" + PRESENCE = "presence" + RISK = "risk" + SEVERITY = "severity" + LOCATION = "location" + FINISH = "finish" + INSULATION = "insulation" + POINTING = "pointing" + SPALLING = "spalling" + LINTELS = "lintels" + CLADDING = "cladding" + CATEGORY = "category" + QUANTITY = "quantity" diff --git a/backend/condition/domain/asset_condition.py b/backend/condition/domain/asset_condition.py index a489090f..cd57d9ff 100644 --- a/backend/condition/domain/asset_condition.py +++ b/backend/condition/domain/asset_condition.py @@ -1,16 +1,27 @@ from dataclasses import dataclass -from typing import Optional from datetime import date +from typing import Optional +from xml.dom.minidom import Element -from backend.condition.domain.lbwf_element import LbwfElement +from backend.condition.domain.aspect_type import AspectType +from backend.condition.domain.element import Element @dataclass class AssetCondition: uprn: int - element: LbwfElement # TODO: should HHSRS elements be handled differently? - condition_description: str # TODO: this probably needs to be some sort of enum so it's searchable/filterable on the frontend. Could be hard to map from string though - quantity: int - renewal_year: Optional[int] = None - source: Optional[str] = None + + element: Element + aspect_type: AspectType + + value: Optional[str] = None + + quantity: Optional[int] = None install_date: Optional[date] = None + renewal_year: Optional[int] = None + + element_instance: Optional[int] = None + element_location: Optional[str] = None + + source_system: Optional[str] = None + comments: Optional[str] = None diff --git a/backend/condition/domain/element.py b/backend/condition/domain/element.py new file mode 100644 index 00000000..d9698ddf --- /dev/null +++ b/backend/condition/domain/element.py @@ -0,0 +1,152 @@ +from enum import Enum + + +class Element(str, Enum): + + # ====================== + # PROPERTY / GENERAL + # ====================== + PROPERTY_TYPE = "property_type" + PROPERTY_CONSTRUCTION_TYPE = "property_construction_type" + PROPERTY_CLASSIFICATION = "property_classification" + PROPERTY_AGE_BAND = "property_age_band" + STOREY_COUNT = "storey_count" + FLOOR_LEVEL_FRONT_DOOR = "floor_level_front_door" + ACCESSIBLE_HOUSING_REGISTER = "accessible_housing_register" + ASBESTOS = "asbestos" + + # ====================== + # EXTERNAL – ROOF + # ====================== + ROOF_COVERING = "roof_covering" + ROOF_STRUCTURE = "roof_structure" + ROOF_CHIMNEY = "roof_chimney" + ROOF_FASCIA = "roof_fascia" + ROOF_SOFFIT = "roof_soffit" + RAINWATER_GOODS = "rainwater_goods" + ROOF_PORCH_CANOPY = "roof_porch_canopy" + LOFT_INSULATION = "loft_insulation" + + # ====================== + # EXTERNAL – WALLS + # ====================== + EXTERNAL_WALL = "external_wall" + + # ====================== + # EXTERNAL – WINDOWS + # ====================== + WINDOWS = "windows" + COMMUNAL_WINDOWS = "communal_windows" + SECONDARY_GLAZING = "secondary_glazing" + + # ====================== + # EXTERNAL – DOORS + # ====================== + FRONT_DOOR = "front_door" + REAR_DOOR = "rear_door" + STORE_DOOR = "store_door" + GARAGE_DOOR = "garage_door" + COMMUNAL_ENTRANCE_DOOR = "communal_entrance_door" + + # ====================== + # EXTERNAL – AREAS + # ====================== + PATHS_AND_HARDSTANDINGS = "paths_and_hardstandings" + PARKING_AREAS = "parking_areas" + BOUNDARY_WALLS = "boundary_walls" + FENCING = "fencing" + GATES = "gates" + RETAINING_WALLS = "retaining_walls" + PRIVATE_BALCONY = "private_balcony" + BALCONY_BALUSTRADE = "balcony_balustrade" + OUTBUILDINGS = "outbuildings" + GARAGE_STRUCTURE = "garage_structure" + + # ====================== + # INTERNAL – KITCHEN + # ====================== + KITCHEN = "kitchen" + KITCHEN_SPACE_LAYOUT = "kitchen_space_layout" + TENANT_INSTALLED_KITCHEN = "tenant_installed_kitchen" + + # ====================== + # INTERNAL – BATHROOM + # ====================== + BATHROOM = "bathroom" + + # ====================== + # INTERNAL – HEATING / WATER + # ====================== + HEATING_BOILER = "heating_boiler" + HEATING_DISTRIBUTION = "heating_distribution" + HEATING_EXTENT = "heating_extent" + SECONDARY_HEATING = "secondary_heating" + HOT_WATER_SYSTEM = "hot_water_system" + COLD_WATER_STORAGE = "cold_water_storage" + PROGRAMMABLE_HEATING = "programmable_heating" + + # ====================== + # INTERNAL – ELECTRICS / FIRE + # ====================== + ELECTRICAL_WIRING = "electrical_wiring" + CONSUMER_UNIT = "consumer_unit" + SMOKE_DETECTION = "smoke_detection" + HEAT_DETECTION = "heat_detection" + CARBON_MONOXIDE_DETECTION = "carbon_monoxide_detection" + FIRE_DOOR_RATING = "fire_door_rating" + + # ====================== + # COMMUNAL SYSTEMS + # ====================== + COMMUNAL_HEATING = "communal_heating" + COMMUNAL_BOILER = "communal_boiler" + COMMUNAL_ELECTRICS = "communal_electrics" + COMMUNAL_FIRE_ALARM = "communal_fire_alarm" + COMMUNAL_EMERGENCY_LIGHTING = "communal_emergency_lighting" + COMMUNAL_LIFT = "communal_lift" + COMMUNAL_DOOR_ENTRY = "communal_door_entry" + COMMUNAL_CCTV = "communal_cctv" + COMMUNAL_BIN_STORE = "communal_bin_store" + COMMUNAL_REFUSE_CHUTE = "communal_refuse_chute" + + # ========================================================== + # HHSRS – ALL 29 HAZARDS + # ========================================================== + + # --- Physiological requirements (4) + HHSRS_DAMP_AND_MOULD = "hhsrs_damp_and_mould" + HHSRS_EXCESS_COLD = "hhsrs_excess_cold" + HHSRS_EXCESS_HEAT = "hhsrs_excess_heat" + HHSRS_ASBESTOS_AND_MMF = "hhsrs_asbestos_and_mmf" + + # --- Psychological requirements (4) + HHSRS_CROWDING_AND_SPACE = "hhsrs_crowding_and_space" + HHSRS_ENTRY_BY_INTRUDERS = "hhsrs_entry_by_intruders" + HHSRS_LIGHTING = "hhsrs_lighting" + HHSRS_NOISE = "hhsrs_noise" + + # --- Protection against infection (6) + HHSRS_DOMESTIC_HYGIENE_PESTS_REFUSE = "hhsrs_domestic_hygiene_pests_refuse" + HHSRS_FOOD_SAFETY = "hhsrs_food_safety" + HHSRS_PERSONAL_HYGIENE_SANITATION = "hhsrs_personal_hygiene_sanitation" + HHSRS_WATER_SUPPLY = "hhsrs_water_supply" + HHSRS_FALLS_ASSOCIATED_WITH_BATHS = "hhsrs_falls_associated_with_baths" + HHSRS_SURFACES_MOULD = "hhsrs_surfaces_mould" + + # --- Protection against accidents (10) + HHSRS_FALLS_ON_LEVEL_SURFACES = "hhsrs_falls_on_level_surfaces" + HHSRS_FALLS_ON_STAIRS = "hhsrs_falls_on_stairs" + HHSRS_FALLS_BETWEEN_LEVELS = "hhsrs_falls_between_levels" + HHSRS_ELECTRICAL_HAZARDS = "hhsrs_electrical_hazards" + HHSRS_FIRE = "hhsrs_fire" + HHSRS_FLAMES_HOT_SURFACES = "hhsrs_flames_hot_surfaces" + HHSRS_COLLISION_AND_ENTRAPMENT = "hhsrs_collision_and_entrapment" + HHSRS_EXPLOSION = "hhsrs_explosion" + HHSRS_STRUCTURAL_COLLAPSE = "hhsrs_structural_collapse" + HHSRS_UNSAFE_GAS = "hhsrs_unsafe_gas" + + # --- Protection against pollution (4) + HHSRS_CARBON_MONOXIDE = "hhsrs_carbon_monoxide" + HHSRS_LEAD = "hhsrs_lead" + HHSRS_RADIATION = "hhsrs_radiation" + HHSRS_UNCOMBUSTED_FUEL_GAS = "hhsrs_uncombusted_fuel_gas" diff --git a/backend/condition/tests/mapping/test_lbwf_mapper.py b/backend/condition/tests/mapping/test_lbwf_mapper.py index c007b575..f930fdb4 100644 --- a/backend/condition/tests/mapping/test_lbwf_mapper.py +++ b/backend/condition/tests/mapping/test_lbwf_mapper.py @@ -1,7 +1,10 @@ from typing import List +from xml.dom.minidom import Element import pytest from datetime import date +from backend.condition.domain.aspect_type import AspectType +from backend.condition.domain.element import Element from backend.condition.domain.mapping.lbwf_mapper import LbwfMapper from backend.condition.parsing.records.lbwf.lbwf_house import LbwfHouse from backend.condition.parsing.records.lbwf.lbwf_asset_condition import ( @@ -85,6 +88,27 @@ def test_lbwf_mapper_maps_house(): remaining_life=None, element_comments="Source of Data = ACT", ), + LbwfAssetCondition( + prop_ref=100, + domna=100, + address="123 Fake Street, London, A10 1AB", + ownership="LBWF_OWNED", + prop_status="OCCP", + prop_type="HOU", + prop_sub_type="TERRACED", + element_group="ASSETS", + element_code="HHSRSASB", + element_code_description="Asbestos (and MMF)", + attribute_code="TYPRISK", + attribute_code_description="Category 4 - Typical Risk", + element_date_value=None, + element_numerical_value=None, + element_text_value=None, + quantity=None, + install_date=None, + remaining_life=None, + element_comments="Source of Data = ACT", + ), LbwfAssetCondition( prop_ref=100, domna=100, @@ -158,9 +182,9 @@ def test_lbwf_mapper_maps_house(): prop_sub_type="TERRACED", element_group="ASSETS", element_code="EXTWALLFN1", - element_code_description="Wall Finish 1 in External Area", - attribute_code="RENDERPBBL", - attribute_code_description="Render or Pebbledash Wall Finish 1 in External Area", + element_code_description="Wall Finish 2 in External Area", + attribute_code="SMTHRENDER", + attribute_code_description="Smooth Render Wall Finish 1 in External Area", element_date_value=None, element_numerical_value=None, element_text_value=None, @@ -178,66 +202,102 @@ def test_lbwf_mapper_maps_house(): expected_assets: List[AssetCondition] = [ AssetCondition( uprn=1, - element=LbwfElement.AHR_CAT, - condition_description="General Needs", + element=Element.ACCESSIBLE_HOUSING_REGISTER, + aspect_type=AspectType.CATEGORY, + element_instance=None, + value="General Needs", quantity=1, renewal_year=None, - source=None, install_date=None, + comments=None, ), AssetCondition( uprn=1, - element=LbwfElement.FLVL, - condition_description="Ground Floor", + element=Element.FLOOR_LEVEL_FRONT_DOOR, + aspect_type=AspectType.LOCATION, + element_instance=None, + value="Ground Floor", quantity=1, renewal_year=None, - source=None, install_date=None, + comments=None, ), AssetCondition( uprn=1, - element=LbwfElement.ASBESTOS, - condition_description="Yes", + element=Element.ASBESTOS, + aspect_type=AspectType.PRESENCE, + element_instance=None, + value="Yes", quantity=None, renewal_year=None, - source="Source of Data = ACT", install_date=None, + comments="Source of Data = ACT", ), AssetCondition( uprn=1, - element=LbwfElement.INTBTHRLOC, - condition_description="Bathroom on Entrance Level in Property", + element=Element.HHSRS_ASBESTOS_AND_MMF, + aspect_type=AspectType.RISK, + element_instance=None, + value="Category 4 - Typical Risk", + quantity=None, + renewal_year=None, + install_date=None, + comments="Source of Data = ACT", + ), + AssetCondition( + uprn=1, + element=Element.BATHROOM, + aspect_type=AspectType.LOCATION, + element_instance=None, + value="Bathroom on Entrance Level in Property", quantity=1, renewal_year=None, - source="Source of Data = Codeman", install_date=None, + comments="Source of Data = Codeman", ), AssetCondition( uprn=1, - element=LbwfElement.INTCHEXTNT, - condition_description="No Central Heating in Property", + element=Element.HEATING_EXTENT, + aspect_type=AspectType.CONFIGURATION, + element_instance=None, + value="No Central Heating in Property", quantity=1, renewal_year=None, - source="Source of Data = Codeman", install_date=None, + comments="Source of Data = Codeman", ), AssetCondition( uprn=1, - element=LbwfElement.HHSRSFIRE, - condition_description="Category 4 - Typical Risk", + element=Element.HHSRS_FIRE, + aspect_type=AspectType.RISK, + element_instance=None, + value="Category 4 - Typical Risk", quantity=1, renewal_year=None, - source="Source of Data = Morgan Sindall", install_date=None, + comments="Source of Data = Morgan Sindall", ), AssetCondition( uprn=1, - element=LbwfElement.EXTWALLFN1, - condition_description="Render or Pebbledash Wall Finish 1 in External Area", + element=Element.EXTERNAL_WALL, + aspect_type=AspectType.FINISH, + element_instance=1, + value="Render or Pebbledash", quantity=1, renewal_year=2052, - source="Source of Data = Codeman", install_date=date(2009, 4, 1), + comments="Source of Data = Codeman", + ), + AssetCondition( + uprn=1, + element=Element.EXTERNAL_WALL, + aspect_type=AspectType.FINISH, + element_instance=2, + value="Smooth Render Wall Finish 1 in External Area", + quantity=1, + renewal_year=2052, + install_date=date(2009, 4, 1), + comments="Source of Data = Codeman", ), ] From fa72d162395f3e70c34600757afda49c98cacdd1 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 22 Jan 2026 15:21:31 +0000 Subject: [PATCH 21/50] small tidies --- .../condition/domain/mapping/{ => lbwf}/lbwf_mapper.py | 0 .../domain/mapping/{ => peabody}/peabody_mapper.py | 1 - backend/condition/parsing/factory.py | 8 +++++--- backend/condition/processor.py | 5 +++-- backend/condition/tests/mapping/test_lbwf_mapper.py | 3 +-- backend/condition/tests/mapping/test_peabody_mapper.py | 3 +-- 6 files changed, 10 insertions(+), 10 deletions(-) rename backend/condition/domain/mapping/{ => lbwf}/lbwf_mapper.py (100%) rename backend/condition/domain/mapping/{ => peabody}/peabody_mapper.py (90%) diff --git a/backend/condition/domain/mapping/lbwf_mapper.py b/backend/condition/domain/mapping/lbwf/lbwf_mapper.py similarity index 100% rename from backend/condition/domain/mapping/lbwf_mapper.py rename to backend/condition/domain/mapping/lbwf/lbwf_mapper.py diff --git a/backend/condition/domain/mapping/peabody_mapper.py b/backend/condition/domain/mapping/peabody/peabody_mapper.py similarity index 90% rename from backend/condition/domain/mapping/peabody_mapper.py rename to backend/condition/domain/mapping/peabody/peabody_mapper.py index 4c647380..8413b888 100644 --- a/backend/condition/domain/mapping/peabody_mapper.py +++ b/backend/condition/domain/mapping/peabody/peabody_mapper.py @@ -1,7 +1,6 @@ from typing import Any, List, Optional from backend.condition.domain.asset_condition import AssetCondition -from backend.condition.domain.lbwf_element import LbwfElement from backend.condition.domain.mapping.mapper import Mapper from backend.condition.parsing.records.peabody.peabody_asset_condition import ( PeabodyAssetCondition, diff --git a/backend/condition/parsing/factory.py b/backend/condition/parsing/factory.py index 3a28df78..7233a1df 100644 --- a/backend/condition/parsing/factory.py +++ b/backend/condition/parsing/factory.py @@ -1,21 +1,23 @@ -from backend.condition.domain.mapping.lbwf_mapper import LbwfMapper +from backend.condition.domain.mapping.lbwf.lbwf_mapper import LbwfMapper from backend.condition.domain.mapping.mapper import Mapper from backend.condition.file_type import FileType from backend.condition.parsing.parser import Parser from backend.condition.parsing.lbwf_parser import LbwfParser from backend.condition.parsing.peabody_parser import PeabodyParser + def select_parser(file_type: FileType) -> Parser: if file_type is FileType.LBWF: return LbwfParser() - + if file_type is FileType.Peabody: return PeabodyParser() raise ValueError("Unrecognised file type, unable to instantiate Parser") + def select_mapper(file_type: FileType) -> Mapper: if file_type is FileType.LBWF: return LbwfMapper() - + raise ValueError("Unrecognised file type, unable to instantiate Mapper") diff --git a/backend/condition/processor.py b/backend/condition/processor.py index cc44e38a..a48e22f4 100644 --- a/backend/condition/processor.py +++ b/backend/condition/processor.py @@ -8,6 +8,7 @@ from utils.logger import setup_logger from backend.condition.file_type import FileType, detect_file_type from backend.condition.parsing.factory import select_parser, select_mapper + def process_file(file_stream: BinaryIO, source_key: str) -> None: print(f"[processor] Received file: {source_key}") @@ -19,10 +20,10 @@ def process_file(file_stream: BinaryIO, source_key: str) -> None: # Orchestration raw_properties: List[Any] = parser.parse(file_stream) - survey_year = datetime.now().year # TODO: get this from filepath or elsewhere + survey_year = datetime.now().year # TODO: get this from filepath or elsewhere assets: List[AssetCondition] = [] for p in raw_properties: assets.extend(mapper.map_asset_conditions_for_property(p, survey_year)) - print(assets) # temp \ No newline at end of file + print(assets) # temp diff --git a/backend/condition/tests/mapping/test_lbwf_mapper.py b/backend/condition/tests/mapping/test_lbwf_mapper.py index f930fdb4..f4266ac4 100644 --- a/backend/condition/tests/mapping/test_lbwf_mapper.py +++ b/backend/condition/tests/mapping/test_lbwf_mapper.py @@ -5,12 +5,11 @@ from datetime import date from backend.condition.domain.aspect_type import AspectType from backend.condition.domain.element import Element -from backend.condition.domain.mapping.lbwf_mapper import LbwfMapper +from backend.condition.domain.mapping.lbwf.lbwf_mapper import LbwfMapper from backend.condition.parsing.records.lbwf.lbwf_house import LbwfHouse from backend.condition.parsing.records.lbwf.lbwf_asset_condition import ( LbwfAssetCondition, ) -from backend.condition.domain.lbwf_element import LbwfElement from backend.condition.domain.asset_condition import AssetCondition diff --git a/backend/condition/tests/mapping/test_peabody_mapper.py b/backend/condition/tests/mapping/test_peabody_mapper.py index 2d2446e5..de027fe7 100644 --- a/backend/condition/tests/mapping/test_peabody_mapper.py +++ b/backend/condition/tests/mapping/test_peabody_mapper.py @@ -1,7 +1,6 @@ from datetime import datetime -from backend.condition.domain.mapping.peabody_mapper import PeabodyMapper -from backend.condition.domain.lbwf_element import LbwfElement +from backend.condition.domain.mapping.peabody.peabody_mapper import PeabodyMapper from backend.condition.parsing.records.peabody.peabody_asset_condition import ( PeabodyAssetCondition, ) From 212d62e8358eaee6f722b90d295660074d08537c Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 22 Jan 2026 16:28:10 +0000 Subject: [PATCH 22/50] =?UTF-8?q?Map=20to=20new=20dataclasses=20from=20LBW?= =?UTF-8?q?F=20objects=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/condition/domain/aspect_type.py | 9 + backend/condition/domain/asset_condition.py | 1 - backend/condition/domain/element.py | 18 +- .../domain/mapping/lbwf/lbwf_element_map.py | 351 ++++++++++++++++++ .../domain/mapping/lbwf/lbwf_mapper.py | 25 +- .../tests/mapping/test_lbwf_mapper.py | 36 +- 6 files changed, 417 insertions(+), 23 deletions(-) create mode 100644 backend/condition/domain/mapping/lbwf/lbwf_element_map.py diff --git a/backend/condition/domain/aspect_type.py b/backend/condition/domain/aspect_type.py index 45d0f24b..0f9a406a 100644 --- a/backend/condition/domain/aspect_type.py +++ b/backend/condition/domain/aspect_type.py @@ -5,6 +5,7 @@ class AspectType(str, Enum): MATERIAL = "material" CONDITION = "condition" TYPE = "type" + AREA = "area" CONFIGURATION = "configuration" PRESENCE = "presence" RISK = "risk" @@ -18,3 +19,11 @@ class AspectType(str, Enum): CLADDING = "cladding" CATEGORY = "category" QUANTITY = "quantity" + ADEQUACY = "adequacy" + RATING = "rating" + STRATEGY = "strategy" + EXTENT = "extent" + DISTRIBUTION = "distribution" + STRUCTURE = "structure" + COVERING = "covering" + FIRE_RATING = "fire_rating" diff --git a/backend/condition/domain/asset_condition.py b/backend/condition/domain/asset_condition.py index cd57d9ff..1b157a6b 100644 --- a/backend/condition/domain/asset_condition.py +++ b/backend/condition/domain/asset_condition.py @@ -21,7 +21,6 @@ class AssetCondition: renewal_year: Optional[int] = None element_instance: Optional[int] = None - element_location: Optional[str] = None source_system: Optional[str] = None comments: Optional[str] = None diff --git a/backend/condition/domain/element.py b/backend/condition/domain/element.py index d9698ddf..e082bd4f 100644 --- a/backend/condition/domain/element.py +++ b/backend/condition/domain/element.py @@ -6,7 +6,7 @@ class Element(str, Enum): # ====================== # PROPERTY / GENERAL # ====================== - PROPERTY_TYPE = "property_type" + PROPERTY = "property" PROPERTY_CONSTRUCTION_TYPE = "property_construction_type" PROPERTY_CLASSIFICATION = "property_classification" PROPERTY_AGE_BAND = "property_age_band" @@ -14,18 +14,15 @@ class Element(str, Enum): FLOOR_LEVEL_FRONT_DOOR = "floor_level_front_door" ACCESSIBLE_HOUSING_REGISTER = "accessible_housing_register" ASBESTOS = "asbestos" + QUALITY_STANDARD = "quality_standard" # ====================== # EXTERNAL – ROOF # ====================== - ROOF_COVERING = "roof_covering" - ROOF_STRUCTURE = "roof_structure" - ROOF_CHIMNEY = "roof_chimney" - ROOF_FASCIA = "roof_fascia" - ROOF_SOFFIT = "roof_soffit" + ROOF = "roof" RAINWATER_GOODS = "rainwater_goods" - ROOF_PORCH_CANOPY = "roof_porch_canopy" LOFT_INSULATION = "loft_insulation" + PORCH_CANOPY = "porch_canopy" # ====================== # EXTERNAL – WALLS @@ -35,13 +32,14 @@ class Element(str, Enum): # ====================== # EXTERNAL – WINDOWS # ====================== - WINDOWS = "windows" + EXTERNAL_WINDOWS = "external_windows" COMMUNAL_WINDOWS = "communal_windows" SECONDARY_GLAZING = "secondary_glazing" # ====================== # EXTERNAL – DOORS # ====================== + EXTERNAL_DOOR = "external_door" FRONT_DOOR = "front_door" REAR_DOOR = "rear_door" STORE_DOOR = "store_door" @@ -84,6 +82,9 @@ class Element(str, Enum): HOT_WATER_SYSTEM = "hot_water_system" COLD_WATER_STORAGE = "cold_water_storage" PROGRAMMABLE_HEATING = "programmable_heating" + HEATING_SYSTEM = "heating_system" + BOILER_FUEL = "boiler_fuel" + WATER_HEATING = "water_heating" # ====================== # INTERNAL – ELECTRICS / FIRE @@ -94,6 +95,7 @@ class Element(str, Enum): HEAT_DETECTION = "heat_detection" CARBON_MONOXIDE_DETECTION = "carbon_monoxide_detection" FIRE_DOOR_RATING = "fire_door_rating" + FIRE_RISK_ASSESSMENT = "fire" # ====================== # COMMUNAL SYSTEMS diff --git a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py new file mode 100644 index 00000000..6927e2fd --- /dev/null +++ b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py @@ -0,0 +1,351 @@ +from dataclasses import dataclass +from typing import Optional + +from backend.condition.domain.element import Element +from backend.condition.domain.aspect_type import AspectType + + +@dataclass(frozen=True) +class LbwfElementMapping: + element: Element + aspect_type: AspectType + element_instance: Optional[int] = None + + +LBWF_ELEMENT_MAP: dict[str, LbwfElementMapping] = { + # ========================================================== + # PROPERTY / GENERAL + # ========================================================== + "AHR_CAT": LbwfElementMapping( + element=Element.ACCESSIBLE_HOUSING_REGISTER, + aspect_type=AspectType.CATEGORY, + ), + "ASSETSAREA": LbwfElementMapping( + element=Element.PROPERTY, + aspect_type=AspectType.AREA, + ), + # "DECNTHMINC": LbwfElementMapping( + # element=Element.DECENT_HOMES, + # aspect_type=AspectType.INCLUSION, + # ), # Ignore this one + "QUALITYSTD": LbwfElementMapping( + element=Element.QUALITY_STANDARD, + aspect_type=AspectType.TYPE, + ), + "EXTSTOREY": LbwfElementMapping( + element=Element.PROPERTY, + aspect_type=AspectType.CONFIGURATION, + ), + "FLVL": LbwfElementMapping( + element=Element.FLOOR_LEVEL_FRONT_DOOR, + aspect_type=AspectType.LOCATION, + ), + # ========================================================== + # ASBESTOS (NON-HHSRS RECORD) + # ========================================================== + "ASBESTOS": LbwfElementMapping( + element=Element.ASBESTOS, + aspect_type=AspectType.PRESENCE, + ), + # ========================================================== + # INTERNAL – BATHROOMS & KITCHENS + # ========================================================== + "INTBTHRLOC": LbwfElementMapping( + element=Element.BATHROOM, + aspect_type=AspectType.LOCATION, + ), + "INTBTHADEQ": LbwfElementMapping( + element=Element.BATHROOM, + aspect_type=AspectType.ADEQUACY, + ), + "INTKITADEQ": LbwfElementMapping( + element=Element.KITCHEN, + aspect_type=AspectType.ADEQUACY, + ), + "INTCKRLOC": LbwfElementMapping( + element=Element.KITCHEN, + aspect_type=AspectType.LOCATION, + ), + # ========================================================== + # INTERNAL – HEATING + # ========================================================== + "INTCHEXTNT": LbwfElementMapping( + element=Element.HEATING_EXTENT, + aspect_type=AspectType.CONFIGURATION, + ), + "INTCHDIST": LbwfElementMapping( + element=Element.HEATING_DISTRIBUTION, + aspect_type=AspectType.TYPE, + ), + "INTCHBLR": LbwfElementMapping( + element=Element.HEATING_BOILER, + aspect_type=AspectType.TYPE, + ), + # ========================================================== + # INTERNAL – FIRE + # ========================================================== + "FRARISKRTG": LbwfElementMapping( + element=Element.FIRE_RISK_ASSESSMENT, + aspect_type=AspectType.RATING, + ), + "FRATYPE": LbwfElementMapping( + element=Element.FIRE_RISK_ASSESSMENT, + aspect_type=AspectType.TYPE, + ), + "FRAEVACSTR": LbwfElementMapping( + element=Element.FIRE_RISK_ASSESSMENT, + aspect_type=AspectType.STRATEGY, + ), + "INTSMKDET": LbwfElementMapping( + element=Element.SMOKE_DETECTION, + aspect_type=AspectType.PRESENCE, + ), + "INTCHEXTNT": LbwfElementMapping( + element=Element.HEATING_SYSTEM, + aspect_type=AspectType.EXTENT, + ), + # ========================================================== + # HEATING & SERVICES + # ========================================================== + "INTBOILERF": LbwfElementMapping( + element=Element.BOILER_FUEL, + aspect_type=AspectType.TYPE, + ), + "INTHTDISYS": LbwfElementMapping( + element=Element.HEATING_SYSTEM, + aspect_type=AspectType.DISTRIBUTION, + ), + "INTWTRHTNG": LbwfElementMapping( + element=Element.WATER_HEATING, + aspect_type=AspectType.TYPE, + ), + # ========================================================== + # EXTERNAL – WALLS (INSTANCED) + # ========================================================== + "EXTWALLSTR": LbwfElementMapping( + element=Element.EXTERNAL_WALL, + aspect_type=AspectType.STRUCTURE, + element_instance=1, + ), + "EXTWALLFN1": LbwfElementMapping( + element=Element.EXTERNAL_WALL, + aspect_type=AspectType.FINISH, + element_instance=1, + ), + "EXTWALLFN2": LbwfElementMapping( + element=Element.EXTERNAL_WALL, + aspect_type=AspectType.FINISH, + element_instance=2, + ), + "EXTWALLINS": LbwfElementMapping( + element=Element.EXTERNAL_WALL, + aspect_type=AspectType.INSULATION, + ), + "EXTWALLSPL": LbwfElementMapping( + element=Element.EXTERNAL_WALL, + aspect_type=AspectType.CONDITION, + ), + # ========================================================== + # EXTERNAL – ROOFS (INSTANCED) + # ========================================================== + "EXTRFSTR1": LbwfElementMapping( + element=Element.ROOF, + aspect_type=AspectType.STRUCTURE, + element_instance=1, + ), + "EXTRFSTR2": LbwfElementMapping( + element=Element.ROOF, + aspect_type=AspectType.STRUCTURE, + element_instance=2, + ), + "EXTRFSTR3": LbwfElementMapping( + element=Element.ROOF, + aspect_type=AspectType.STRUCTURE, + element_instance=3, + ), + "EXTROOF1": LbwfElementMapping( + element=Element.ROOF, + aspect_type=AspectType.COVERING, + element_instance=1, + ), + "EXTROOF2": LbwfElementMapping( + element=Element.ROOF, + aspect_type=AspectType.COVERING, + element_instance=2, + ), + "EXTROOF3": LbwfElementMapping( + element=Element.ROOF, + aspect_type=AspectType.COVERING, + element_instance=3, + ), + # ========================================================== + # EXTERNAL – DOORS & WINDOWS + # ========================================================== + "INTFRDOOR": LbwfElementMapping( + element=Element.EXTERNAL_DOOR, + aspect_type=AspectType.TYPE, + ), + "INTFRDRFRR": LbwfElementMapping( + element=Element.EXTERNAL_DOOR, + aspect_type=AspectType.FIRE_RATING, + ), + "EXTBKSDDR1": LbwfElementMapping( + element=Element.EXTERNAL_DOOR, + aspect_type=AspectType.TYPE, + element_instance=1, + ), + "EXTBKSDDR2": LbwfElementMapping( + element=Element.EXTERNAL_DOOR, + aspect_type=AspectType.TYPE, + element_instance=2, + ), + "INTWDWTYPE": LbwfElementMapping( + element=Element.EXTERNAL_WINDOWS, + aspect_type=AspectType.TYPE, + ), + "EXTWNDWS1": LbwfElementMapping( + element=Element.EXTERNAL_WINDOWS, + aspect_type=AspectType.TYPE, + element_instance=1, + ), + "EXTWNDWS2": LbwfElementMapping( + element=Element.EXTERNAL_WINDOWS, + aspect_type=AspectType.TYPE, + element_instance=2, + ), + # ========================================================== + # HHSRS – PHYSIOLOGICAL REQUIREMENTS + # ========================================================== + "HHSRSDAMP": LbwfElementMapping( + element=Element.HHSRS_DAMP_AND_MOULD, + aspect_type=AspectType.RISK, + ), + "HHSRSCOLD": LbwfElementMapping( + element=Element.HHSRS_EXCESS_COLD, + aspect_type=AspectType.RISK, + ), + "HHSRSHEAT": LbwfElementMapping( + element=Element.HHSRS_EXCESS_HEAT, + aspect_type=AspectType.RISK, + ), + "HHSRSASB": LbwfElementMapping( + element=Element.HHSRS_ASBESTOS_AND_MMF, + aspect_type=AspectType.RISK, + ), + # ========================================================== + # HHSRS – PSYCHOLOGICAL REQUIREMENTS + # ========================================================== + "HHSRSCROWD": LbwfElementMapping( + element=Element.HHSRS_CROWDING_AND_SPACE, + aspect_type=AspectType.RISK, + ), + "HHSRSENTRY": LbwfElementMapping( + element=Element.HHSRS_ENTRY_BY_INTRUDERS, + aspect_type=AspectType.RISK, + ), + "HHSRSENTRP": LbwfElementMapping( # collision / entrapment + element=Element.HHSRS_COLLISION_AND_ENTRAPMENT, + aspect_type=AspectType.RISK, + ), + "HHSRSLIGHT": LbwfElementMapping( + element=Element.HHSRS_LIGHTING, + aspect_type=AspectType.RISK, + ), + "HHSRSNOISE": LbwfElementMapping( + element=Element.HHSRS_NOISE, + aspect_type=AspectType.RISK, + ), + # ========================================================== + # HHSRS – PROTECTION AGAINST INFECTION + # ========================================================== + "HHSRSDOMES": LbwfElementMapping( + element=Element.HHSRS_DOMESTIC_HYGIENE_PESTS_REFUSE, + aspect_type=AspectType.RISK, + ), + "HHSRSFOOD": LbwfElementMapping( + element=Element.HHSRS_FOOD_SAFETY, + aspect_type=AspectType.RISK, + ), + "HHSRSPERS": LbwfElementMapping( + element=Element.HHSRS_PERSONAL_HYGIENE_SANITATION, + aspect_type=AspectType.RISK, + ), + "HHSRSWATER": LbwfElementMapping( + element=Element.HHSRS_WATER_SUPPLY, + aspect_type=AspectType.RISK, + ), + "HHSRSFBATH": LbwfElementMapping( + element=Element.HHSRS_FALLS_ASSOCIATED_WITH_BATHS, + aspect_type=AspectType.RISK, + ), + "HHSRSPOSI": LbwfElementMapping( + element=Element.HHSRS_SURFACES_MOULD, + aspect_type=AspectType.RISK, + ), + # ========================================================== + # HHSRS – PROTECTION AGAINST ACCIDENTS + # ========================================================== + "HHSRSFLEVE": LbwfElementMapping( + element=Element.HHSRS_FALLS_ON_LEVEL_SURFACES, + aspect_type=AspectType.RISK, + ), + "HHSRSFSTAI": LbwfElementMapping( + element=Element.HHSRS_FALLS_ON_STAIRS, + aspect_type=AspectType.RISK, + ), + "HHSRSFBETW": LbwfElementMapping( + element=Element.HHSRS_FALLS_BETWEEN_LEVELS, + aspect_type=AspectType.RISK, + ), + "HHSRSELEC": LbwfElementMapping( + element=Element.HHSRS_ELECTRICAL_HAZARDS, + aspect_type=AspectType.RISK, + ), + "HHSRSFIRE": LbwfElementMapping( + element=Element.HHSRS_FIRE, + aspect_type=AspectType.RISK, + ), + "HHSRSFLAME": LbwfElementMapping( + element=Element.HHSRS_FLAMES_HOT_SURFACES, + aspect_type=AspectType.RISK, + ), + "HHSRSEXPLO": LbwfElementMapping( + element=Element.HHSRS_EXPLOSION, + aspect_type=AspectType.RISK, + ), + "HHSRSSTRUC": LbwfElementMapping( + element=Element.HHSRS_STRUCTURAL_COLLAPSE, + aspect_type=AspectType.RISK, + ), + # ========================================================== + # HHSRS – PROTECTION AGAINST POLLUTION + # ========================================================== + "HHSRSCO": LbwfElementMapping( + element=Element.HHSRS_CARBON_MONOXIDE, + aspect_type=AspectType.RISK, + ), + "HHSRSFUEL": LbwfElementMapping( + element=Element.HHSRS_UNSAFE_GAS, + aspect_type=AspectType.RISK, + ), + "HHSRSNO2": LbwfElementMapping( + element=Element.HHSRS_UNCOMBUSTED_FUEL_GAS, + aspect_type=AspectType.RISK, + ), + "HHSRSSO2": LbwfElementMapping( + element=Element.HHSRS_UNCOMBUSTED_FUEL_GAS, + aspect_type=AspectType.RISK, + ), + "HHSRSLEAD": LbwfElementMapping( + element=Element.HHSRS_LEAD, + aspect_type=AspectType.RISK, + ), + "HHSRSRADIA": LbwfElementMapping( + element=Element.HHSRS_RADIATION, + aspect_type=AspectType.RISK, + ), + "HHSRSBIOC": LbwfElementMapping( + element=Element.HHSRS_UNCOMBUSTED_FUEL_GAS, + aspect_type=AspectType.RISK, + ), +} diff --git a/backend/condition/domain/mapping/lbwf/lbwf_mapper.py b/backend/condition/domain/mapping/lbwf/lbwf_mapper.py index dcd1d748..01f48a35 100644 --- a/backend/condition/domain/mapping/lbwf/lbwf_mapper.py +++ b/backend/condition/domain/mapping/lbwf/lbwf_mapper.py @@ -1,7 +1,11 @@ from typing import Any, List, Optional from backend.condition.domain.asset_condition import AssetCondition -from backend.condition.domain.lbwf_element import LbwfElement +from backend.condition.domain.element import Element +from backend.condition.domain.mapping.lbwf.lbwf_element_map import ( + LbwfElementMapping, + LBWF_ELEMENT_MAP, +) from backend.condition.domain.mapping.mapper import Mapper from backend.condition.parsing.records.lbwf.lbwf_asset_condition import ( LbwfAssetCondition, @@ -26,7 +30,9 @@ class LbwfMapper(Mapper): uprn: int = client_data.uprn for raw_asset in client_data.assets: try: - element: LbwfElement = LbwfMapper._map_element(raw_asset.element_code) + element_mapping: LbwfElementMapping = LbwfMapper._map_element( + raw_asset.element_code + ) except: logger.warning( f"Unrecognised LBWF Asset Element Code: {raw_asset.element_code}. Skipping record" @@ -36,22 +42,25 @@ class LbwfMapper(Mapper): mapped_assets.append( AssetCondition( uprn=uprn, - element=element, - condition_description=raw_asset.attribute_code_description, + element=element_mapping.element, + aspect_type=element_mapping.aspect_type, + value=raw_asset.attribute_code_description, quantity=raw_asset.quantity, + install_date=raw_asset.install_date, renewal_year=LbwfMapper._calculate_renewal_year( raw_asset, survey_year ), - source=raw_asset.element_comments, - install_date=raw_asset.install_date, + element_instance=element_mapping.element_instance, + source_system=None, # Once we know the system name we'll set it here + comments=raw_asset.element_comments, ) ) return mapped_assets @staticmethod - def _map_element(lbwf_element_code: LbwfAssetCondition) -> LbwfElement: - return LbwfElement[lbwf_element_code] + def _map_element(lbwf_element_code: str) -> LbwfElementMapping: + return LBWF_ELEMENT_MAP[lbwf_element_code] @staticmethod def _calculate_renewal_year( diff --git a/backend/condition/tests/mapping/test_lbwf_mapper.py b/backend/condition/tests/mapping/test_lbwf_mapper.py index f4266ac4..918b6fea 100644 --- a/backend/condition/tests/mapping/test_lbwf_mapper.py +++ b/backend/condition/tests/mapping/test_lbwf_mapper.py @@ -181,9 +181,30 @@ def test_lbwf_mapper_maps_house(): prop_sub_type="TERRACED", element_group="ASSETS", element_code="EXTWALLFN1", + element_code_description="Wall Finish 1 in External Area", + attribute_code="SMTHRENDER", + attribute_code_description="Render or Pebbledash in External Area", + element_date_value=None, + element_numerical_value=None, + element_text_value=None, + quantity=1, + install_date=date(2009, 4, 1), + remaining_life=26, + element_comments="Source of Data = Codeman", + ), + LbwfAssetCondition( + prop_ref=100, + domna=100, + address="123 Fake Street, London, A10 1AB", + ownership="LBWF_OWNED", + prop_status="OCCP", + prop_type="HOU", + prop_sub_type="TERRACED", + element_group="ASSETS", + element_code="EXTWALLFN2", element_code_description="Wall Finish 2 in External Area", attribute_code="SMTHRENDER", - attribute_code_description="Smooth Render Wall Finish 1 in External Area", + attribute_code_description="Smooth Render Wall Finish 2 in External Area", element_date_value=None, element_numerical_value=None, element_text_value=None, @@ -256,8 +277,8 @@ def test_lbwf_mapper_maps_house(): ), AssetCondition( uprn=1, - element=Element.HEATING_EXTENT, - aspect_type=AspectType.CONFIGURATION, + element=Element.HEATING_SYSTEM, + aspect_type=AspectType.EXTENT, element_instance=None, value="No Central Heating in Property", quantity=1, @@ -281,7 +302,7 @@ def test_lbwf_mapper_maps_house(): element=Element.EXTERNAL_WALL, aspect_type=AspectType.FINISH, element_instance=1, - value="Render or Pebbledash", + value="Render or Pebbledash in External Area", quantity=1, renewal_year=2052, install_date=date(2009, 4, 1), @@ -292,7 +313,7 @@ def test_lbwf_mapper_maps_house(): element=Element.EXTERNAL_WALL, aspect_type=AspectType.FINISH, element_instance=2, - value="Smooth Render Wall Finish 1 in External Area", + value="Smooth Render Wall Finish 2 in External Area", quantity=1, renewal_year=2052, install_date=date(2009, 4, 1), @@ -306,4 +327,7 @@ def test_lbwf_mapper_maps_house(): ) # assert - assert actual_assets == expected_assets + assert len(actual_assets) == len(expected_assets) + + for i, (actual, expected) in enumerate(zip(actual_assets, expected_assets)): + assert actual == expected, f"Mismatch at index {i}" From 7f74c892e65acfd524c837dae1fe8b36b0496c4c Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 22 Jan 2026 17:04:06 +0000 Subject: [PATCH 23/50] make a note of missing element codes and tidy up HHSRS --- backend/condition/domain/element.py | 26 ++-- .../domain/mapping/lbwf/lbwf_element_map.py | 133 +++++++++++------- 2 files changed, 96 insertions(+), 63 deletions(-) diff --git a/backend/condition/domain/element.py b/backend/condition/domain/element.py index e082bd4f..c8fb6167 100644 --- a/backend/condition/domain/element.py +++ b/backend/condition/domain/element.py @@ -115,27 +115,25 @@ class Element(str, Enum): # HHSRS – ALL 29 HAZARDS # ========================================================== - # --- Physiological requirements (4) HHSRS_DAMP_AND_MOULD = "hhsrs_damp_and_mould" HHSRS_EXCESS_COLD = "hhsrs_excess_cold" HHSRS_EXCESS_HEAT = "hhsrs_excess_heat" HHSRS_ASBESTOS_AND_MMF = "hhsrs_asbestos_and_mmf" - - # --- Psychological requirements (4) + HHSRS_BIOCIDES = "hhsrs_biocides" + HHSRS_CARBON_MONOXIDE = "hhsrs_carbon_monoxide" + HHSRS_LEAD = "hhsrs_lead" + HHSRS_RADIATION = "hhsrs_radiation" + HHSRS_UNCOMBUSTED_FUEL_GAS = "hhsrs_uncombusted_fuel_gas" + HHSRS_VOLATILE_ORGANIC_COMPOUNDS = "hhsrs_volatile_organic_compounds" HHSRS_CROWDING_AND_SPACE = "hhsrs_crowding_and_space" HHSRS_ENTRY_BY_INTRUDERS = "hhsrs_entry_by_intruders" HHSRS_LIGHTING = "hhsrs_lighting" HHSRS_NOISE = "hhsrs_noise" - - # --- Protection against infection (6) HHSRS_DOMESTIC_HYGIENE_PESTS_REFUSE = "hhsrs_domestic_hygiene_pests_refuse" HHSRS_FOOD_SAFETY = "hhsrs_food_safety" HHSRS_PERSONAL_HYGIENE_SANITATION = "hhsrs_personal_hygiene_sanitation" HHSRS_WATER_SUPPLY = "hhsrs_water_supply" HHSRS_FALLS_ASSOCIATED_WITH_BATHS = "hhsrs_falls_associated_with_baths" - HHSRS_SURFACES_MOULD = "hhsrs_surfaces_mould" - - # --- Protection against accidents (10) HHSRS_FALLS_ON_LEVEL_SURFACES = "hhsrs_falls_on_level_surfaces" HHSRS_FALLS_ON_STAIRS = "hhsrs_falls_on_stairs" HHSRS_FALLS_BETWEEN_LEVELS = "hhsrs_falls_between_levels" @@ -143,12 +141,8 @@ class Element(str, Enum): HHSRS_FIRE = "hhsrs_fire" HHSRS_FLAMES_HOT_SURFACES = "hhsrs_flames_hot_surfaces" HHSRS_COLLISION_AND_ENTRAPMENT = "hhsrs_collision_and_entrapment" - HHSRS_EXPLOSION = "hhsrs_explosion" + HHSRS_COLLISION_HAZARDS_LOW_HEADROOM = "hhsrs_collision_hazards_low_headroom" + HHSRS_EXPLOSIONS = "hhsrs_explosions" + HHSRS_ERGONOMICS = "hhsrs_ergonomics" HHSRS_STRUCTURAL_COLLAPSE = "hhsrs_structural_collapse" - HHSRS_UNSAFE_GAS = "hhsrs_unsafe_gas" - - # --- Protection against pollution (4) - HHSRS_CARBON_MONOXIDE = "hhsrs_carbon_monoxide" - HHSRS_LEAD = "hhsrs_lead" - HHSRS_RADIATION = "hhsrs_radiation" - HHSRS_UNCOMBUSTED_FUEL_GAS = "hhsrs_uncombusted_fuel_gas" + HHSRS_AMENITIES = "hhsrs_amenities" diff --git a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py index 6927e2fd..da13a6c8 100644 --- a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py +++ b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py @@ -214,7 +214,7 @@ LBWF_ELEMENT_MAP: dict[str, LbwfElementMapping] = { element_instance=2, ), # ========================================================== - # HHSRS – PHYSIOLOGICAL REQUIREMENTS + # HHSRS # ========================================================== "HHSRSDAMP": LbwfElementMapping( element=Element.HHSRS_DAMP_AND_MOULD, @@ -232,9 +232,29 @@ LBWF_ELEMENT_MAP: dict[str, LbwfElementMapping] = { element=Element.HHSRS_ASBESTOS_AND_MMF, aspect_type=AspectType.RISK, ), - # ========================================================== - # HHSRS – PSYCHOLOGICAL REQUIREMENTS - # ========================================================== + "HHSRSBIOCIDES": LbwfElementMapping( + element=Element.HHSRS_BIOCIDES, + aspect_type=AspectType.RISK, + ), + "HHSRSCO": LbwfElementMapping( + element=Element.HHSRS_CARBON_MONOXIDE, + aspect_type=AspectType.RISK, + ), + "HHSRSLEAD": LbwfElementMapping( + element=Element.HHSRS_LEAD, + aspect_type=AspectType.RISK, + ), + "HHSRSRADIA": LbwfElementMapping( + element=Element.HHSRS_RADIATION, + aspect_type=AspectType.RISK, + ), + "HHSRSFUEL": LbwfElementMapping( + element=Element.HHSRS_UNCOMBUSTED_FUEL_GAS, + aspect_type=AspectType.RISK, + ), + "HHSRSORGAN": LbwfElementMapping( + element=Element.HHSRS_VOLATILE_ORGANIC_COMPOUNDS, aspect_type=AspectType.Risk + ), "HHSRSCROWD": LbwfElementMapping( element=Element.HHSRS_CROWDING_AND_SPACE, aspect_type=AspectType.RISK, @@ -243,10 +263,6 @@ LBWF_ELEMENT_MAP: dict[str, LbwfElementMapping] = { element=Element.HHSRS_ENTRY_BY_INTRUDERS, aspect_type=AspectType.RISK, ), - "HHSRSENTRP": LbwfElementMapping( # collision / entrapment - element=Element.HHSRS_COLLISION_AND_ENTRAPMENT, - aspect_type=AspectType.RISK, - ), "HHSRSLIGHT": LbwfElementMapping( element=Element.HHSRS_LIGHTING, aspect_type=AspectType.RISK, @@ -255,9 +271,6 @@ LBWF_ELEMENT_MAP: dict[str, LbwfElementMapping] = { element=Element.HHSRS_NOISE, aspect_type=AspectType.RISK, ), - # ========================================================== - # HHSRS – PROTECTION AGAINST INFECTION - # ========================================================== "HHSRSDOMES": LbwfElementMapping( element=Element.HHSRS_DOMESTIC_HYGIENE_PESTS_REFUSE, aspect_type=AspectType.RISK, @@ -278,13 +291,6 @@ LBWF_ELEMENT_MAP: dict[str, LbwfElementMapping] = { element=Element.HHSRS_FALLS_ASSOCIATED_WITH_BATHS, aspect_type=AspectType.RISK, ), - "HHSRSPOSI": LbwfElementMapping( - element=Element.HHSRS_SURFACES_MOULD, - aspect_type=AspectType.RISK, - ), - # ========================================================== - # HHSRS – PROTECTION AGAINST ACCIDENTS - # ========================================================== "HHSRSFLEVE": LbwfElementMapping( element=Element.HHSRS_FALLS_ON_LEVEL_SURFACES, aspect_type=AspectType.RISK, @@ -309,43 +315,76 @@ LBWF_ELEMENT_MAP: dict[str, LbwfElementMapping] = { element=Element.HHSRS_FLAMES_HOT_SURFACES, aspect_type=AspectType.RISK, ), + "HHSRSENTRP": LbwfElementMapping( + element=Element.HHSRS_COLLISION_AND_ENTRAPMENT, + aspect_type=AspectType.RISK, + ), "HHSRSEXPLO": LbwfElementMapping( - element=Element.HHSRS_EXPLOSION, + element=Element.HHSRS_EXPLOSIONS, aspect_type=AspectType.RISK, ), "HHSRSSTRUC": LbwfElementMapping( element=Element.HHSRS_STRUCTURAL_COLLAPSE, aspect_type=AspectType.RISK, ), - # ========================================================== - # HHSRS – PROTECTION AGAINST POLLUTION - # ========================================================== - "HHSRSCO": LbwfElementMapping( - element=Element.HHSRS_CARBON_MONOXIDE, - aspect_type=AspectType.RISK, + "HHSRSCLOW": LbwfElementMapping( + element=Element.HHSRS_COLLISION_AND_ENTRAPMENT, aspect_type=AspectType.Risk ), - "HHSRSFUEL": LbwfElementMapping( - element=Element.HHSRS_UNSAFE_GAS, - aspect_type=AspectType.RISK, - ), - "HHSRSNO2": LbwfElementMapping( - element=Element.HHSRS_UNCOMBUSTED_FUEL_GAS, - aspect_type=AspectType.RISK, - ), - "HHSRSSO2": LbwfElementMapping( - element=Element.HHSRS_UNCOMBUSTED_FUEL_GAS, - aspect_type=AspectType.RISK, - ), - "HHSRSLEAD": LbwfElementMapping( - element=Element.HHSRS_LEAD, - aspect_type=AspectType.RISK, - ), - "HHSRSRADIA": LbwfElementMapping( - element=Element.HHSRS_RADIATION, - aspect_type=AspectType.RISK, - ), - "HHSRSBIOC": LbwfElementMapping( - element=Element.HHSRS_UNCOMBUSTED_FUEL_GAS, + "HHSRSPOSI": LbwfElementMapping( + element=Element.HHSRS_AMENITIES, aspect_type=AspectType.RISK, ), } + +# Unhandled: +# DECNTHMINC +# EICINSFREQ +# EXTBALCONY +# EXTBPOINTG +# EXTCHIMNEY +# EXTDRPKERB +# EXTDWNPTYP +# EXTEXTDECS +# EXTFASOFBR +# EXTGARDOOR +# EXTGARROOF +# EXTGARSTDR +# EXTGARSTRF +# EXTGARSTWD +# EXTGARWDWS +# EXTGUTRTYP +# EXTHARDSTD +# EXTINTDWNP +# EXTLINTELS +# EXTOUTBOH +# EXTPARKING +# EXTPCHCNPY +# EXTPTFRDR1 +# EXTSTRDOOR +# EXTSTRINSP +# EXTSTRROOF +# EXTSTRWDWS +# FFHHDAMP +# FFHHDRNWC +# FFHHHCWAT +# FFHHNEGLC +# FFHHNONAT +# FFHHNOVEN +# FFHHPRPCK +# FFHHUNLAY +# FFHHUNSTA +# INTACCRAMP +# INTADDWCW +# INTBTHREML +# INTCOMHTG +# INTELECTRC +# INTFLRLVL +# INTGASAVAI +# INTHEATREC +# INTHTIMP +# INTKITREML +# INTLOFTINS +# INTNSEINSL +# INTPROGHTG +# INTSTEPSFD +# INTTNTINST From 949c9f684de8555bdfae21fee0dbfeece29bef5b Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 23 Jan 2026 11:40:32 +0000 Subject: [PATCH 24/50] fix typos in lbwf element mapper --- backend/condition/domain/mapping/lbwf/lbwf_element_map.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py index da13a6c8..750a76c6 100644 --- a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py +++ b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py @@ -253,7 +253,7 @@ LBWF_ELEMENT_MAP: dict[str, LbwfElementMapping] = { aspect_type=AspectType.RISK, ), "HHSRSORGAN": LbwfElementMapping( - element=Element.HHSRS_VOLATILE_ORGANIC_COMPOUNDS, aspect_type=AspectType.Risk + element=Element.HHSRS_VOLATILE_ORGANIC_COMPOUNDS, aspect_type=AspectType.RISK ), "HHSRSCROWD": LbwfElementMapping( element=Element.HHSRS_CROWDING_AND_SPACE, @@ -328,7 +328,7 @@ LBWF_ELEMENT_MAP: dict[str, LbwfElementMapping] = { aspect_type=AspectType.RISK, ), "HHSRSCLOW": LbwfElementMapping( - element=Element.HHSRS_COLLISION_AND_ENTRAPMENT, aspect_type=AspectType.Risk + element=Element.HHSRS_COLLISION_AND_ENTRAPMENT, aspect_type=AspectType.RISK ), "HHSRSPOSI": LbwfElementMapping( element=Element.HHSRS_AMENITIES, From d480339ba616ecd50d592b2481a012f1bedd49ce Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 23 Jan 2026 12:09:19 +0000 Subject: [PATCH 25/50] =?UTF-8?q?Map=20to=20dataclasses=20from=20Peabody?= =?UTF-8?q?=20objects=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/condition/domain/lbwf_element.py | 131 --------------- .../domain/mapping/element_mapping.py | 12 ++ .../domain/mapping/lbwf/lbwf_element_map.py | 153 +++++++++--------- .../domain/mapping/lbwf/lbwf_mapper.py | 12 +- backend/condition/domain/mapping/mapper.py | 9 +- .../mapping/peabody/peabody_element_map.py | 40 +++++ .../domain/mapping/peabody/peabody_mapper.py | 2 +- .../tests/mapping/test_peabody_mapper.py | 44 +++-- 8 files changed, 169 insertions(+), 234 deletions(-) delete mode 100644 backend/condition/domain/lbwf_element.py create mode 100644 backend/condition/domain/mapping/element_mapping.py create mode 100644 backend/condition/domain/mapping/peabody/peabody_element_map.py diff --git a/backend/condition/domain/lbwf_element.py b/backend/condition/domain/lbwf_element.py deleted file mode 100644 index 52928c72..00000000 --- a/backend/condition/domain/lbwf_element.py +++ /dev/null @@ -1,131 +0,0 @@ -from enum import StrEnum - - -class LbwfElement(StrEnum): - AHR_CAT = "Accessible Housing Register Category" - ASBESTOS = "Asbestos Present" - ASSETSAREA = "Assets Area for Decent Homes and Investment" - DECNTHMINC = "Include for Decent Homes Reporting - LBWF Stock" - EICINSFREQ = "EICR - Elec Install Conditions Report Inspection Frequency" - EXTBALCONY = "Private Balconies in External Area" - EXTBKSDDR1 = "Back and Side Doors 1 in External Area" - EXTBKSDDR2 = "Back and Side Doors 2 in External Area" - EXTBPOINTG = "Brickwork Pointing in External Area" - EXTCHIMNEY = "Chimneys in External Area" - EXTDWNPTYP = "Downpipes in External Area" - EXTDRPKERB = "Drop Kerb in External Area" - EXTEXTDECS = "External Decorations in External Area" - EXTFASOFBR = "Fascia / Soffit / Bargeboard in External Area" - EXTGARDOOR = "Garage Door in External Area" - EXTGARROOF = "Garage Roof in External Area" - EXTGARSTDR = "Garage and Store Doors in External Area" - EXTGARSTRF = "Garage and Store Roofs in External Area" - EXTGARSTWD = "Garage and Store Windows in External Area" - EXTGARWDWS = "Garage Windows in External Area" - EXTGUTRTYP = "Gutters in External Area" - EXTHARDSTD = "Hardstanding in External Area" - EXTINTDWNP = "Internal Downpipes in External Area" - EXTLINTELS = "Lintels in External Area" - EXTOUTBOH = "Overhaul of Outbuilding in External Area" - EXTPARKING = "Parking in External Area" - EXTPCHCNPY = "Porch and / or Canopy in External Area" - EXTPTFRDR1 = "Patio and French Doors 1 in External Area" - EXTROOF1 = "Roof Covering 1 in External Area" - EXTROOF2 = "Roof Covering 2 in External Area" - EXTROOF3 = "Roof Covering 3 in External Area" - EXTRFSTR1 = "Roof Structure 1 in External Area" - EXTRFSTR2 = "Roof Structure 2 in External Area" - EXTRFSTR3 = "Roof Structure 3 in External Area" - EXTSTOREY = "Number of Storeys within the Property or Block" - EXTSTRDOOR = "Store Door in External Area" - EXTSTRINSP = "Structural Defects in External Area" - EXTSTRROOF = "Store Roof in External Area" - EXTSTRWDWS = "Store Windows in External Area" - EXTWALLFN1 = "Wall Finish 1 in External Area" - EXTWALLFN2 = "Wall Finish 2 in External Area" - EXTWALLINS = "Wall Insulation Improvement in External Area" - EXTWALLSPL = "Wall Spalling in External Area" - EXTWALLSTR = "Wall Structure in External Area" - EXTWNDWS1 = "Windows 1 in External Area" - EXTWNDWS2 = "Windows 2 in External Area" - FFHHDAMP = "Fitness for Human Habitation - Serious problem with damp" - FFHHDRNWC = ( - "Fitness for Human Habitation - Problems with the drainage or the lavatories" - ) - FFHHHCWAT = ( - "Fitness for Human Habitation - Problem with the supply of hot and cold water" - ) - FFHHNEGLC = ( - "Fitness for Human Habitation - Building neglected and is in a bad condition" - ) - FFHHNONAT = "Fitness for Human Habitation - Not enough natural light" - FFHHNOVEN = "Fitness for Human Habitation - Not enough ventilation" - FFHHPRPCK = ( - "Fitness for Human Habitation - Difficult to prepare and cook food or wash up" - ) - FFHHUNLAY = "Fitness for Human Habitation - Unsafe layout" - FFHHUNSTA = "Fitness for Human Habitation - Building is unstable" - FRARISKRTG = "Fire Risk Assessment Rating" - FRAEVACSTR = "Fire Risk Assessment Evacuation Strategy" - FRATYPE = "Fire Risk Assessment Type" - FLVL = "Floor Level of Front Door" - HHSRSASB = "Asbestos (and MMF)" - HHSRSBIOC = "Biocides" - HHSRSCO = "Carbon monoxide" - HHSRSCOLD = "Excess cold" - HHSRSCLOW = "Collision hazards and low headroom" - HHSRSCROWD = "Crowding and space" - HHSRSDAMP = "Damp and mould growth" - HHSRSDOMES = "Domestic hygeine, Pests and Refuse" - HHSRSELEC = "Electrical hazards" - HHSRSENTRP = "Collision and entrapment" - HHSRSENTRY = "Entry by intruders" - HHSRSEXPLO = "Explosions" - HHSRSFBATH = "Falls associated with baths etc" - HHSRSFBETW = "Falling between levels" - HHSRSFIRE = "Fire" - HHSRSFLAME = "Flames, hot surfaces etc" - HHSRSFLEVE = "Falling on level surfaces etc" - HHSRSFOOD = "Food safety" - HHSRSFSTAI = "Falling on stairs etc" - HHSRSFUEL = "Uncombusted fuel gas" - HHSRSHEAT = "Excess heat" - HHSRSLEAD = "Lead" - HHSRSLIGHT = "Lighting" - HHSRSNO2 = "Nitrogen dioxide" - HHSRSNOISE = "Noise" - HHSRSORGAN = "Volatile organic compounds" - HHSRSPERS = "Personal hygeine, Sanitation and Drainage" - HHSRSPOSI = "Position and operability of amenities etc" - HHSRSRADIA = "Radiation" - HHSRSSO2 = "Sulphur dioxide and smoke" - HHSRSSTRUC = "Structural collapse and falling elements" - HHSRSWATER = "Water supply" - INTACCRAMP = "Access Ramp 1:12 Gradient to Property" - INTADDWCW = "Additional WCs and / or WHBs in Property" - INTBTHADEQ = "Adequacy of Bathroom Location in Property" - INTBTHREML = "Source of Bathroom Remaining Life in Property" - INTBTHRLOC = "Location of Bathroom in Property" - INTBOILERF = "Boiler Fuel in Property" - INTCHEXTNT = "Extent of Central Heating in Property" - INTCKRLOC = "Adequacy of Cooker Location in Property" - INTCOMHTG = "Community Heating in Property" - INTELECTRC = "Electrics Required in Property" - INTFLRLVL = "Floor Level Location for Property" - INTFRDOOR = "Type and Location of Front Door in Property" - INTFRDRFRR = "Front Door Fire Rating in Property" - INTGASAVAI = "Gas Available in Property" - INTHEATREC = "Heat Recovery Units in Property" - INTHTDISYS = "Heating Distribution System in Property" - INTHTIMP = "Heating Improvement Required in Property" - INTKITADEQ = "Adequacy of Kitchen and Type in Property" - INTKITREML = "Source of Kitchen Remaining Life in Property" - INTLOFTINS = "Size in mm of Loft Insulation Thickness in Property" - INTNSEINSL = "Adequacy of Noise Insulation in Property" - INTPROGHTG = "Programmable Heating in Property" - INTSMKDET = "Smoke Detectors in Property" - INTSTEPSFD = "Number of Steps to Front Door for Property" - INTTNTINST = "Tenant Installed Kitchen in Property" - INTWDWTYPE = "Windows in Property" - INTWTRHTNG = "Type of Water Heating in Property" - QUALITYSTD = "Quality standard" diff --git a/backend/condition/domain/mapping/element_mapping.py b/backend/condition/domain/mapping/element_mapping.py new file mode 100644 index 00000000..01e1f316 --- /dev/null +++ b/backend/condition/domain/mapping/element_mapping.py @@ -0,0 +1,12 @@ +from dataclasses import dataclass +from typing import Optional +from xml.dom.minidom import Element + +from backend.condition.domain.aspect_type import AspectType + + +@dataclass(frozen=True) +class ElementMapping: + element: Element + aspect_type: AspectType + element_instance: Optional[int] = None diff --git a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py index 750a76c6..047013f4 100644 --- a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py +++ b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py @@ -1,179 +1,170 @@ -from dataclasses import dataclass -from typing import Optional - from backend.condition.domain.element import Element from backend.condition.domain.aspect_type import AspectType +from backend.condition.domain.mapping.element_mapping import ElementMapping -@dataclass(frozen=True) -class LbwfElementMapping: - element: Element - aspect_type: AspectType - element_instance: Optional[int] = None - - -LBWF_ELEMENT_MAP: dict[str, LbwfElementMapping] = { +LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { # ========================================================== # PROPERTY / GENERAL # ========================================================== - "AHR_CAT": LbwfElementMapping( + "AHR_CAT": ElementMapping( element=Element.ACCESSIBLE_HOUSING_REGISTER, aspect_type=AspectType.CATEGORY, ), - "ASSETSAREA": LbwfElementMapping( + "ASSETSAREA": ElementMapping( element=Element.PROPERTY, aspect_type=AspectType.AREA, ), - # "DECNTHMINC": LbwfElementMapping( + # "DECNTHMINC": ElementMapping( # element=Element.DECENT_HOMES, # aspect_type=AspectType.INCLUSION, # ), # Ignore this one - "QUALITYSTD": LbwfElementMapping( + "QUALITYSTD": ElementMapping( element=Element.QUALITY_STANDARD, aspect_type=AspectType.TYPE, ), - "EXTSTOREY": LbwfElementMapping( + "EXTSTOREY": ElementMapping( element=Element.PROPERTY, aspect_type=AspectType.CONFIGURATION, ), - "FLVL": LbwfElementMapping( + "FLVL": ElementMapping( element=Element.FLOOR_LEVEL_FRONT_DOOR, aspect_type=AspectType.LOCATION, ), # ========================================================== # ASBESTOS (NON-HHSRS RECORD) # ========================================================== - "ASBESTOS": LbwfElementMapping( + "ASBESTOS": ElementMapping( element=Element.ASBESTOS, aspect_type=AspectType.PRESENCE, ), # ========================================================== # INTERNAL – BATHROOMS & KITCHENS # ========================================================== - "INTBTHRLOC": LbwfElementMapping( + "INTBTHRLOC": ElementMapping( element=Element.BATHROOM, aspect_type=AspectType.LOCATION, ), - "INTBTHADEQ": LbwfElementMapping( + "INTBTHADEQ": ElementMapping( element=Element.BATHROOM, aspect_type=AspectType.ADEQUACY, ), - "INTKITADEQ": LbwfElementMapping( + "INTKITADEQ": ElementMapping( element=Element.KITCHEN, aspect_type=AspectType.ADEQUACY, ), - "INTCKRLOC": LbwfElementMapping( + "INTCKRLOC": ElementMapping( element=Element.KITCHEN, aspect_type=AspectType.LOCATION, ), # ========================================================== # INTERNAL – HEATING # ========================================================== - "INTCHEXTNT": LbwfElementMapping( + "INTCHEXTNT": ElementMapping( element=Element.HEATING_EXTENT, aspect_type=AspectType.CONFIGURATION, ), - "INTCHDIST": LbwfElementMapping( + "INTCHDIST": ElementMapping( element=Element.HEATING_DISTRIBUTION, aspect_type=AspectType.TYPE, ), - "INTCHBLR": LbwfElementMapping( + "INTCHBLR": ElementMapping( element=Element.HEATING_BOILER, aspect_type=AspectType.TYPE, ), # ========================================================== # INTERNAL – FIRE # ========================================================== - "FRARISKRTG": LbwfElementMapping( + "FRARISKRTG": ElementMapping( element=Element.FIRE_RISK_ASSESSMENT, aspect_type=AspectType.RATING, ), - "FRATYPE": LbwfElementMapping( + "FRATYPE": ElementMapping( element=Element.FIRE_RISK_ASSESSMENT, aspect_type=AspectType.TYPE, ), - "FRAEVACSTR": LbwfElementMapping( + "FRAEVACSTR": ElementMapping( element=Element.FIRE_RISK_ASSESSMENT, aspect_type=AspectType.STRATEGY, ), - "INTSMKDET": LbwfElementMapping( + "INTSMKDET": ElementMapping( element=Element.SMOKE_DETECTION, aspect_type=AspectType.PRESENCE, ), - "INTCHEXTNT": LbwfElementMapping( + "INTCHEXTNT": ElementMapping( element=Element.HEATING_SYSTEM, aspect_type=AspectType.EXTENT, ), # ========================================================== # HEATING & SERVICES # ========================================================== - "INTBOILERF": LbwfElementMapping( + "INTBOILERF": ElementMapping( element=Element.BOILER_FUEL, aspect_type=AspectType.TYPE, ), - "INTHTDISYS": LbwfElementMapping( + "INTHTDISYS": ElementMapping( element=Element.HEATING_SYSTEM, aspect_type=AspectType.DISTRIBUTION, ), - "INTWTRHTNG": LbwfElementMapping( + "INTWTRHTNG": ElementMapping( element=Element.WATER_HEATING, aspect_type=AspectType.TYPE, ), # ========================================================== # EXTERNAL – WALLS (INSTANCED) # ========================================================== - "EXTWALLSTR": LbwfElementMapping( + "EXTWALLSTR": ElementMapping( element=Element.EXTERNAL_WALL, aspect_type=AspectType.STRUCTURE, element_instance=1, ), - "EXTWALLFN1": LbwfElementMapping( + "EXTWALLFN1": ElementMapping( element=Element.EXTERNAL_WALL, aspect_type=AspectType.FINISH, element_instance=1, ), - "EXTWALLFN2": LbwfElementMapping( + "EXTWALLFN2": ElementMapping( element=Element.EXTERNAL_WALL, aspect_type=AspectType.FINISH, element_instance=2, ), - "EXTWALLINS": LbwfElementMapping( + "EXTWALLINS": ElementMapping( element=Element.EXTERNAL_WALL, aspect_type=AspectType.INSULATION, ), - "EXTWALLSPL": LbwfElementMapping( + "EXTWALLSPL": ElementMapping( element=Element.EXTERNAL_WALL, aspect_type=AspectType.CONDITION, ), # ========================================================== # EXTERNAL – ROOFS (INSTANCED) # ========================================================== - "EXTRFSTR1": LbwfElementMapping( + "EXTRFSTR1": ElementMapping( element=Element.ROOF, aspect_type=AspectType.STRUCTURE, element_instance=1, ), - "EXTRFSTR2": LbwfElementMapping( + "EXTRFSTR2": ElementMapping( element=Element.ROOF, aspect_type=AspectType.STRUCTURE, element_instance=2, ), - "EXTRFSTR3": LbwfElementMapping( + "EXTRFSTR3": ElementMapping( element=Element.ROOF, aspect_type=AspectType.STRUCTURE, element_instance=3, ), - "EXTROOF1": LbwfElementMapping( + "EXTROOF1": ElementMapping( element=Element.ROOF, aspect_type=AspectType.COVERING, element_instance=1, ), - "EXTROOF2": LbwfElementMapping( + "EXTROOF2": ElementMapping( element=Element.ROOF, aspect_type=AspectType.COVERING, element_instance=2, ), - "EXTROOF3": LbwfElementMapping( + "EXTROOF3": ElementMapping( element=Element.ROOF, aspect_type=AspectType.COVERING, element_instance=3, @@ -181,34 +172,34 @@ LBWF_ELEMENT_MAP: dict[str, LbwfElementMapping] = { # ========================================================== # EXTERNAL – DOORS & WINDOWS # ========================================================== - "INTFRDOOR": LbwfElementMapping( + "INTFRDOOR": ElementMapping( element=Element.EXTERNAL_DOOR, aspect_type=AspectType.TYPE, ), - "INTFRDRFRR": LbwfElementMapping( + "INTFRDRFRR": ElementMapping( element=Element.EXTERNAL_DOOR, aspect_type=AspectType.FIRE_RATING, ), - "EXTBKSDDR1": LbwfElementMapping( + "EXTBKSDDR1": ElementMapping( element=Element.EXTERNAL_DOOR, aspect_type=AspectType.TYPE, element_instance=1, ), - "EXTBKSDDR2": LbwfElementMapping( + "EXTBKSDDR2": ElementMapping( element=Element.EXTERNAL_DOOR, aspect_type=AspectType.TYPE, element_instance=2, ), - "INTWDWTYPE": LbwfElementMapping( + "INTWDWTYPE": ElementMapping( element=Element.EXTERNAL_WINDOWS, aspect_type=AspectType.TYPE, ), - "EXTWNDWS1": LbwfElementMapping( + "EXTWNDWS1": ElementMapping( element=Element.EXTERNAL_WINDOWS, aspect_type=AspectType.TYPE, element_instance=1, ), - "EXTWNDWS2": LbwfElementMapping( + "EXTWNDWS2": ElementMapping( element=Element.EXTERNAL_WINDOWS, aspect_type=AspectType.TYPE, element_instance=2, @@ -216,121 +207,121 @@ LBWF_ELEMENT_MAP: dict[str, LbwfElementMapping] = { # ========================================================== # HHSRS # ========================================================== - "HHSRSDAMP": LbwfElementMapping( + "HHSRSDAMP": ElementMapping( element=Element.HHSRS_DAMP_AND_MOULD, aspect_type=AspectType.RISK, ), - "HHSRSCOLD": LbwfElementMapping( + "HHSRSCOLD": ElementMapping( element=Element.HHSRS_EXCESS_COLD, aspect_type=AspectType.RISK, ), - "HHSRSHEAT": LbwfElementMapping( + "HHSRSHEAT": ElementMapping( element=Element.HHSRS_EXCESS_HEAT, aspect_type=AspectType.RISK, ), - "HHSRSASB": LbwfElementMapping( + "HHSRSASB": ElementMapping( element=Element.HHSRS_ASBESTOS_AND_MMF, aspect_type=AspectType.RISK, ), - "HHSRSBIOCIDES": LbwfElementMapping( + "HHSRSBIOCIDES": ElementMapping( element=Element.HHSRS_BIOCIDES, aspect_type=AspectType.RISK, ), - "HHSRSCO": LbwfElementMapping( + "HHSRSCO": ElementMapping( element=Element.HHSRS_CARBON_MONOXIDE, aspect_type=AspectType.RISK, ), - "HHSRSLEAD": LbwfElementMapping( + "HHSRSLEAD": ElementMapping( element=Element.HHSRS_LEAD, aspect_type=AspectType.RISK, ), - "HHSRSRADIA": LbwfElementMapping( + "HHSRSRADIA": ElementMapping( element=Element.HHSRS_RADIATION, aspect_type=AspectType.RISK, ), - "HHSRSFUEL": LbwfElementMapping( + "HHSRSFUEL": ElementMapping( element=Element.HHSRS_UNCOMBUSTED_FUEL_GAS, aspect_type=AspectType.RISK, ), - "HHSRSORGAN": LbwfElementMapping( + "HHSRSORGAN": ElementMapping( element=Element.HHSRS_VOLATILE_ORGANIC_COMPOUNDS, aspect_type=AspectType.RISK ), - "HHSRSCROWD": LbwfElementMapping( + "HHSRSCROWD": ElementMapping( element=Element.HHSRS_CROWDING_AND_SPACE, aspect_type=AspectType.RISK, ), - "HHSRSENTRY": LbwfElementMapping( + "HHSRSENTRY": ElementMapping( element=Element.HHSRS_ENTRY_BY_INTRUDERS, aspect_type=AspectType.RISK, ), - "HHSRSLIGHT": LbwfElementMapping( + "HHSRSLIGHT": ElementMapping( element=Element.HHSRS_LIGHTING, aspect_type=AspectType.RISK, ), - "HHSRSNOISE": LbwfElementMapping( + "HHSRSNOISE": ElementMapping( element=Element.HHSRS_NOISE, aspect_type=AspectType.RISK, ), - "HHSRSDOMES": LbwfElementMapping( + "HHSRSDOMES": ElementMapping( element=Element.HHSRS_DOMESTIC_HYGIENE_PESTS_REFUSE, aspect_type=AspectType.RISK, ), - "HHSRSFOOD": LbwfElementMapping( + "HHSRSFOOD": ElementMapping( element=Element.HHSRS_FOOD_SAFETY, aspect_type=AspectType.RISK, ), - "HHSRSPERS": LbwfElementMapping( + "HHSRSPERS": ElementMapping( element=Element.HHSRS_PERSONAL_HYGIENE_SANITATION, aspect_type=AspectType.RISK, ), - "HHSRSWATER": LbwfElementMapping( + "HHSRSWATER": ElementMapping( element=Element.HHSRS_WATER_SUPPLY, aspect_type=AspectType.RISK, ), - "HHSRSFBATH": LbwfElementMapping( + "HHSRSFBATH": ElementMapping( element=Element.HHSRS_FALLS_ASSOCIATED_WITH_BATHS, aspect_type=AspectType.RISK, ), - "HHSRSFLEVE": LbwfElementMapping( + "HHSRSFLEVE": ElementMapping( element=Element.HHSRS_FALLS_ON_LEVEL_SURFACES, aspect_type=AspectType.RISK, ), - "HHSRSFSTAI": LbwfElementMapping( + "HHSRSFSTAI": ElementMapping( element=Element.HHSRS_FALLS_ON_STAIRS, aspect_type=AspectType.RISK, ), - "HHSRSFBETW": LbwfElementMapping( + "HHSRSFBETW": ElementMapping( element=Element.HHSRS_FALLS_BETWEEN_LEVELS, aspect_type=AspectType.RISK, ), - "HHSRSELEC": LbwfElementMapping( + "HHSRSELEC": ElementMapping( element=Element.HHSRS_ELECTRICAL_HAZARDS, aspect_type=AspectType.RISK, ), - "HHSRSFIRE": LbwfElementMapping( + "HHSRSFIRE": ElementMapping( element=Element.HHSRS_FIRE, aspect_type=AspectType.RISK, ), - "HHSRSFLAME": LbwfElementMapping( + "HHSRSFLAME": ElementMapping( element=Element.HHSRS_FLAMES_HOT_SURFACES, aspect_type=AspectType.RISK, ), - "HHSRSENTRP": LbwfElementMapping( + "HHSRSENTRP": ElementMapping( element=Element.HHSRS_COLLISION_AND_ENTRAPMENT, aspect_type=AspectType.RISK, ), - "HHSRSEXPLO": LbwfElementMapping( + "HHSRSEXPLO": ElementMapping( element=Element.HHSRS_EXPLOSIONS, aspect_type=AspectType.RISK, ), - "HHSRSSTRUC": LbwfElementMapping( + "HHSRSSTRUC": ElementMapping( element=Element.HHSRS_STRUCTURAL_COLLAPSE, aspect_type=AspectType.RISK, ), - "HHSRSCLOW": LbwfElementMapping( + "HHSRSCLOW": ElementMapping( element=Element.HHSRS_COLLISION_AND_ENTRAPMENT, aspect_type=AspectType.RISK ), - "HHSRSPOSI": LbwfElementMapping( + "HHSRSPOSI": ElementMapping( element=Element.HHSRS_AMENITIES, aspect_type=AspectType.RISK, ), diff --git a/backend/condition/domain/mapping/lbwf/lbwf_mapper.py b/backend/condition/domain/mapping/lbwf/lbwf_mapper.py index 01f48a35..635e5898 100644 --- a/backend/condition/domain/mapping/lbwf/lbwf_mapper.py +++ b/backend/condition/domain/mapping/lbwf/lbwf_mapper.py @@ -2,10 +2,8 @@ from typing import Any, List, Optional from backend.condition.domain.asset_condition import AssetCondition from backend.condition.domain.element import Element -from backend.condition.domain.mapping.lbwf.lbwf_element_map import ( - LbwfElementMapping, - LBWF_ELEMENT_MAP, -) +from backend.condition.domain.mapping.element_mapping import ElementMapping +from backend.condition.domain.mapping.lbwf.lbwf_element_map import LBWF_ELEMENT_MAP from backend.condition.domain.mapping.mapper import Mapper from backend.condition.parsing.records.lbwf.lbwf_asset_condition import ( LbwfAssetCondition, @@ -19,7 +17,7 @@ logger = setup_logger() class LbwfMapper(Mapper): def map_asset_conditions_for_property( - self, client_data: Any, survey_year: Optional[int] + self, client_data: Any, survey_year: Optional[int] = None ) -> List[AssetCondition]: assert isinstance( client_data, LbwfHouse @@ -30,7 +28,7 @@ class LbwfMapper(Mapper): uprn: int = client_data.uprn for raw_asset in client_data.assets: try: - element_mapping: LbwfElementMapping = LbwfMapper._map_element( + element_mapping: ElementMapping = LbwfMapper._map_element( raw_asset.element_code ) except: @@ -59,7 +57,7 @@ class LbwfMapper(Mapper): return mapped_assets @staticmethod - def _map_element(lbwf_element_code: str) -> LbwfElementMapping: + def _map_element(lbwf_element_code: str) -> ElementMapping: return LBWF_ELEMENT_MAP[lbwf_element_code] @staticmethod diff --git a/backend/condition/domain/mapping/mapper.py b/backend/condition/domain/mapping/mapper.py index 4e51d46b..c0b07184 100644 --- a/backend/condition/domain/mapping/mapper.py +++ b/backend/condition/domain/mapping/mapper.py @@ -3,9 +3,12 @@ from typing import Any, List, Optional from backend.condition.domain.asset_condition import AssetCondition + class Mapper(ABC): @abstractmethod - def map_asset_conditions_for_property(self, client_data: Any, survey_year: Optional[int]) -> List[AssetCondition]: - #TODO: client_data should be properly typed - pass \ No newline at end of file + def map_asset_conditions_for_property( + self, client_data: Any, survey_year: Optional[int] = None + ) -> List[AssetCondition]: + # TODO: client_data should be properly typed + pass diff --git a/backend/condition/domain/mapping/peabody/peabody_element_map.py b/backend/condition/domain/mapping/peabody/peabody_element_map.py new file mode 100644 index 00000000..2a1203c0 --- /dev/null +++ b/backend/condition/domain/mapping/peabody/peabody_element_map.py @@ -0,0 +1,40 @@ +from backend.condition.domain.mapping.element_mapping import ElementMapping + + +PEABODY_ELEMENT_MAP = { + # -------------------- + # GENERAL + # -------------------- + ("100", "1"): ElementMapping(element="property", aspect="type"), + ("100", "3"): ElementMapping(element="property", aspect="age_band"), + ("100", "14"): ElementMapping(element="property", aspect="construction_type"), + # -------------------- + # EXTERNALS + # -------------------- + ("120", "1"): ElementMapping(element="external_wall", aspect="structure"), + ("120", "2"): ElementMapping(element="external_wall", aspect="finish"), + ("110", "1"): ElementMapping(element="roof", aspect="covering"), + # -------------------- + # INTERNALS + # -------------------- + ("160", "1"): ElementMapping(element="kitchen", aspect="condition"), + ("190", "1"): ElementMapping(element="bathroom", aspect="condition"), + # -------------------- + # HHSRS (PEABODY) + # -------------------- + ("54", "1"): ElementMapping( + element="hhsrs", aspect="risk", is_hhsrs=True, hhsrs_hazard_id=1 + ), + ("54", "2"): ElementMapping( + element="hhsrs", aspect="risk", is_hhsrs=True, hhsrs_hazard_id=2 + ), + ("54", "4"): ElementMapping( + element="hhsrs", aspect="risk", is_hhsrs=True, hhsrs_hazard_id=4 + ), + ("54", "24"): ElementMapping( + element="hhsrs", aspect="risk", is_hhsrs=True, hhsrs_hazard_id=24 + ), + ("54", "29"): ElementMapping( + element="hhsrs", aspect="risk", is_hhsrs=True, hhsrs_hazard_id=29 + ), +} diff --git a/backend/condition/domain/mapping/peabody/peabody_mapper.py b/backend/condition/domain/mapping/peabody/peabody_mapper.py index 8413b888..3f0ee931 100644 --- a/backend/condition/domain/mapping/peabody/peabody_mapper.py +++ b/backend/condition/domain/mapping/peabody/peabody_mapper.py @@ -13,6 +13,6 @@ logger = setup_logger() class PeabodyMapper(Mapper): def map_asset_conditions_for_property( - self, client_data: Any, survey_year: Optional[int] + self, client_data: Any, survey_year: Optional[int] = None ) -> List[AssetCondition]: raise NotImplementedError diff --git a/backend/condition/tests/mapping/test_peabody_mapper.py b/backend/condition/tests/mapping/test_peabody_mapper.py index de027fe7..ca3f78a4 100644 --- a/backend/condition/tests/mapping/test_peabody_mapper.py +++ b/backend/condition/tests/mapping/test_peabody_mapper.py @@ -1,5 +1,8 @@ from datetime import datetime +from typing import List +from backend.condition.domain.aspect_type import AspectType +from backend.condition.domain.element import Element from backend.condition.domain.mapping.peabody.peabody_mapper import PeabodyMapper from backend.condition.parsing.records.peabody.peabody_asset_condition import ( PeabodyAssetCondition, @@ -18,23 +21,42 @@ def test_peabody_mapper_maps_property(): full_address="FLAT 1 RANDOM SQUARE FAKE STREET LONDON E1 1EE", location_type_code=1, parent_lo_reference="RAND1000", - element_code=50, - element="Internal", - sub_element_code=3, - sub_element="CCU", - material_code=2, - material_or_answer="RCD/MCB CCU", - renewal_quantity=1, - renewal_year=2038, - renewal_cost=500, + element_code=130, + element="WINDOWS", + sub_element_code=1, + sub_element="Windows", + material_code=1, + material_or_answer="UPVC Double Glazed", + renewal_quantity=8, + renewal_year=2036, + renewal_cost=4800, cloned="N", lo_type_code=1, - condition_survey_date=datetime(2024, 2, 15, 0, 0, 0), + condition_survey_date=datetime(2024, 2, 15, 12, 47, 0), ) ], ) + mapper = PeabodyMapper() + expected_assets: List[AssetCondition] = [ + AssetCondition( + uprn=1, + element=Element.EXTERNAL_WINDOWS, + aspect_type=AspectType.MATERIAL, + value="UPVC Double Glazed", + quantity=8, + install_date=None, + renewal_year=2036, + element_instance=None, + source_system=None, + comments=None, + ) + ] # act + actual_assets = mapper.map_asset_conditions_for_property(peabody_property) # assert - assert False # temp + assert len(actual_assets) == len(expected_assets) + + for i, (actual, expected) in enumerate(zip(actual_assets, expected_assets)): + assert actual == expected, f"Mismatch at index {i}" From 42f9821f1b4a9f279e716cd9ed2725754d161b41 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 23 Jan 2026 12:50:48 +0000 Subject: [PATCH 26/50] =?UTF-8?q?Map=20to=20dataclasses=20from=20Peabody?= =?UTF-8?q?=20objects=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mapping/peabody/peabody_element_map.py | 70 +++++++++++-------- .../domain/mapping/peabody/peabody_mapper.py | 44 +++++++++++- 2 files changed, 85 insertions(+), 29 deletions(-) diff --git a/backend/condition/domain/mapping/peabody/peabody_element_map.py b/backend/condition/domain/mapping/peabody/peabody_element_map.py index 2a1203c0..5b89c578 100644 --- a/backend/condition/domain/mapping/peabody/peabody_element_map.py +++ b/backend/condition/domain/mapping/peabody/peabody_element_map.py @@ -1,40 +1,54 @@ +from backend.condition.domain.aspect_type import AspectType +from backend.condition.domain.element import Element from backend.condition.domain.mapping.element_mapping import ElementMapping PEABODY_ELEMENT_MAP = { - # -------------------- - # GENERAL - # -------------------- - ("100", "1"): ElementMapping(element="property", aspect="type"), - ("100", "3"): ElementMapping(element="property", aspect="age_band"), - ("100", "14"): ElementMapping(element="property", aspect="construction_type"), - # -------------------- - # EXTERNALS - # -------------------- - ("120", "1"): ElementMapping(element="external_wall", aspect="structure"), - ("120", "2"): ElementMapping(element="external_wall", aspect="finish"), - ("110", "1"): ElementMapping(element="roof", aspect="covering"), - # -------------------- - # INTERNALS - # -------------------- - ("160", "1"): ElementMapping(element="kitchen", aspect="condition"), - ("190", "1"): ElementMapping(element="bathroom", aspect="condition"), + # ========================================================== + # PROPERTY / GENERAL + # ========================================================== + (100, 1): ElementMapping(element=Element.PROPERTY, aspect_type=AspectType.TYPE), + # (100, 3): ElementMapping(element=Element.PROPERTY, aspect_type=AspectType.AGE), + # (100, 14): ElementMapping(element="property", aspect_type="construction_type"), + # ========================================================== + # EXTERNAL – WALLS + # ========================================================== + (120, 1): ElementMapping( + element=Element.EXTERNAL_WALL, aspect_type=AspectType.STRUCTURE + ), + (120, 2): ElementMapping( + element=Element.EXTERNAL_WALL, aspect_type=AspectType.FINISH + ), + # ========================================================== + # EXTERNAL – ROOFS + # ========================================================== + (110, 1): ElementMapping(element=Element.ROOF, aspect_type=AspectType.COVERING), + # ========================================================== + # EXTERNAL – DOORS & WINDOWS + # ========================================================== + (130, 1): ElementMapping( + element=Element.EXTERNAL_WINDOWS, aspect_type=AspectType.MATERIAL + ), + # ========================================================== + # INTERNAL – BATHROOMS & KITCHENS + # ========================================================== + (160, 1): ElementMapping(element=Element.KITCHEN, aspect_type=AspectType.CONDITION), + (190, 1): ElementMapping( + element=Element.BATHROOM, aspect_type=AspectType.CONDITION + ), # -------------------- # HHSRS (PEABODY) # -------------------- - ("54", "1"): ElementMapping( - element="hhsrs", aspect="risk", is_hhsrs=True, hhsrs_hazard_id=1 + (54, 1): ElementMapping( + element=Element.HHSRS_DAMP_AND_MOULD, aspect_type=AspectType.RISK ), - ("54", "2"): ElementMapping( - element="hhsrs", aspect="risk", is_hhsrs=True, hhsrs_hazard_id=2 + (54, 4): ElementMapping( + element=Element.HHSRS_ASBESTOS_AND_MMF, aspect_type=AspectType.RISK ), - ("54", "4"): ElementMapping( - element="hhsrs", aspect="risk", is_hhsrs=True, hhsrs_hazard_id=4 + (54, 15): ElementMapping( + element=Element.HHSRS_DOMESTIC_HYGIENE_PESTS_REFUSE, aspect_type=AspectType.RISK ), - ("54", "24"): ElementMapping( - element="hhsrs", aspect="risk", is_hhsrs=True, hhsrs_hazard_id=24 - ), - ("54", "29"): ElementMapping( - element="hhsrs", aspect="risk", is_hhsrs=True, hhsrs_hazard_id=29 + (54, 29): ElementMapping( + element=Element.HHSRS_STRUCTURAL_COLLAPSE, aspect_type=AspectType.RISK ), } diff --git a/backend/condition/domain/mapping/peabody/peabody_mapper.py b/backend/condition/domain/mapping/peabody/peabody_mapper.py index 3f0ee931..44dbd56e 100644 --- a/backend/condition/domain/mapping/peabody/peabody_mapper.py +++ b/backend/condition/domain/mapping/peabody/peabody_mapper.py @@ -1,6 +1,10 @@ from typing import Any, List, Optional from backend.condition.domain.asset_condition import AssetCondition +from backend.condition.domain.mapping.element_mapping import ElementMapping +from backend.condition.domain.mapping.peabody.peabody_element_map import ( + PEABODY_ELEMENT_MAP, +) from backend.condition.domain.mapping.mapper import Mapper from backend.condition.parsing.records.peabody.peabody_asset_condition import ( PeabodyAssetCondition, @@ -15,4 +19,42 @@ class PeabodyMapper(Mapper): def map_asset_conditions_for_property( self, client_data: Any, survey_year: Optional[int] = None ) -> List[AssetCondition]: - raise NotImplementedError + assert isinstance( + client_data, PeabodyProperty + ) # TODO: think of a better way to do this + + mapped_assets: List[AssetCondition] = [] + + uprn: int = client_data.uprn + for raw_asset in client_data.assets: + try: + element_mapping: ElementMapping = PeabodyMapper._map_element( + raw_asset.element_code, raw_asset.sub_element_code + ) + except: + logger.warning( + f"""Unrecognised Peabody Asset Element: {raw_asset.element} ({raw_asset.element_code}), + Sub-Element: {raw_asset.sub_element} ({raw_asset.sub_element_code}). Skipping record""" + ) + continue + + mapped_assets.append( + AssetCondition( + uprn=uprn, + element=element_mapping.element, + aspect_type=element_mapping.aspect_type, + value=raw_asset.material_or_answer, + quantity=raw_asset.renewal_quantity, + install_date=None, # Not available in peabody data + renewal_year=raw_asset.renewal_year, + element_instance=element_mapping.element_instance, + source_system=None, # Once we know the system name we'll set it here + comments=None, # Not available in peabody data + ) + ) + + return mapped_assets + + @staticmethod + def _map_element(element_code: int, sub_element_code: int) -> ElementMapping: + return PEABODY_ELEMENT_MAP[(element_code, sub_element_code)] From 4c16632b2f942eb426ccaeac768e8415cce629e9 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 23 Jan 2026 14:01:21 +0000 Subject: [PATCH 27/50] process both file types in local runner --- backend/condition/local_runner.py | 23 ++++++++++++++++------- backend/condition/parsing/factory.py | 4 ++++ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/backend/condition/local_runner.py b/backend/condition/local_runner.py index 28f9b06c..404f64d4 100644 --- a/backend/condition/local_runner.py +++ b/backend/condition/local_runner.py @@ -2,6 +2,7 @@ from pathlib import Path from backend.condition.processor import process_file + def main() -> None: try: # Works in scripts / debugger / pytest @@ -12,14 +13,22 @@ def main() -> None: path: Path = ROOT_DIR / "condition" / "sample_data" - lbwf_path: Path = path / "lbwf" / "LBWF - Example Asset Data September 2025.xlsx" # TODO: get this from s3 as part of devcontainer init + # TODO: get these from s3, maybe as part of devcontainer init + lbwf_path: Path = path / "lbwf" / "LBWF - Example Asset Data September 2025.xlsx" + peabody_path: Path = ( + path + / "peabody" + / "2026_01_06 - Peabody - Stock Condition Data - Survey Records - D Lower.xlsx" + ) + filepaths = [lbwf_path, peabody_path] + + for fp in filepaths: + with fp.open("rb") as f: + process_file( + file_stream=f, + source_key=fp.as_posix(), + ) - with lbwf_path.open("rb") as f: - process_file( - file_stream=f, - source_key=lbwf_path.as_posix(), - ) if __name__ == "__main__": main() - diff --git a/backend/condition/parsing/factory.py b/backend/condition/parsing/factory.py index 7233a1df..68ca0292 100644 --- a/backend/condition/parsing/factory.py +++ b/backend/condition/parsing/factory.py @@ -1,5 +1,6 @@ from backend.condition.domain.mapping.lbwf.lbwf_mapper import LbwfMapper from backend.condition.domain.mapping.mapper import Mapper +from backend.condition.domain.mapping.peabody.peabody_mapper import PeabodyMapper from backend.condition.file_type import FileType from backend.condition.parsing.parser import Parser from backend.condition.parsing.lbwf_parser import LbwfParser @@ -20,4 +21,7 @@ def select_mapper(file_type: FileType) -> Mapper: if file_type is FileType.LBWF: return LbwfMapper() + if file_type is FileType.Peabody: + return PeabodyMapper() + raise ValueError("Unrecognised file type, unable to instantiate Mapper") From 42cfcf604c5181ff64651acf93b6a3ae38cfcccc Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 23 Jan 2026 14:52:52 +0000 Subject: [PATCH 28/50] Extra test case, and note unhandled elements --- backend/condition/domain/aspect_type.py | 1 + .../mapping/peabody/peabody_element_map.py | 155 ++++++++++++++++++ .../domain/mapping/peabody/peabody_mapper.py | 3 - .../tests/mapping/test_peabody_mapper.py | 34 +++- 4 files changed, 188 insertions(+), 5 deletions(-) diff --git a/backend/condition/domain/aspect_type.py b/backend/condition/domain/aspect_type.py index 0f9a406a..d4db10bc 100644 --- a/backend/condition/domain/aspect_type.py +++ b/backend/condition/domain/aspect_type.py @@ -27,3 +27,4 @@ class AspectType(str, Enum): STRUCTURE = "structure" COVERING = "covering" FIRE_RATING = "fire_rating" + EXTERNAL_DECORATION = "external_decoration" diff --git a/backend/condition/domain/mapping/peabody/peabody_element_map.py b/backend/condition/domain/mapping/peabody/peabody_element_map.py index 5b89c578..80b3aa70 100644 --- a/backend/condition/domain/mapping/peabody/peabody_element_map.py +++ b/backend/condition/domain/mapping/peabody/peabody_element_map.py @@ -8,6 +8,9 @@ PEABODY_ELEMENT_MAP = { # PROPERTY / GENERAL # ========================================================== (100, 1): ElementMapping(element=Element.PROPERTY, aspect_type=AspectType.TYPE), + (100, 15): ElementMapping( + element=Element.PROPERTY, aspect_type=AspectType.EXTERNAL_DECORATION + ), # (100, 3): ElementMapping(element=Element.PROPERTY, aspect_type=AspectType.AGE), # (100, 14): ElementMapping(element="property", aspect_type="construction_type"), # ========================================================== @@ -52,3 +55,155 @@ PEABODY_ELEMENT_MAP = { element=Element.HHSRS_STRUCTURAL_COLLAPSE, aspect_type=AspectType.RISK ), } + + +# unhandled +# 'Element: ROOFS - Code: 110, Sub-Element: Chimney - Code: 3', +# 'Element: ROOFS - Code: 110, Sub-Element: Fascia - Code: 4', +# 'Element: ROOFS - Code: 110, Sub-Element: Rainwater Goods - Code: 6', +# 'Element: WINDOWS - Code: 130, Sub-Element: Communal Windows - Code: 2', +# 'Element: DOORS - Code: 140, Sub-Element: Main Doors - Code: 1', +# 'Element: DOORS - Code: 140, Sub-Element: Block Entrance Doors - Code: 4', +# 'Element: EXTERNAL AREAS - Code: 150, Sub-Element: Paving - Code: 1', +# 'Element: EXTERNAL AREAS - Code: 150, Sub-Element: Boundaries - Code: 4', +# 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Boiler - Code: 1', +# 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Heating - Code: 2', +# 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Electrics - Code: 3', +# 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Fire Detection - Code: 4', +# 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Floor Covering - Code: 6', +# 'Element: ROOFS - Code: 110, Sub-Element: Soffit - Code: 5', +# 'Element: External - Code: 53, Sub-Element: Window Type 01 - Code: 38', +# 'Element: ROOFS - Code: 110, Sub-Element: Secondary Roof - Code: 2', +# 'Element: HEATING - Code: 170, Sub-Element: Boiler - Code: 1', +# 'Element: HEATING - Code: 170, Sub-Element: Heating Distribution - Code: 2', +# 'Element: ELECTRICS - Code: 180, Sub-Element: Wiring - Code: 1', +# 'Element: ELECTRICS - Code: 180, Sub-Element: Consumer Unit - Code: 2', +# 'Element: ELECTRICS - Code: 180, Sub-Element: Smoke Detectors - Code: 3', +# 'Element: KITCHEN - Code: 160, Sub-Element: Kitchen space and layout - Code: 2', +# 'Element: HEATING - Code: 170, Sub-Element: Secondary Heating - Code: 3', +# 'Element: BATHROOM - Code: 190, Sub-Element: Secondary Toilet - Code: 2', +# 'Element: ELECTRICS - Code: 180, Sub-Element: Carbon Monoxide Alarms - Code: 4', +# 'Element: ROOFS - Code: 110, Sub-Element: Porch/Bay/Canopy - Code: 8', +# 'Element: HEATING - Code: 170, Sub-Element: Hot Water - Code: 5', +# 'Element: DOORS - Code: 140, Sub-Element: Garage Doors - Code: 3', +# 'Element: HEATING - Code: 170, Sub-Element: Cold Water - Code: 4', +# 'Element: DOORS - Code: 140, Sub-Element: Store Doors - Code: 2', +# 'Element: EXTERNAL AREAS - Code: 150, Sub-Element: Hardstanding - Code: 2', +# 'Element: WALLS - Code: 120, Sub-Element: Wall Insulation - Code: 3', +# 'Element: EXTERNAL AREAS - Code: 150, Sub-Element: Roads - Code: 3', +# 'Element: ROOFS - Code: 110, Sub-Element: Loft Insulation - Code: 7', +# 'Element: EXTERNAL AREAS - Code: 150, Sub-Element: Outbuilding - Code: 5', +# 'Element: Internal - Code: 50, Sub-Element: Additional WC - Code: 1', +# 'Element: Internal - Code: 50, Sub-Element: Carbon Monoxide Detector Type - Code: 2', +# 'Element: Internal - Code: 50, Sub-Element: CCU - Code: 3', +# 'Element: Internal - Code: 50, Sub-Element: Central Heating Boiler - Code: 4', +# 'Element: Internal - Code: 50, Sub-Element: Extractor Fan Bathroom - Code: 9', +# 'Element: Internal - Code: 50, Sub-Element: Extractor Fan Kitchen - Code: 10', +# 'Element: Internal - Code: 50, Sub-Element: Heat Detector Type - Code: 11', +# 'Element: Internal - Code: 50, Sub-Element: Kitchen Type - Code: 14', +# 'Element: Internal - Code: 50, Sub-Element: Primary Bathroom Type - Code: 18', +# 'Element: Internal - Code: 50, Sub-Element: Smoke Detector Type - Code: 21', +# 'Element: EXTERNAL AREAS - Code: 150, Sub-Element: Garage - Code: 6', +# 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Lifts - Code: 5', +# 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Kitchen - Code: 7', +# 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Bathroom - Code: 8', +# 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Toilets - Code: 9', +# 'Element: Internal - Code: 50, Sub-Element: Wiring - Code: 24', +# 'Element: External - Code: 53, Sub-Element: Front Door Material - Code: 8', +# 'Element: External - Code: 53, Sub-Element: Primary Wall Finish - Code: 23', +# 'Element: PASSENGER LIFTS - Code: 210, Sub-Element: Lift - Code: 2', +# 'Element: Internal - Code: 50, Sub-Element: Heating Distribution Type - Code: 12', +# 'Element: External - Code: 53, Sub-Element: Downpipes - Code: 3', +# 'Element: External - Code: 53, Sub-Element: Fascia/Soffits/Bargeboards - Code: 6', +# 'Element: External - Code: 53, Sub-Element: Gutters - Code: 15', +# 'Element: External - Code: 53, Sub-Element: Paths & Hardstandings - Code: 18', +# 'Element: External - Code: 53, Sub-Element: Pitched Roof Covering Material - Code: 21', +# 'Element: Internal - Code: 50, Sub-Element: Secondary Bathroom Type - Code: 20', +# 'Element: External - Code: 53, Sub-Element: Chimney - Code: 2', +# 'Element: External - Code: 53, Sub-Element: External Decoration - Code: 4', +# 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Gates - Code: 10', +# 'Element: GENERAL - Code: 100, Sub-Element: Property Age Band - Code: 3', +# 'Element: GENERAL - Code: 100, Sub-Element: Construction Type - Code: 14', +# 'Element: GENERAL - Code: 100, Sub-Element: Classification - Code: 16', +# 'Element: Communal - Code: 51, Sub-Element: Common Balcony/Walkway - Code: 3', +# 'Element: Communal - Code: 51, Sub-Element: Common Bin Store Doors - Code: 5', +# 'Element: Communal - Code: 51, Sub-Element: Common Bin Store Walls - Code: 7', +# 'Element: Communal - Code: 51, Sub-Element: Common Primary Entrance Material - Code: 28', +# 'Element: External - Code: 53, Sub-Element: Parking Areas - Code: 17', +# 'Element: External - Code: 53, Sub-Element: Front Fencing - Code: 9', +# 'Element: External - Code: 53, Sub-Element: Retaining Walls - Code: 28', +# 'Element: Communal - Code: 51, Sub-Element: Common Internal Decorations - Code: 20', +# 'Element: Communal - Code: 51, Sub-Element: Common Internal Floor Finish - Code: 22', +# 'Element: Communal - Code: 51, Sub-Element: Common Walkways Finish - Code: 36', +# 'Element: External - Code: 53, Sub-Element: Boundary Walls - Code: 1', +# 'Element: External - Code: 53, Sub-Element: Flat Roof Covering Material - Code: 7', +# 'Element: External - Code: 53, Sub-Element: Porch/Canopy - Code: 22', +# 'Element: External - Code: 53, Sub-Element: Private Balcony - Code: 24', +# 'Element: External - Code: 53, Sub-Element: Rear Gate - Code: 27', +# 'Element: Communal - Code: 51, Sub-Element: Common External Doors Other - Code: 17', +# 'Element: Communal - Code: 51, Sub-Element: Common Stair Finish - Code: 32', +# 'Element: External - Code: 53, Sub-Element: Front Gate - Code: 10', +# 'Element: External - Code: 53, Sub-Element: Rear Fencing - Code: 26', +# 'Element: External - Code: 53, Sub-Element: Side Fencing - Code: 31', +# 'Element: Communal - Code: 51, Sub-Element: Common Aerial - Code: 1', +# 'Element: Communal - Code: 51, Sub-Element: Common AOV - Code: 2', +# 'Element: Communal - Code: 51, Sub-Element: Common Door Entry System - Code: 14', +# 'Element: Communal - Code: 51, Sub-Element: Common Fire Alarm - Code: 19', +# 'Element: Communal - Code: 51, Sub-Element: Common Internal Doors - Code: 21', +# 'Element: External - Code: 53, Sub-Element: Store Door Material - Code: 35', +# 'Element: External - Code: 53, Sub-Element: Secondary Wall Finish - Code: 30', +# 'Element: Communal - Code: 51, Sub-Element: Common Emergency Lighting - Code: 16', +# 'Element: Communal - Code: 51, Sub-Element: Common Lateral Mains - Code: 24', +# 'Element: Communal - Code: 51, Sub-Element: Common Lighting - Code: 25', +# 'Element: Communal - Code: 51, Sub-Element: Common Store Roof - Code: 34', +# 'Element: Communal - Code: 51, Sub-Element: Common Store Walls - Code: 35', +# 'Element: External - Code: 53, Sub-Element: Cladding Material - Code: 41', +# 'Element: External - Code: 53, Sub-Element: Spandrel Panels - Code: 40', +# 'Element: Communal - Code: 51, Sub-Element: Common CCTV - Code: 11', +# 'Element: Communal - Code: 51, Sub-Element: Common Kitchen - Code: 23', +# 'Element: Communal - Code: 51, Sub-Element: Common Secondary Entrance Material - Code: 30', +# 'Element: Communal - Code: 51, Sub-Element: Common Warden Call System - Code: 37', +# 'Element: External - Code: 53, Sub-Element: Lintels - Code: 16', +# 'Element: Communal - Code: 51, Sub-Element: Common Boiler - Code: 9', +# 'Element: External - Code: 53, Sub-Element: Soil & Vent Material - Code: 32', +# 'Element: Communal - Code: 51, Sub-Element: Common Passenger Lift - Code: 27', +# 'Element: Communal - Code: 51, Sub-Element: Common Store Doors - Code: 33', +# 'Element: External - Code: 53, Sub-Element: Window Type 02 - Code: 39', +# 'Element: Communal - Code: 51, Sub-Element: Common BMS - Code: 8', +# 'Element: Communal - Code: 51, Sub-Element: Common Booster Pump - Code: 10', +# 'Element: Communal - Code: 51, Sub-Element: Common Dry Riser - Code: 15', +# 'Element: Communal - Code: 51, Sub-Element: Common Lightning Conductor - Code: 26', +# 'Element: Communal - Code: 51, Sub-Element: Common Bin Store Roof - Code: 6', +# 'Element: Communal - Code: 51, Sub-Element: Common Bathroom - Code: 4', +# 'Element: Communal - Code: 51, Sub-Element: Common WC - Code: 38', +# 'Element: External - Code: 53, Sub-Element: Wall Insulation - Code: 36', +# 'Element: External - Code: 53, Sub-Element: Garage Door - Code: 12', +# 'Element: Communal - Code: 51, Sub-Element: Common Cold Water Storage Tank - Code: 13', +# 'Element: Communal - Code: 51, Sub-Element: Common Sprinker - Code: 31', +# 'Element: External - Code: 53, Sub-Element: Garage Walls - Code: 14', +# 'Element: Communal - Code: 51, Sub-Element: Communal Plug Sockets - Code: 40', +# 'Element: Communal - Code: 51, Sub-Element: Common Wet Riser - Code: 39', +# 'Element: Communal - Code: 51, Sub-Element: Common Refuse Chute - Code: 29', +# 'Element: External - Code: 53, Sub-Element: Secondary Glazing - Code: 29', +# 'Element: External - Code: 53, Sub-Element: Solar Thermals - Code: 34', +# 'Element: External - Code: 53, Sub-Element: Garage Roof - Code: 13', +# 'Element: External - Code: 53, Sub-Element: Patio/French Door - Code: 19', +# 'Element: External - Code: 53, Sub-Element: Rear Door Material - Code: 25', +# 'Element: Internal - Code: 50, Sub-Element: Party Wall Fire Break - Code: 16', +# 'Element: Internal - Code: 50, Sub-Element: Boiler Type - Code: 25', +# 'Element: External - Code: 53, Sub-Element: Roof Structure - Code: 47', +# 'Element: External - Code: 53, Sub-Element: Front Door Type - Code: 43', +# 'Element: Communal - Code: 51, Sub-Element: Common Cirulation Space - Code: 12', +# 'Element: External - Code: 53, Sub-Element: External Noise Insulation - Code: 5', +# 'Element: Internal - Code: 50, Sub-Element: Door Entry Handset - Code: 8', +# 'Element: Internal - Code: 50, Sub-Element: Cold Water Storage Tank - Code: 6', +# 'Element: Internal - Code: 50, Sub-Element: Programmable Heating - Code: 19', +# 'Element: Internal - Code: 50, Sub-Element: Central Heating Extent - Code: 5', +# 'Element: Internal - Code: 50, Sub-Element: Kitchen Space & Layout - Code: 13', +# 'Element: Internal - Code: 50, Sub-Element: Loft Insulation - Code: 15', +# 'Element: Internal - Code: 50, Sub-Element: Stairlift - Code: 22', +# 'Element: Internal - Code: 50, Sub-Element: Primary Bathroom Location - Code: 17', +# 'Element: Internal - Code: 50, Sub-Element: Disabled Hoist Tracking - Code: 7', +# 'Element: External - Code: 53, Sub-Element: Garage Type - Code: 44', +# 'Element: External - Code: 53, Sub-Element: Private Balcony Balustrade Material - Code: 45', +# 'Element: Internal - Code: 50, Sub-Element: Disabled Facilities - Code: 26' diff --git a/backend/condition/domain/mapping/peabody/peabody_mapper.py b/backend/condition/domain/mapping/peabody/peabody_mapper.py index 44dbd56e..dea07756 100644 --- a/backend/condition/domain/mapping/peabody/peabody_mapper.py +++ b/backend/condition/domain/mapping/peabody/peabody_mapper.py @@ -6,9 +6,6 @@ from backend.condition.domain.mapping.peabody.peabody_element_map import ( PEABODY_ELEMENT_MAP, ) from backend.condition.domain.mapping.mapper import Mapper -from backend.condition.parsing.records.peabody.peabody_asset_condition import ( - PeabodyAssetCondition, -) from backend.condition.parsing.records.peabody.peabody_property import PeabodyProperty from utils.logger import setup_logger diff --git a/backend/condition/tests/mapping/test_peabody_mapper.py b/backend/condition/tests/mapping/test_peabody_mapper.py index ca3f78a4..7fad77f7 100644 --- a/backend/condition/tests/mapping/test_peabody_mapper.py +++ b/backend/condition/tests/mapping/test_peabody_mapper.py @@ -33,7 +33,25 @@ def test_peabody_mapper_maps_property(): cloned="N", lo_type_code=1, condition_survey_date=datetime(2024, 2, 15, 12, 47, 0), - ) + ), + PeabodyAssetCondition( + lo_reference="1000RAND0000", + full_address="FLAT 1 RANDOM SQUARE FAKE STREET LONDON E1 1EE", + location_type_code=1, + parent_lo_reference="RAND1000", + element_code=100, + element="GENERAL", + sub_element_code=15, + sub_element="External Decoration", + material_code=2, + material_or_answer="Normal", + renewal_quantity=1, + renewal_year=2029, + renewal_cost=1500, + cloned="N", + lo_type_code=1, + condition_survey_date=datetime(2024, 2, 15, 12, 47, 0), + ), ], ) mapper = PeabodyMapper() @@ -50,7 +68,19 @@ def test_peabody_mapper_maps_property(): element_instance=None, source_system=None, comments=None, - ) + ), + AssetCondition( + uprn=1, + element=Element.PROPERTY, + aspect_type=AspectType.EXTERNAL_DECORATION, + value="Normal", + quantity=1, + install_date=None, + renewal_year=2029, + element_instance=None, + source_system=None, + comments=None, + ), ] # act actual_assets = mapper.map_asset_conditions_for_property(peabody_property) From 7741373671093bc0c4bdaa2fd01aabc70dc9950d Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 23 Jan 2026 15:54:53 +0000 Subject: [PATCH 29/50] Map more peabody elements --- backend/condition/domain/aspect_type.py | 1 + backend/condition/domain/element.py | 8 ++ .../mapping/peabody/peabody_element_map.py | 104 +++++++++++++----- 3 files changed, 86 insertions(+), 27 deletions(-) diff --git a/backend/condition/domain/aspect_type.py b/backend/condition/domain/aspect_type.py index d4db10bc..94522b03 100644 --- a/backend/condition/domain/aspect_type.py +++ b/backend/condition/domain/aspect_type.py @@ -28,3 +28,4 @@ class AspectType(str, Enum): COVERING = "covering" FIRE_RATING = "fire_rating" EXTERNAL_DECORATION = "external_decoration" + WORK_REQUIRED = "work_required" diff --git a/backend/condition/domain/element.py b/backend/condition/domain/element.py index c8fb6167..1d109002 100644 --- a/backend/condition/domain/element.py +++ b/backend/condition/domain/element.py @@ -23,6 +23,9 @@ class Element(str, Enum): RAINWATER_GOODS = "rainwater_goods" LOFT_INSULATION = "loft_insulation" PORCH_CANOPY = "porch_canopy" + CHIMNEY = "chimney" + FASCIA = "fascia" + SOFFIT = "soffit" # ====================== # EXTERNAL – WALLS @@ -45,6 +48,8 @@ class Element(str, Enum): STORE_DOOR = "store_door" GARAGE_DOOR = "garage_door" COMMUNAL_ENTRANCE_DOOR = "communal_entrance_door" + MAIN_DOOR = "main_door" + BLOCK_ENTRANCE_DOOR = "block_entrance_door" # ====================== # EXTERNAL – AREAS @@ -59,6 +64,8 @@ class Element(str, Enum): BALCONY_BALUSTRADE = "balcony_balustrade" OUTBUILDINGS = "outbuildings" GARAGE_STRUCTURE = "garage_structure" + PAVING = "paving" + ROADS = "roads" # ====================== # INTERNAL – KITCHEN @@ -110,6 +117,7 @@ class Element(str, Enum): COMMUNAL_CCTV = "communal_cctv" COMMUNAL_BIN_STORE = "communal_bin_store" COMMUNAL_REFUSE_CHUTE = "communal_refuse_chute" + COMMUNAL_FLOOR_COVERING = "communal_floor_covering" # ========================================================== # HHSRS – ALL 29 HAZARDS diff --git a/backend/condition/domain/mapping/peabody/peabody_element_map.py b/backend/condition/domain/mapping/peabody/peabody_element_map.py index 80b3aa70..12c4bf66 100644 --- a/backend/condition/domain/mapping/peabody/peabody_element_map.py +++ b/backend/condition/domain/mapping/peabody/peabody_element_map.py @@ -25,13 +25,65 @@ PEABODY_ELEMENT_MAP = { # ========================================================== # EXTERNAL – ROOFS # ========================================================== - (110, 1): ElementMapping(element=Element.ROOF, aspect_type=AspectType.COVERING), + (110, 1): ElementMapping( + element=Element.ROOF, aspect_type=AspectType.MATERIAL, element_instance=1 + ), + (110, 2): ElementMapping( + element=Element.ROOF, aspect_type=AspectType.MATERIAL, element_instance=1 + ), + (110, 3): ElementMapping( + element=Element.CHIMNEY, aspect_type=AspectType.WORK_REQUIRED + ), + (110, 4): ElementMapping(element=Element.FASCIA, aspect_type=AspectType.MATERIAL), + (110, 5): ElementMapping(element=Element.SOFFIT, aspect_type=AspectType.MATERIAL), + (110, 6): ElementMapping( + element=Element.RAINWATER_GOODS, aspect_type=AspectType.MATERIAL + ), + (110, 7): ElementMapping( + element=Element.LOFT_INSULATION, + aspect_type=AspectType.WORK_REQUIRED, # possibly not the right aspect type + ), + (110, 8): ElementMapping( + element=Element.PORCH_CANOPY, aspect_type=AspectType.MATERIAL + ), # ========================================================== # EXTERNAL – DOORS & WINDOWS # ========================================================== (130, 1): ElementMapping( element=Element.EXTERNAL_WINDOWS, aspect_type=AspectType.MATERIAL ), + (130, 2): ElementMapping( + element=Element.COMMUNAL_WINDOWS, aspect_type=AspectType.MATERIAL + ), + (140, 1): ElementMapping( + element=Element.MAIN_DOOR, aspect_type=AspectType.MATERIAL + ), + (140, 2): ElementMapping( + element=Element.STORE_DOOR, aspect_type=AspectType.MATERIAL + ), + (140, 3): ElementMapping( + element=Element.GARAGE_DOOR, aspect_type=AspectType.MATERIAL + ), + (140, 4): ElementMapping( + element=Element.BLOCK_ENTRANCE_DOOR, aspect_type=AspectType.MATERIAL + ), + # ========================================================== + # EXTERNAL AREAS + # ========================================================== + (150, 1): ElementMapping( + element=Element.BLOCK_ENTRANCE_DOOR, aspect_type=AspectType.MATERIAL + ), + (150, 2): ElementMapping( + element=Element.PATHS_AND_HARDSTANDINGS, aspect_type=AspectType.MATERIAL + ), + (150, 3): ElementMapping(element=Element.ROADS, aspect_type=AspectType.MATERIAL), + (150, 4): ElementMapping( + element=Element.BOUNDARY_WALLS, aspect_type=AspectType.MATERIAL + ), + (150, 5): ElementMapping(element=Element.OUTBUILDINGS, aspect_type=AspectType.TYPE), + (150, 6): ElementMapping( + element=Element.GARAGE_STRUCTURE, aspect_type=AspectType.TYPE + ), # ========================================================== # INTERNAL – BATHROOMS & KITCHENS # ========================================================== @@ -39,9 +91,30 @@ PEABODY_ELEMENT_MAP = { (190, 1): ElementMapping( element=Element.BATHROOM, aspect_type=AspectType.CONDITION ), - # -------------------- - # HHSRS (PEABODY) - # -------------------- + # ========================================================== + # COMMUNAL SYSTEMS + # ========================================================== + (200, 1): ElementMapping( + element=Element.COMMUNAL_BOILER, aspect_type=AspectType.TYPE + ), + (200, 2): ElementMapping( + element=Element.COMMUNAL_HEATING, aspect_type=AspectType.TYPE + ), + (200, 3): ElementMapping( + element=Element.COMMUNAL_ELECTRICS, aspect_type=AspectType.TYPE + ), + (200, 4): ElementMapping( + element=Element.COMMUNAL_FIRE_ALARM, aspect_type=AspectType.TYPE + ), + (200, 5): ElementMapping( + element=Element.COMMUNAL_LIFT, aspect_type=AspectType.TYPE + ), + (200, 6): ElementMapping( + element=Element.COMMUNAL_FLOOR_COVERING, aspect_type=AspectType.MATERIAL + ), + # ========================================================== + # HHSRS + # ========================================================== (54, 1): ElementMapping( element=Element.HHSRS_DAMP_AND_MOULD, aspect_type=AspectType.RISK ), @@ -58,22 +131,7 @@ PEABODY_ELEMENT_MAP = { # unhandled -# 'Element: ROOFS - Code: 110, Sub-Element: Chimney - Code: 3', -# 'Element: ROOFS - Code: 110, Sub-Element: Fascia - Code: 4', -# 'Element: ROOFS - Code: 110, Sub-Element: Rainwater Goods - Code: 6', -# 'Element: WINDOWS - Code: 130, Sub-Element: Communal Windows - Code: 2', -# 'Element: DOORS - Code: 140, Sub-Element: Main Doors - Code: 1', -# 'Element: DOORS - Code: 140, Sub-Element: Block Entrance Doors - Code: 4', -# 'Element: EXTERNAL AREAS - Code: 150, Sub-Element: Paving - Code: 1', -# 'Element: EXTERNAL AREAS - Code: 150, Sub-Element: Boundaries - Code: 4', -# 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Boiler - Code: 1', -# 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Heating - Code: 2', -# 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Electrics - Code: 3', -# 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Fire Detection - Code: 4', -# 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Floor Covering - Code: 6', -# 'Element: ROOFS - Code: 110, Sub-Element: Soffit - Code: 5', # 'Element: External - Code: 53, Sub-Element: Window Type 01 - Code: 38', -# 'Element: ROOFS - Code: 110, Sub-Element: Secondary Roof - Code: 2', # 'Element: HEATING - Code: 170, Sub-Element: Boiler - Code: 1', # 'Element: HEATING - Code: 170, Sub-Element: Heating Distribution - Code: 2', # 'Element: ELECTRICS - Code: 180, Sub-Element: Wiring - Code: 1', @@ -83,16 +141,9 @@ PEABODY_ELEMENT_MAP = { # 'Element: HEATING - Code: 170, Sub-Element: Secondary Heating - Code: 3', # 'Element: BATHROOM - Code: 190, Sub-Element: Secondary Toilet - Code: 2', # 'Element: ELECTRICS - Code: 180, Sub-Element: Carbon Monoxide Alarms - Code: 4', -# 'Element: ROOFS - Code: 110, Sub-Element: Porch/Bay/Canopy - Code: 8', # 'Element: HEATING - Code: 170, Sub-Element: Hot Water - Code: 5', -# 'Element: DOORS - Code: 140, Sub-Element: Garage Doors - Code: 3', # 'Element: HEATING - Code: 170, Sub-Element: Cold Water - Code: 4', -# 'Element: DOORS - Code: 140, Sub-Element: Store Doors - Code: 2', -# 'Element: EXTERNAL AREAS - Code: 150, Sub-Element: Hardstanding - Code: 2', # 'Element: WALLS - Code: 120, Sub-Element: Wall Insulation - Code: 3', -# 'Element: EXTERNAL AREAS - Code: 150, Sub-Element: Roads - Code: 3', -# 'Element: ROOFS - Code: 110, Sub-Element: Loft Insulation - Code: 7', -# 'Element: EXTERNAL AREAS - Code: 150, Sub-Element: Outbuilding - Code: 5', # 'Element: Internal - Code: 50, Sub-Element: Additional WC - Code: 1', # 'Element: Internal - Code: 50, Sub-Element: Carbon Monoxide Detector Type - Code: 2', # 'Element: Internal - Code: 50, Sub-Element: CCU - Code: 3', @@ -103,7 +154,6 @@ PEABODY_ELEMENT_MAP = { # 'Element: Internal - Code: 50, Sub-Element: Kitchen Type - Code: 14', # 'Element: Internal - Code: 50, Sub-Element: Primary Bathroom Type - Code: 18', # 'Element: Internal - Code: 50, Sub-Element: Smoke Detector Type - Code: 21', -# 'Element: EXTERNAL AREAS - Code: 150, Sub-Element: Garage - Code: 6', # 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Lifts - Code: 5', # 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Kitchen - Code: 7', # 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Bathroom - Code: 8', From 8cdbe107e5f9c306efc1b6d0e12cf05e99057d0c Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 23 Jan 2026 16:21:33 +0000 Subject: [PATCH 30/50] add TODO to is_block_level --- .../records/peabody/peabody_asset_condition.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/backend/condition/parsing/records/peabody/peabody_asset_condition.py b/backend/condition/parsing/records/peabody/peabody_asset_condition.py index 01215a26..a74dc359 100644 --- a/backend/condition/parsing/records/peabody/peabody_asset_condition.py +++ b/backend/condition/parsing/records/peabody/peabody_asset_condition.py @@ -4,6 +4,7 @@ from dataclasses import dataclass from datetime import datetime from typing import Optional + @dataclass class PeabodyAssetCondition: lo_reference: str @@ -25,17 +26,19 @@ class PeabodyAssetCondition: @property def is_block_level(self) -> bool: + # TODO: maybe use block codes from other Peabody dataset to do this instead + if not self.full_address: return False address = self.full_address.upper() block_level_patterns = [ - r"\bBLOCK\b", # BLOCK MILNE HOUSE - r"\bFLATS\b", # FLATS A-D - r"\b\d+[A-Z]?-\d+[A-Z]?\b", # 1-80, 9A-9H - r"\b\d+[A-Z]-[A-Z]\b", # 81A-B - r"\b\d+\s*&\s*\d+\b", # 73 & 74 + r"\bBLOCK\b", # BLOCK MILNE HOUSE + r"\bFLATS\b", # FLATS A-D + r"\b\d+[A-Z]?-\d+[A-Z]?\b", # 1-80, 9A-9H + r"\b\d+[A-Z]-[A-Z]\b", # 81A-B + r"\b\d+\s*&\s*\d+\b", # 73 & 74 ] - return any(re.search(pattern, address) for pattern in block_level_patterns) \ No newline at end of file + return any(re.search(pattern, address) for pattern in block_level_patterns) From 2d77511650219f4a6f931c318a88107156a587ca Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 23 Jan 2026 17:11:11 +0000 Subject: [PATCH 31/50] Map remaining Peabody EXTERNAL elements --- backend/condition/domain/element.py | 23 ++- .../mapping/peabody/peabody_element_map.py | 160 +++++++++++++----- .../tests/mapping/test_peabody_mapper.py | 4 +- 3 files changed, 137 insertions(+), 50 deletions(-) diff --git a/backend/condition/domain/element.py b/backend/condition/domain/element.py index 1d109002..fed5ab3b 100644 --- a/backend/condition/domain/element.py +++ b/backend/condition/domain/element.py @@ -20,17 +20,30 @@ class Element(str, Enum): # EXTERNAL – ROOF # ====================== ROOF = "roof" + PITCHED_ROOF_COVERING = "pitched_roof_covering" + FLAT_ROOF_COVERING = "flat_roof_covering" RAINWATER_GOODS = "rainwater_goods" LOFT_INSULATION = "loft_insulation" PORCH_CANOPY = "porch_canopy" CHIMNEY = "chimney" FASCIA = "fascia" SOFFIT = "soffit" + FASCIA_SOFFIT_BARGEBOARDS = "fascia_soffit_bargeboards" + GUTTERS = "gutters" + GARAGE_ROOF = "garage_roof" # ====================== # EXTERNAL – WALLS # ====================== EXTERNAL_WALL = "external_wall" + EXTERNAL_NOISE_INSULATION = "external_noise_insulation" + PRIMARY_WALL = "primary_wall" + SECONDARY_WALL = "secondary_wall" + DOWNPIPES = "downpipes" + EXTERNAL_DECORATION = "external_decoration" + CLADDING = "cladding" + SPANDREL_PANELS = "spandrel_panels" + GARAGE_WALLS = "garage_walls" # ====================== # EXTERNAL – WINDOWS @@ -50,6 +63,8 @@ class Element(str, Enum): COMMUNAL_ENTRANCE_DOOR = "communal_entrance_door" MAIN_DOOR = "main_door" BLOCK_ENTRANCE_DOOR = "block_entrance_door" + LINTEL = "lintel" + PATIO_FRENCH_DOOR = "patio_french_door" # ====================== # EXTERNAL – AREAS @@ -57,7 +72,11 @@ class Element(str, Enum): PATHS_AND_HARDSTANDINGS = "paths_and_hardstandings" PARKING_AREAS = "parking_areas" BOUNDARY_WALLS = "boundary_walls" - FENCING = "fencing" + FRONT_FENCING = "front_fencing" + REAR_FENCING = "rear_fencing" + SIDE_FENCING = "side_fencing" + REAR_GATE = "rear_gate" + FRONT_GATE = "front_gate" GATES = "gates" RETAINING_WALLS = "retaining_walls" PRIVATE_BALCONY = "private_balcony" @@ -66,6 +85,8 @@ class Element(str, Enum): GARAGE_STRUCTURE = "garage_structure" PAVING = "paving" ROADS = "roads" + SOIL_AND_VENT = "soil_and_vent" + SOLAR_THERMALS = "solar_thermals" # ====================== # INTERNAL – KITCHEN diff --git a/backend/condition/domain/mapping/peabody/peabody_element_map.py b/backend/condition/domain/mapping/peabody/peabody_element_map.py index 12c4bf66..81aa8b9e 100644 --- a/backend/condition/domain/mapping/peabody/peabody_element_map.py +++ b/backend/condition/domain/mapping/peabody/peabody_element_map.py @@ -8,14 +8,39 @@ PEABODY_ELEMENT_MAP = { # PROPERTY / GENERAL # ========================================================== (100, 1): ElementMapping(element=Element.PROPERTY, aspect_type=AspectType.TYPE), - (100, 15): ElementMapping( - element=Element.PROPERTY, aspect_type=AspectType.EXTERNAL_DECORATION - ), # (100, 3): ElementMapping(element=Element.PROPERTY, aspect_type=AspectType.AGE), # (100, 14): ElementMapping(element="property", aspect_type="construction_type"), # ========================================================== # EXTERNAL – WALLS # ========================================================== + (53, 1): ElementMapping( + element=Element.BOUNDARY_WALLS, aspect_type=AspectType.PRESENCE + ), + (53, 4): ElementMapping( + element=Element.EXTERNAL_DECORATION, aspect_type=AspectType.PRESENCE + ), + (53, 4): ElementMapping( + element=Element.EXTERNAL_NOISE_INSULATION, aspect_type=AspectType.ADEQUACY + ), + (53, 14): ElementMapping( + element=Element.GARAGE_WALLS, aspect_type=AspectType.MATERIAL + ), + (53, 23): ElementMapping( + element=Element.PRIMARY_WALL, aspect_type=AspectType.FINISH + ), + (53, 30): ElementMapping( + element=Element.SECONDARY_WALL, aspect_type=AspectType.FINISH + ), # Should this be combined with primary wall, with different instance value? + (53, 36): ElementMapping( + element=Element.EXTERNAL_WALL, aspect_type=AspectType.INSULATION + ), + (53, 40): ElementMapping( + element=Element.SPANDREL_PANELS, aspect_type=AspectType.MATERIAL + ), + (53, 41): ElementMapping(element=Element.CLADDING, aspect_type=AspectType.MATERIAL), + (100, 15): ElementMapping( + element=Element.EXTERNAL_DECORATION, aspect_type=AspectType.CONDITION + ), (120, 1): ElementMapping( element=Element.EXTERNAL_WALL, aspect_type=AspectType.STRUCTURE ), @@ -25,6 +50,22 @@ PEABODY_ELEMENT_MAP = { # ========================================================== # EXTERNAL – ROOFS # ========================================================== + (53, 2): ElementMapping(element=Element.CHIMNEY, aspect_type=AspectType.PRESENCE), + (53, 6): ElementMapping( + element=Element.FASCIA_SOFFIT_BARGEBOARDS, aspect_type=AspectType.MATERIAL + ), + (53, 7): ElementMapping( + element=Element.FLAT_ROOF_COVERING, aspect_type=AspectType.MATERIAL + ), + (53, 13): ElementMapping( + element=Element.GARAGE_ROOF, aspect_type=AspectType.MATERIAL + ), + (53, 15): ElementMapping(element=Element.GUTTERS, aspect_type=AspectType.MATERIAL), + (53, 18): ElementMapping( + element=Element.PITCHED_ROOF_COVERING, aspect_type=AspectType.MATERIAL + ), + (53, 22): ElementMapping(element=Element.PORCH_CANOPY, aspect_type=AspectType.TYPE), + (53, 47): ElementMapping(element=Element.ROOF, aspect_type=AspectType.STRUCTURE), (110, 1): ElementMapping( element=Element.ROOF, aspect_type=AspectType.MATERIAL, element_instance=1 ), @@ -49,6 +90,36 @@ PEABODY_ELEMENT_MAP = { # ========================================================== # EXTERNAL – DOORS & WINDOWS # ========================================================== + (53, 8): ElementMapping( + element=Element.FRONT_DOOR, aspect_type=AspectType.MATERIAL + ), + (53, 12): ElementMapping( + element=Element.GARAGE_DOOR, aspect_type=AspectType.MATERIAL + ), + (53, 16): ElementMapping(element=Element.LINTEL, aspect_type=AspectType.PRESENCE), + (53, 19): ElementMapping( + element=Element.PATIO_FRENCH_DOOR, aspect_type=AspectType.MATERIAL + ), + (53, 25): ElementMapping( + element=Element.REAR_DOOR, aspect_type=AspectType.MATERIAL + ), + (53, 29): ElementMapping( + element=Element.SECONDARY_GLAZING, aspect_type=AspectType.PRESENCE + ), + (53, 35): ElementMapping( + element=Element.STORE_DOOR, aspect_type=AspectType.MATERIAL + ), + (53, 38): ElementMapping( + element=Element.EXTERNAL_WINDOWS, + aspect_type=AspectType.TYPE, + element_instance=1, + ), + (53, 39): ElementMapping( + element=Element.EXTERNAL_WINDOWS, + aspect_type=AspectType.TYPE, + element_instance=2, + ), + (53, 43): ElementMapping(element=Element.FRONT_DOOR, aspect_type=AspectType.TYPE), (130, 1): ElementMapping( element=Element.EXTERNAL_WINDOWS, aspect_type=AspectType.MATERIAL ), @@ -60,22 +131,58 @@ PEABODY_ELEMENT_MAP = { ), (140, 2): ElementMapping( element=Element.STORE_DOOR, aspect_type=AspectType.MATERIAL - ), + ), # Duplicate of (53, 35) (140, 3): ElementMapping( element=Element.GARAGE_DOOR, aspect_type=AspectType.MATERIAL - ), + ), # Duplicate of (53, 12) (140, 4): ElementMapping( element=Element.BLOCK_ENTRANCE_DOOR, aspect_type=AspectType.MATERIAL ), # ========================================================== # EXTERNAL AREAS # ========================================================== + (53, 3): ElementMapping(element=Element.DOWNPIPES, aspect_type=AspectType.MATERIAL), + (53, 9): ElementMapping( + element=Element.FRONT_FENCING, aspect_type=AspectType.MATERIAL + ), + (53, 10): ElementMapping(element=Element.FRONT_GATE, aspect_type=AspectType.TYPE), + (53, 17): ElementMapping( + element=Element.PARKING_AREAS, aspect_type=AspectType.MATERIAL + ), + (53, 18): ElementMapping( + element=Element.PATHS_AND_HARDSTANDINGS, aspect_type=AspectType.MATERIAL + ), + (53, 24): ElementMapping( + element=Element.PRIVATE_BALCONY, aspect_type=AspectType.PRESENCE + ), + (53, 26): ElementMapping( + element=Element.REAR_FENCING, aspect_type=AspectType.MATERIAL + ), + (53, 27): ElementMapping(element=Element.REAR_GATE, aspect_type=AspectType.TYPE), + (53, 28): ElementMapping( + element=Element.RETAINING_WALLS, aspect_type=AspectType.PRESENCE + ), + (53, 31): ElementMapping( + element=Element.SIDE_FENCING, aspect_type=AspectType.MATERIAL + ), + (53, 32): ElementMapping( + element=Element.SOIL_AND_VENT, aspect_type=AspectType.MATERIAL + ), + (53, 34): ElementMapping( + element=Element.SOLAR_THERMALS, aspect_type=AspectType.PRESENCE + ), + (53, 44): ElementMapping( + element=Element.GARAGE_STRUCTURE, aspect_type=AspectType.TYPE + ), + (53, 45): ElementMapping( + element=Element.BALCONY_BALUSTRADE, aspect_type=AspectType.MATERIAL + ), (150, 1): ElementMapping( element=Element.BLOCK_ENTRANCE_DOOR, aspect_type=AspectType.MATERIAL ), (150, 2): ElementMapping( element=Element.PATHS_AND_HARDSTANDINGS, aspect_type=AspectType.MATERIAL - ), + ), # Duplicate of (53, 18) - correct? (150, 3): ElementMapping(element=Element.ROADS, aspect_type=AspectType.MATERIAL), (150, 4): ElementMapping( element=Element.BOUNDARY_WALLS, aspect_type=AspectType.MATERIAL @@ -131,7 +238,6 @@ PEABODY_ELEMENT_MAP = { # unhandled -# 'Element: External - Code: 53, Sub-Element: Window Type 01 - Code: 38', # 'Element: HEATING - Code: 170, Sub-Element: Boiler - Code: 1', # 'Element: HEATING - Code: 170, Sub-Element: Heating Distribution - Code: 2', # 'Element: ELECTRICS - Code: 180, Sub-Element: Wiring - Code: 1', @@ -159,18 +265,9 @@ PEABODY_ELEMENT_MAP = { # 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Bathroom - Code: 8', # 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Toilets - Code: 9', # 'Element: Internal - Code: 50, Sub-Element: Wiring - Code: 24', -# 'Element: External - Code: 53, Sub-Element: Front Door Material - Code: 8', -# 'Element: External - Code: 53, Sub-Element: Primary Wall Finish - Code: 23', # 'Element: PASSENGER LIFTS - Code: 210, Sub-Element: Lift - Code: 2', # 'Element: Internal - Code: 50, Sub-Element: Heating Distribution Type - Code: 12', -# 'Element: External - Code: 53, Sub-Element: Downpipes - Code: 3', -# 'Element: External - Code: 53, Sub-Element: Fascia/Soffits/Bargeboards - Code: 6', -# 'Element: External - Code: 53, Sub-Element: Gutters - Code: 15', -# 'Element: External - Code: 53, Sub-Element: Paths & Hardstandings - Code: 18', -# 'Element: External - Code: 53, Sub-Element: Pitched Roof Covering Material - Code: 21', # 'Element: Internal - Code: 50, Sub-Element: Secondary Bathroom Type - Code: 20', -# 'Element: External - Code: 53, Sub-Element: Chimney - Code: 2', -# 'Element: External - Code: 53, Sub-Element: External Decoration - Code: 4', # 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Gates - Code: 10', # 'Element: GENERAL - Code: 100, Sub-Element: Property Age Band - Code: 3', # 'Element: GENERAL - Code: 100, Sub-Element: Construction Type - Code: 14', @@ -179,46 +276,28 @@ PEABODY_ELEMENT_MAP = { # 'Element: Communal - Code: 51, Sub-Element: Common Bin Store Doors - Code: 5', # 'Element: Communal - Code: 51, Sub-Element: Common Bin Store Walls - Code: 7', # 'Element: Communal - Code: 51, Sub-Element: Common Primary Entrance Material - Code: 28', -# 'Element: External - Code: 53, Sub-Element: Parking Areas - Code: 17', -# 'Element: External - Code: 53, Sub-Element: Front Fencing - Code: 9', -# 'Element: External - Code: 53, Sub-Element: Retaining Walls - Code: 28', # 'Element: Communal - Code: 51, Sub-Element: Common Internal Decorations - Code: 20', # 'Element: Communal - Code: 51, Sub-Element: Common Internal Floor Finish - Code: 22', # 'Element: Communal - Code: 51, Sub-Element: Common Walkways Finish - Code: 36', -# 'Element: External - Code: 53, Sub-Element: Boundary Walls - Code: 1', -# 'Element: External - Code: 53, Sub-Element: Flat Roof Covering Material - Code: 7', -# 'Element: External - Code: 53, Sub-Element: Porch/Canopy - Code: 22', -# 'Element: External - Code: 53, Sub-Element: Private Balcony - Code: 24', -# 'Element: External - Code: 53, Sub-Element: Rear Gate - Code: 27', # 'Element: Communal - Code: 51, Sub-Element: Common External Doors Other - Code: 17', # 'Element: Communal - Code: 51, Sub-Element: Common Stair Finish - Code: 32', -# 'Element: External - Code: 53, Sub-Element: Front Gate - Code: 10', -# 'Element: External - Code: 53, Sub-Element: Rear Fencing - Code: 26', -# 'Element: External - Code: 53, Sub-Element: Side Fencing - Code: 31', # 'Element: Communal - Code: 51, Sub-Element: Common Aerial - Code: 1', # 'Element: Communal - Code: 51, Sub-Element: Common AOV - Code: 2', # 'Element: Communal - Code: 51, Sub-Element: Common Door Entry System - Code: 14', # 'Element: Communal - Code: 51, Sub-Element: Common Fire Alarm - Code: 19', # 'Element: Communal - Code: 51, Sub-Element: Common Internal Doors - Code: 21', -# 'Element: External - Code: 53, Sub-Element: Store Door Material - Code: 35', -# 'Element: External - Code: 53, Sub-Element: Secondary Wall Finish - Code: 30', # 'Element: Communal - Code: 51, Sub-Element: Common Emergency Lighting - Code: 16', # 'Element: Communal - Code: 51, Sub-Element: Common Lateral Mains - Code: 24', # 'Element: Communal - Code: 51, Sub-Element: Common Lighting - Code: 25', # 'Element: Communal - Code: 51, Sub-Element: Common Store Roof - Code: 34', # 'Element: Communal - Code: 51, Sub-Element: Common Store Walls - Code: 35', -# 'Element: External - Code: 53, Sub-Element: Cladding Material - Code: 41', -# 'Element: External - Code: 53, Sub-Element: Spandrel Panels - Code: 40', # 'Element: Communal - Code: 51, Sub-Element: Common CCTV - Code: 11', # 'Element: Communal - Code: 51, Sub-Element: Common Kitchen - Code: 23', # 'Element: Communal - Code: 51, Sub-Element: Common Secondary Entrance Material - Code: 30', # 'Element: Communal - Code: 51, Sub-Element: Common Warden Call System - Code: 37', -# 'Element: External - Code: 53, Sub-Element: Lintels - Code: 16', # 'Element: Communal - Code: 51, Sub-Element: Common Boiler - Code: 9', -# 'Element: External - Code: 53, Sub-Element: Soil & Vent Material - Code: 32', # 'Element: Communal - Code: 51, Sub-Element: Common Passenger Lift - Code: 27', # 'Element: Communal - Code: 51, Sub-Element: Common Store Doors - Code: 33', -# 'Element: External - Code: 53, Sub-Element: Window Type 02 - Code: 39', # 'Element: Communal - Code: 51, Sub-Element: Common BMS - Code: 8', # 'Element: Communal - Code: 51, Sub-Element: Common Booster Pump - Code: 10', # 'Element: Communal - Code: 51, Sub-Element: Common Dry Riser - Code: 15', @@ -226,25 +305,14 @@ PEABODY_ELEMENT_MAP = { # 'Element: Communal - Code: 51, Sub-Element: Common Bin Store Roof - Code: 6', # 'Element: Communal - Code: 51, Sub-Element: Common Bathroom - Code: 4', # 'Element: Communal - Code: 51, Sub-Element: Common WC - Code: 38', -# 'Element: External - Code: 53, Sub-Element: Wall Insulation - Code: 36', -# 'Element: External - Code: 53, Sub-Element: Garage Door - Code: 12', # 'Element: Communal - Code: 51, Sub-Element: Common Cold Water Storage Tank - Code: 13', # 'Element: Communal - Code: 51, Sub-Element: Common Sprinker - Code: 31', -# 'Element: External - Code: 53, Sub-Element: Garage Walls - Code: 14', # 'Element: Communal - Code: 51, Sub-Element: Communal Plug Sockets - Code: 40', # 'Element: Communal - Code: 51, Sub-Element: Common Wet Riser - Code: 39', # 'Element: Communal - Code: 51, Sub-Element: Common Refuse Chute - Code: 29', -# 'Element: External - Code: 53, Sub-Element: Secondary Glazing - Code: 29', -# 'Element: External - Code: 53, Sub-Element: Solar Thermals - Code: 34', -# 'Element: External - Code: 53, Sub-Element: Garage Roof - Code: 13', -# 'Element: External - Code: 53, Sub-Element: Patio/French Door - Code: 19', -# 'Element: External - Code: 53, Sub-Element: Rear Door Material - Code: 25', # 'Element: Internal - Code: 50, Sub-Element: Party Wall Fire Break - Code: 16', # 'Element: Internal - Code: 50, Sub-Element: Boiler Type - Code: 25', -# 'Element: External - Code: 53, Sub-Element: Roof Structure - Code: 47', -# 'Element: External - Code: 53, Sub-Element: Front Door Type - Code: 43', # 'Element: Communal - Code: 51, Sub-Element: Common Cirulation Space - Code: 12', -# 'Element: External - Code: 53, Sub-Element: External Noise Insulation - Code: 5', # 'Element: Internal - Code: 50, Sub-Element: Door Entry Handset - Code: 8', # 'Element: Internal - Code: 50, Sub-Element: Cold Water Storage Tank - Code: 6', # 'Element: Internal - Code: 50, Sub-Element: Programmable Heating - Code: 19', @@ -254,6 +322,4 @@ PEABODY_ELEMENT_MAP = { # 'Element: Internal - Code: 50, Sub-Element: Stairlift - Code: 22', # 'Element: Internal - Code: 50, Sub-Element: Primary Bathroom Location - Code: 17', # 'Element: Internal - Code: 50, Sub-Element: Disabled Hoist Tracking - Code: 7', -# 'Element: External - Code: 53, Sub-Element: Garage Type - Code: 44', -# 'Element: External - Code: 53, Sub-Element: Private Balcony Balustrade Material - Code: 45', # 'Element: Internal - Code: 50, Sub-Element: Disabled Facilities - Code: 26' diff --git a/backend/condition/tests/mapping/test_peabody_mapper.py b/backend/condition/tests/mapping/test_peabody_mapper.py index 7fad77f7..a975a308 100644 --- a/backend/condition/tests/mapping/test_peabody_mapper.py +++ b/backend/condition/tests/mapping/test_peabody_mapper.py @@ -71,8 +71,8 @@ def test_peabody_mapper_maps_property(): ), AssetCondition( uprn=1, - element=Element.PROPERTY, - aspect_type=AspectType.EXTERNAL_DECORATION, + element=Element.EXTERNAL_DECORATION, + aspect_type=AspectType.CONDITION, value="Normal", quantity=1, install_date=None, From 793ae8098f69062291490c4bbc33ee5202188743 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Mon, 26 Jan 2026 10:25:03 +0000 Subject: [PATCH 32/50] More peabody -> domain mapping --- backend/condition/domain/element.py | 8 +- .../domain/mapping/lbwf/lbwf_element_map.py | 4 +- .../mapping/peabody/peabody_element_map.py | 101 +++++++++++++----- 3 files changed, 84 insertions(+), 29 deletions(-) diff --git a/backend/condition/domain/element.py b/backend/condition/domain/element.py index fed5ab3b..5fbd35dc 100644 --- a/backend/condition/domain/element.py +++ b/backend/condition/domain/element.py @@ -15,6 +15,7 @@ class Element(str, Enum): ACCESSIBLE_HOUSING_REGISTER = "accessible_housing_register" ASBESTOS = "asbestos" QUALITY_STANDARD = "quality_standard" + CCU = "ccu" # ====================== # EXTERNAL – ROOF @@ -94,18 +95,22 @@ class Element(str, Enum): KITCHEN = "kitchen" KITCHEN_SPACE_LAYOUT = "kitchen_space_layout" TENANT_INSTALLED_KITCHEN = "tenant_installed_kitchen" + KITCHEN_EXTRACTOR_FAN = "kitchen_extractor_fan" # ====================== # INTERNAL – BATHROOM # ====================== BATHROOM = "bathroom" + SECONDARY_BATHROOM = "secondary_bathroom" + SECONDARY_TOILET = "secondary_toilet" + BATHROOM_EXTRACTOR_FAN = "bathroom_extractor_fan" # ====================== # INTERNAL – HEATING / WATER # ====================== + CENTRAL_HEATING = "central_heating" HEATING_BOILER = "heating_boiler" HEATING_DISTRIBUTION = "heating_distribution" - HEATING_EXTENT = "heating_extent" SECONDARY_HEATING = "secondary_heating" HOT_WATER_SYSTEM = "hot_water_system" COLD_WATER_STORAGE = "cold_water_storage" @@ -113,6 +118,7 @@ class Element(str, Enum): HEATING_SYSTEM = "heating_system" BOILER_FUEL = "boiler_fuel" WATER_HEATING = "water_heating" + PROGRAMMABLE_HEATING = "programmable_heating" # ====================== # INTERNAL – ELECTRICS / FIRE diff --git a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py index 047013f4..be8a50b2 100644 --- a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py +++ b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py @@ -61,8 +61,8 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { # INTERNAL – HEATING # ========================================================== "INTCHEXTNT": ElementMapping( - element=Element.HEATING_EXTENT, - aspect_type=AspectType.CONFIGURATION, + element=Element.CENTRAL_HEATING, + aspect_type=AspectType.EXTENT, ), "INTCHDIST": ElementMapping( element=Element.HEATING_DISTRIBUTION, diff --git a/backend/condition/domain/mapping/peabody/peabody_element_map.py b/backend/condition/domain/mapping/peabody/peabody_element_map.py index 81aa8b9e..8c29c60b 100644 --- a/backend/condition/domain/mapping/peabody/peabody_element_map.py +++ b/backend/condition/domain/mapping/peabody/peabody_element_map.py @@ -10,6 +10,16 @@ PEABODY_ELEMENT_MAP = { (100, 1): ElementMapping(element=Element.PROPERTY, aspect_type=AspectType.TYPE), # (100, 3): ElementMapping(element=Element.PROPERTY, aspect_type=AspectType.AGE), # (100, 14): ElementMapping(element="property", aspect_type="construction_type"), + (50, 2): ElementMapping( + element=Element.CARBON_MONOXIDE_DETECTION, aspect_type=AspectType.TYPE + ), + (50, 3): ElementMapping(element=Element.CCU, aspect_type=AspectType.TYPE), + (50, 11): ElementMapping( + element=Element.HEAT_DETECTION, aspect_type=AspectType.TYPE + ), + (50, 21): ElementMapping( + element=Element.SMOKE_DETECTION, aspect_type=AspectType.TYPE + ), # ========================================================== # EXTERNAL – WALLS # ========================================================== @@ -47,6 +57,9 @@ PEABODY_ELEMENT_MAP = { (120, 2): ElementMapping( element=Element.EXTERNAL_WALL, aspect_type=AspectType.FINISH ), + (120, 3): ElementMapping( + element=Element.PRIMARY_WALL, aspect_type=AspectType.INSULATION + ), # This code element code is actually "WALL" not "external wall" - correct? # ========================================================== # EXTERNAL – ROOFS # ========================================================== @@ -194,10 +207,36 @@ PEABODY_ELEMENT_MAP = { # ========================================================== # INTERNAL – BATHROOMS & KITCHENS # ========================================================== + (50, 1): ElementMapping( + element=Element.SECONDARY_TOILET, aspect_type=AspectType.PRESENCE + ), + (50, 9): ElementMapping( + element=Element.BATHROOM_EXTRACTOR_FAN, aspect_type=AspectType.PRESENCE + ), + (50, 9): ElementMapping(element=Element.KITCHEN, aspect_type=AspectType.TYPE), + (50, 10): ElementMapping( + element=Element.KITCHEN_EXTRACTOR_FAN, aspect_type=AspectType.PRESENCE + ), + (50, 13): ElementMapping( + element=Element.KITCHEN_SPACE_LAYOUT, aspect_type=AspectType.ADEQUACY + ), + (50, 17): ElementMapping(element=Element.BATHRROM, aspect_type=AspectType.LOCATION), + (50, 18): ElementMapping( + element=Element.BATHROOM, aspect_type=AspectType.TYPE + ), # Actually "Primary bathroom type" - ok like this? + (50, 20): ElementMapping( + element=Element.BATHROOM, aspect_type=AspectType.TYPE, element_instance=2 + ), # Actually "Secondary bathroom type" - ok like this? (160, 1): ElementMapping(element=Element.KITCHEN, aspect_type=AspectType.CONDITION), + (160, 2): ElementMapping( + element=Element.KITCHEN_SPACE_LAYOUT, aspect_type=AspectType.ADEQUACY + ), (190, 1): ElementMapping( element=Element.BATHROOM, aspect_type=AspectType.CONDITION ), + (190, 2): ElementMapping( + element=Element.SECONDARY_TOILET, aspect_type=AspectType.TYPE + ), # ========================================================== # COMMUNAL SYSTEMS # ========================================================== @@ -220,6 +259,42 @@ PEABODY_ELEMENT_MAP = { element=Element.COMMUNAL_FLOOR_COVERING, aspect_type=AspectType.MATERIAL ), # ========================================================== + # INTERNAL – HEATING + # ========================================================== + (50, 4): ElementMapping( + element=Element.HEATING_BOILER, aspect_type=AspectType.PRESENCE + ), # This is actually "Central heating boiler" - ok like this? + (50, 5): ElementMapping( + element=Element.CENTRAL_HEATING, aspect_type=AspectType.EXTENT + ), + (50, 6): ElementMapping( + element=Element.COLD_WATER_STORAGE, aspect_type=AspectType.PRESENCE + ), + (50, 12): ElementMapping( + element=Element.HEATING_DISTRIBUTION, aspect_type=AspectType.TYPE + ), + (50, 19): ElementMapping( + element=Element.PROGRAMMABLE_HEATING, aspect_type=AspectType.TYPE + ), + (50, 25): ElementMapping( + element=Element.HEATING_BOILER, aspect_type=AspectType.TYPE + ), + (170, 1): ElementMapping( + element=Element.HEATING_BOILER, aspect_type=AspectType.TYPE + ), # Duplicate of (50,25) - correct? + (170, 2): ElementMapping( + element=Element.HEATING_DISTRIBUTION, aspect_type=AspectType.TYPE + ), # Duplicate of (50,12) - correct? + (170, 3): ElementMapping( + element=Element.SECONDARY_HEATING, aspect_type=AspectType.TYPE + ), + (170, 4): ElementMapping( + element=Element.COLD_WATER_STORAGE, aspect_type=AspectType.TYPE + ), + (170, 5): ElementMapping( + element=Element.HOT_WATER_SYSTEM, aspect_type=AspectType.TYPE + ), + # ========================================================== # HHSRS # ========================================================== (54, 1): ElementMapping( @@ -238,36 +313,16 @@ PEABODY_ELEMENT_MAP = { # unhandled -# 'Element: HEATING - Code: 170, Sub-Element: Boiler - Code: 1', -# 'Element: HEATING - Code: 170, Sub-Element: Heating Distribution - Code: 2', # 'Element: ELECTRICS - Code: 180, Sub-Element: Wiring - Code: 1', # 'Element: ELECTRICS - Code: 180, Sub-Element: Consumer Unit - Code: 2', # 'Element: ELECTRICS - Code: 180, Sub-Element: Smoke Detectors - Code: 3', -# 'Element: KITCHEN - Code: 160, Sub-Element: Kitchen space and layout - Code: 2', -# 'Element: HEATING - Code: 170, Sub-Element: Secondary Heating - Code: 3', -# 'Element: BATHROOM - Code: 190, Sub-Element: Secondary Toilet - Code: 2', # 'Element: ELECTRICS - Code: 180, Sub-Element: Carbon Monoxide Alarms - Code: 4', -# 'Element: HEATING - Code: 170, Sub-Element: Hot Water - Code: 5', -# 'Element: HEATING - Code: 170, Sub-Element: Cold Water - Code: 4', -# 'Element: WALLS - Code: 120, Sub-Element: Wall Insulation - Code: 3', -# 'Element: Internal - Code: 50, Sub-Element: Additional WC - Code: 1', -# 'Element: Internal - Code: 50, Sub-Element: Carbon Monoxide Detector Type - Code: 2', -# 'Element: Internal - Code: 50, Sub-Element: CCU - Code: 3', -# 'Element: Internal - Code: 50, Sub-Element: Central Heating Boiler - Code: 4', -# 'Element: Internal - Code: 50, Sub-Element: Extractor Fan Bathroom - Code: 9', -# 'Element: Internal - Code: 50, Sub-Element: Extractor Fan Kitchen - Code: 10', -# 'Element: Internal - Code: 50, Sub-Element: Heat Detector Type - Code: 11', -# 'Element: Internal - Code: 50, Sub-Element: Kitchen Type - Code: 14', -# 'Element: Internal - Code: 50, Sub-Element: Primary Bathroom Type - Code: 18', -# 'Element: Internal - Code: 50, Sub-Element: Smoke Detector Type - Code: 21', # 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Lifts - Code: 5', # 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Kitchen - Code: 7', # 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Bathroom - Code: 8', # 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Toilets - Code: 9', # 'Element: Internal - Code: 50, Sub-Element: Wiring - Code: 24', # 'Element: PASSENGER LIFTS - Code: 210, Sub-Element: Lift - Code: 2', -# 'Element: Internal - Code: 50, Sub-Element: Heating Distribution Type - Code: 12', -# 'Element: Internal - Code: 50, Sub-Element: Secondary Bathroom Type - Code: 20', # 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Gates - Code: 10', # 'Element: GENERAL - Code: 100, Sub-Element: Property Age Band - Code: 3', # 'Element: GENERAL - Code: 100, Sub-Element: Construction Type - Code: 14', @@ -311,15 +366,9 @@ PEABODY_ELEMENT_MAP = { # 'Element: Communal - Code: 51, Sub-Element: Common Wet Riser - Code: 39', # 'Element: Communal - Code: 51, Sub-Element: Common Refuse Chute - Code: 29', # 'Element: Internal - Code: 50, Sub-Element: Party Wall Fire Break - Code: 16', -# 'Element: Internal - Code: 50, Sub-Element: Boiler Type - Code: 25', # 'Element: Communal - Code: 51, Sub-Element: Common Cirulation Space - Code: 12', # 'Element: Internal - Code: 50, Sub-Element: Door Entry Handset - Code: 8', -# 'Element: Internal - Code: 50, Sub-Element: Cold Water Storage Tank - Code: 6', -# 'Element: Internal - Code: 50, Sub-Element: Programmable Heating - Code: 19', -# 'Element: Internal - Code: 50, Sub-Element: Central Heating Extent - Code: 5', -# 'Element: Internal - Code: 50, Sub-Element: Kitchen Space & Layout - Code: 13', # 'Element: Internal - Code: 50, Sub-Element: Loft Insulation - Code: 15', # 'Element: Internal - Code: 50, Sub-Element: Stairlift - Code: 22', -# 'Element: Internal - Code: 50, Sub-Element: Primary Bathroom Location - Code: 17', # 'Element: Internal - Code: 50, Sub-Element: Disabled Hoist Tracking - Code: 7', # 'Element: Internal - Code: 50, Sub-Element: Disabled Facilities - Code: 26' From 3da9a643e0cdd014adf0bf633e69e75fbccdc443 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Mon, 26 Jan 2026 12:37:57 +0000 Subject: [PATCH 33/50] more peabody mappings --- backend/condition/domain/aspect_type.py | 4 + backend/condition/domain/element.py | 25 +++- .../mapping/peabody/peabody_element_map.py | 111 +++++++++++++----- 3 files changed, 110 insertions(+), 30 deletions(-) diff --git a/backend/condition/domain/aspect_type.py b/backend/condition/domain/aspect_type.py index 94522b03..2dc2be58 100644 --- a/backend/condition/domain/aspect_type.py +++ b/backend/condition/domain/aspect_type.py @@ -29,3 +29,7 @@ class AspectType(str, Enum): FIRE_RATING = "fire_rating" EXTERNAL_DECORATION = "external_decoration" WORK_REQUIRED = "work_required" + AGE_BAND = "age_band" + CONSTRUCTION_TYPE = "construction_type" + CLASSIFICATION = "classification" + SYSTEM = "system" diff --git a/backend/condition/domain/element.py b/backend/condition/domain/element.py index 5fbd35dc..b146d09b 100644 --- a/backend/condition/domain/element.py +++ b/backend/condition/domain/element.py @@ -16,6 +16,8 @@ class Element(str, Enum): ASBESTOS = "asbestos" QUALITY_STANDARD = "quality_standard" CCU = "ccu" + PASSENGER_LIFT = "passenger_lift" + STAIRLIFT = "stairlift" # ====================== # EXTERNAL – ROOF @@ -130,21 +132,40 @@ class Element(str, Enum): CARBON_MONOXIDE_DETECTION = "carbon_monoxide_detection" FIRE_DOOR_RATING = "fire_door_rating" FIRE_RISK_ASSESSMENT = "fire" + INTERNAL_WIRING = ( + "internal_wiring" # Is this definitely different from ELECTRICAL_WIRING? + ) # ====================== - # COMMUNAL SYSTEMS + # COMMUNAL # ====================== COMMUNAL_HEATING = "communal_heating" COMMUNAL_BOILER = "communal_boiler" COMMUNAL_ELECTRICS = "communal_electrics" COMMUNAL_FIRE_ALARM = "communal_fire_alarm" COMMUNAL_EMERGENCY_LIGHTING = "communal_emergency_lighting" - COMMUNAL_LIFT = "communal_lift" COMMUNAL_DOOR_ENTRY = "communal_door_entry" COMMUNAL_CCTV = "communal_cctv" COMMUNAL_BIN_STORE = "communal_bin_store" + COMMUNAL_BIN_STORE_DOORS = "communal_bin_store_doors" + COMMUNAL_BIN_STORE_WALLS = "communal_bin_store_wall" COMMUNAL_REFUSE_CHUTE = "communal_refuse_chute" COMMUNAL_FLOOR_COVERING = "communal_floor_covering" + COMMUNAL_KITCHEN = "communal_kitchen" + COMMUNAL_BATHROOM = "communal_bathroom" + COMMUNAL_TOILETS = "communal_toilets" + COMMUNAL_GATES = "communal_gates" + COMMUNAL_LIFT = "communal_lift" + COMMUNAL_PASSENGER_LIFT = "communal_passenger_lift" + COMMUNAL_BALCONY_WALKWAY = "communal_balcony_walkway" + COMMUNAL_PRIMARY_ENTRANCE = "communal_primary_entrance" + COMMUNAL_INTERNAL_DECORATIONS = "communal_internal_decorations" + COMMUNAL_INTERNAL_FLOOR = "communal_internal_floor" + COMMUNAL_WALKWAYS = "communal_walkways" + COMMUNAL_EXTERNAL_DOORS = "communal_external_doors" + COMMUNAL_STAIRS = "communal_stairs" + COMMUNAL_AERIAL = "communal_aerial" + COMMUNAL_AOV = "communal_aov" # ========================================================== # HHSRS – ALL 29 HAZARDS diff --git a/backend/condition/domain/mapping/peabody/peabody_element_map.py b/backend/condition/domain/mapping/peabody/peabody_element_map.py index 8c29c60b..08e63568 100644 --- a/backend/condition/domain/mapping/peabody/peabody_element_map.py +++ b/backend/condition/domain/mapping/peabody/peabody_element_map.py @@ -20,6 +20,19 @@ PEABODY_ELEMENT_MAP = { (50, 21): ElementMapping( element=Element.SMOKE_DETECTION, aspect_type=AspectType.TYPE ), + (50, 22): ElementMapping( + element=Element.STAIRLIFT, aspect_type=AspectType.PRESENCE + ), + (100, 3): ElementMapping(element=Element.PROPERTY, aspect_type=AspectType.AGE_BAND), + (100, 14): ElementMapping( + element=Element.PROPERTY, aspect_type=AspectType.CONSTRUCTION_TYPE + ), + (100, 16): ElementMapping( + element=Element.PROPERTY, aspect_type=AspectType.CLASSIFICATION + ), + (210, 2): ElementMapping( + element=Element.PASSENGER_LIFT, aspect_type=AspectType.TYPE + ), # ========================================================== # EXTERNAL – WALLS # ========================================================== @@ -238,8 +251,47 @@ PEABODY_ELEMENT_MAP = { element=Element.SECONDARY_TOILET, aspect_type=AspectType.TYPE ), # ========================================================== - # COMMUNAL SYSTEMS + # COMMUNAL # ========================================================== + (51, 1): ElementMapping( + element=Element.COMMUNAL_AERIAL, aspect_type=AspectType.PRESENCE + ), + (51, 2): ElementMapping( + element=Element.COMMUNAL_AOV, aspect_type=AspectType.PRESENCE + ), + (51, 3): ElementMapping( + element=Element.COMMUNAL_BALCONY_WALKWAY, aspect_type=AspectType.PRESENCE + ), + (51, 5): ElementMapping( + element=Element.COMMUNAL_BIN_STORE_DOORS, aspect_type=AspectType.PRESENCE + ), + (51, 7): ElementMapping( + element=Element.COMMUNAL_BIN_STORE_WALLS, aspect_type=AspectType.MATERIAL + ), + (51, 14): ElementMapping( + element=Element.COMMUNAL_DOOR_ENTRY, aspect_type=AspectType.SYSTEM + ), + (51, 17): ElementMapping( + element=Element.COMMUNAL_EXTERNAL_DOORS, aspect_type=AspectType.MATERIAL + ), + (51, 20): ElementMapping( + element=Element.COMMUNAL_INTERNAL_DECORATIONS, aspect_type=AspectType.PRESENCE + ), + (51, 22): ElementMapping( + element=Element.COMMUNAL_INTERNAL_FLOOR, aspect_type=AspectType.FINISH + ), + (51, 27): ElementMapping( + element=Element.COMMUNAL_PASSENGER_LIFT, aspect_type=AspectType.TYPE + ), + (51, 28): ElementMapping( + element=Element.COMMUNAL_PRIMARY_ENTRANCE, aspect_type=AspectType.MATERIAL + ), + (51, 32): ElementMapping( + element=Element.COMMUNAL_STAIRS, aspect_type=AspectType.FINISH + ), + (51, 36): ElementMapping( + element=Element.COMMUNAL_WALKWAYS, aspect_type=AspectType.FINISH + ), (200, 1): ElementMapping( element=Element.COMMUNAL_BOILER, aspect_type=AspectType.TYPE ), @@ -258,6 +310,18 @@ PEABODY_ELEMENT_MAP = { (200, 6): ElementMapping( element=Element.COMMUNAL_FLOOR_COVERING, aspect_type=AspectType.MATERIAL ), + (200, 7): ElementMapping( + element=Element.COMMUNAL_KITCHEN, aspect_type=AspectType.TYPE + ), + (200, 8): ElementMapping( + element=Element.COMMUNAL_BATHROOM, aspect_type=AspectType.TYPE + ), + (200, 9): ElementMapping( + element=Element.COMMUNAL_TOILETS, aspect_type=AspectType.TYPE + ), + (200, 10): ElementMapping( + element=Element.COMMUNAL_GATES, aspect_type=AspectType.TYPE + ), # ========================================================== # INTERNAL – HEATING # ========================================================== @@ -295,6 +359,24 @@ PEABODY_ELEMENT_MAP = { element=Element.HOT_WATER_SYSTEM, aspect_type=AspectType.TYPE ), # ========================================================== + # ELECTRICS + # ========================================================== + (50, 24): ElementMapping( + element=Element.INTERNAL_WIRING, aspect_type=AspectType.MATERIAL + ), + (180, 1): ElementMapping( + element=Element.ELECTRICAL_WIRING, aspect_type=AspectType.WORK_REQUIRED + ), # Not certain about the AspectType - only example in the sample data is "Full Rewire" + (180, 2): ElementMapping( + element=Element.CONSUMER_UNIT, aspect_type=AspectType.TYPE + ), + (180, 3): ElementMapping( + element=Element.SMOKE_DETECTION, aspect_type=AspectType.TYPE + ), # Duplicate of (50, 21) - correct? + (180, 4): ElementMapping( + element=Element.CARBON_MONOXIDE_DETECTION, aspect_type=AspectType.TYPE + ), # Duplicate of (50, 2) - correct? + # ========================================================== # HHSRS # ========================================================== (54, 1): ElementMapping( @@ -313,31 +395,6 @@ PEABODY_ELEMENT_MAP = { # unhandled -# 'Element: ELECTRICS - Code: 180, Sub-Element: Wiring - Code: 1', -# 'Element: ELECTRICS - Code: 180, Sub-Element: Consumer Unit - Code: 2', -# 'Element: ELECTRICS - Code: 180, Sub-Element: Smoke Detectors - Code: 3', -# 'Element: ELECTRICS - Code: 180, Sub-Element: Carbon Monoxide Alarms - Code: 4', -# 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Lifts - Code: 5', -# 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Kitchen - Code: 7', -# 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Bathroom - Code: 8', -# 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Toilets - Code: 9', -# 'Element: Internal - Code: 50, Sub-Element: Wiring - Code: 24', -# 'Element: PASSENGER LIFTS - Code: 210, Sub-Element: Lift - Code: 2', -# 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Gates - Code: 10', -# 'Element: GENERAL - Code: 100, Sub-Element: Property Age Band - Code: 3', -# 'Element: GENERAL - Code: 100, Sub-Element: Construction Type - Code: 14', -# 'Element: GENERAL - Code: 100, Sub-Element: Classification - Code: 16', -# 'Element: Communal - Code: 51, Sub-Element: Common Balcony/Walkway - Code: 3', -# 'Element: Communal - Code: 51, Sub-Element: Common Bin Store Doors - Code: 5', -# 'Element: Communal - Code: 51, Sub-Element: Common Bin Store Walls - Code: 7', -# 'Element: Communal - Code: 51, Sub-Element: Common Primary Entrance Material - Code: 28', -# 'Element: Communal - Code: 51, Sub-Element: Common Internal Decorations - Code: 20', -# 'Element: Communal - Code: 51, Sub-Element: Common Internal Floor Finish - Code: 22', -# 'Element: Communal - Code: 51, Sub-Element: Common Walkways Finish - Code: 36', -# 'Element: Communal - Code: 51, Sub-Element: Common External Doors Other - Code: 17', -# 'Element: Communal - Code: 51, Sub-Element: Common Stair Finish - Code: 32', -# 'Element: Communal - Code: 51, Sub-Element: Common Aerial - Code: 1', -# 'Element: Communal - Code: 51, Sub-Element: Common AOV - Code: 2', # 'Element: Communal - Code: 51, Sub-Element: Common Door Entry System - Code: 14', # 'Element: Communal - Code: 51, Sub-Element: Common Fire Alarm - Code: 19', # 'Element: Communal - Code: 51, Sub-Element: Common Internal Doors - Code: 21', @@ -351,7 +408,6 @@ PEABODY_ELEMENT_MAP = { # 'Element: Communal - Code: 51, Sub-Element: Common Secondary Entrance Material - Code: 30', # 'Element: Communal - Code: 51, Sub-Element: Common Warden Call System - Code: 37', # 'Element: Communal - Code: 51, Sub-Element: Common Boiler - Code: 9', -# 'Element: Communal - Code: 51, Sub-Element: Common Passenger Lift - Code: 27', # 'Element: Communal - Code: 51, Sub-Element: Common Store Doors - Code: 33', # 'Element: Communal - Code: 51, Sub-Element: Common BMS - Code: 8', # 'Element: Communal - Code: 51, Sub-Element: Common Booster Pump - Code: 10', @@ -369,6 +425,5 @@ PEABODY_ELEMENT_MAP = { # 'Element: Communal - Code: 51, Sub-Element: Common Cirulation Space - Code: 12', # 'Element: Internal - Code: 50, Sub-Element: Door Entry Handset - Code: 8', # 'Element: Internal - Code: 50, Sub-Element: Loft Insulation - Code: 15', -# 'Element: Internal - Code: 50, Sub-Element: Stairlift - Code: 22', # 'Element: Internal - Code: 50, Sub-Element: Disabled Hoist Tracking - Code: 7', # 'Element: Internal - Code: 50, Sub-Element: Disabled Facilities - Code: 26' From 1bd7117097682faafda932d289fa24c32487a2cd Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Mon, 26 Jan 2026 14:27:52 +0000 Subject: [PATCH 34/50] final peabody element mappings --- backend/condition/domain/element.py | 23 ++- .../mapping/peabody/peabody_element_map.py | 140 +++++++++++++----- 2 files changed, 123 insertions(+), 40 deletions(-) diff --git a/backend/condition/domain/element.py b/backend/condition/domain/element.py index b146d09b..72687aed 100644 --- a/backend/condition/domain/element.py +++ b/backend/condition/domain/element.py @@ -18,6 +18,8 @@ class Element(str, Enum): CCU = "ccu" PASSENGER_LIFT = "passenger_lift" STAIRLIFT = "stairlift" + DISABLED_HOIST_TRACKING = "disabled_hoist_tracking" + DISABLED_FACILITIES = "disabled_facilities" # ====================== # EXTERNAL – ROOF @@ -47,6 +49,7 @@ class Element(str, Enum): CLADDING = "cladding" SPANDREL_PANELS = "spandrel_panels" GARAGE_WALLS = "garage_walls" + PARTY_WALL_FIRE_BREAK = "party_wall_fire_break" # ====================== # EXTERNAL – WINDOWS @@ -68,6 +71,7 @@ class Element(str, Enum): BLOCK_ENTRANCE_DOOR = "block_entrance_door" LINTEL = "lintel" PATIO_FRENCH_DOOR = "patio_french_door" + DOOR_ENTRY_HANDSET = "door_entry_handset" # ====================== # EXTERNAL – AREAS @@ -149,6 +153,7 @@ class Element(str, Enum): COMMUNAL_BIN_STORE = "communal_bin_store" COMMUNAL_BIN_STORE_DOORS = "communal_bin_store_doors" COMMUNAL_BIN_STORE_WALLS = "communal_bin_store_wall" + COMMUNAL_BIN_STORE_ROOF = "communal_bin_store_roof" COMMUNAL_REFUSE_CHUTE = "communal_refuse_chute" COMMUNAL_FLOOR_COVERING = "communal_floor_covering" COMMUNAL_KITCHEN = "communal_kitchen" @@ -158,7 +163,7 @@ class Element(str, Enum): COMMUNAL_LIFT = "communal_lift" COMMUNAL_PASSENGER_LIFT = "communal_passenger_lift" COMMUNAL_BALCONY_WALKWAY = "communal_balcony_walkway" - COMMUNAL_PRIMARY_ENTRANCE = "communal_primary_entrance" + COMMUNAL_ENTRANCE = "communal_entrance" COMMUNAL_INTERNAL_DECORATIONS = "communal_internal_decorations" COMMUNAL_INTERNAL_FLOOR = "communal_internal_floor" COMMUNAL_WALKWAYS = "communal_walkways" @@ -166,6 +171,22 @@ class Element(str, Enum): COMMUNAL_STAIRS = "communal_stairs" COMMUNAL_AERIAL = "communal_aerial" COMMUNAL_AOV = "communal_aov" + COMMUNAL_INTERNAL_DOORS = "communal_internal_doors" + COMMUNAL_LATERAL_MAINS = "communal_lateral_mains" + COMMUNAL_LIGHTING = "communal_lighting" + COMMUNAL_LIGHTING_CONDUCTOR = "communal_lighting_conductor" + COMMUNAL_STORE_ROOF = "communal_store_roof" + COMMUNAL_STORE_WALLS = "communal_store_walls" + COMMUNAL_STORE_DOORS = "communal_store_doors" + COMMUNAL_WARDEN_CALL_SYSTEM = "communal_warden_call_system" + COMMUNAL_BMS = "communal_bms" + COMMUNAL_BOOSTER_PUMP = "communal_booster_pump" + COMMUNAL_DRY_RISER = "communal_dry_riser" + COMMUNAL_WET_RISER = "communal_wet_riser" + COMMUNAL_COLD_WATER_STORAGE = "communal_cold_water_storage" + COMMUNAL_SPRINKLER = "communal_sprinkler" + COMMUNAL_PLUG_SOCKETS = "communal_plug_sockets" + COMMUNAL_CIRCULATION_SPACE = "communal_circulation_space" # ========================================================== # HHSRS – ALL 29 HAZARDS diff --git a/backend/condition/domain/mapping/peabody/peabody_element_map.py b/backend/condition/domain/mapping/peabody/peabody_element_map.py index 08e63568..7a266a9f 100644 --- a/backend/condition/domain/mapping/peabody/peabody_element_map.py +++ b/backend/condition/domain/mapping/peabody/peabody_element_map.py @@ -14,6 +14,9 @@ PEABODY_ELEMENT_MAP = { element=Element.CARBON_MONOXIDE_DETECTION, aspect_type=AspectType.TYPE ), (50, 3): ElementMapping(element=Element.CCU, aspect_type=AspectType.TYPE), + (50, 7): ElementMapping( + element=Element.DISABLED_HOIST_TRACKING, aspect_type=AspectType.PRESENCE + ), (50, 11): ElementMapping( element=Element.HEAT_DETECTION, aspect_type=AspectType.TYPE ), @@ -23,6 +26,9 @@ PEABODY_ELEMENT_MAP = { (50, 22): ElementMapping( element=Element.STAIRLIFT, aspect_type=AspectType.PRESENCE ), + (50, 26): ElementMapping( + element=Element.DISABLED_FACILITIES, aspect_type=AspectType.TYPE + ), (100, 3): ElementMapping(element=Element.PROPERTY, aspect_type=AspectType.AGE_BAND), (100, 14): ElementMapping( element=Element.PROPERTY, aspect_type=AspectType.CONSTRUCTION_TYPE @@ -36,6 +42,9 @@ PEABODY_ELEMENT_MAP = { # ========================================================== # EXTERNAL – WALLS # ========================================================== + (50, 16): ElementMapping( + element=Element.PARTY_WALL_FIRE_BREAK, aspect_type=AspectType.PRESENCE + ), (53, 1): ElementMapping( element=Element.BOUNDARY_WALLS, aspect_type=AspectType.PRESENCE ), @@ -76,6 +85,9 @@ PEABODY_ELEMENT_MAP = { # ========================================================== # EXTERNAL – ROOFS # ========================================================== + (50, 15): ElementMapping( + element=Element.LOFT_INSULATION, aspect_type=AspectType.TYPE + ), (53, 2): ElementMapping(element=Element.CHIMNEY, aspect_type=AspectType.PRESENCE), (53, 6): ElementMapping( element=Element.FASCIA_SOFFIT_BARGEBOARDS, aspect_type=AspectType.MATERIAL @@ -116,6 +128,9 @@ PEABODY_ELEMENT_MAP = { # ========================================================== # EXTERNAL – DOORS & WINDOWS # ========================================================== + (50, 8): ElementMapping( + element=Element.DOOR_ENTRY_HANDSET, aspect_type=AspectType.PRESENCE + ), (53, 8): ElementMapping( element=Element.FRONT_DOOR, aspect_type=AspectType.MATERIAL ), @@ -262,39 +277,121 @@ PEABODY_ELEMENT_MAP = { (51, 3): ElementMapping( element=Element.COMMUNAL_BALCONY_WALKWAY, aspect_type=AspectType.PRESENCE ), + (51, 4): ElementMapping( + element=Element.COMMUNAL_BATHROOM, aspect_type=AspectType.TYPE + ), (51, 5): ElementMapping( element=Element.COMMUNAL_BIN_STORE_DOORS, aspect_type=AspectType.PRESENCE ), + (51, 6): ElementMapping( + element=Element.COMMUNAL_BIN_STORE_ROOF, aspect_type=AspectType.PRESENCE + ), (51, 7): ElementMapping( element=Element.COMMUNAL_BIN_STORE_WALLS, aspect_type=AspectType.MATERIAL ), + (51, 8): ElementMapping( + element=Element.COMMUNAL_BMS, aspect_type=AspectType.PRESENCE + ), + (51, 9): ElementMapping( + element=Element.COMMUNAL_BOILER, aspect_type=AspectType.TYPE + ), + (51, 10): ElementMapping( + element=Element.COMMUNAL_BOOSTER_PUMP, aspect_type=AspectType.PRESENCE + ), + (51, 11): ElementMapping( + element=Element.COMMUNAL_CCTV, aspect_type=AspectType.PRESENCE + ), + (51, 12): ElementMapping( + element=Element.COMMUNAL_CIRCULATION_SPACE, aspect_type=AspectType.ADEQUACY + ), + (51, 13): ElementMapping( + element=Element.COMMUNAL_COLD_WATER_STORAGE, aspect_type=AspectType.PRESENCE + ), (51, 14): ElementMapping( element=Element.COMMUNAL_DOOR_ENTRY, aspect_type=AspectType.SYSTEM ), + (51, 15): ElementMapping( + element=Element.COMMUNAL_DRY_RISER, aspect_type=AspectType.PRESENCE + ), + (51, 16): ElementMapping( + element=Element.COMMUNAL_EMERGENCY_LIGHTING, aspect_type=AspectType.PRESENCE + ), (51, 17): ElementMapping( element=Element.COMMUNAL_EXTERNAL_DOORS, aspect_type=AspectType.MATERIAL ), + (51, 19): ElementMapping( + element=Element.COMMUNAL_FIRE_ALARM, aspect_type=AspectType.TYPE + ), (51, 20): ElementMapping( element=Element.COMMUNAL_INTERNAL_DECORATIONS, aspect_type=AspectType.PRESENCE ), + (51, 21): ElementMapping( + element=Element.COMMUNAL_INTERNAL_DOORS, aspect_type=AspectType.MATERIAL + ), (51, 22): ElementMapping( element=Element.COMMUNAL_INTERNAL_FLOOR, aspect_type=AspectType.FINISH ), + (51, 23): ElementMapping( + element=Element.COMMUNAL_KITCHEN, aspect_type=AspectType.TYPE + ), + (51, 24): ElementMapping( + element=Element.COMMUNAL_LATERAL_MAINS, aspect_type=AspectType.PRESENCE + ), + (51, 25): ElementMapping( + element=Element.COMMUNAL_LIGHTING, aspect_type=AspectType.PRESENCE + ), + (51, 26): ElementMapping( + element=Element.COMMUNAL_LIGHTING_CONDUCTOR, aspect_type=AspectType.PRESENCE + ), (51, 27): ElementMapping( element=Element.COMMUNAL_PASSENGER_LIFT, aspect_type=AspectType.TYPE ), (51, 28): ElementMapping( - element=Element.COMMUNAL_PRIMARY_ENTRANCE, aspect_type=AspectType.MATERIAL + element=Element.COMMUNAL_ENTRANCE, + aspect_type=AspectType.MATERIAL, + element_instance=1, + ), + (51, 30): ElementMapping( + element=Element.COMMUNAL_ENTRANCE, + aspect_type=AspectType.FINISH, + element_instance=2, + ), + (51, 14): ElementMapping( + element=Element.COMMUNAL_SPRINKLER, aspect_type=AspectType.PRESENCE + ), + (51, 29): ElementMapping( + element=Element.COMMUNAL_REFUSE_CHUTE, aspect_type=AspectType.PRESENCE ), (51, 32): ElementMapping( element=Element.COMMUNAL_STAIRS, aspect_type=AspectType.FINISH ), + (51, 33): ElementMapping( + element=Element.COMMUNAL_STORE_DOORS, aspect_type=AspectType.MATERIAL + ), + (51, 34): ElementMapping( + element=Element.COMMUNAL_STORE_ROOF, aspect_type=AspectType.MATERIAL + ), + (51, 35): ElementMapping( + element=Element.COMMUNAL_STORE_WALLS, aspect_type=AspectType.MATERIAL + ), (51, 36): ElementMapping( element=Element.COMMUNAL_WALKWAYS, aspect_type=AspectType.FINISH ), + (51, 37): ElementMapping( + element=Element.COMMUNAL_WARDEN_CALL_SYSTEM, aspect_type=AspectType.PRESENCE + ), + (51, 38): ElementMapping( + element=Element.COMMUNAL_TOILETS, aspect_type=AspectType.TYPE + ), + (51, 39): ElementMapping( + element=Element.COMMUNAL_WET_RISER, aspect_type=AspectType.PRESENCE + ), + (51, 40): ElementMapping( + element=Element.COMMUNAL_PLUG_SOCKETS, aspect_type=AspectType.PRESENCE + ), (200, 1): ElementMapping( element=Element.COMMUNAL_BOILER, aspect_type=AspectType.TYPE - ), + ), # Duplicate of (51, 9) - correct? (200, 2): ElementMapping( element=Element.COMMUNAL_HEATING, aspect_type=AspectType.TYPE ), @@ -315,10 +412,10 @@ PEABODY_ELEMENT_MAP = { ), (200, 8): ElementMapping( element=Element.COMMUNAL_BATHROOM, aspect_type=AspectType.TYPE - ), + ), # Duplicate of (51, 4) - correct? (200, 9): ElementMapping( element=Element.COMMUNAL_TOILETS, aspect_type=AspectType.TYPE - ), + ), # Duplicate of (51, 38) - correct? (200, 10): ElementMapping( element=Element.COMMUNAL_GATES, aspect_type=AspectType.TYPE ), @@ -392,38 +489,3 @@ PEABODY_ELEMENT_MAP = { element=Element.HHSRS_STRUCTURAL_COLLAPSE, aspect_type=AspectType.RISK ), } - - -# unhandled -# 'Element: Communal - Code: 51, Sub-Element: Common Door Entry System - Code: 14', -# 'Element: Communal - Code: 51, Sub-Element: Common Fire Alarm - Code: 19', -# 'Element: Communal - Code: 51, Sub-Element: Common Internal Doors - Code: 21', -# 'Element: Communal - Code: 51, Sub-Element: Common Emergency Lighting - Code: 16', -# 'Element: Communal - Code: 51, Sub-Element: Common Lateral Mains - Code: 24', -# 'Element: Communal - Code: 51, Sub-Element: Common Lighting - Code: 25', -# 'Element: Communal - Code: 51, Sub-Element: Common Store Roof - Code: 34', -# 'Element: Communal - Code: 51, Sub-Element: Common Store Walls - Code: 35', -# 'Element: Communal - Code: 51, Sub-Element: Common CCTV - Code: 11', -# 'Element: Communal - Code: 51, Sub-Element: Common Kitchen - Code: 23', -# 'Element: Communal - Code: 51, Sub-Element: Common Secondary Entrance Material - Code: 30', -# 'Element: Communal - Code: 51, Sub-Element: Common Warden Call System - Code: 37', -# 'Element: Communal - Code: 51, Sub-Element: Common Boiler - Code: 9', -# 'Element: Communal - Code: 51, Sub-Element: Common Store Doors - Code: 33', -# 'Element: Communal - Code: 51, Sub-Element: Common BMS - Code: 8', -# 'Element: Communal - Code: 51, Sub-Element: Common Booster Pump - Code: 10', -# 'Element: Communal - Code: 51, Sub-Element: Common Dry Riser - Code: 15', -# 'Element: Communal - Code: 51, Sub-Element: Common Lightning Conductor - Code: 26', -# 'Element: Communal - Code: 51, Sub-Element: Common Bin Store Roof - Code: 6', -# 'Element: Communal - Code: 51, Sub-Element: Common Bathroom - Code: 4', -# 'Element: Communal - Code: 51, Sub-Element: Common WC - Code: 38', -# 'Element: Communal - Code: 51, Sub-Element: Common Cold Water Storage Tank - Code: 13', -# 'Element: Communal - Code: 51, Sub-Element: Common Sprinker - Code: 31', -# 'Element: Communal - Code: 51, Sub-Element: Communal Plug Sockets - Code: 40', -# 'Element: Communal - Code: 51, Sub-Element: Common Wet Riser - Code: 39', -# 'Element: Communal - Code: 51, Sub-Element: Common Refuse Chute - Code: 29', -# 'Element: Internal - Code: 50, Sub-Element: Party Wall Fire Break - Code: 16', -# 'Element: Communal - Code: 51, Sub-Element: Common Cirulation Space - Code: 12', -# 'Element: Internal - Code: 50, Sub-Element: Door Entry Handset - Code: 8', -# 'Element: Internal - Code: 50, Sub-Element: Loft Insulation - Code: 15', -# 'Element: Internal - Code: 50, Sub-Element: Disabled Hoist Tracking - Code: 7', -# 'Element: Internal - Code: 50, Sub-Element: Disabled Facilities - Code: 26' From eaf793011b43b4f0655a45d8526401a39bf0f114 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Mon, 26 Jan 2026 16:20:21 +0000 Subject: [PATCH 35/50] remaining lbwf mappings --- backend/condition/domain/element.py | 38 +- .../domain/mapping/lbwf/lbwf_element_map.py | 275 ++++++++--- .../mapping/peabody/peabody_element_map.py | 429 ++++++++++++------ 3 files changed, 548 insertions(+), 194 deletions(-) diff --git a/backend/condition/domain/element.py b/backend/condition/domain/element.py index 72687aed..f78f2d52 100644 --- a/backend/condition/domain/element.py +++ b/backend/condition/domain/element.py @@ -11,6 +11,7 @@ class Element(str, Enum): PROPERTY_CLASSIFICATION = "property_classification" PROPERTY_AGE_BAND = "property_age_band" STOREY_COUNT = "storey_count" + FLOOR_LEVEL = "floor_level" FLOOR_LEVEL_FRONT_DOOR = "floor_level_front_door" ACCESSIBLE_HOUSING_REGISTER = "accessible_housing_register" ASBESTOS = "asbestos" @@ -20,6 +21,7 @@ class Element(str, Enum): STAIRLIFT = "stairlift" DISABLED_HOIST_TRACKING = "disabled_hoist_tracking" DISABLED_FACILITIES = "disabled_facilities" + STEPS_TO_FRONT_DOOR = "steps_to_front_door" # ====================== # EXTERNAL – ROOF @@ -35,7 +37,9 @@ class Element(str, Enum): SOFFIT = "soffit" FASCIA_SOFFIT_BARGEBOARDS = "fascia_soffit_bargeboards" GUTTERS = "gutters" + STORE_ROOF = "store_roof" GARAGE_ROOF = "garage_roof" + GARAGE_AND_STORE_ROOF = "garage_and_store_roof" # ====================== # EXTERNAL – WALLS @@ -50,6 +54,8 @@ class Element(str, Enum): SPANDREL_PANELS = "spandrel_panels" GARAGE_WALLS = "garage_walls" PARTY_WALL_FIRE_BREAK = "party_wall_fire_break" + EXTERNAL_BRICKWORK_POINTING = "external_brickwork_pointing" + INTERNAL_DOWNPIPES_EXTERNAL_AREA = "internal_downpipes_in_external_area" # ====================== # EXTERNAL – WINDOWS @@ -57,6 +63,9 @@ class Element(str, Enum): EXTERNAL_WINDOWS = "external_windows" COMMUNAL_WINDOWS = "communal_windows" SECONDARY_GLAZING = "secondary_glazing" + STORE_WINDOWS = "store_windows" + GARAGE_WINDOWS = "garage_windows" + GARAGE_AND_STORE_WINDOWS = "garage_and_store_windows" # ====================== # EXTERNAL – DOORS @@ -66,6 +75,7 @@ class Element(str, Enum): REAR_DOOR = "rear_door" STORE_DOOR = "store_door" GARAGE_DOOR = "garage_door" + GARAGE_AND_STORE_DOOR = "garage_and_store_door" COMMUNAL_ENTRANCE_DOOR = "communal_entrance_door" MAIN_DOOR = "main_door" BLOCK_ENTRANCE_DOOR = "block_entrance_door" @@ -94,6 +104,10 @@ class Element(str, Enum): ROADS = "roads" SOIL_AND_VENT = "soil_and_vent" SOLAR_THERMALS = "solar_thermals" + DROP_KERB = "drop_kerb" + OUTBUILDING_OVERHAUL = "outbuilding_overhaul" + EXTERNAL_STRUCTURAL_DEFECTS = "external_structural_defects" + ACCESS_RAMP = "access_ramp" # ====================== # INTERNAL – KITCHEN @@ -110,6 +124,9 @@ class Element(str, Enum): SECONDARY_BATHROOM = "secondary_bathroom" SECONDARY_TOILET = "secondary_toilet" BATHROOM_EXTRACTOR_FAN = "bathroom_extractor_fan" + ADDITIONAL_WC_OR_WHB = "additional_wc_or_whb" + BATHROOM_REMAINING_LIFE_SOURCE = "bathroom_remaining_life_source" + KITCHEN_REMAINING_LIFE_SOURCE = "kitchen_remaining_life_source" # ====================== # INTERNAL – HEATING / WATER @@ -120,11 +137,16 @@ class Element(str, Enum): SECONDARY_HEATING = "secondary_heating" HOT_WATER_SYSTEM = "hot_water_system" COLD_WATER_STORAGE = "cold_water_storage" - PROGRAMMABLE_HEATING = "programmable_heating" HEATING_SYSTEM = "heating_system" BOILER_FUEL = "boiler_fuel" WATER_HEATING = "water_heating" PROGRAMMABLE_HEATING = "programmable_heating" + COMMUNITY_HEATING = ( + "community_heating" # Is this definitely different from COMMUNAL_HEATING? + ) + GAS_AVAILABLE = "gas_available" + HEAT_RECOVERY_UNITS = "heat_recovery_units" + HEATING_IMPROVEMENTS = "heating_improvements" # ====================== # INTERNAL – ELECTRICS / FIRE @@ -139,6 +161,7 @@ class Element(str, Enum): INTERNAL_WIRING = ( "internal_wiring" # Is this definitely different from ELECTRICAL_WIRING? ) + ELECTRICS = "electrics" # ====================== # COMMUNAL @@ -188,6 +211,19 @@ class Element(str, Enum): COMMUNAL_PLUG_SOCKETS = "communal_plug_sockets" COMMUNAL_CIRCULATION_SPACE = "communal_circulation_space" + # ====================== + # FITNESS FOR HUMAN HABITATION + # ====================== + FFHH_DAMP = "ffhh_damp" + FFHH_HOT_AND_COLD_WATER = "ffhh_hold_and_cold_water" + FFHH_DRAINAGE_LAVATORIES = "ffhh_drainage_or_lavatories" + FFHH_NEGLECTED = "ffhh_neglected_and_in_bad_condition" + FFHH_NATURAL_LIGHT = "ffhh_natural_light" + FFHH_VENTILATION = "ffhh_ventilation" + FFHH_FOOD_PREP_AND_WASHUP = "ffhh_prepare_and_cook_food_or_wash_up" + FFHH_UNSAFE_LAYOUT = "ffhh_unsafe_layout" + FFHH_UNSTABLE_BUILDING = "ffhh_unstable_building" + # ========================================================== # HHSRS – ALL 29 HAZARDS # ========================================================== diff --git a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py index be8a50b2..02722b11 100644 --- a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py +++ b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py @@ -31,6 +31,18 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { element=Element.FLOOR_LEVEL_FRONT_DOOR, aspect_type=AspectType.LOCATION, ), + "INTFLRLVL": ElementMapping( + element=Element.FLOOR_LEVEL, + aspect_type=AspectType.LOCATION, + ), + "INTNSEINSL": ElementMapping( + element=Element.EXTERNAL_NOISE_INSULATION, # Maybe this shouldn't be "EXTERNAL_" + aspect_type=AspectType.ADEQUACY, + ), + "INTSTEPSFD": ElementMapping( + element=Element.STEPS_TO_FRONT_DOOR, + aspect_type=AspectType.QUANTITY, + ), # ========================================================== # ASBESTOS (NON-HHSRS RECORD) # ========================================================== @@ -57,21 +69,22 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { element=Element.KITCHEN, aspect_type=AspectType.LOCATION, ), - # ========================================================== - # INTERNAL – HEATING - # ========================================================== - "INTCHEXTNT": ElementMapping( - element=Element.CENTRAL_HEATING, - aspect_type=AspectType.EXTENT, + "INTADDWCW": ElementMapping( + element=Element.ADDITIONAL_WC_OR_WHB, + aspect_type=AspectType.PRESENCE, ), - "INTCHDIST": ElementMapping( - element=Element.HEATING_DISTRIBUTION, + "INTBTHREML": ElementMapping( + element=Element.BATHROOM_REMAINING_LIFE_SOURCE, aspect_type=AspectType.TYPE, ), - "INTCHBLR": ElementMapping( - element=Element.HEATING_BOILER, + "INTKITREML": ElementMapping( + element=Element.KITCHEN_REMAINING_LIFE_SOURCE, aspect_type=AspectType.TYPE, ), + "INTTNTINST": ElementMapping( + element=Element.TENANT_INSTALLED_KITCHEN, + aspect_type=AspectType.TYPE, # Not certain about this aspect type - need more data + ), # ========================================================== # INTERNAL – FIRE # ========================================================== @@ -98,6 +111,18 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { # ========================================================== # HEATING & SERVICES # ========================================================== + "INTCHEXTNT": ElementMapping( + element=Element.CENTRAL_HEATING, + aspect_type=AspectType.EXTENT, + ), + "INTCHDIST": ElementMapping( + element=Element.HEATING_DISTRIBUTION, + aspect_type=AspectType.TYPE, + ), + "INTCHBLR": ElementMapping( + element=Element.HEATING_BOILER, + aspect_type=AspectType.TYPE, + ), "INTBOILERF": ElementMapping( element=Element.BOILER_FUEL, aspect_type=AspectType.TYPE, @@ -110,6 +135,30 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { element=Element.WATER_HEATING, aspect_type=AspectType.TYPE, ), + "INTCOMHTG": ElementMapping( + element=Element.COMMUNITY_HEATING, + aspect_type=AspectType.TYPE, + ), + "INTELECTRC": ElementMapping( + element=Element.ELECTRICS, + aspect_type=AspectType.WORK_REQUIRED, # Not certain about this aspect type - need more data + ), + "INTGASAVAI": ElementMapping( + element=Element.GAS_AVAILABLE, + aspect_type=AspectType.PRESENCE, # Maybe should be AspectType.TYPE ? + ), + "INTHEATREC": ElementMapping( + element=Element.HEAT_RECOVERY_UNITS, + aspect_type=AspectType.PRESENCE, + ), + "INTHTIMP": ElementMapping( + element=Element.GAS_AVAILABLE, + aspect_type=AspectType.WORK_REQUIRED, + ), + "INTPROGHTG": ElementMapping( + element=Element.PROGRAMMABLE_HEATING, + aspect_type=AspectType.TYPE, # Should maybe be PRESENCE, but set to TYPE for consistency with Peabody data + ), # ========================================================== # EXTERNAL – WALLS (INSTANCED) # ========================================================== @@ -136,6 +185,14 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { element=Element.EXTERNAL_WALL, aspect_type=AspectType.CONDITION, ), + "EXTDWNPTYP": ElementMapping( + element=Element.DOWNPIPES, + aspect_type=AspectType.MATERIAL, + ), + "EXTGUTRTYP": ElementMapping( + element=Element.GUTTERS, + aspect_type=AspectType.MATERIAL, + ), # ========================================================== # EXTERNAL – ROOFS (INSTANCED) # ========================================================== @@ -169,6 +226,30 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { aspect_type=AspectType.COVERING, element_instance=3, ), + "EXTCHIMNEY": ElementMapping( + element=Element.CHIMNEY, + aspect_type=AspectType.WORK_REQUIRED, + ), + "EXTFASOFBR": ElementMapping( + element=Element.FASCIA_SOFFIT_BARGEBOARDS, + aspect_type=AspectType.MATERIAL, + ), + "EXTGARROOF": ElementMapping( + element=Element.GARAGE_ROOF, + aspect_type=AspectType.MATERIAL, + ), + "EXTGARSTRF": ElementMapping( + element=Element.GARAGE_AND_STORE_ROOF, + aspect_type=AspectType.MATERIAL, + ), + "EXTSTRROOF": ElementMapping( + element=Element.STORE_ROOF, + aspect_type=AspectType.MATERIAL, + ), + "INTLOFTINS": ElementMapping( + element=Element.LOFT_INSULATION, + aspect_type=AspectType.TYPE, + ), # ========================================================== # EXTERNAL – DOORS & WINDOWS # ========================================================== @@ -204,6 +285,125 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { aspect_type=AspectType.TYPE, element_instance=2, ), + "EXTGARDOOR": ElementMapping( + element=Element.GARAGE_DOOR, + aspect_type=AspectType.MATERIAL, + ), + "EXTGARSTDR": ElementMapping( + element=Element.GARAGE_AND_STORE_DOOR, + aspect_type=AspectType.MATERIAL, + ), + "EXTSTRDOOR": ElementMapping( + element=Element.STORE_DOOR, + aspect_type=AspectType.MATERIAL, + ), + "EXTGARWDWS": ElementMapping( + element=Element.GARAGE_WINDOWS, + aspect_type=AspectType.MATERIAL, + ), + "EXTSTRWDWS": ElementMapping( + element=Element.STORE_WINDOWS, + aspect_type=AspectType.MATERIAL, + ), + "EXTGARSTWD": ElementMapping( + element=Element.GARAGE_AND_STORE_WINDOWS, + aspect_type=AspectType.MATERIAL, + ), + "EXTLINTELS": ElementMapping( + element=Element.LINTEL, + aspect_type=AspectType.PRESENCE, + ), + "EXTPTFRDR1": ElementMapping( + element=Element.PATIO_FRENCH_DOOR, + aspect_type=AspectType.MATERIAL, + element_instance=1, + ), + # ========================================================== + # EXTERNAL AREAS + # ========================================================== + "EXTBALCONY": ElementMapping( + element=Element.PRIVATE_BALCONY, + aspect_type=AspectType.PRESENCE, + ), + "EXTBPOINTG": ElementMapping( + element=Element.EXTERNAL_BRICKWORK_POINTING, + aspect_type=AspectType.PRESENCE, + ), + "EXTDRPKERB": ElementMapping( + element=Element.DROP_KERB, + aspect_type=AspectType.PRESENCE, + ), + "EXTEXTDECS": ElementMapping( + element=Element.EXTERNAL_DECORATION, + aspect_type=AspectType.PRESENCE, + ), + "EXTHARDSTD": ElementMapping( + element=Element.PATHS_AND_HARDSTANDINGS, + aspect_type=AspectType.MATERIAL, + ), + "EXTINTDWNP": ElementMapping( + element=Element.INTERNAL_DOWNPIPES_EXTERNAL_AREA, + aspect_type=AspectType.MATERIAL, + ), + "EXTOUTBOH": ElementMapping( + element=Element.OUTBUILDING_OVERHAUL, + aspect_type=AspectType.TYPE, + ), + "EXTPARKING": ElementMapping( + element=Element.PARKING_AREAS, + aspect_type=AspectType.PRESENCE, + ), + "EXTPCHCNPY": ElementMapping( + element=Element.PORCH_CANOPY, + aspect_type=AspectType.TYPE, + ), + "EXTSTRINSP": ElementMapping( + element=Element.EXTERNAL_STRUCTURAL_DEFECTS, + aspect_type=AspectType.TYPE, # Need more sample data to know whether this is the correct aspect type + ), + "INTACCRAMP": ElementMapping( + element=Element.ACCESS_RAMP, + aspect_type=AspectType.TYPE, # # Need more sample data to know whether this is the correct aspect type + ), + # ====================== + # FITNESS FOR HUMAN HABITATION + # ====================== + "FFHHDAMP": ElementMapping( + element=Element.FFHH_DAMP, + aspect_type=AspectType.RISK, + ), + "FFHHHCWAT": ElementMapping( + element=Element.FFHH_HOT_AND_COLD_WATER, + aspect_type=AspectType.RISK, + ), + "FFHHDRNWC": ElementMapping( + element=Element.FFHH_DRAINAGE_LAVATORIES, + aspect_type=AspectType.RISK, + ), + "FFHHNEGLC": ElementMapping( + element=Element.FFHH_NEGLECTED, + aspect_type=AspectType.RISK, + ), + "FFHHNONAT": ElementMapping( + element=Element.FFHH_NATURAL_LIGHT, + aspect_type=AspectType.RISK, + ), + "FFHHNOVEN": ElementMapping( + element=Element.FFHH_VENTILATION, + aspect_type=AspectType.RISK, + ), + "FFHHPRPCK": ElementMapping( + element=Element.FFHH_FOOD_PREP_AND_WASHUP, + aspect_type=AspectType.RISK, + ), + "FFHHUNLAY": ElementMapping( + element=Element.FFHH_UNSAFE_LAYOUT, + aspect_type=AspectType.RISK, + ), + "FFHHUNSTA": ElementMapping( + element=Element.FFHH_UNSTABLE_BUILDING, + aspect_type=AspectType.RISK, + ), # ========================================================== # HHSRS # ========================================================== @@ -244,7 +444,8 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { aspect_type=AspectType.RISK, ), "HHSRSORGAN": ElementMapping( - element=Element.HHSRS_VOLATILE_ORGANIC_COMPOUNDS, aspect_type=AspectType.RISK + element=Element.HHSRS_VOLATILE_ORGANIC_COMPOUNDS, + aspect_type=AspectType.RISK, ), "HHSRSCROWD": ElementMapping( element=Element.HHSRS_CROWDING_AND_SPACE, @@ -319,7 +520,8 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { aspect_type=AspectType.RISK, ), "HHSRSCLOW": ElementMapping( - element=Element.HHSRS_COLLISION_AND_ENTRAPMENT, aspect_type=AspectType.RISK + element=Element.HHSRS_COLLISION_AND_ENTRAPMENT, + aspect_type=AspectType.RISK, ), "HHSRSPOSI": ElementMapping( element=Element.HHSRS_AMENITIES, @@ -330,52 +532,3 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { # Unhandled: # DECNTHMINC # EICINSFREQ -# EXTBALCONY -# EXTBPOINTG -# EXTCHIMNEY -# EXTDRPKERB -# EXTDWNPTYP -# EXTEXTDECS -# EXTFASOFBR -# EXTGARDOOR -# EXTGARROOF -# EXTGARSTDR -# EXTGARSTRF -# EXTGARSTWD -# EXTGARWDWS -# EXTGUTRTYP -# EXTHARDSTD -# EXTINTDWNP -# EXTLINTELS -# EXTOUTBOH -# EXTPARKING -# EXTPCHCNPY -# EXTPTFRDR1 -# EXTSTRDOOR -# EXTSTRINSP -# EXTSTRROOF -# EXTSTRWDWS -# FFHHDAMP -# FFHHDRNWC -# FFHHHCWAT -# FFHHNEGLC -# FFHHNONAT -# FFHHNOVEN -# FFHHPRPCK -# FFHHUNLAY -# FFHHUNSTA -# INTACCRAMP -# INTADDWCW -# INTBTHREML -# INTCOMHTG -# INTELECTRC -# INTFLRLVL -# INTGASAVAI -# INTHEATREC -# INTHTIMP -# INTKITREML -# INTLOFTINS -# INTNSEINSL -# INTPROGHTG -# INTSTEPSFD -# INTTNTINST diff --git a/backend/condition/domain/mapping/peabody/peabody_element_map.py b/backend/condition/domain/mapping/peabody/peabody_element_map.py index 7a266a9f..8fe2ccb9 100644 --- a/backend/condition/domain/mapping/peabody/peabody_element_map.py +++ b/backend/condition/domain/mapping/peabody/peabody_element_map.py @@ -86,69 +86,109 @@ PEABODY_ELEMENT_MAP = { # EXTERNAL – ROOFS # ========================================================== (50, 15): ElementMapping( - element=Element.LOFT_INSULATION, aspect_type=AspectType.TYPE + element=Element.LOFT_INSULATION, + aspect_type=AspectType.TYPE, + ), + (53, 2): ElementMapping( + element=Element.CHIMNEY, + aspect_type=AspectType.PRESENCE, ), - (53, 2): ElementMapping(element=Element.CHIMNEY, aspect_type=AspectType.PRESENCE), (53, 6): ElementMapping( - element=Element.FASCIA_SOFFIT_BARGEBOARDS, aspect_type=AspectType.MATERIAL + element=Element.FASCIA_SOFFIT_BARGEBOARDS, + aspect_type=AspectType.MATERIAL, ), (53, 7): ElementMapping( - element=Element.FLAT_ROOF_COVERING, aspect_type=AspectType.MATERIAL + element=Element.FLAT_ROOF_COVERING, + aspect_type=AspectType.MATERIAL, ), (53, 13): ElementMapping( - element=Element.GARAGE_ROOF, aspect_type=AspectType.MATERIAL + element=Element.GARAGE_ROOF, + aspect_type=AspectType.MATERIAL, + ), + (53, 15): ElementMapping( + element=Element.GUTTERS, + aspect_type=AspectType.MATERIAL, ), - (53, 15): ElementMapping(element=Element.GUTTERS, aspect_type=AspectType.MATERIAL), (53, 18): ElementMapping( - element=Element.PITCHED_ROOF_COVERING, aspect_type=AspectType.MATERIAL + element=Element.PITCHED_ROOF_COVERING, + aspect_type=AspectType.MATERIAL, + ), + (53, 22): ElementMapping( + element=Element.PORCH_CANOPY, + aspect_type=AspectType.TYPE, + ), + (53, 47): ElementMapping( + element=Element.ROOF, + aspect_type=AspectType.STRUCTURE, ), - (53, 22): ElementMapping(element=Element.PORCH_CANOPY, aspect_type=AspectType.TYPE), - (53, 47): ElementMapping(element=Element.ROOF, aspect_type=AspectType.STRUCTURE), (110, 1): ElementMapping( - element=Element.ROOF, aspect_type=AspectType.MATERIAL, element_instance=1 + element=Element.ROOF, + aspect_type=AspectType.MATERIAL, + element_instance=1, ), (110, 2): ElementMapping( - element=Element.ROOF, aspect_type=AspectType.MATERIAL, element_instance=1 + element=Element.ROOF, + aspect_type=AspectType.MATERIAL, + element_instance=1, ), (110, 3): ElementMapping( - element=Element.CHIMNEY, aspect_type=AspectType.WORK_REQUIRED + element=Element.CHIMNEY, + aspect_type=AspectType.WORK_REQUIRED, + ), + (110, 4): ElementMapping( + element=Element.FASCIA, + aspect_type=AspectType.MATERIAL, + ), + (110, 5): ElementMapping( + element=Element.SOFFIT, + aspect_type=AspectType.MATERIAL, ), - (110, 4): ElementMapping(element=Element.FASCIA, aspect_type=AspectType.MATERIAL), - (110, 5): ElementMapping(element=Element.SOFFIT, aspect_type=AspectType.MATERIAL), (110, 6): ElementMapping( - element=Element.RAINWATER_GOODS, aspect_type=AspectType.MATERIAL + element=Element.RAINWATER_GOODS, + aspect_type=AspectType.MATERIAL, ), (110, 7): ElementMapping( element=Element.LOFT_INSULATION, aspect_type=AspectType.WORK_REQUIRED, # possibly not the right aspect type ), (110, 8): ElementMapping( - element=Element.PORCH_CANOPY, aspect_type=AspectType.MATERIAL + element=Element.PORCH_CANOPY, + aspect_type=AspectType.MATERIAL, ), # ========================================================== # EXTERNAL – DOORS & WINDOWS # ========================================================== (50, 8): ElementMapping( - element=Element.DOOR_ENTRY_HANDSET, aspect_type=AspectType.PRESENCE + element=Element.DOOR_ENTRY_HANDSET, + aspect_type=AspectType.PRESENCE, ), (53, 8): ElementMapping( - element=Element.FRONT_DOOR, aspect_type=AspectType.MATERIAL + element=Element.FRONT_DOOR, + aspect_type=AspectType.MATERIAL, ), (53, 12): ElementMapping( - element=Element.GARAGE_DOOR, aspect_type=AspectType.MATERIAL + element=Element.GARAGE_DOOR, + aspect_type=AspectType.MATERIAL, + ), + (53, 16): ElementMapping( + element=Element.LINTEL, + aspect_type=AspectType.PRESENCE, ), - (53, 16): ElementMapping(element=Element.LINTEL, aspect_type=AspectType.PRESENCE), (53, 19): ElementMapping( - element=Element.PATIO_FRENCH_DOOR, aspect_type=AspectType.MATERIAL + element=Element.PATIO_FRENCH_DOOR, + aspect_type=AspectType.MATERIAL, ), (53, 25): ElementMapping( - element=Element.REAR_DOOR, aspect_type=AspectType.MATERIAL + element=Element.REAR_DOOR, + aspect_type=AspectType.MATERIAL, ), (53, 29): ElementMapping( - element=Element.SECONDARY_GLAZING, aspect_type=AspectType.PRESENCE + element=Element.SECONDARY_GLAZING, + aspect_type=AspectType.PRESENCE, ), (53, 35): ElementMapping( - element=Element.STORE_DOOR, aspect_type=AspectType.MATERIAL + element=Element.STORE_DOOR, + aspect_type=AspectType.MATERIAL, ), (53, 38): ElementMapping( element=Element.EXTERNAL_WINDOWS, @@ -160,191 +200,275 @@ PEABODY_ELEMENT_MAP = { aspect_type=AspectType.TYPE, element_instance=2, ), - (53, 43): ElementMapping(element=Element.FRONT_DOOR, aspect_type=AspectType.TYPE), + (53, 43): ElementMapping( + element=Element.FRONT_DOOR, + aspect_type=AspectType.TYPE, + ), (130, 1): ElementMapping( - element=Element.EXTERNAL_WINDOWS, aspect_type=AspectType.MATERIAL + element=Element.EXTERNAL_WINDOWS, + aspect_type=AspectType.MATERIAL, ), (130, 2): ElementMapping( - element=Element.COMMUNAL_WINDOWS, aspect_type=AspectType.MATERIAL + element=Element.COMMUNAL_WINDOWS, + aspect_type=AspectType.MATERIAL, ), (140, 1): ElementMapping( - element=Element.MAIN_DOOR, aspect_type=AspectType.MATERIAL + element=Element.MAIN_DOOR, + aspect_type=AspectType.MATERIAL, ), (140, 2): ElementMapping( - element=Element.STORE_DOOR, aspect_type=AspectType.MATERIAL + element=Element.STORE_DOOR, + aspect_type=AspectType.MATERIAL, ), # Duplicate of (53, 35) (140, 3): ElementMapping( - element=Element.GARAGE_DOOR, aspect_type=AspectType.MATERIAL + element=Element.GARAGE_DOOR, + aspect_type=AspectType.MATERIAL, ), # Duplicate of (53, 12) (140, 4): ElementMapping( - element=Element.BLOCK_ENTRANCE_DOOR, aspect_type=AspectType.MATERIAL + element=Element.BLOCK_ENTRANCE_DOOR, + aspect_type=AspectType.MATERIAL, ), # ========================================================== # EXTERNAL AREAS # ========================================================== - (53, 3): ElementMapping(element=Element.DOWNPIPES, aspect_type=AspectType.MATERIAL), - (53, 9): ElementMapping( - element=Element.FRONT_FENCING, aspect_type=AspectType.MATERIAL + (53, 3): ElementMapping( + element=Element.DOWNPIPES, + aspect_type=AspectType.MATERIAL, + ), + (53, 9): ElementMapping( + element=Element.FRONT_FENCING, + aspect_type=AspectType.MATERIAL, + ), + (53, 10): ElementMapping( + element=Element.FRONT_GATE, + aspect_type=AspectType.TYPE, ), - (53, 10): ElementMapping(element=Element.FRONT_GATE, aspect_type=AspectType.TYPE), (53, 17): ElementMapping( - element=Element.PARKING_AREAS, aspect_type=AspectType.MATERIAL + element=Element.PARKING_AREAS, + aspect_type=AspectType.MATERIAL, ), (53, 18): ElementMapping( - element=Element.PATHS_AND_HARDSTANDINGS, aspect_type=AspectType.MATERIAL + element=Element.PATHS_AND_HARDSTANDINGS, + aspect_type=AspectType.MATERIAL, ), (53, 24): ElementMapping( - element=Element.PRIVATE_BALCONY, aspect_type=AspectType.PRESENCE + element=Element.PRIVATE_BALCONY, + aspect_type=AspectType.PRESENCE, ), (53, 26): ElementMapping( - element=Element.REAR_FENCING, aspect_type=AspectType.MATERIAL + element=Element.REAR_FENCING, + aspect_type=AspectType.MATERIAL, + ), + (53, 27): ElementMapping( + element=Element.REAR_GATE, + aspect_type=AspectType.TYPE, ), - (53, 27): ElementMapping(element=Element.REAR_GATE, aspect_type=AspectType.TYPE), (53, 28): ElementMapping( - element=Element.RETAINING_WALLS, aspect_type=AspectType.PRESENCE + element=Element.RETAINING_WALLS, + aspect_type=AspectType.PRESENCE, ), (53, 31): ElementMapping( - element=Element.SIDE_FENCING, aspect_type=AspectType.MATERIAL + element=Element.SIDE_FENCING, + aspect_type=AspectType.MATERIAL, ), (53, 32): ElementMapping( - element=Element.SOIL_AND_VENT, aspect_type=AspectType.MATERIAL + element=Element.SOIL_AND_VENT, + aspect_type=AspectType.MATERIAL, ), (53, 34): ElementMapping( - element=Element.SOLAR_THERMALS, aspect_type=AspectType.PRESENCE + element=Element.SOLAR_THERMALS, + aspect_type=AspectType.PRESENCE, ), (53, 44): ElementMapping( - element=Element.GARAGE_STRUCTURE, aspect_type=AspectType.TYPE + element=Element.GARAGE_STRUCTURE, + aspect_type=AspectType.TYPE, ), (53, 45): ElementMapping( - element=Element.BALCONY_BALUSTRADE, aspect_type=AspectType.MATERIAL + element=Element.BALCONY_BALUSTRADE, + aspect_type=AspectType.MATERIAL, ), (150, 1): ElementMapping( - element=Element.BLOCK_ENTRANCE_DOOR, aspect_type=AspectType.MATERIAL + element=Element.BLOCK_ENTRANCE_DOOR, + aspect_type=AspectType.MATERIAL, ), (150, 2): ElementMapping( - element=Element.PATHS_AND_HARDSTANDINGS, aspect_type=AspectType.MATERIAL + element=Element.PATHS_AND_HARDSTANDINGS, + aspect_type=AspectType.MATERIAL, ), # Duplicate of (53, 18) - correct? - (150, 3): ElementMapping(element=Element.ROADS, aspect_type=AspectType.MATERIAL), - (150, 4): ElementMapping( - element=Element.BOUNDARY_WALLS, aspect_type=AspectType.MATERIAL + (150, 3): ElementMapping( + element=Element.ROADS, + aspect_type=AspectType.MATERIAL, + ), + (150, 4): ElementMapping( + element=Element.BOUNDARY_WALLS, + aspect_type=AspectType.MATERIAL, + ), + (150, 5): ElementMapping( + element=Element.OUTBUILDINGS, + aspect_type=AspectType.TYPE, ), - (150, 5): ElementMapping(element=Element.OUTBUILDINGS, aspect_type=AspectType.TYPE), (150, 6): ElementMapping( - element=Element.GARAGE_STRUCTURE, aspect_type=AspectType.TYPE + element=Element.GARAGE_STRUCTURE, + aspect_type=AspectType.TYPE, ), # ========================================================== # INTERNAL – BATHROOMS & KITCHENS # ========================================================== (50, 1): ElementMapping( - element=Element.SECONDARY_TOILET, aspect_type=AspectType.PRESENCE + element=Element.SECONDARY_TOILET, + aspect_type=AspectType.PRESENCE, ), (50, 9): ElementMapping( - element=Element.BATHROOM_EXTRACTOR_FAN, aspect_type=AspectType.PRESENCE + element=Element.BATHROOM_EXTRACTOR_FAN, + aspect_type=AspectType.PRESENCE, + ), + (50, 9): ElementMapping( + element=Element.KITCHEN, + aspect_type=AspectType.TYPE, ), - (50, 9): ElementMapping(element=Element.KITCHEN, aspect_type=AspectType.TYPE), (50, 10): ElementMapping( - element=Element.KITCHEN_EXTRACTOR_FAN, aspect_type=AspectType.PRESENCE + element=Element.KITCHEN_EXTRACTOR_FAN, + aspect_type=AspectType.PRESENCE, ), (50, 13): ElementMapping( - element=Element.KITCHEN_SPACE_LAYOUT, aspect_type=AspectType.ADEQUACY + element=Element.KITCHEN_SPACE_LAYOUT, + aspect_type=AspectType.ADEQUACY, + ), + (50, 17): ElementMapping( + element=Element.BATHRROM, + aspect_type=AspectType.LOCATION, ), - (50, 17): ElementMapping(element=Element.BATHRROM, aspect_type=AspectType.LOCATION), (50, 18): ElementMapping( - element=Element.BATHROOM, aspect_type=AspectType.TYPE + element=Element.BATHROOM, + aspect_type=AspectType.TYPE, ), # Actually "Primary bathroom type" - ok like this? (50, 20): ElementMapping( - element=Element.BATHROOM, aspect_type=AspectType.TYPE, element_instance=2 + element=Element.BATHROOM, + aspect_type=AspectType.TYPE, + element_instance=2, ), # Actually "Secondary bathroom type" - ok like this? - (160, 1): ElementMapping(element=Element.KITCHEN, aspect_type=AspectType.CONDITION), + (160, 1): ElementMapping( + element=Element.KITCHEN, + aspect_type=AspectType.CONDITION, + ), (160, 2): ElementMapping( - element=Element.KITCHEN_SPACE_LAYOUT, aspect_type=AspectType.ADEQUACY + element=Element.KITCHEN_SPACE_LAYOUT, + aspect_type=AspectType.ADEQUACY, ), (190, 1): ElementMapping( - element=Element.BATHROOM, aspect_type=AspectType.CONDITION + element=Element.BATHROOM, + aspect_type=AspectType.CONDITION, ), (190, 2): ElementMapping( - element=Element.SECONDARY_TOILET, aspect_type=AspectType.TYPE + element=Element.SECONDARY_TOILET, + aspect_type=AspectType.TYPE, ), # ========================================================== # COMMUNAL # ========================================================== (51, 1): ElementMapping( - element=Element.COMMUNAL_AERIAL, aspect_type=AspectType.PRESENCE + element=Element.COMMUNAL_AERIAL, + aspect_type=AspectType.PRESENCE, ), (51, 2): ElementMapping( - element=Element.COMMUNAL_AOV, aspect_type=AspectType.PRESENCE + element=Element.COMMUNAL_AOV, + aspect_type=AspectType.PRESENCE, ), (51, 3): ElementMapping( - element=Element.COMMUNAL_BALCONY_WALKWAY, aspect_type=AspectType.PRESENCE + element=Element.COMMUNAL_BALCONY_WALKWAY, + aspect_type=AspectType.PRESENCE, ), (51, 4): ElementMapping( - element=Element.COMMUNAL_BATHROOM, aspect_type=AspectType.TYPE + element=Element.COMMUNAL_BATHROOM, + aspect_type=AspectType.TYPE, ), (51, 5): ElementMapping( - element=Element.COMMUNAL_BIN_STORE_DOORS, aspect_type=AspectType.PRESENCE + element=Element.COMMUNAL_BIN_STORE_DOORS, + aspect_type=AspectType.PRESENCE, ), (51, 6): ElementMapping( - element=Element.COMMUNAL_BIN_STORE_ROOF, aspect_type=AspectType.PRESENCE + element=Element.COMMUNAL_BIN_STORE_ROOF, + aspect_type=AspectType.PRESENCE, ), (51, 7): ElementMapping( - element=Element.COMMUNAL_BIN_STORE_WALLS, aspect_type=AspectType.MATERIAL + element=Element.COMMUNAL_BIN_STORE_WALLS, + aspect_type=AspectType.MATERIAL, ), (51, 8): ElementMapping( - element=Element.COMMUNAL_BMS, aspect_type=AspectType.PRESENCE + element=Element.COMMUNAL_BMS, + aspect_type=AspectType.PRESENCE, ), (51, 9): ElementMapping( - element=Element.COMMUNAL_BOILER, aspect_type=AspectType.TYPE + element=Element.COMMUNAL_BOILER, + aspect_type=AspectType.TYPE, ), (51, 10): ElementMapping( - element=Element.COMMUNAL_BOOSTER_PUMP, aspect_type=AspectType.PRESENCE + element=Element.COMMUNAL_BOOSTER_PUMP, + aspect_type=AspectType.PRESENCE, ), (51, 11): ElementMapping( - element=Element.COMMUNAL_CCTV, aspect_type=AspectType.PRESENCE + element=Element.COMMUNAL_CCTV, + aspect_type=AspectType.PRESENCE, ), (51, 12): ElementMapping( - element=Element.COMMUNAL_CIRCULATION_SPACE, aspect_type=AspectType.ADEQUACY + element=Element.COMMUNAL_CIRCULATION_SPACE, + aspect_type=AspectType.ADEQUACY, ), (51, 13): ElementMapping( - element=Element.COMMUNAL_COLD_WATER_STORAGE, aspect_type=AspectType.PRESENCE + element=Element.COMMUNAL_COLD_WATER_STORAGE, + aspect_type=AspectType.PRESENCE, ), (51, 14): ElementMapping( - element=Element.COMMUNAL_DOOR_ENTRY, aspect_type=AspectType.SYSTEM + element=Element.COMMUNAL_DOOR_ENTRY, + aspect_type=AspectType.SYSTEM, ), (51, 15): ElementMapping( - element=Element.COMMUNAL_DRY_RISER, aspect_type=AspectType.PRESENCE + element=Element.COMMUNAL_DRY_RISER, + aspect_type=AspectType.PRESENCE, ), (51, 16): ElementMapping( - element=Element.COMMUNAL_EMERGENCY_LIGHTING, aspect_type=AspectType.PRESENCE + element=Element.COMMUNAL_EMERGENCY_LIGHTING, + aspect_type=AspectType.PRESENCE, ), (51, 17): ElementMapping( - element=Element.COMMUNAL_EXTERNAL_DOORS, aspect_type=AspectType.MATERIAL + element=Element.COMMUNAL_EXTERNAL_DOORS, + aspect_type=AspectType.MATERIAL, ), (51, 19): ElementMapping( - element=Element.COMMUNAL_FIRE_ALARM, aspect_type=AspectType.TYPE + element=Element.COMMUNAL_FIRE_ALARM, + aspect_type=AspectType.TYPE, ), (51, 20): ElementMapping( - element=Element.COMMUNAL_INTERNAL_DECORATIONS, aspect_type=AspectType.PRESENCE + element=Element.COMMUNAL_INTERNAL_DECORATIONS, + aspect_type=AspectType.PRESENCE, ), (51, 21): ElementMapping( - element=Element.COMMUNAL_INTERNAL_DOORS, aspect_type=AspectType.MATERIAL + element=Element.COMMUNAL_INTERNAL_DOORS, + aspect_type=AspectType.MATERIAL, ), (51, 22): ElementMapping( - element=Element.COMMUNAL_INTERNAL_FLOOR, aspect_type=AspectType.FINISH + element=Element.COMMUNAL_INTERNAL_FLOOR, + aspect_type=AspectType.FINISH, ), (51, 23): ElementMapping( - element=Element.COMMUNAL_KITCHEN, aspect_type=AspectType.TYPE + element=Element.COMMUNAL_KITCHEN, + aspect_type=AspectType.TYPE, ), (51, 24): ElementMapping( - element=Element.COMMUNAL_LATERAL_MAINS, aspect_type=AspectType.PRESENCE + element=Element.COMMUNAL_LATERAL_MAINS, + aspect_type=AspectType.PRESENCE, ), (51, 25): ElementMapping( - element=Element.COMMUNAL_LIGHTING, aspect_type=AspectType.PRESENCE + element=Element.COMMUNAL_LIGHTING, + aspect_type=AspectType.PRESENCE, ), (51, 26): ElementMapping( - element=Element.COMMUNAL_LIGHTING_CONDUCTOR, aspect_type=AspectType.PRESENCE + element=Element.COMMUNAL_LIGHTING_CONDUCTOR, + aspect_type=AspectType.PRESENCE, ), (51, 27): ElementMapping( - element=Element.COMMUNAL_PASSENGER_LIFT, aspect_type=AspectType.TYPE + element=Element.COMMUNAL_PASSENGER_LIFT, + aspect_type=AspectType.TYPE, ), (51, 28): ElementMapping( element=Element.COMMUNAL_ENTRANCE, @@ -357,135 +481,176 @@ PEABODY_ELEMENT_MAP = { element_instance=2, ), (51, 14): ElementMapping( - element=Element.COMMUNAL_SPRINKLER, aspect_type=AspectType.PRESENCE + element=Element.COMMUNAL_SPRINKLER, + aspect_type=AspectType.PRESENCE, ), (51, 29): ElementMapping( - element=Element.COMMUNAL_REFUSE_CHUTE, aspect_type=AspectType.PRESENCE + element=Element.COMMUNAL_REFUSE_CHUTE, + aspect_type=AspectType.PRESENCE, ), (51, 32): ElementMapping( - element=Element.COMMUNAL_STAIRS, aspect_type=AspectType.FINISH + element=Element.COMMUNAL_STAIRS, + aspect_type=AspectType.FINISH, ), (51, 33): ElementMapping( - element=Element.COMMUNAL_STORE_DOORS, aspect_type=AspectType.MATERIAL + element=Element.COMMUNAL_STORE_DOORS, + aspect_type=AspectType.MATERIAL, ), (51, 34): ElementMapping( - element=Element.COMMUNAL_STORE_ROOF, aspect_type=AspectType.MATERIAL + element=Element.COMMUNAL_STORE_ROOF, + aspect_type=AspectType.MATERIAL, ), (51, 35): ElementMapping( - element=Element.COMMUNAL_STORE_WALLS, aspect_type=AspectType.MATERIAL + element=Element.COMMUNAL_STORE_WALLS, + aspect_type=AspectType.MATERIAL, ), (51, 36): ElementMapping( - element=Element.COMMUNAL_WALKWAYS, aspect_type=AspectType.FINISH + element=Element.COMMUNAL_WALKWAYS, + aspect_type=AspectType.FINISH, ), (51, 37): ElementMapping( - element=Element.COMMUNAL_WARDEN_CALL_SYSTEM, aspect_type=AspectType.PRESENCE + element=Element.COMMUNAL_WARDEN_CALL_SYSTEM, + aspect_type=AspectType.PRESENCE, ), (51, 38): ElementMapping( - element=Element.COMMUNAL_TOILETS, aspect_type=AspectType.TYPE + element=Element.COMMUNAL_TOILETS, + aspect_type=AspectType.TYPE, ), (51, 39): ElementMapping( - element=Element.COMMUNAL_WET_RISER, aspect_type=AspectType.PRESENCE + element=Element.COMMUNAL_WET_RISER, + aspect_type=AspectType.PRESENCE, ), (51, 40): ElementMapping( - element=Element.COMMUNAL_PLUG_SOCKETS, aspect_type=AspectType.PRESENCE + element=Element.COMMUNAL_PLUG_SOCKETS, + aspect_type=AspectType.PRESENCE, ), (200, 1): ElementMapping( - element=Element.COMMUNAL_BOILER, aspect_type=AspectType.TYPE + element=Element.COMMUNAL_BOILER, + aspect_type=AspectType.TYPE, ), # Duplicate of (51, 9) - correct? (200, 2): ElementMapping( - element=Element.COMMUNAL_HEATING, aspect_type=AspectType.TYPE + element=Element.COMMUNAL_HEATING, + aspect_type=AspectType.TYPE, ), (200, 3): ElementMapping( - element=Element.COMMUNAL_ELECTRICS, aspect_type=AspectType.TYPE + element=Element.COMMUNAL_ELECTRICS, + aspect_type=AspectType.TYPE, ), (200, 4): ElementMapping( - element=Element.COMMUNAL_FIRE_ALARM, aspect_type=AspectType.TYPE + element=Element.COMMUNAL_FIRE_ALARM, + aspect_type=AspectType.TYPE, ), (200, 5): ElementMapping( - element=Element.COMMUNAL_LIFT, aspect_type=AspectType.TYPE + element=Element.COMMUNAL_LIFT, + aspect_type=AspectType.TYPE, ), (200, 6): ElementMapping( - element=Element.COMMUNAL_FLOOR_COVERING, aspect_type=AspectType.MATERIAL + element=Element.COMMUNAL_FLOOR_COVERING, + aspect_type=AspectType.MATERIAL, ), (200, 7): ElementMapping( - element=Element.COMMUNAL_KITCHEN, aspect_type=AspectType.TYPE + element=Element.COMMUNAL_KITCHEN, + aspect_type=AspectType.TYPE, ), (200, 8): ElementMapping( - element=Element.COMMUNAL_BATHROOM, aspect_type=AspectType.TYPE + element=Element.COMMUNAL_BATHROOM, + aspect_type=AspectType.TYPE, ), # Duplicate of (51, 4) - correct? (200, 9): ElementMapping( - element=Element.COMMUNAL_TOILETS, aspect_type=AspectType.TYPE + element=Element.COMMUNAL_TOILETS, + aspect_type=AspectType.TYPE, ), # Duplicate of (51, 38) - correct? (200, 10): ElementMapping( - element=Element.COMMUNAL_GATES, aspect_type=AspectType.TYPE + element=Element.COMMUNAL_GATES, + aspect_type=AspectType.TYPE, ), # ========================================================== # INTERNAL – HEATING # ========================================================== (50, 4): ElementMapping( - element=Element.HEATING_BOILER, aspect_type=AspectType.PRESENCE + element=Element.HEATING_BOILER, + aspect_type=AspectType.PRESENCE, ), # This is actually "Central heating boiler" - ok like this? (50, 5): ElementMapping( - element=Element.CENTRAL_HEATING, aspect_type=AspectType.EXTENT + element=Element.CENTRAL_HEATING, + aspect_type=AspectType.EXTENT, ), (50, 6): ElementMapping( - element=Element.COLD_WATER_STORAGE, aspect_type=AspectType.PRESENCE + element=Element.COLD_WATER_STORAGE, + aspect_type=AspectType.PRESENCE, ), (50, 12): ElementMapping( - element=Element.HEATING_DISTRIBUTION, aspect_type=AspectType.TYPE + element=Element.HEATING_DISTRIBUTION, + aspect_type=AspectType.TYPE, ), (50, 19): ElementMapping( - element=Element.PROGRAMMABLE_HEATING, aspect_type=AspectType.TYPE + element=Element.PROGRAMMABLE_HEATING, + aspect_type=AspectType.TYPE, ), (50, 25): ElementMapping( - element=Element.HEATING_BOILER, aspect_type=AspectType.TYPE + element=Element.HEATING_BOILER, + aspect_type=AspectType.TYPE, ), (170, 1): ElementMapping( - element=Element.HEATING_BOILER, aspect_type=AspectType.TYPE + element=Element.HEATING_BOILER, + aspect_type=AspectType.TYPE, ), # Duplicate of (50,25) - correct? (170, 2): ElementMapping( - element=Element.HEATING_DISTRIBUTION, aspect_type=AspectType.TYPE + element=Element.HEATING_DISTRIBUTION, + aspect_type=AspectType.TYPE, ), # Duplicate of (50,12) - correct? (170, 3): ElementMapping( - element=Element.SECONDARY_HEATING, aspect_type=AspectType.TYPE + element=Element.SECONDARY_HEATING, + aspect_type=AspectType.TYPE, ), (170, 4): ElementMapping( - element=Element.COLD_WATER_STORAGE, aspect_type=AspectType.TYPE + element=Element.COLD_WATER_STORAGE, + aspect_type=AspectType.TYPE, ), (170, 5): ElementMapping( - element=Element.HOT_WATER_SYSTEM, aspect_type=AspectType.TYPE + element=Element.HOT_WATER_SYSTEM, + aspect_type=AspectType.TYPE, ), # ========================================================== # ELECTRICS # ========================================================== (50, 24): ElementMapping( - element=Element.INTERNAL_WIRING, aspect_type=AspectType.MATERIAL + element=Element.INTERNAL_WIRING, + aspect_type=AspectType.MATERIAL, ), (180, 1): ElementMapping( - element=Element.ELECTRICAL_WIRING, aspect_type=AspectType.WORK_REQUIRED + element=Element.ELECTRICAL_WIRING, + aspect_type=AspectType.WORK_REQUIRED, ), # Not certain about the AspectType - only example in the sample data is "Full Rewire" (180, 2): ElementMapping( - element=Element.CONSUMER_UNIT, aspect_type=AspectType.TYPE + element=Element.CONSUMER_UNIT, + aspect_type=AspectType.TYPE, ), (180, 3): ElementMapping( - element=Element.SMOKE_DETECTION, aspect_type=AspectType.TYPE + element=Element.SMOKE_DETECTION, + aspect_type=AspectType.TYPE, ), # Duplicate of (50, 21) - correct? (180, 4): ElementMapping( - element=Element.CARBON_MONOXIDE_DETECTION, aspect_type=AspectType.TYPE + element=Element.CARBON_MONOXIDE_DETECTION, + aspect_type=AspectType.TYPE, ), # Duplicate of (50, 2) - correct? # ========================================================== # HHSRS # ========================================================== (54, 1): ElementMapping( - element=Element.HHSRS_DAMP_AND_MOULD, aspect_type=AspectType.RISK + element=Element.HHSRS_DAMP_AND_MOULD, + aspect_type=AspectType.RISK, ), (54, 4): ElementMapping( - element=Element.HHSRS_ASBESTOS_AND_MMF, aspect_type=AspectType.RISK + element=Element.HHSRS_ASBESTOS_AND_MMF, + aspect_type=AspectType.RISK, ), (54, 15): ElementMapping( - element=Element.HHSRS_DOMESTIC_HYGIENE_PESTS_REFUSE, aspect_type=AspectType.RISK + element=Element.HHSRS_DOMESTIC_HYGIENE_PESTS_REFUSE, + aspect_type=AspectType.RISK, ), (54, 29): ElementMapping( - element=Element.HHSRS_STRUCTURAL_COLLAPSE, aspect_type=AspectType.RISK + element=Element.HHSRS_STRUCTURAL_COLLAPSE, + aspect_type=AspectType.RISK, ), } From d2ce135f07f3b7fcda1f512b49b52c0e860c0de5 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Mon, 26 Jan 2026 16:45:36 +0000 Subject: [PATCH 36/50] final missing mappings --- .../domain/mapping/lbwf/lbwf_element_map.py | 14 ++++-- .../domain/mapping/lbwf/lbwf_mapper.py | 50 ++++++++++--------- .../mapping/peabody/peabody_element_map.py | 12 +++-- backend/condition/processor.py | 2 +- 4 files changed, 44 insertions(+), 34 deletions(-) diff --git a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py index 02722b11..8d6ea858 100644 --- a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py +++ b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py @@ -423,7 +423,7 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { element=Element.HHSRS_ASBESTOS_AND_MMF, aspect_type=AspectType.RISK, ), - "HHSRSBIOCIDES": ElementMapping( + "HHSRSBIOC": ElementMapping( element=Element.HHSRS_BIOCIDES, aspect_type=AspectType.RISK, ), @@ -431,6 +431,14 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { element=Element.HHSRS_CARBON_MONOXIDE, aspect_type=AspectType.RISK, ), + "HHSRSNO2": ElementMapping( + element=Element.HHSRS_CARBON_MONOXIDE, + aspect_type=AspectType.RISK, + ), # Duplicate of HHSRSCO; I think they relate to the same HHSRS hazard + "HHSRSSO2": ElementMapping( + element=Element.HHSRS_CARBON_MONOXIDE, + aspect_type=AspectType.RISK, + ), # Duplicate of HHSRSCO; I think they relate to the same HHSRS hazard "HHSRSLEAD": ElementMapping( element=Element.HHSRS_LEAD, aspect_type=AspectType.RISK, @@ -528,7 +536,3 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { aspect_type=AspectType.RISK, ), } - -# Unhandled: -# DECNTHMINC -# EICINSFREQ diff --git a/backend/condition/domain/mapping/lbwf/lbwf_mapper.py b/backend/condition/domain/mapping/lbwf/lbwf_mapper.py index 635e5898..3d7b7349 100644 --- a/backend/condition/domain/mapping/lbwf/lbwf_mapper.py +++ b/backend/condition/domain/mapping/lbwf/lbwf_mapper.py @@ -27,32 +27,34 @@ class LbwfMapper(Mapper): uprn: int = client_data.uprn for raw_asset in client_data.assets: - try: - element_mapping: ElementMapping = LbwfMapper._map_element( - raw_asset.element_code - ) - except: - logger.warning( - f"Unrecognised LBWF Asset Element Code: {raw_asset.element_code}. Skipping record" - ) - continue + # Ignore metadata rows + if raw_asset.element_code not in ["EICINSFREQ", "DECNTHMINC"]: + try: + element_mapping: ElementMapping = LbwfMapper._map_element( + raw_asset.element_code + ) + except: + logger.warning( + f"Unrecognised LBWF Asset Element Code: {raw_asset.element_code}. Skipping record" + ) + continue - mapped_assets.append( - AssetCondition( - uprn=uprn, - element=element_mapping.element, - aspect_type=element_mapping.aspect_type, - value=raw_asset.attribute_code_description, - quantity=raw_asset.quantity, - install_date=raw_asset.install_date, - renewal_year=LbwfMapper._calculate_renewal_year( - raw_asset, survey_year - ), - element_instance=element_mapping.element_instance, - source_system=None, # Once we know the system name we'll set it here - comments=raw_asset.element_comments, + mapped_assets.append( + AssetCondition( + uprn=uprn, + element=element_mapping.element, + aspect_type=element_mapping.aspect_type, + value=raw_asset.attribute_code_description, + quantity=raw_asset.quantity, + install_date=raw_asset.install_date, + renewal_year=LbwfMapper._calculate_renewal_year( + raw_asset, survey_year + ), + element_instance=element_mapping.element_instance, + source_system=None, # Once we know the system name we'll set it here + comments=raw_asset.element_comments, + ) ) - ) return mapped_assets diff --git a/backend/condition/domain/mapping/peabody/peabody_element_map.py b/backend/condition/domain/mapping/peabody/peabody_element_map.py index 8fe2ccb9..2485136b 100644 --- a/backend/condition/domain/mapping/peabody/peabody_element_map.py +++ b/backend/condition/domain/mapping/peabody/peabody_element_map.py @@ -51,7 +51,7 @@ PEABODY_ELEMENT_MAP = { (53, 4): ElementMapping( element=Element.EXTERNAL_DECORATION, aspect_type=AspectType.PRESENCE ), - (53, 4): ElementMapping( + (53, 5): ElementMapping( element=Element.EXTERNAL_NOISE_INSULATION, aspect_type=AspectType.ADEQUACY ), (53, 14): ElementMapping( @@ -109,7 +109,7 @@ PEABODY_ELEMENT_MAP = { element=Element.GUTTERS, aspect_type=AspectType.MATERIAL, ), - (53, 18): ElementMapping( + (53, 21): ElementMapping( element=Element.PITCHED_ROOF_COVERING, aspect_type=AspectType.MATERIAL, ), @@ -334,8 +334,12 @@ PEABODY_ELEMENT_MAP = { element=Element.KITCHEN_SPACE_LAYOUT, aspect_type=AspectType.ADEQUACY, ), + (50, 14): ElementMapping( + element=Element.KITCHEN, + aspect_type=AspectType.TYPE, + ), (50, 17): ElementMapping( - element=Element.BATHRROM, + element=Element.BATHROOM, aspect_type=AspectType.LOCATION, ), (50, 18): ElementMapping( @@ -480,7 +484,7 @@ PEABODY_ELEMENT_MAP = { aspect_type=AspectType.FINISH, element_instance=2, ), - (51, 14): ElementMapping( + (51, 31): ElementMapping( element=Element.COMMUNAL_SPRINKLER, aspect_type=AspectType.PRESENCE, ), diff --git a/backend/condition/processor.py b/backend/condition/processor.py index a48e22f4..903c9f23 100644 --- a/backend/condition/processor.py +++ b/backend/condition/processor.py @@ -26,4 +26,4 @@ def process_file(file_stream: BinaryIO, source_key: str) -> None: for p in raw_properties: assets.extend(mapper.map_asset_conditions_for_property(p, survey_year)) - print(assets) # temp + print("done") # temp From cfc73d8f90ac429ef34590c08a0029dda4f14a9d Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Mon, 26 Jan 2026 17:07:22 +0000 Subject: [PATCH 37/50] fix broken test --- backend/condition/tests/mapping/test_lbwf_mapper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/condition/tests/mapping/test_lbwf_mapper.py b/backend/condition/tests/mapping/test_lbwf_mapper.py index 918b6fea..907bd250 100644 --- a/backend/condition/tests/mapping/test_lbwf_mapper.py +++ b/backend/condition/tests/mapping/test_lbwf_mapper.py @@ -277,7 +277,7 @@ def test_lbwf_mapper_maps_house(): ), AssetCondition( uprn=1, - element=Element.HEATING_SYSTEM, + element=Element.CENTRAL_HEATING, aspect_type=AspectType.EXTENT, element_instance=None, value="No Central Heating in Property", From 5ab07d69030cb60cd62040ed82d68be3eb516ef5 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 27 Jan 2026 09:07:12 +0000 Subject: [PATCH 38/50] make element keys and values consistent --- backend/condition/domain/element.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/condition/domain/element.py b/backend/condition/domain/element.py index f78f2d52..2f6bac42 100644 --- a/backend/condition/domain/element.py +++ b/backend/condition/domain/element.py @@ -55,7 +55,7 @@ class Element(str, Enum): GARAGE_WALLS = "garage_walls" PARTY_WALL_FIRE_BREAK = "party_wall_fire_break" EXTERNAL_BRICKWORK_POINTING = "external_brickwork_pointing" - INTERNAL_DOWNPIPES_EXTERNAL_AREA = "internal_downpipes_in_external_area" + INTERNAL_DOWNPIPES_EXTERNAL_AREA = "internal_downpipes_external_area" # ====================== # EXTERNAL – WINDOWS @@ -157,7 +157,7 @@ class Element(str, Enum): HEAT_DETECTION = "heat_detection" CARBON_MONOXIDE_DETECTION = "carbon_monoxide_detection" FIRE_DOOR_RATING = "fire_door_rating" - FIRE_RISK_ASSESSMENT = "fire" + FIRE_RISK_ASSESSMENT = "fire_risk_assessment" INTERNAL_WIRING = ( "internal_wiring" # Is this definitely different from ELECTRICAL_WIRING? ) @@ -175,7 +175,7 @@ class Element(str, Enum): COMMUNAL_CCTV = "communal_cctv" COMMUNAL_BIN_STORE = "communal_bin_store" COMMUNAL_BIN_STORE_DOORS = "communal_bin_store_doors" - COMMUNAL_BIN_STORE_WALLS = "communal_bin_store_wall" + COMMUNAL_BIN_STORE_WALLS = "communal_bin_store_walls" COMMUNAL_BIN_STORE_ROOF = "communal_bin_store_roof" COMMUNAL_REFUSE_CHUTE = "communal_refuse_chute" COMMUNAL_FLOOR_COVERING = "communal_floor_covering" @@ -216,11 +216,11 @@ class Element(str, Enum): # ====================== FFHH_DAMP = "ffhh_damp" FFHH_HOT_AND_COLD_WATER = "ffhh_hold_and_cold_water" - FFHH_DRAINAGE_LAVATORIES = "ffhh_drainage_or_lavatories" - FFHH_NEGLECTED = "ffhh_neglected_and_in_bad_condition" + FFHH_DRAINAGE_LAVATORIES = "ffhh_drainage_lavatories" + FFHH_NEGLECTED = "ffhh_neglected" FFHH_NATURAL_LIGHT = "ffhh_natural_light" FFHH_VENTILATION = "ffhh_ventilation" - FFHH_FOOD_PREP_AND_WASHUP = "ffhh_prepare_and_cook_food_or_wash_up" + FFHH_FOOD_PREP_AND_WASHUP = "ffhh_food_prep_and_washup" FFHH_UNSAFE_LAYOUT = "ffhh_unsafe_layout" FFHH_UNSTABLE_BUILDING = "ffhh_unstable_building" From 9509306e3d8621f5346daabdf94c5fa70cc552e3 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 27 Jan 2026 13:08:35 +0000 Subject: [PATCH 39/50] =?UTF-8?q?Add=20aspect=20instance=20to=20asset=20co?= =?UTF-8?q?ndition=20and=20modify=20how=20peabody=20walls=20are=20mapped?= =?UTF-8?q?=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/condition/domain/asset_condition.py | 4 +- .../domain/mapping/element_mapping.py | 1 + .../mapping/peabody/peabody_element_map.py | 2 +- .../tests/mapping/test_peabody_mapper.py | 114 ++++++++++++++++++ 4 files changed, 118 insertions(+), 3 deletions(-) diff --git a/backend/condition/domain/asset_condition.py b/backend/condition/domain/asset_condition.py index 1b157a6b..8b054f45 100644 --- a/backend/condition/domain/asset_condition.py +++ b/backend/condition/domain/asset_condition.py @@ -13,6 +13,8 @@ class AssetCondition: element: Element aspect_type: AspectType + element_instance: Optional[int] = None + aspect_instance: Optional[int] = None value: Optional[str] = None @@ -20,7 +22,5 @@ class AssetCondition: install_date: Optional[date] = None renewal_year: Optional[int] = None - element_instance: Optional[int] = None - source_system: Optional[str] = None comments: Optional[str] = None diff --git a/backend/condition/domain/mapping/element_mapping.py b/backend/condition/domain/mapping/element_mapping.py index 01e1f316..c93862c8 100644 --- a/backend/condition/domain/mapping/element_mapping.py +++ b/backend/condition/domain/mapping/element_mapping.py @@ -10,3 +10,4 @@ class ElementMapping: element: Element aspect_type: AspectType element_instance: Optional[int] = None + aspect_instance: Optional[int] = None diff --git a/backend/condition/domain/mapping/peabody/peabody_element_map.py b/backend/condition/domain/mapping/peabody/peabody_element_map.py index 2485136b..1f9cceee 100644 --- a/backend/condition/domain/mapping/peabody/peabody_element_map.py +++ b/backend/condition/domain/mapping/peabody/peabody_element_map.py @@ -58,7 +58,7 @@ PEABODY_ELEMENT_MAP = { element=Element.GARAGE_WALLS, aspect_type=AspectType.MATERIAL ), (53, 23): ElementMapping( - element=Element.PRIMARY_WALL, aspect_type=AspectType.FINISH + element=Element.EXTERNAL_WALL, aspect_type=AspectType.FINISH ), (53, 30): ElementMapping( element=Element.SECONDARY_WALL, aspect_type=AspectType.FINISH diff --git a/backend/condition/tests/mapping/test_peabody_mapper.py b/backend/condition/tests/mapping/test_peabody_mapper.py index a975a308..9997dfa8 100644 --- a/backend/condition/tests/mapping/test_peabody_mapper.py +++ b/backend/condition/tests/mapping/test_peabody_mapper.py @@ -90,3 +90,117 @@ def test_peabody_mapper_maps_property(): for i, (actual, expected) in enumerate(zip(actual_assets, expected_assets)): assert actual == expected, f"Mismatch at index {i}" + + +def test_wall_primary_and_secondary_wall_finish_map_correctly(): + # arrange + peabody_property = PeabodyProperty( + uprn=1, + assets=[ + PeabodyAssetCondition( + lo_reference="1000RAND0000", + full_address="FLAT 1 RANDOM SQUARE FAKE STREET LONDON E1 1EE", + location_type_code=1, + parent_lo_reference="RAND1000", + element_code=53, + element="External", + sub_element_code=23, + sub_element="Primary Wall Finish", + material_code=4, + material_or_answer="Pointed", + renewal_quantity=65, + renewal_year=2045, + renewal_cost=3835, + cloned="N", + lo_type_code=1, + condition_survey_date=datetime(2024, 2, 15, 12, 47, 0), + ), + PeabodyAssetCondition( + lo_reference="1000RAND0000", + full_address="FLAT 1 RANDOM SQUARE FAKE STREET LONDON E1 1EE", + location_type_code=1, + parent_lo_reference="RAND1000", + element_code=120, + element="WALLS", + sub_element_code=2, + sub_element="Wall Finish", + material_code=1, + material_or_answer="Pointing", + renewal_quantity=1, + renewal_year=2069, + renewal_cost=2450, + cloned="N", + lo_type_code=1, + condition_survey_date=datetime(2014, 2, 15, 12, 47, 0), + ), + PeabodyAssetCondition( + lo_reference="1000RAND0000", + full_address="FLAT 1 RANDOM SQUARE FAKE STREET LONDON E1 1EE", + location_type_code=1, + parent_lo_reference="RAND1000", + element_code=53, + element="External", + sub_element_code=30, + sub_element="Secondary Wall Finish", + material_code=8, + material_or_answer="Tile Hung", + renewal_quantity=8, + renewal_year=2049, + renewal_cost=472, + cloned="N", + lo_type_code=1, + condition_survey_date=datetime(2014, 2, 15, 12, 47, 0), + ), + ], + ) + mapper = PeabodyMapper() + + expected_assets: List[AssetCondition] = [ + AssetCondition( + uprn=1, + element=Element.EXTERNAL_WALLS, + aspect_type=AspectType.FINISH, + value="Pointed", + element_instance=1, + aspect_instance=1, + quantity=65, + install_date=None, + renewal_year=2045, + source_system=None, + comments=None, + ), + AssetCondition( + uprn=1, + element=Element.EXTERNAL_WALLS, + aspect_type=AspectType.FINISH, + value="Pointing", + element_instance=1, + aspect_instance=1, + quantity=1, + install_date=None, + renewal_year=2069, + source_system=None, + comments=None, + ), + AssetCondition( + uprn=1, + element=Element.EXTERNAL_WALLS, + aspect_type=AspectType.FINISH, + value="Tile Hung", + element_instance=1, + aspect_instance=2, + quantity=8, + install_date=None, + renewal_year=2049, + source_system=None, + comments=None, + ), + ] + # act + actual_assets = mapper.map_asset_conditions_for_property(peabody_property) + + # assert + assert len(actual_assets) == len(expected_assets) + + for i, (actual, expected) in enumerate(zip(actual_assets, expected_assets)): + assert actual == expected, f"Mismatch at index {i}" From 6a2bb26baeacee877bcd1477925ac0d4acb0d9a2 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 27 Jan 2026 13:29:06 +0000 Subject: [PATCH 40/50] Redefine conditions data structures. Mapping tests broken --- ...asset_condition.py => aspect_condition.py} | 15 +- backend/condition/domain/element.py | 269 +------------- backend/condition/domain/element_type.py | 261 ++++++++++++++ .../domain/mapping/lbwf/lbwf_element_map.py | 242 ++++++------- .../domain/mapping/lbwf/lbwf_mapper.py | 7 +- backend/condition/domain/mapping/mapper.py | 4 +- .../mapping/peabody/peabody_element_map.py | 330 +++++++++--------- .../domain/mapping/peabody/peabody_mapper.py | 8 +- .../domain/property_condition_survey.py | 14 + backend/condition/processor.py | 4 +- .../tests/mapping/test_lbwf_mapper.py | 44 +-- .../tests/mapping/test_peabody_mapper.py | 28 +- 12 files changed, 623 insertions(+), 603 deletions(-) rename backend/condition/domain/{asset_condition.py => aspect_condition.py} (57%) create mode 100644 backend/condition/domain/element_type.py create mode 100644 backend/condition/domain/property_condition_survey.py diff --git a/backend/condition/domain/asset_condition.py b/backend/condition/domain/aspect_condition.py similarity index 57% rename from backend/condition/domain/asset_condition.py rename to backend/condition/domain/aspect_condition.py index 8b054f45..75b46b09 100644 --- a/backend/condition/domain/asset_condition.py +++ b/backend/condition/domain/aspect_condition.py @@ -1,26 +1,17 @@ from dataclasses import dataclass -from datetime import date from typing import Optional -from xml.dom.minidom import Element +from datetime import date from backend.condition.domain.aspect_type import AspectType -from backend.condition.domain.element import Element @dataclass -class AssetCondition: - uprn: int - - element: Element +class AspectCondition: aspect_type: AspectType - element_instance: Optional[int] = None - aspect_instance: Optional[int] = None + aspect_instance: int value: Optional[str] = None - quantity: Optional[int] = None install_date: Optional[date] = None renewal_year: Optional[int] = None - - source_system: Optional[str] = None comments: Optional[str] = None diff --git a/backend/condition/domain/element.py b/backend/condition/domain/element.py index 2f6bac42..7aca11fd 100644 --- a/backend/condition/domain/element.py +++ b/backend/condition/domain/element.py @@ -1,261 +1,12 @@ -from enum import Enum +from dataclasses import dataclass +from typing import List + +from backend.condition.domain.aspect_condition import AspectCondition +from backend.condition.domain.element_type import ElementType -class Element(str, Enum): - - # ====================== - # PROPERTY / GENERAL - # ====================== - PROPERTY = "property" - PROPERTY_CONSTRUCTION_TYPE = "property_construction_type" - PROPERTY_CLASSIFICATION = "property_classification" - PROPERTY_AGE_BAND = "property_age_band" - STOREY_COUNT = "storey_count" - FLOOR_LEVEL = "floor_level" - FLOOR_LEVEL_FRONT_DOOR = "floor_level_front_door" - ACCESSIBLE_HOUSING_REGISTER = "accessible_housing_register" - ASBESTOS = "asbestos" - QUALITY_STANDARD = "quality_standard" - CCU = "ccu" - PASSENGER_LIFT = "passenger_lift" - STAIRLIFT = "stairlift" - DISABLED_HOIST_TRACKING = "disabled_hoist_tracking" - DISABLED_FACILITIES = "disabled_facilities" - STEPS_TO_FRONT_DOOR = "steps_to_front_door" - - # ====================== - # EXTERNAL – ROOF - # ====================== - ROOF = "roof" - PITCHED_ROOF_COVERING = "pitched_roof_covering" - FLAT_ROOF_COVERING = "flat_roof_covering" - RAINWATER_GOODS = "rainwater_goods" - LOFT_INSULATION = "loft_insulation" - PORCH_CANOPY = "porch_canopy" - CHIMNEY = "chimney" - FASCIA = "fascia" - SOFFIT = "soffit" - FASCIA_SOFFIT_BARGEBOARDS = "fascia_soffit_bargeboards" - GUTTERS = "gutters" - STORE_ROOF = "store_roof" - GARAGE_ROOF = "garage_roof" - GARAGE_AND_STORE_ROOF = "garage_and_store_roof" - - # ====================== - # EXTERNAL – WALLS - # ====================== - EXTERNAL_WALL = "external_wall" - EXTERNAL_NOISE_INSULATION = "external_noise_insulation" - PRIMARY_WALL = "primary_wall" - SECONDARY_WALL = "secondary_wall" - DOWNPIPES = "downpipes" - EXTERNAL_DECORATION = "external_decoration" - CLADDING = "cladding" - SPANDREL_PANELS = "spandrel_panels" - GARAGE_WALLS = "garage_walls" - PARTY_WALL_FIRE_BREAK = "party_wall_fire_break" - EXTERNAL_BRICKWORK_POINTING = "external_brickwork_pointing" - INTERNAL_DOWNPIPES_EXTERNAL_AREA = "internal_downpipes_external_area" - - # ====================== - # EXTERNAL – WINDOWS - # ====================== - EXTERNAL_WINDOWS = "external_windows" - COMMUNAL_WINDOWS = "communal_windows" - SECONDARY_GLAZING = "secondary_glazing" - STORE_WINDOWS = "store_windows" - GARAGE_WINDOWS = "garage_windows" - GARAGE_AND_STORE_WINDOWS = "garage_and_store_windows" - - # ====================== - # EXTERNAL – DOORS - # ====================== - EXTERNAL_DOOR = "external_door" - FRONT_DOOR = "front_door" - REAR_DOOR = "rear_door" - STORE_DOOR = "store_door" - GARAGE_DOOR = "garage_door" - GARAGE_AND_STORE_DOOR = "garage_and_store_door" - COMMUNAL_ENTRANCE_DOOR = "communal_entrance_door" - MAIN_DOOR = "main_door" - BLOCK_ENTRANCE_DOOR = "block_entrance_door" - LINTEL = "lintel" - PATIO_FRENCH_DOOR = "patio_french_door" - DOOR_ENTRY_HANDSET = "door_entry_handset" - - # ====================== - # EXTERNAL – AREAS - # ====================== - PATHS_AND_HARDSTANDINGS = "paths_and_hardstandings" - PARKING_AREAS = "parking_areas" - BOUNDARY_WALLS = "boundary_walls" - FRONT_FENCING = "front_fencing" - REAR_FENCING = "rear_fencing" - SIDE_FENCING = "side_fencing" - REAR_GATE = "rear_gate" - FRONT_GATE = "front_gate" - GATES = "gates" - RETAINING_WALLS = "retaining_walls" - PRIVATE_BALCONY = "private_balcony" - BALCONY_BALUSTRADE = "balcony_balustrade" - OUTBUILDINGS = "outbuildings" - GARAGE_STRUCTURE = "garage_structure" - PAVING = "paving" - ROADS = "roads" - SOIL_AND_VENT = "soil_and_vent" - SOLAR_THERMALS = "solar_thermals" - DROP_KERB = "drop_kerb" - OUTBUILDING_OVERHAUL = "outbuilding_overhaul" - EXTERNAL_STRUCTURAL_DEFECTS = "external_structural_defects" - ACCESS_RAMP = "access_ramp" - - # ====================== - # INTERNAL – KITCHEN - # ====================== - KITCHEN = "kitchen" - KITCHEN_SPACE_LAYOUT = "kitchen_space_layout" - TENANT_INSTALLED_KITCHEN = "tenant_installed_kitchen" - KITCHEN_EXTRACTOR_FAN = "kitchen_extractor_fan" - - # ====================== - # INTERNAL – BATHROOM - # ====================== - BATHROOM = "bathroom" - SECONDARY_BATHROOM = "secondary_bathroom" - SECONDARY_TOILET = "secondary_toilet" - BATHROOM_EXTRACTOR_FAN = "bathroom_extractor_fan" - ADDITIONAL_WC_OR_WHB = "additional_wc_or_whb" - BATHROOM_REMAINING_LIFE_SOURCE = "bathroom_remaining_life_source" - KITCHEN_REMAINING_LIFE_SOURCE = "kitchen_remaining_life_source" - - # ====================== - # INTERNAL – HEATING / WATER - # ====================== - CENTRAL_HEATING = "central_heating" - HEATING_BOILER = "heating_boiler" - HEATING_DISTRIBUTION = "heating_distribution" - SECONDARY_HEATING = "secondary_heating" - HOT_WATER_SYSTEM = "hot_water_system" - COLD_WATER_STORAGE = "cold_water_storage" - HEATING_SYSTEM = "heating_system" - BOILER_FUEL = "boiler_fuel" - WATER_HEATING = "water_heating" - PROGRAMMABLE_HEATING = "programmable_heating" - COMMUNITY_HEATING = ( - "community_heating" # Is this definitely different from COMMUNAL_HEATING? - ) - GAS_AVAILABLE = "gas_available" - HEAT_RECOVERY_UNITS = "heat_recovery_units" - HEATING_IMPROVEMENTS = "heating_improvements" - - # ====================== - # INTERNAL – ELECTRICS / FIRE - # ====================== - ELECTRICAL_WIRING = "electrical_wiring" - CONSUMER_UNIT = "consumer_unit" - SMOKE_DETECTION = "smoke_detection" - HEAT_DETECTION = "heat_detection" - CARBON_MONOXIDE_DETECTION = "carbon_monoxide_detection" - FIRE_DOOR_RATING = "fire_door_rating" - FIRE_RISK_ASSESSMENT = "fire_risk_assessment" - INTERNAL_WIRING = ( - "internal_wiring" # Is this definitely different from ELECTRICAL_WIRING? - ) - ELECTRICS = "electrics" - - # ====================== - # COMMUNAL - # ====================== - COMMUNAL_HEATING = "communal_heating" - COMMUNAL_BOILER = "communal_boiler" - COMMUNAL_ELECTRICS = "communal_electrics" - COMMUNAL_FIRE_ALARM = "communal_fire_alarm" - COMMUNAL_EMERGENCY_LIGHTING = "communal_emergency_lighting" - COMMUNAL_DOOR_ENTRY = "communal_door_entry" - COMMUNAL_CCTV = "communal_cctv" - COMMUNAL_BIN_STORE = "communal_bin_store" - COMMUNAL_BIN_STORE_DOORS = "communal_bin_store_doors" - COMMUNAL_BIN_STORE_WALLS = "communal_bin_store_walls" - COMMUNAL_BIN_STORE_ROOF = "communal_bin_store_roof" - COMMUNAL_REFUSE_CHUTE = "communal_refuse_chute" - COMMUNAL_FLOOR_COVERING = "communal_floor_covering" - COMMUNAL_KITCHEN = "communal_kitchen" - COMMUNAL_BATHROOM = "communal_bathroom" - COMMUNAL_TOILETS = "communal_toilets" - COMMUNAL_GATES = "communal_gates" - COMMUNAL_LIFT = "communal_lift" - COMMUNAL_PASSENGER_LIFT = "communal_passenger_lift" - COMMUNAL_BALCONY_WALKWAY = "communal_balcony_walkway" - COMMUNAL_ENTRANCE = "communal_entrance" - COMMUNAL_INTERNAL_DECORATIONS = "communal_internal_decorations" - COMMUNAL_INTERNAL_FLOOR = "communal_internal_floor" - COMMUNAL_WALKWAYS = "communal_walkways" - COMMUNAL_EXTERNAL_DOORS = "communal_external_doors" - COMMUNAL_STAIRS = "communal_stairs" - COMMUNAL_AERIAL = "communal_aerial" - COMMUNAL_AOV = "communal_aov" - COMMUNAL_INTERNAL_DOORS = "communal_internal_doors" - COMMUNAL_LATERAL_MAINS = "communal_lateral_mains" - COMMUNAL_LIGHTING = "communal_lighting" - COMMUNAL_LIGHTING_CONDUCTOR = "communal_lighting_conductor" - COMMUNAL_STORE_ROOF = "communal_store_roof" - COMMUNAL_STORE_WALLS = "communal_store_walls" - COMMUNAL_STORE_DOORS = "communal_store_doors" - COMMUNAL_WARDEN_CALL_SYSTEM = "communal_warden_call_system" - COMMUNAL_BMS = "communal_bms" - COMMUNAL_BOOSTER_PUMP = "communal_booster_pump" - COMMUNAL_DRY_RISER = "communal_dry_riser" - COMMUNAL_WET_RISER = "communal_wet_riser" - COMMUNAL_COLD_WATER_STORAGE = "communal_cold_water_storage" - COMMUNAL_SPRINKLER = "communal_sprinkler" - COMMUNAL_PLUG_SOCKETS = "communal_plug_sockets" - COMMUNAL_CIRCULATION_SPACE = "communal_circulation_space" - - # ====================== - # FITNESS FOR HUMAN HABITATION - # ====================== - FFHH_DAMP = "ffhh_damp" - FFHH_HOT_AND_COLD_WATER = "ffhh_hold_and_cold_water" - FFHH_DRAINAGE_LAVATORIES = "ffhh_drainage_lavatories" - FFHH_NEGLECTED = "ffhh_neglected" - FFHH_NATURAL_LIGHT = "ffhh_natural_light" - FFHH_VENTILATION = "ffhh_ventilation" - FFHH_FOOD_PREP_AND_WASHUP = "ffhh_food_prep_and_washup" - FFHH_UNSAFE_LAYOUT = "ffhh_unsafe_layout" - FFHH_UNSTABLE_BUILDING = "ffhh_unstable_building" - - # ========================================================== - # HHSRS – ALL 29 HAZARDS - # ========================================================== - - HHSRS_DAMP_AND_MOULD = "hhsrs_damp_and_mould" - HHSRS_EXCESS_COLD = "hhsrs_excess_cold" - HHSRS_EXCESS_HEAT = "hhsrs_excess_heat" - HHSRS_ASBESTOS_AND_MMF = "hhsrs_asbestos_and_mmf" - HHSRS_BIOCIDES = "hhsrs_biocides" - HHSRS_CARBON_MONOXIDE = "hhsrs_carbon_monoxide" - HHSRS_LEAD = "hhsrs_lead" - HHSRS_RADIATION = "hhsrs_radiation" - HHSRS_UNCOMBUSTED_FUEL_GAS = "hhsrs_uncombusted_fuel_gas" - HHSRS_VOLATILE_ORGANIC_COMPOUNDS = "hhsrs_volatile_organic_compounds" - HHSRS_CROWDING_AND_SPACE = "hhsrs_crowding_and_space" - HHSRS_ENTRY_BY_INTRUDERS = "hhsrs_entry_by_intruders" - HHSRS_LIGHTING = "hhsrs_lighting" - HHSRS_NOISE = "hhsrs_noise" - HHSRS_DOMESTIC_HYGIENE_PESTS_REFUSE = "hhsrs_domestic_hygiene_pests_refuse" - HHSRS_FOOD_SAFETY = "hhsrs_food_safety" - HHSRS_PERSONAL_HYGIENE_SANITATION = "hhsrs_personal_hygiene_sanitation" - HHSRS_WATER_SUPPLY = "hhsrs_water_supply" - HHSRS_FALLS_ASSOCIATED_WITH_BATHS = "hhsrs_falls_associated_with_baths" - HHSRS_FALLS_ON_LEVEL_SURFACES = "hhsrs_falls_on_level_surfaces" - HHSRS_FALLS_ON_STAIRS = "hhsrs_falls_on_stairs" - HHSRS_FALLS_BETWEEN_LEVELS = "hhsrs_falls_between_levels" - HHSRS_ELECTRICAL_HAZARDS = "hhsrs_electrical_hazards" - HHSRS_FIRE = "hhsrs_fire" - HHSRS_FLAMES_HOT_SURFACES = "hhsrs_flames_hot_surfaces" - HHSRS_COLLISION_AND_ENTRAPMENT = "hhsrs_collision_and_entrapment" - HHSRS_COLLISION_HAZARDS_LOW_HEADROOM = "hhsrs_collision_hazards_low_headroom" - HHSRS_EXPLOSIONS = "hhsrs_explosions" - HHSRS_ERGONOMICS = "hhsrs_ergonomics" - HHSRS_STRUCTURAL_COLLAPSE = "hhsrs_structural_collapse" - HHSRS_AMENITIES = "hhsrs_amenities" +@dataclass +class Element: + element: ElementType + element_instance: int + aspect_conditions: List[AspectCondition] diff --git a/backend/condition/domain/element_type.py b/backend/condition/domain/element_type.py new file mode 100644 index 00000000..32897895 --- /dev/null +++ b/backend/condition/domain/element_type.py @@ -0,0 +1,261 @@ +from enum import Enum + + +class ElementType(str, Enum): + + # ====================== + # PROPERTY / GENERAL + # ====================== + PROPERTY = "property" + PROPERTY_CONSTRUCTION_TYPE = "property_construction_type" + PROPERTY_CLASSIFICATION = "property_classification" + PROPERTY_AGE_BAND = "property_age_band" + STOREY_COUNT = "storey_count" + FLOOR_LEVEL = "floor_level" + FLOOR_LEVEL_FRONT_DOOR = "floor_level_front_door" + ACCESSIBLE_HOUSING_REGISTER = "accessible_housing_register" + ASBESTOS = "asbestos" + QUALITY_STANDARD = "quality_standard" + CCU = "ccu" + PASSENGER_LIFT = "passenger_lift" + STAIRLIFT = "stairlift" + DISABLED_HOIST_TRACKING = "disabled_hoist_tracking" + DISABLED_FACILITIES = "disabled_facilities" + STEPS_TO_FRONT_DOOR = "steps_to_front_door" + + # ====================== + # EXTERNAL – ROOF + # ====================== + ROOF = "roof" + PITCHED_ROOF_COVERING = "pitched_roof_covering" + FLAT_ROOF_COVERING = "flat_roof_covering" + RAINWATER_GOODS = "rainwater_goods" + LOFT_INSULATION = "loft_insulation" + PORCH_CANOPY = "porch_canopy" + CHIMNEY = "chimney" + FASCIA = "fascia" + SOFFIT = "soffit" + FASCIA_SOFFIT_BARGEBOARDS = "fascia_soffit_bargeboards" + GUTTERS = "gutters" + STORE_ROOF = "store_roof" + GARAGE_ROOF = "garage_roof" + GARAGE_AND_STORE_ROOF = "garage_and_store_roof" + + # ====================== + # EXTERNAL – WALLS + # ====================== + EXTERNAL_WALL = "external_wall" + EXTERNAL_NOISE_INSULATION = "external_noise_insulation" + PRIMARY_WALL = "primary_wall" + SECONDARY_WALL = "secondary_wall" + DOWNPIPES = "downpipes" + EXTERNAL_DECORATION = "external_decoration" + CLADDING = "cladding" + SPANDREL_PANELS = "spandrel_panels" + GARAGE_WALLS = "garage_walls" + PARTY_WALL_FIRE_BREAK = "party_wall_fire_break" + EXTERNAL_BRICKWORK_POINTING = "external_brickwork_pointing" + INTERNAL_DOWNPIPES_EXTERNAL_AREA = "internal_downpipes_external_area" + + # ====================== + # EXTERNAL – WINDOWS + # ====================== + EXTERNAL_WINDOWS = "external_windows" + COMMUNAL_WINDOWS = "communal_windows" + SECONDARY_GLAZING = "secondary_glazing" + STORE_WINDOWS = "store_windows" + GARAGE_WINDOWS = "garage_windows" + GARAGE_AND_STORE_WINDOWS = "garage_and_store_windows" + + # ====================== + # EXTERNAL – DOORS + # ====================== + EXTERNAL_DOOR = "external_door" + FRONT_DOOR = "front_door" + REAR_DOOR = "rear_door" + STORE_DOOR = "store_door" + GARAGE_DOOR = "garage_door" + GARAGE_AND_STORE_DOOR = "garage_and_store_door" + COMMUNAL_ENTRANCE_DOOR = "communal_entrance_door" + MAIN_DOOR = "main_door" + BLOCK_ENTRANCE_DOOR = "block_entrance_door" + LINTEL = "lintel" + PATIO_FRENCH_DOOR = "patio_french_door" + DOOR_ENTRY_HANDSET = "door_entry_handset" + + # ====================== + # EXTERNAL – AREAS + # ====================== + PATHS_AND_HARDSTANDINGS = "paths_and_hardstandings" + PARKING_AREAS = "parking_areas" + BOUNDARY_WALLS = "boundary_walls" + FRONT_FENCING = "front_fencing" + REAR_FENCING = "rear_fencing" + SIDE_FENCING = "side_fencing" + REAR_GATE = "rear_gate" + FRONT_GATE = "front_gate" + GATES = "gates" + RETAINING_WALLS = "retaining_walls" + PRIVATE_BALCONY = "private_balcony" + BALCONY_BALUSTRADE = "balcony_balustrade" + OUTBUILDINGS = "outbuildings" + GARAGE_STRUCTURE = "garage_structure" + PAVING = "paving" + ROADS = "roads" + SOIL_AND_VENT = "soil_and_vent" + SOLAR_THERMALS = "solar_thermals" + DROP_KERB = "drop_kerb" + OUTBUILDING_OVERHAUL = "outbuilding_overhaul" + EXTERNAL_STRUCTURAL_DEFECTS = "external_structural_defects" + ACCESS_RAMP = "access_ramp" + + # ====================== + # INTERNAL – KITCHEN + # ====================== + KITCHEN = "kitchen" + KITCHEN_SPACE_LAYOUT = "kitchen_space_layout" + TENANT_INSTALLED_KITCHEN = "tenant_installed_kitchen" + KITCHEN_EXTRACTOR_FAN = "kitchen_extractor_fan" + + # ====================== + # INTERNAL – BATHROOM + # ====================== + BATHROOM = "bathroom" + SECONDARY_BATHROOM = "secondary_bathroom" + SECONDARY_TOILET = "secondary_toilet" + BATHROOM_EXTRACTOR_FAN = "bathroom_extractor_fan" + ADDITIONAL_WC_OR_WHB = "additional_wc_or_whb" + BATHROOM_REMAINING_LIFE_SOURCE = "bathroom_remaining_life_source" + KITCHEN_REMAINING_LIFE_SOURCE = "kitchen_remaining_life_source" + + # ====================== + # INTERNAL – HEATING / WATER + # ====================== + CENTRAL_HEATING = "central_heating" + HEATING_BOILER = "heating_boiler" + HEATING_DISTRIBUTION = "heating_distribution" + SECONDARY_HEATING = "secondary_heating" + HOT_WATER_SYSTEM = "hot_water_system" + COLD_WATER_STORAGE = "cold_water_storage" + HEATING_SYSTEM = "heating_system" + BOILER_FUEL = "boiler_fuel" + WATER_HEATING = "water_heating" + PROGRAMMABLE_HEATING = "programmable_heating" + COMMUNITY_HEATING = ( + "community_heating" # Is this definitely different from COMMUNAL_HEATING? + ) + GAS_AVAILABLE = "gas_available" + HEAT_RECOVERY_UNITS = "heat_recovery_units" + HEATING_IMPROVEMENTS = "heating_improvements" + + # ====================== + # INTERNAL – ELECTRICS / FIRE + # ====================== + ELECTRICAL_WIRING = "electrical_wiring" + CONSUMER_UNIT = "consumer_unit" + SMOKE_DETECTION = "smoke_detection" + HEAT_DETECTION = "heat_detection" + CARBON_MONOXIDE_DETECTION = "carbon_monoxide_detection" + FIRE_DOOR_RATING = "fire_door_rating" + FIRE_RISK_ASSESSMENT = "fire_risk_assessment" + INTERNAL_WIRING = ( + "internal_wiring" # Is this definitely different from ELECTRICAL_WIRING? + ) + ELECTRICS = "electrics" + + # ====================== + # COMMUNAL + # ====================== + COMMUNAL_HEATING = "communal_heating" + COMMUNAL_BOILER = "communal_boiler" + COMMUNAL_ELECTRICS = "communal_electrics" + COMMUNAL_FIRE_ALARM = "communal_fire_alarm" + COMMUNAL_EMERGENCY_LIGHTING = "communal_emergency_lighting" + COMMUNAL_DOOR_ENTRY = "communal_door_entry" + COMMUNAL_CCTV = "communal_cctv" + COMMUNAL_BIN_STORE = "communal_bin_store" + COMMUNAL_BIN_STORE_DOORS = "communal_bin_store_doors" + COMMUNAL_BIN_STORE_WALLS = "communal_bin_store_walls" + COMMUNAL_BIN_STORE_ROOF = "communal_bin_store_roof" + COMMUNAL_REFUSE_CHUTE = "communal_refuse_chute" + COMMUNAL_FLOOR_COVERING = "communal_floor_covering" + COMMUNAL_KITCHEN = "communal_kitchen" + COMMUNAL_BATHROOM = "communal_bathroom" + COMMUNAL_TOILETS = "communal_toilets" + COMMUNAL_GATES = "communal_gates" + COMMUNAL_LIFT = "communal_lift" + COMMUNAL_PASSENGER_LIFT = "communal_passenger_lift" + COMMUNAL_BALCONY_WALKWAY = "communal_balcony_walkway" + COMMUNAL_ENTRANCE = "communal_entrance" + COMMUNAL_INTERNAL_DECORATIONS = "communal_internal_decorations" + COMMUNAL_INTERNAL_FLOOR = "communal_internal_floor" + COMMUNAL_WALKWAYS = "communal_walkways" + COMMUNAL_EXTERNAL_DOORS = "communal_external_doors" + COMMUNAL_STAIRS = "communal_stairs" + COMMUNAL_AERIAL = "communal_aerial" + COMMUNAL_AOV = "communal_aov" + COMMUNAL_INTERNAL_DOORS = "communal_internal_doors" + COMMUNAL_LATERAL_MAINS = "communal_lateral_mains" + COMMUNAL_LIGHTING = "communal_lighting" + COMMUNAL_LIGHTING_CONDUCTOR = "communal_lighting_conductor" + COMMUNAL_STORE_ROOF = "communal_store_roof" + COMMUNAL_STORE_WALLS = "communal_store_walls" + COMMUNAL_STORE_DOORS = "communal_store_doors" + COMMUNAL_WARDEN_CALL_SYSTEM = "communal_warden_call_system" + COMMUNAL_BMS = "communal_bms" + COMMUNAL_BOOSTER_PUMP = "communal_booster_pump" + COMMUNAL_DRY_RISER = "communal_dry_riser" + COMMUNAL_WET_RISER = "communal_wet_riser" + COMMUNAL_COLD_WATER_STORAGE = "communal_cold_water_storage" + COMMUNAL_SPRINKLER = "communal_sprinkler" + COMMUNAL_PLUG_SOCKETS = "communal_plug_sockets" + COMMUNAL_CIRCULATION_SPACE = "communal_circulation_space" + + # ====================== + # FITNESS FOR HUMAN HABITATION + # ====================== + FFHH_DAMP = "ffhh_damp" + FFHH_HOT_AND_COLD_WATER = "ffhh_hold_and_cold_water" + FFHH_DRAINAGE_LAVATORIES = "ffhh_drainage_lavatories" + FFHH_NEGLECTED = "ffhh_neglected" + FFHH_NATURAL_LIGHT = "ffhh_natural_light" + FFHH_VENTILATION = "ffhh_ventilation" + FFHH_FOOD_PREP_AND_WASHUP = "ffhh_food_prep_and_washup" + FFHH_UNSAFE_LAYOUT = "ffhh_unsafe_layout" + FFHH_UNSTABLE_BUILDING = "ffhh_unstable_building" + + # ========================================================== + # HHSRS – ALL 29 HAZARDS + # ========================================================== + + HHSRS_DAMP_AND_MOULD = "hhsrs_damp_and_mould" + HHSRS_EXCESS_COLD = "hhsrs_excess_cold" + HHSRS_EXCESS_HEAT = "hhsrs_excess_heat" + HHSRS_ASBESTOS_AND_MMF = "hhsrs_asbestos_and_mmf" + HHSRS_BIOCIDES = "hhsrs_biocides" + HHSRS_CARBON_MONOXIDE = "hhsrs_carbon_monoxide" + HHSRS_LEAD = "hhsrs_lead" + HHSRS_RADIATION = "hhsrs_radiation" + HHSRS_UNCOMBUSTED_FUEL_GAS = "hhsrs_uncombusted_fuel_gas" + HHSRS_VOLATILE_ORGANIC_COMPOUNDS = "hhsrs_volatile_organic_compounds" + HHSRS_CROWDING_AND_SPACE = "hhsrs_crowding_and_space" + HHSRS_ENTRY_BY_INTRUDERS = "hhsrs_entry_by_intruders" + HHSRS_LIGHTING = "hhsrs_lighting" + HHSRS_NOISE = "hhsrs_noise" + HHSRS_DOMESTIC_HYGIENE_PESTS_REFUSE = "hhsrs_domestic_hygiene_pests_refuse" + HHSRS_FOOD_SAFETY = "hhsrs_food_safety" + HHSRS_PERSONAL_HYGIENE_SANITATION = "hhsrs_personal_hygiene_sanitation" + HHSRS_WATER_SUPPLY = "hhsrs_water_supply" + HHSRS_FALLS_ASSOCIATED_WITH_BATHS = "hhsrs_falls_associated_with_baths" + HHSRS_FALLS_ON_LEVEL_SURFACES = "hhsrs_falls_on_level_surfaces" + HHSRS_FALLS_ON_STAIRS = "hhsrs_falls_on_stairs" + HHSRS_FALLS_BETWEEN_LEVELS = "hhsrs_falls_between_levels" + HHSRS_ELECTRICAL_HAZARDS = "hhsrs_electrical_hazards" + HHSRS_FIRE = "hhsrs_fire" + HHSRS_FLAMES_HOT_SURFACES = "hhsrs_flames_hot_surfaces" + HHSRS_COLLISION_AND_ENTRAPMENT = "hhsrs_collision_and_entrapment" + HHSRS_COLLISION_HAZARDS_LOW_HEADROOM = "hhsrs_collision_hazards_low_headroom" + HHSRS_EXPLOSIONS = "hhsrs_explosions" + HHSRS_ERGONOMICS = "hhsrs_ergonomics" + HHSRS_STRUCTURAL_COLLAPSE = "hhsrs_structural_collapse" + HHSRS_AMENITIES = "hhsrs_amenities" diff --git a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py index 8d6ea858..dfd9ca4e 100644 --- a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py +++ b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py @@ -1,4 +1,4 @@ -from backend.condition.domain.element import Element +from backend.condition.domain.element_type import ElementType from backend.condition.domain.aspect_type import AspectType from backend.condition.domain.mapping.element_mapping import ElementMapping @@ -8,11 +8,11 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { # PROPERTY / GENERAL # ========================================================== "AHR_CAT": ElementMapping( - element=Element.ACCESSIBLE_HOUSING_REGISTER, + element=ElementType.ACCESSIBLE_HOUSING_REGISTER, aspect_type=AspectType.CATEGORY, ), "ASSETSAREA": ElementMapping( - element=Element.PROPERTY, + element=ElementType.PROPERTY, aspect_type=AspectType.AREA, ), # "DECNTHMINC": ElementMapping( @@ -20,301 +20,301 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { # aspect_type=AspectType.INCLUSION, # ), # Ignore this one "QUALITYSTD": ElementMapping( - element=Element.QUALITY_STANDARD, + element=ElementType.QUALITY_STANDARD, aspect_type=AspectType.TYPE, ), "EXTSTOREY": ElementMapping( - element=Element.PROPERTY, + element=ElementType.PROPERTY, aspect_type=AspectType.CONFIGURATION, ), "FLVL": ElementMapping( - element=Element.FLOOR_LEVEL_FRONT_DOOR, + element=ElementType.FLOOR_LEVEL_FRONT_DOOR, aspect_type=AspectType.LOCATION, ), "INTFLRLVL": ElementMapping( - element=Element.FLOOR_LEVEL, + element=ElementType.FLOOR_LEVEL, aspect_type=AspectType.LOCATION, ), "INTNSEINSL": ElementMapping( - element=Element.EXTERNAL_NOISE_INSULATION, # Maybe this shouldn't be "EXTERNAL_" + element=ElementType.EXTERNAL_NOISE_INSULATION, # Maybe this shouldn't be "EXTERNAL_" aspect_type=AspectType.ADEQUACY, ), "INTSTEPSFD": ElementMapping( - element=Element.STEPS_TO_FRONT_DOOR, + element=ElementType.STEPS_TO_FRONT_DOOR, aspect_type=AspectType.QUANTITY, ), # ========================================================== # ASBESTOS (NON-HHSRS RECORD) # ========================================================== "ASBESTOS": ElementMapping( - element=Element.ASBESTOS, + element=ElementType.ASBESTOS, aspect_type=AspectType.PRESENCE, ), # ========================================================== # INTERNAL – BATHROOMS & KITCHENS # ========================================================== "INTBTHRLOC": ElementMapping( - element=Element.BATHROOM, + element=ElementType.BATHROOM, aspect_type=AspectType.LOCATION, ), "INTBTHADEQ": ElementMapping( - element=Element.BATHROOM, + element=ElementType.BATHROOM, aspect_type=AspectType.ADEQUACY, ), "INTKITADEQ": ElementMapping( - element=Element.KITCHEN, + element=ElementType.KITCHEN, aspect_type=AspectType.ADEQUACY, ), "INTCKRLOC": ElementMapping( - element=Element.KITCHEN, + element=ElementType.KITCHEN, aspect_type=AspectType.LOCATION, ), "INTADDWCW": ElementMapping( - element=Element.ADDITIONAL_WC_OR_WHB, + element=ElementType.ADDITIONAL_WC_OR_WHB, aspect_type=AspectType.PRESENCE, ), "INTBTHREML": ElementMapping( - element=Element.BATHROOM_REMAINING_LIFE_SOURCE, + element=ElementType.BATHROOM_REMAINING_LIFE_SOURCE, aspect_type=AspectType.TYPE, ), "INTKITREML": ElementMapping( - element=Element.KITCHEN_REMAINING_LIFE_SOURCE, + element=ElementType.KITCHEN_REMAINING_LIFE_SOURCE, aspect_type=AspectType.TYPE, ), "INTTNTINST": ElementMapping( - element=Element.TENANT_INSTALLED_KITCHEN, + element=ElementType.TENANT_INSTALLED_KITCHEN, aspect_type=AspectType.TYPE, # Not certain about this aspect type - need more data ), # ========================================================== # INTERNAL – FIRE # ========================================================== "FRARISKRTG": ElementMapping( - element=Element.FIRE_RISK_ASSESSMENT, + element=ElementType.FIRE_RISK_ASSESSMENT, aspect_type=AspectType.RATING, ), "FRATYPE": ElementMapping( - element=Element.FIRE_RISK_ASSESSMENT, + element=ElementType.FIRE_RISK_ASSESSMENT, aspect_type=AspectType.TYPE, ), "FRAEVACSTR": ElementMapping( - element=Element.FIRE_RISK_ASSESSMENT, + element=ElementType.FIRE_RISK_ASSESSMENT, aspect_type=AspectType.STRATEGY, ), "INTSMKDET": ElementMapping( - element=Element.SMOKE_DETECTION, + element=ElementType.SMOKE_DETECTION, aspect_type=AspectType.PRESENCE, ), "INTCHEXTNT": ElementMapping( - element=Element.HEATING_SYSTEM, + element=ElementType.HEATING_SYSTEM, aspect_type=AspectType.EXTENT, ), # ========================================================== # HEATING & SERVICES # ========================================================== "INTCHEXTNT": ElementMapping( - element=Element.CENTRAL_HEATING, + element=ElementType.CENTRAL_HEATING, aspect_type=AspectType.EXTENT, ), "INTCHDIST": ElementMapping( - element=Element.HEATING_DISTRIBUTION, + element=ElementType.HEATING_DISTRIBUTION, aspect_type=AspectType.TYPE, ), "INTCHBLR": ElementMapping( - element=Element.HEATING_BOILER, + element=ElementType.HEATING_BOILER, aspect_type=AspectType.TYPE, ), "INTBOILERF": ElementMapping( - element=Element.BOILER_FUEL, + element=ElementType.BOILER_FUEL, aspect_type=AspectType.TYPE, ), "INTHTDISYS": ElementMapping( - element=Element.HEATING_SYSTEM, + element=ElementType.HEATING_SYSTEM, aspect_type=AspectType.DISTRIBUTION, ), "INTWTRHTNG": ElementMapping( - element=Element.WATER_HEATING, + element=ElementType.WATER_HEATING, aspect_type=AspectType.TYPE, ), "INTCOMHTG": ElementMapping( - element=Element.COMMUNITY_HEATING, + element=ElementType.COMMUNITY_HEATING, aspect_type=AspectType.TYPE, ), "INTELECTRC": ElementMapping( - element=Element.ELECTRICS, + element=ElementType.ELECTRICS, aspect_type=AspectType.WORK_REQUIRED, # Not certain about this aspect type - need more data ), "INTGASAVAI": ElementMapping( - element=Element.GAS_AVAILABLE, + element=ElementType.GAS_AVAILABLE, aspect_type=AspectType.PRESENCE, # Maybe should be AspectType.TYPE ? ), "INTHEATREC": ElementMapping( - element=Element.HEAT_RECOVERY_UNITS, + element=ElementType.HEAT_RECOVERY_UNITS, aspect_type=AspectType.PRESENCE, ), "INTHTIMP": ElementMapping( - element=Element.GAS_AVAILABLE, + element=ElementType.GAS_AVAILABLE, aspect_type=AspectType.WORK_REQUIRED, ), "INTPROGHTG": ElementMapping( - element=Element.PROGRAMMABLE_HEATING, + element=ElementType.PROGRAMMABLE_HEATING, aspect_type=AspectType.TYPE, # Should maybe be PRESENCE, but set to TYPE for consistency with Peabody data ), # ========================================================== # EXTERNAL – WALLS (INSTANCED) # ========================================================== "EXTWALLSTR": ElementMapping( - element=Element.EXTERNAL_WALL, + element=ElementType.EXTERNAL_WALL, aspect_type=AspectType.STRUCTURE, element_instance=1, ), "EXTWALLFN1": ElementMapping( - element=Element.EXTERNAL_WALL, + element=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH, element_instance=1, ), "EXTWALLFN2": ElementMapping( - element=Element.EXTERNAL_WALL, + element=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH, element_instance=2, ), "EXTWALLINS": ElementMapping( - element=Element.EXTERNAL_WALL, + element=ElementType.EXTERNAL_WALL, aspect_type=AspectType.INSULATION, ), "EXTWALLSPL": ElementMapping( - element=Element.EXTERNAL_WALL, + element=ElementType.EXTERNAL_WALL, aspect_type=AspectType.CONDITION, ), "EXTDWNPTYP": ElementMapping( - element=Element.DOWNPIPES, + element=ElementType.DOWNPIPES, aspect_type=AspectType.MATERIAL, ), "EXTGUTRTYP": ElementMapping( - element=Element.GUTTERS, + element=ElementType.GUTTERS, aspect_type=AspectType.MATERIAL, ), # ========================================================== # EXTERNAL – ROOFS (INSTANCED) # ========================================================== "EXTRFSTR1": ElementMapping( - element=Element.ROOF, + element=ElementType.ROOF, aspect_type=AspectType.STRUCTURE, element_instance=1, ), "EXTRFSTR2": ElementMapping( - element=Element.ROOF, + element=ElementType.ROOF, aspect_type=AspectType.STRUCTURE, element_instance=2, ), "EXTRFSTR3": ElementMapping( - element=Element.ROOF, + element=ElementType.ROOF, aspect_type=AspectType.STRUCTURE, element_instance=3, ), "EXTROOF1": ElementMapping( - element=Element.ROOF, + element=ElementType.ROOF, aspect_type=AspectType.COVERING, element_instance=1, ), "EXTROOF2": ElementMapping( - element=Element.ROOF, + element=ElementType.ROOF, aspect_type=AspectType.COVERING, element_instance=2, ), "EXTROOF3": ElementMapping( - element=Element.ROOF, + element=ElementType.ROOF, aspect_type=AspectType.COVERING, element_instance=3, ), "EXTCHIMNEY": ElementMapping( - element=Element.CHIMNEY, + element=ElementType.CHIMNEY, aspect_type=AspectType.WORK_REQUIRED, ), "EXTFASOFBR": ElementMapping( - element=Element.FASCIA_SOFFIT_BARGEBOARDS, + element=ElementType.FASCIA_SOFFIT_BARGEBOARDS, aspect_type=AspectType.MATERIAL, ), "EXTGARROOF": ElementMapping( - element=Element.GARAGE_ROOF, + element=ElementType.GARAGE_ROOF, aspect_type=AspectType.MATERIAL, ), "EXTGARSTRF": ElementMapping( - element=Element.GARAGE_AND_STORE_ROOF, + element=ElementType.GARAGE_AND_STORE_ROOF, aspect_type=AspectType.MATERIAL, ), "EXTSTRROOF": ElementMapping( - element=Element.STORE_ROOF, + element=ElementType.STORE_ROOF, aspect_type=AspectType.MATERIAL, ), "INTLOFTINS": ElementMapping( - element=Element.LOFT_INSULATION, + element=ElementType.LOFT_INSULATION, aspect_type=AspectType.TYPE, ), # ========================================================== # EXTERNAL – DOORS & WINDOWS # ========================================================== "INTFRDOOR": ElementMapping( - element=Element.EXTERNAL_DOOR, + element=ElementType.EXTERNAL_DOOR, aspect_type=AspectType.TYPE, ), "INTFRDRFRR": ElementMapping( - element=Element.EXTERNAL_DOOR, + element=ElementType.EXTERNAL_DOOR, aspect_type=AspectType.FIRE_RATING, ), "EXTBKSDDR1": ElementMapping( - element=Element.EXTERNAL_DOOR, + element=ElementType.EXTERNAL_DOOR, aspect_type=AspectType.TYPE, element_instance=1, ), "EXTBKSDDR2": ElementMapping( - element=Element.EXTERNAL_DOOR, + element=ElementType.EXTERNAL_DOOR, aspect_type=AspectType.TYPE, element_instance=2, ), "INTWDWTYPE": ElementMapping( - element=Element.EXTERNAL_WINDOWS, + element=ElementType.EXTERNAL_WINDOWS, aspect_type=AspectType.TYPE, ), "EXTWNDWS1": ElementMapping( - element=Element.EXTERNAL_WINDOWS, + element=ElementType.EXTERNAL_WINDOWS, aspect_type=AspectType.TYPE, element_instance=1, ), "EXTWNDWS2": ElementMapping( - element=Element.EXTERNAL_WINDOWS, + element=ElementType.EXTERNAL_WINDOWS, aspect_type=AspectType.TYPE, element_instance=2, ), "EXTGARDOOR": ElementMapping( - element=Element.GARAGE_DOOR, + element=ElementType.GARAGE_DOOR, aspect_type=AspectType.MATERIAL, ), "EXTGARSTDR": ElementMapping( - element=Element.GARAGE_AND_STORE_DOOR, + element=ElementType.GARAGE_AND_STORE_DOOR, aspect_type=AspectType.MATERIAL, ), "EXTSTRDOOR": ElementMapping( - element=Element.STORE_DOOR, + element=ElementType.STORE_DOOR, aspect_type=AspectType.MATERIAL, ), "EXTGARWDWS": ElementMapping( - element=Element.GARAGE_WINDOWS, + element=ElementType.GARAGE_WINDOWS, aspect_type=AspectType.MATERIAL, ), "EXTSTRWDWS": ElementMapping( - element=Element.STORE_WINDOWS, + element=ElementType.STORE_WINDOWS, aspect_type=AspectType.MATERIAL, ), "EXTGARSTWD": ElementMapping( - element=Element.GARAGE_AND_STORE_WINDOWS, + element=ElementType.GARAGE_AND_STORE_WINDOWS, aspect_type=AspectType.MATERIAL, ), "EXTLINTELS": ElementMapping( - element=Element.LINTEL, + element=ElementType.LINTEL, aspect_type=AspectType.PRESENCE, ), "EXTPTFRDR1": ElementMapping( - element=Element.PATIO_FRENCH_DOOR, + element=ElementType.PATIO_FRENCH_DOOR, aspect_type=AspectType.MATERIAL, element_instance=1, ), @@ -322,217 +322,217 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { # EXTERNAL AREAS # ========================================================== "EXTBALCONY": ElementMapping( - element=Element.PRIVATE_BALCONY, + element=ElementType.PRIVATE_BALCONY, aspect_type=AspectType.PRESENCE, ), "EXTBPOINTG": ElementMapping( - element=Element.EXTERNAL_BRICKWORK_POINTING, + element=ElementType.EXTERNAL_BRICKWORK_POINTING, aspect_type=AspectType.PRESENCE, ), "EXTDRPKERB": ElementMapping( - element=Element.DROP_KERB, + element=ElementType.DROP_KERB, aspect_type=AspectType.PRESENCE, ), "EXTEXTDECS": ElementMapping( - element=Element.EXTERNAL_DECORATION, + element=ElementType.EXTERNAL_DECORATION, aspect_type=AspectType.PRESENCE, ), "EXTHARDSTD": ElementMapping( - element=Element.PATHS_AND_HARDSTANDINGS, + element=ElementType.PATHS_AND_HARDSTANDINGS, aspect_type=AspectType.MATERIAL, ), "EXTINTDWNP": ElementMapping( - element=Element.INTERNAL_DOWNPIPES_EXTERNAL_AREA, + element=ElementType.INTERNAL_DOWNPIPES_EXTERNAL_AREA, aspect_type=AspectType.MATERIAL, ), "EXTOUTBOH": ElementMapping( - element=Element.OUTBUILDING_OVERHAUL, + element=ElementType.OUTBUILDING_OVERHAUL, aspect_type=AspectType.TYPE, ), "EXTPARKING": ElementMapping( - element=Element.PARKING_AREAS, + element=ElementType.PARKING_AREAS, aspect_type=AspectType.PRESENCE, ), "EXTPCHCNPY": ElementMapping( - element=Element.PORCH_CANOPY, + element=ElementType.PORCH_CANOPY, aspect_type=AspectType.TYPE, ), "EXTSTRINSP": ElementMapping( - element=Element.EXTERNAL_STRUCTURAL_DEFECTS, + element=ElementType.EXTERNAL_STRUCTURAL_DEFECTS, aspect_type=AspectType.TYPE, # Need more sample data to know whether this is the correct aspect type ), "INTACCRAMP": ElementMapping( - element=Element.ACCESS_RAMP, + element=ElementType.ACCESS_RAMP, aspect_type=AspectType.TYPE, # # Need more sample data to know whether this is the correct aspect type ), # ====================== # FITNESS FOR HUMAN HABITATION # ====================== "FFHHDAMP": ElementMapping( - element=Element.FFHH_DAMP, + element=ElementType.FFHH_DAMP, aspect_type=AspectType.RISK, ), "FFHHHCWAT": ElementMapping( - element=Element.FFHH_HOT_AND_COLD_WATER, + element=ElementType.FFHH_HOT_AND_COLD_WATER, aspect_type=AspectType.RISK, ), "FFHHDRNWC": ElementMapping( - element=Element.FFHH_DRAINAGE_LAVATORIES, + element=ElementType.FFHH_DRAINAGE_LAVATORIES, aspect_type=AspectType.RISK, ), "FFHHNEGLC": ElementMapping( - element=Element.FFHH_NEGLECTED, + element=ElementType.FFHH_NEGLECTED, aspect_type=AspectType.RISK, ), "FFHHNONAT": ElementMapping( - element=Element.FFHH_NATURAL_LIGHT, + element=ElementType.FFHH_NATURAL_LIGHT, aspect_type=AspectType.RISK, ), "FFHHNOVEN": ElementMapping( - element=Element.FFHH_VENTILATION, + element=ElementType.FFHH_VENTILATION, aspect_type=AspectType.RISK, ), "FFHHPRPCK": ElementMapping( - element=Element.FFHH_FOOD_PREP_AND_WASHUP, + element=ElementType.FFHH_FOOD_PREP_AND_WASHUP, aspect_type=AspectType.RISK, ), "FFHHUNLAY": ElementMapping( - element=Element.FFHH_UNSAFE_LAYOUT, + element=ElementType.FFHH_UNSAFE_LAYOUT, aspect_type=AspectType.RISK, ), "FFHHUNSTA": ElementMapping( - element=Element.FFHH_UNSTABLE_BUILDING, + element=ElementType.FFHH_UNSTABLE_BUILDING, aspect_type=AspectType.RISK, ), # ========================================================== # HHSRS # ========================================================== "HHSRSDAMP": ElementMapping( - element=Element.HHSRS_DAMP_AND_MOULD, + element=ElementType.HHSRS_DAMP_AND_MOULD, aspect_type=AspectType.RISK, ), "HHSRSCOLD": ElementMapping( - element=Element.HHSRS_EXCESS_COLD, + element=ElementType.HHSRS_EXCESS_COLD, aspect_type=AspectType.RISK, ), "HHSRSHEAT": ElementMapping( - element=Element.HHSRS_EXCESS_HEAT, + element=ElementType.HHSRS_EXCESS_HEAT, aspect_type=AspectType.RISK, ), "HHSRSASB": ElementMapping( - element=Element.HHSRS_ASBESTOS_AND_MMF, + element=ElementType.HHSRS_ASBESTOS_AND_MMF, aspect_type=AspectType.RISK, ), "HHSRSBIOC": ElementMapping( - element=Element.HHSRS_BIOCIDES, + element=ElementType.HHSRS_BIOCIDES, aspect_type=AspectType.RISK, ), "HHSRSCO": ElementMapping( - element=Element.HHSRS_CARBON_MONOXIDE, + element=ElementType.HHSRS_CARBON_MONOXIDE, aspect_type=AspectType.RISK, ), "HHSRSNO2": ElementMapping( - element=Element.HHSRS_CARBON_MONOXIDE, + element=ElementType.HHSRS_CARBON_MONOXIDE, aspect_type=AspectType.RISK, ), # Duplicate of HHSRSCO; I think they relate to the same HHSRS hazard "HHSRSSO2": ElementMapping( - element=Element.HHSRS_CARBON_MONOXIDE, + element=ElementType.HHSRS_CARBON_MONOXIDE, aspect_type=AspectType.RISK, ), # Duplicate of HHSRSCO; I think they relate to the same HHSRS hazard "HHSRSLEAD": ElementMapping( - element=Element.HHSRS_LEAD, + element=ElementType.HHSRS_LEAD, aspect_type=AspectType.RISK, ), "HHSRSRADIA": ElementMapping( - element=Element.HHSRS_RADIATION, + element=ElementType.HHSRS_RADIATION, aspect_type=AspectType.RISK, ), "HHSRSFUEL": ElementMapping( - element=Element.HHSRS_UNCOMBUSTED_FUEL_GAS, + element=ElementType.HHSRS_UNCOMBUSTED_FUEL_GAS, aspect_type=AspectType.RISK, ), "HHSRSORGAN": ElementMapping( - element=Element.HHSRS_VOLATILE_ORGANIC_COMPOUNDS, + element=ElementType.HHSRS_VOLATILE_ORGANIC_COMPOUNDS, aspect_type=AspectType.RISK, ), "HHSRSCROWD": ElementMapping( - element=Element.HHSRS_CROWDING_AND_SPACE, + element=ElementType.HHSRS_CROWDING_AND_SPACE, aspect_type=AspectType.RISK, ), "HHSRSENTRY": ElementMapping( - element=Element.HHSRS_ENTRY_BY_INTRUDERS, + element=ElementType.HHSRS_ENTRY_BY_INTRUDERS, aspect_type=AspectType.RISK, ), "HHSRSLIGHT": ElementMapping( - element=Element.HHSRS_LIGHTING, + element=ElementType.HHSRS_LIGHTING, aspect_type=AspectType.RISK, ), "HHSRSNOISE": ElementMapping( - element=Element.HHSRS_NOISE, + element=ElementType.HHSRS_NOISE, aspect_type=AspectType.RISK, ), "HHSRSDOMES": ElementMapping( - element=Element.HHSRS_DOMESTIC_HYGIENE_PESTS_REFUSE, + element=ElementType.HHSRS_DOMESTIC_HYGIENE_PESTS_REFUSE, aspect_type=AspectType.RISK, ), "HHSRSFOOD": ElementMapping( - element=Element.HHSRS_FOOD_SAFETY, + element=ElementType.HHSRS_FOOD_SAFETY, aspect_type=AspectType.RISK, ), "HHSRSPERS": ElementMapping( - element=Element.HHSRS_PERSONAL_HYGIENE_SANITATION, + element=ElementType.HHSRS_PERSONAL_HYGIENE_SANITATION, aspect_type=AspectType.RISK, ), "HHSRSWATER": ElementMapping( - element=Element.HHSRS_WATER_SUPPLY, + element=ElementType.HHSRS_WATER_SUPPLY, aspect_type=AspectType.RISK, ), "HHSRSFBATH": ElementMapping( - element=Element.HHSRS_FALLS_ASSOCIATED_WITH_BATHS, + element=ElementType.HHSRS_FALLS_ASSOCIATED_WITH_BATHS, aspect_type=AspectType.RISK, ), "HHSRSFLEVE": ElementMapping( - element=Element.HHSRS_FALLS_ON_LEVEL_SURFACES, + element=ElementType.HHSRS_FALLS_ON_LEVEL_SURFACES, aspect_type=AspectType.RISK, ), "HHSRSFSTAI": ElementMapping( - element=Element.HHSRS_FALLS_ON_STAIRS, + element=ElementType.HHSRS_FALLS_ON_STAIRS, aspect_type=AspectType.RISK, ), "HHSRSFBETW": ElementMapping( - element=Element.HHSRS_FALLS_BETWEEN_LEVELS, + element=ElementType.HHSRS_FALLS_BETWEEN_LEVELS, aspect_type=AspectType.RISK, ), "HHSRSELEC": ElementMapping( - element=Element.HHSRS_ELECTRICAL_HAZARDS, + element=ElementType.HHSRS_ELECTRICAL_HAZARDS, aspect_type=AspectType.RISK, ), "HHSRSFIRE": ElementMapping( - element=Element.HHSRS_FIRE, + element=ElementType.HHSRS_FIRE, aspect_type=AspectType.RISK, ), "HHSRSFLAME": ElementMapping( - element=Element.HHSRS_FLAMES_HOT_SURFACES, + element=ElementType.HHSRS_FLAMES_HOT_SURFACES, aspect_type=AspectType.RISK, ), "HHSRSENTRP": ElementMapping( - element=Element.HHSRS_COLLISION_AND_ENTRAPMENT, + element=ElementType.HHSRS_COLLISION_AND_ENTRAPMENT, aspect_type=AspectType.RISK, ), "HHSRSEXPLO": ElementMapping( - element=Element.HHSRS_EXPLOSIONS, + element=ElementType.HHSRS_EXPLOSIONS, aspect_type=AspectType.RISK, ), "HHSRSSTRUC": ElementMapping( - element=Element.HHSRS_STRUCTURAL_COLLAPSE, + element=ElementType.HHSRS_STRUCTURAL_COLLAPSE, aspect_type=AspectType.RISK, ), "HHSRSCLOW": ElementMapping( - element=Element.HHSRS_COLLISION_AND_ENTRAPMENT, + element=ElementType.HHSRS_COLLISION_AND_ENTRAPMENT, aspect_type=AspectType.RISK, ), "HHSRSPOSI": ElementMapping( - element=Element.HHSRS_AMENITIES, + element=ElementType.HHSRS_AMENITIES, aspect_type=AspectType.RISK, ), } diff --git a/backend/condition/domain/mapping/lbwf/lbwf_mapper.py b/backend/condition/domain/mapping/lbwf/lbwf_mapper.py index 3d7b7349..bb5f777d 100644 --- a/backend/condition/domain/mapping/lbwf/lbwf_mapper.py +++ b/backend/condition/domain/mapping/lbwf/lbwf_mapper.py @@ -1,6 +1,5 @@ from typing import Any, List, Optional -from backend.condition.domain.asset_condition import AssetCondition from backend.condition.domain.element import Element from backend.condition.domain.mapping.element_mapping import ElementMapping from backend.condition.domain.mapping.lbwf.lbwf_element_map import LBWF_ELEMENT_MAP @@ -18,12 +17,12 @@ class LbwfMapper(Mapper): def map_asset_conditions_for_property( self, client_data: Any, survey_year: Optional[int] = None - ) -> List[AssetCondition]: + ) -> List[Element]: assert isinstance( client_data, LbwfHouse ) # TODO: think of a better way to do this - mapped_assets: List[AssetCondition] = [] + mapped_assets: List[Element] = [] uprn: int = client_data.uprn for raw_asset in client_data.assets: @@ -40,7 +39,7 @@ class LbwfMapper(Mapper): continue mapped_assets.append( - AssetCondition( + Element( uprn=uprn, element=element_mapping.element, aspect_type=element_mapping.aspect_type, diff --git a/backend/condition/domain/mapping/mapper.py b/backend/condition/domain/mapping/mapper.py index c0b07184..ace6ad73 100644 --- a/backend/condition/domain/mapping/mapper.py +++ b/backend/condition/domain/mapping/mapper.py @@ -1,7 +1,7 @@ from abc import ABC, abstractmethod from typing import Any, List, Optional -from backend.condition.domain.asset_condition import AssetCondition +from backend.condition.domain.element import Element class Mapper(ABC): @@ -9,6 +9,6 @@ class Mapper(ABC): @abstractmethod def map_asset_conditions_for_property( self, client_data: Any, survey_year: Optional[int] = None - ) -> List[AssetCondition]: + ) -> List[Element]: # TODO: client_data should be properly typed pass diff --git a/backend/condition/domain/mapping/peabody/peabody_element_map.py b/backend/condition/domain/mapping/peabody/peabody_element_map.py index 1f9cceee..508b8968 100644 --- a/backend/condition/domain/mapping/peabody/peabody_element_map.py +++ b/backend/condition/domain/mapping/peabody/peabody_element_map.py @@ -1,5 +1,5 @@ from backend.condition.domain.aspect_type import AspectType -from backend.condition.domain.element import Element +from backend.condition.domain.element_type import ElementType from backend.condition.domain.mapping.element_mapping import ElementMapping @@ -7,654 +7,658 @@ PEABODY_ELEMENT_MAP = { # ========================================================== # PROPERTY / GENERAL # ========================================================== - (100, 1): ElementMapping(element=Element.PROPERTY, aspect_type=AspectType.TYPE), + (100, 1): ElementMapping(element=ElementType.PROPERTY, aspect_type=AspectType.TYPE), # (100, 3): ElementMapping(element=Element.PROPERTY, aspect_type=AspectType.AGE), # (100, 14): ElementMapping(element="property", aspect_type="construction_type"), (50, 2): ElementMapping( - element=Element.CARBON_MONOXIDE_DETECTION, aspect_type=AspectType.TYPE + element=ElementType.CARBON_MONOXIDE_DETECTION, aspect_type=AspectType.TYPE ), - (50, 3): ElementMapping(element=Element.CCU, aspect_type=AspectType.TYPE), + (50, 3): ElementMapping(element=ElementType.CCU, aspect_type=AspectType.TYPE), (50, 7): ElementMapping( - element=Element.DISABLED_HOIST_TRACKING, aspect_type=AspectType.PRESENCE + element=ElementType.DISABLED_HOIST_TRACKING, aspect_type=AspectType.PRESENCE ), (50, 11): ElementMapping( - element=Element.HEAT_DETECTION, aspect_type=AspectType.TYPE + element=ElementType.HEAT_DETECTION, aspect_type=AspectType.TYPE ), (50, 21): ElementMapping( - element=Element.SMOKE_DETECTION, aspect_type=AspectType.TYPE + element=ElementType.SMOKE_DETECTION, aspect_type=AspectType.TYPE ), (50, 22): ElementMapping( - element=Element.STAIRLIFT, aspect_type=AspectType.PRESENCE + element=ElementType.STAIRLIFT, aspect_type=AspectType.PRESENCE ), (50, 26): ElementMapping( - element=Element.DISABLED_FACILITIES, aspect_type=AspectType.TYPE + element=ElementType.DISABLED_FACILITIES, aspect_type=AspectType.TYPE + ), + (100, 3): ElementMapping( + element=ElementType.PROPERTY, aspect_type=AspectType.AGE_BAND ), - (100, 3): ElementMapping(element=Element.PROPERTY, aspect_type=AspectType.AGE_BAND), (100, 14): ElementMapping( - element=Element.PROPERTY, aspect_type=AspectType.CONSTRUCTION_TYPE + element=ElementType.PROPERTY, aspect_type=AspectType.CONSTRUCTION_TYPE ), (100, 16): ElementMapping( - element=Element.PROPERTY, aspect_type=AspectType.CLASSIFICATION + element=ElementType.PROPERTY, aspect_type=AspectType.CLASSIFICATION ), (210, 2): ElementMapping( - element=Element.PASSENGER_LIFT, aspect_type=AspectType.TYPE + element=ElementType.PASSENGER_LIFT, aspect_type=AspectType.TYPE ), # ========================================================== # EXTERNAL – WALLS # ========================================================== (50, 16): ElementMapping( - element=Element.PARTY_WALL_FIRE_BREAK, aspect_type=AspectType.PRESENCE + element=ElementType.PARTY_WALL_FIRE_BREAK, aspect_type=AspectType.PRESENCE ), (53, 1): ElementMapping( - element=Element.BOUNDARY_WALLS, aspect_type=AspectType.PRESENCE + element=ElementType.BOUNDARY_WALLS, aspect_type=AspectType.PRESENCE ), (53, 4): ElementMapping( - element=Element.EXTERNAL_DECORATION, aspect_type=AspectType.PRESENCE + element=ElementType.EXTERNAL_DECORATION, aspect_type=AspectType.PRESENCE ), (53, 5): ElementMapping( - element=Element.EXTERNAL_NOISE_INSULATION, aspect_type=AspectType.ADEQUACY + element=ElementType.EXTERNAL_NOISE_INSULATION, aspect_type=AspectType.ADEQUACY ), (53, 14): ElementMapping( - element=Element.GARAGE_WALLS, aspect_type=AspectType.MATERIAL + element=ElementType.GARAGE_WALLS, aspect_type=AspectType.MATERIAL ), (53, 23): ElementMapping( - element=Element.EXTERNAL_WALL, aspect_type=AspectType.FINISH + element=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH ), (53, 30): ElementMapping( - element=Element.SECONDARY_WALL, aspect_type=AspectType.FINISH + element=ElementType.SECONDARY_WALL, aspect_type=AspectType.FINISH ), # Should this be combined with primary wall, with different instance value? (53, 36): ElementMapping( - element=Element.EXTERNAL_WALL, aspect_type=AspectType.INSULATION + element=ElementType.EXTERNAL_WALL, aspect_type=AspectType.INSULATION ), (53, 40): ElementMapping( - element=Element.SPANDREL_PANELS, aspect_type=AspectType.MATERIAL + element=ElementType.SPANDREL_PANELS, aspect_type=AspectType.MATERIAL + ), + (53, 41): ElementMapping( + element=ElementType.CLADDING, aspect_type=AspectType.MATERIAL ), - (53, 41): ElementMapping(element=Element.CLADDING, aspect_type=AspectType.MATERIAL), (100, 15): ElementMapping( - element=Element.EXTERNAL_DECORATION, aspect_type=AspectType.CONDITION + element=ElementType.EXTERNAL_DECORATION, aspect_type=AspectType.CONDITION ), (120, 1): ElementMapping( - element=Element.EXTERNAL_WALL, aspect_type=AspectType.STRUCTURE + element=ElementType.EXTERNAL_WALL, aspect_type=AspectType.STRUCTURE ), (120, 2): ElementMapping( - element=Element.EXTERNAL_WALL, aspect_type=AspectType.FINISH + element=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH ), (120, 3): ElementMapping( - element=Element.PRIMARY_WALL, aspect_type=AspectType.INSULATION + element=ElementType.PRIMARY_WALL, aspect_type=AspectType.INSULATION ), # This code element code is actually "WALL" not "external wall" - correct? # ========================================================== # EXTERNAL – ROOFS # ========================================================== (50, 15): ElementMapping( - element=Element.LOFT_INSULATION, + element=ElementType.LOFT_INSULATION, aspect_type=AspectType.TYPE, ), (53, 2): ElementMapping( - element=Element.CHIMNEY, + element=ElementType.CHIMNEY, aspect_type=AspectType.PRESENCE, ), (53, 6): ElementMapping( - element=Element.FASCIA_SOFFIT_BARGEBOARDS, + element=ElementType.FASCIA_SOFFIT_BARGEBOARDS, aspect_type=AspectType.MATERIAL, ), (53, 7): ElementMapping( - element=Element.FLAT_ROOF_COVERING, + element=ElementType.FLAT_ROOF_COVERING, aspect_type=AspectType.MATERIAL, ), (53, 13): ElementMapping( - element=Element.GARAGE_ROOF, + element=ElementType.GARAGE_ROOF, aspect_type=AspectType.MATERIAL, ), (53, 15): ElementMapping( - element=Element.GUTTERS, + element=ElementType.GUTTERS, aspect_type=AspectType.MATERIAL, ), (53, 21): ElementMapping( - element=Element.PITCHED_ROOF_COVERING, + element=ElementType.PITCHED_ROOF_COVERING, aspect_type=AspectType.MATERIAL, ), (53, 22): ElementMapping( - element=Element.PORCH_CANOPY, + element=ElementType.PORCH_CANOPY, aspect_type=AspectType.TYPE, ), (53, 47): ElementMapping( - element=Element.ROOF, + element=ElementType.ROOF, aspect_type=AspectType.STRUCTURE, ), (110, 1): ElementMapping( - element=Element.ROOF, + element=ElementType.ROOF, aspect_type=AspectType.MATERIAL, element_instance=1, ), (110, 2): ElementMapping( - element=Element.ROOF, + element=ElementType.ROOF, aspect_type=AspectType.MATERIAL, element_instance=1, ), (110, 3): ElementMapping( - element=Element.CHIMNEY, + element=ElementType.CHIMNEY, aspect_type=AspectType.WORK_REQUIRED, ), (110, 4): ElementMapping( - element=Element.FASCIA, + element=ElementType.FASCIA, aspect_type=AspectType.MATERIAL, ), (110, 5): ElementMapping( - element=Element.SOFFIT, + element=ElementType.SOFFIT, aspect_type=AspectType.MATERIAL, ), (110, 6): ElementMapping( - element=Element.RAINWATER_GOODS, + element=ElementType.RAINWATER_GOODS, aspect_type=AspectType.MATERIAL, ), (110, 7): ElementMapping( - element=Element.LOFT_INSULATION, + element=ElementType.LOFT_INSULATION, aspect_type=AspectType.WORK_REQUIRED, # possibly not the right aspect type ), (110, 8): ElementMapping( - element=Element.PORCH_CANOPY, + element=ElementType.PORCH_CANOPY, aspect_type=AspectType.MATERIAL, ), # ========================================================== # EXTERNAL – DOORS & WINDOWS # ========================================================== (50, 8): ElementMapping( - element=Element.DOOR_ENTRY_HANDSET, + element=ElementType.DOOR_ENTRY_HANDSET, aspect_type=AspectType.PRESENCE, ), (53, 8): ElementMapping( - element=Element.FRONT_DOOR, + element=ElementType.FRONT_DOOR, aspect_type=AspectType.MATERIAL, ), (53, 12): ElementMapping( - element=Element.GARAGE_DOOR, + element=ElementType.GARAGE_DOOR, aspect_type=AspectType.MATERIAL, ), (53, 16): ElementMapping( - element=Element.LINTEL, + element=ElementType.LINTEL, aspect_type=AspectType.PRESENCE, ), (53, 19): ElementMapping( - element=Element.PATIO_FRENCH_DOOR, + element=ElementType.PATIO_FRENCH_DOOR, aspect_type=AspectType.MATERIAL, ), (53, 25): ElementMapping( - element=Element.REAR_DOOR, + element=ElementType.REAR_DOOR, aspect_type=AspectType.MATERIAL, ), (53, 29): ElementMapping( - element=Element.SECONDARY_GLAZING, + element=ElementType.SECONDARY_GLAZING, aspect_type=AspectType.PRESENCE, ), (53, 35): ElementMapping( - element=Element.STORE_DOOR, + element=ElementType.STORE_DOOR, aspect_type=AspectType.MATERIAL, ), (53, 38): ElementMapping( - element=Element.EXTERNAL_WINDOWS, + element=ElementType.EXTERNAL_WINDOWS, aspect_type=AspectType.TYPE, element_instance=1, ), (53, 39): ElementMapping( - element=Element.EXTERNAL_WINDOWS, + element=ElementType.EXTERNAL_WINDOWS, aspect_type=AspectType.TYPE, element_instance=2, ), (53, 43): ElementMapping( - element=Element.FRONT_DOOR, + element=ElementType.FRONT_DOOR, aspect_type=AspectType.TYPE, ), (130, 1): ElementMapping( - element=Element.EXTERNAL_WINDOWS, + element=ElementType.EXTERNAL_WINDOWS, aspect_type=AspectType.MATERIAL, ), (130, 2): ElementMapping( - element=Element.COMMUNAL_WINDOWS, + element=ElementType.COMMUNAL_WINDOWS, aspect_type=AspectType.MATERIAL, ), (140, 1): ElementMapping( - element=Element.MAIN_DOOR, + element=ElementType.MAIN_DOOR, aspect_type=AspectType.MATERIAL, ), (140, 2): ElementMapping( - element=Element.STORE_DOOR, + element=ElementType.STORE_DOOR, aspect_type=AspectType.MATERIAL, ), # Duplicate of (53, 35) (140, 3): ElementMapping( - element=Element.GARAGE_DOOR, + element=ElementType.GARAGE_DOOR, aspect_type=AspectType.MATERIAL, ), # Duplicate of (53, 12) (140, 4): ElementMapping( - element=Element.BLOCK_ENTRANCE_DOOR, + element=ElementType.BLOCK_ENTRANCE_DOOR, aspect_type=AspectType.MATERIAL, ), # ========================================================== # EXTERNAL AREAS # ========================================================== (53, 3): ElementMapping( - element=Element.DOWNPIPES, + element=ElementType.DOWNPIPES, aspect_type=AspectType.MATERIAL, ), (53, 9): ElementMapping( - element=Element.FRONT_FENCING, + element=ElementType.FRONT_FENCING, aspect_type=AspectType.MATERIAL, ), (53, 10): ElementMapping( - element=Element.FRONT_GATE, + element=ElementType.FRONT_GATE, aspect_type=AspectType.TYPE, ), (53, 17): ElementMapping( - element=Element.PARKING_AREAS, + element=ElementType.PARKING_AREAS, aspect_type=AspectType.MATERIAL, ), (53, 18): ElementMapping( - element=Element.PATHS_AND_HARDSTANDINGS, + element=ElementType.PATHS_AND_HARDSTANDINGS, aspect_type=AspectType.MATERIAL, ), (53, 24): ElementMapping( - element=Element.PRIVATE_BALCONY, + element=ElementType.PRIVATE_BALCONY, aspect_type=AspectType.PRESENCE, ), (53, 26): ElementMapping( - element=Element.REAR_FENCING, + element=ElementType.REAR_FENCING, aspect_type=AspectType.MATERIAL, ), (53, 27): ElementMapping( - element=Element.REAR_GATE, + element=ElementType.REAR_GATE, aspect_type=AspectType.TYPE, ), (53, 28): ElementMapping( - element=Element.RETAINING_WALLS, + element=ElementType.RETAINING_WALLS, aspect_type=AspectType.PRESENCE, ), (53, 31): ElementMapping( - element=Element.SIDE_FENCING, + element=ElementType.SIDE_FENCING, aspect_type=AspectType.MATERIAL, ), (53, 32): ElementMapping( - element=Element.SOIL_AND_VENT, + element=ElementType.SOIL_AND_VENT, aspect_type=AspectType.MATERIAL, ), (53, 34): ElementMapping( - element=Element.SOLAR_THERMALS, + element=ElementType.SOLAR_THERMALS, aspect_type=AspectType.PRESENCE, ), (53, 44): ElementMapping( - element=Element.GARAGE_STRUCTURE, + element=ElementType.GARAGE_STRUCTURE, aspect_type=AspectType.TYPE, ), (53, 45): ElementMapping( - element=Element.BALCONY_BALUSTRADE, + element=ElementType.BALCONY_BALUSTRADE, aspect_type=AspectType.MATERIAL, ), (150, 1): ElementMapping( - element=Element.BLOCK_ENTRANCE_DOOR, + element=ElementType.BLOCK_ENTRANCE_DOOR, aspect_type=AspectType.MATERIAL, ), (150, 2): ElementMapping( - element=Element.PATHS_AND_HARDSTANDINGS, + element=ElementType.PATHS_AND_HARDSTANDINGS, aspect_type=AspectType.MATERIAL, ), # Duplicate of (53, 18) - correct? (150, 3): ElementMapping( - element=Element.ROADS, + element=ElementType.ROADS, aspect_type=AspectType.MATERIAL, ), (150, 4): ElementMapping( - element=Element.BOUNDARY_WALLS, + element=ElementType.BOUNDARY_WALLS, aspect_type=AspectType.MATERIAL, ), (150, 5): ElementMapping( - element=Element.OUTBUILDINGS, + element=ElementType.OUTBUILDINGS, aspect_type=AspectType.TYPE, ), (150, 6): ElementMapping( - element=Element.GARAGE_STRUCTURE, + element=ElementType.GARAGE_STRUCTURE, aspect_type=AspectType.TYPE, ), # ========================================================== # INTERNAL – BATHROOMS & KITCHENS # ========================================================== (50, 1): ElementMapping( - element=Element.SECONDARY_TOILET, + element=ElementType.SECONDARY_TOILET, aspect_type=AspectType.PRESENCE, ), (50, 9): ElementMapping( - element=Element.BATHROOM_EXTRACTOR_FAN, + element=ElementType.BATHROOM_EXTRACTOR_FAN, aspect_type=AspectType.PRESENCE, ), (50, 9): ElementMapping( - element=Element.KITCHEN, + element=ElementType.KITCHEN, aspect_type=AspectType.TYPE, ), (50, 10): ElementMapping( - element=Element.KITCHEN_EXTRACTOR_FAN, + element=ElementType.KITCHEN_EXTRACTOR_FAN, aspect_type=AspectType.PRESENCE, ), (50, 13): ElementMapping( - element=Element.KITCHEN_SPACE_LAYOUT, + element=ElementType.KITCHEN_SPACE_LAYOUT, aspect_type=AspectType.ADEQUACY, ), (50, 14): ElementMapping( - element=Element.KITCHEN, + element=ElementType.KITCHEN, aspect_type=AspectType.TYPE, ), (50, 17): ElementMapping( - element=Element.BATHROOM, + element=ElementType.BATHROOM, aspect_type=AspectType.LOCATION, ), (50, 18): ElementMapping( - element=Element.BATHROOM, + element=ElementType.BATHROOM, aspect_type=AspectType.TYPE, ), # Actually "Primary bathroom type" - ok like this? (50, 20): ElementMapping( - element=Element.BATHROOM, + element=ElementType.BATHROOM, aspect_type=AspectType.TYPE, element_instance=2, ), # Actually "Secondary bathroom type" - ok like this? (160, 1): ElementMapping( - element=Element.KITCHEN, + element=ElementType.KITCHEN, aspect_type=AspectType.CONDITION, ), (160, 2): ElementMapping( - element=Element.KITCHEN_SPACE_LAYOUT, + element=ElementType.KITCHEN_SPACE_LAYOUT, aspect_type=AspectType.ADEQUACY, ), (190, 1): ElementMapping( - element=Element.BATHROOM, + element=ElementType.BATHROOM, aspect_type=AspectType.CONDITION, ), (190, 2): ElementMapping( - element=Element.SECONDARY_TOILET, + element=ElementType.SECONDARY_TOILET, aspect_type=AspectType.TYPE, ), # ========================================================== # COMMUNAL # ========================================================== (51, 1): ElementMapping( - element=Element.COMMUNAL_AERIAL, + element=ElementType.COMMUNAL_AERIAL, aspect_type=AspectType.PRESENCE, ), (51, 2): ElementMapping( - element=Element.COMMUNAL_AOV, + element=ElementType.COMMUNAL_AOV, aspect_type=AspectType.PRESENCE, ), (51, 3): ElementMapping( - element=Element.COMMUNAL_BALCONY_WALKWAY, + element=ElementType.COMMUNAL_BALCONY_WALKWAY, aspect_type=AspectType.PRESENCE, ), (51, 4): ElementMapping( - element=Element.COMMUNAL_BATHROOM, + element=ElementType.COMMUNAL_BATHROOM, aspect_type=AspectType.TYPE, ), (51, 5): ElementMapping( - element=Element.COMMUNAL_BIN_STORE_DOORS, + element=ElementType.COMMUNAL_BIN_STORE_DOORS, aspect_type=AspectType.PRESENCE, ), (51, 6): ElementMapping( - element=Element.COMMUNAL_BIN_STORE_ROOF, + element=ElementType.COMMUNAL_BIN_STORE_ROOF, aspect_type=AspectType.PRESENCE, ), (51, 7): ElementMapping( - element=Element.COMMUNAL_BIN_STORE_WALLS, + element=ElementType.COMMUNAL_BIN_STORE_WALLS, aspect_type=AspectType.MATERIAL, ), (51, 8): ElementMapping( - element=Element.COMMUNAL_BMS, + element=ElementType.COMMUNAL_BMS, aspect_type=AspectType.PRESENCE, ), (51, 9): ElementMapping( - element=Element.COMMUNAL_BOILER, + element=ElementType.COMMUNAL_BOILER, aspect_type=AspectType.TYPE, ), (51, 10): ElementMapping( - element=Element.COMMUNAL_BOOSTER_PUMP, + element=ElementType.COMMUNAL_BOOSTER_PUMP, aspect_type=AspectType.PRESENCE, ), (51, 11): ElementMapping( - element=Element.COMMUNAL_CCTV, + element=ElementType.COMMUNAL_CCTV, aspect_type=AspectType.PRESENCE, ), (51, 12): ElementMapping( - element=Element.COMMUNAL_CIRCULATION_SPACE, + element=ElementType.COMMUNAL_CIRCULATION_SPACE, aspect_type=AspectType.ADEQUACY, ), (51, 13): ElementMapping( - element=Element.COMMUNAL_COLD_WATER_STORAGE, + element=ElementType.COMMUNAL_COLD_WATER_STORAGE, aspect_type=AspectType.PRESENCE, ), (51, 14): ElementMapping( - element=Element.COMMUNAL_DOOR_ENTRY, + element=ElementType.COMMUNAL_DOOR_ENTRY, aspect_type=AspectType.SYSTEM, ), (51, 15): ElementMapping( - element=Element.COMMUNAL_DRY_RISER, + element=ElementType.COMMUNAL_DRY_RISER, aspect_type=AspectType.PRESENCE, ), (51, 16): ElementMapping( - element=Element.COMMUNAL_EMERGENCY_LIGHTING, + element=ElementType.COMMUNAL_EMERGENCY_LIGHTING, aspect_type=AspectType.PRESENCE, ), (51, 17): ElementMapping( - element=Element.COMMUNAL_EXTERNAL_DOORS, + element=ElementType.COMMUNAL_EXTERNAL_DOORS, aspect_type=AspectType.MATERIAL, ), (51, 19): ElementMapping( - element=Element.COMMUNAL_FIRE_ALARM, + element=ElementType.COMMUNAL_FIRE_ALARM, aspect_type=AspectType.TYPE, ), (51, 20): ElementMapping( - element=Element.COMMUNAL_INTERNAL_DECORATIONS, + element=ElementType.COMMUNAL_INTERNAL_DECORATIONS, aspect_type=AspectType.PRESENCE, ), (51, 21): ElementMapping( - element=Element.COMMUNAL_INTERNAL_DOORS, + element=ElementType.COMMUNAL_INTERNAL_DOORS, aspect_type=AspectType.MATERIAL, ), (51, 22): ElementMapping( - element=Element.COMMUNAL_INTERNAL_FLOOR, + element=ElementType.COMMUNAL_INTERNAL_FLOOR, aspect_type=AspectType.FINISH, ), (51, 23): ElementMapping( - element=Element.COMMUNAL_KITCHEN, + element=ElementType.COMMUNAL_KITCHEN, aspect_type=AspectType.TYPE, ), (51, 24): ElementMapping( - element=Element.COMMUNAL_LATERAL_MAINS, + element=ElementType.COMMUNAL_LATERAL_MAINS, aspect_type=AspectType.PRESENCE, ), (51, 25): ElementMapping( - element=Element.COMMUNAL_LIGHTING, + element=ElementType.COMMUNAL_LIGHTING, aspect_type=AspectType.PRESENCE, ), (51, 26): ElementMapping( - element=Element.COMMUNAL_LIGHTING_CONDUCTOR, + element=ElementType.COMMUNAL_LIGHTING_CONDUCTOR, aspect_type=AspectType.PRESENCE, ), (51, 27): ElementMapping( - element=Element.COMMUNAL_PASSENGER_LIFT, + element=ElementType.COMMUNAL_PASSENGER_LIFT, aspect_type=AspectType.TYPE, ), (51, 28): ElementMapping( - element=Element.COMMUNAL_ENTRANCE, + element=ElementType.COMMUNAL_ENTRANCE, aspect_type=AspectType.MATERIAL, element_instance=1, ), (51, 30): ElementMapping( - element=Element.COMMUNAL_ENTRANCE, + element=ElementType.COMMUNAL_ENTRANCE, aspect_type=AspectType.FINISH, element_instance=2, ), (51, 31): ElementMapping( - element=Element.COMMUNAL_SPRINKLER, + element=ElementType.COMMUNAL_SPRINKLER, aspect_type=AspectType.PRESENCE, ), (51, 29): ElementMapping( - element=Element.COMMUNAL_REFUSE_CHUTE, + element=ElementType.COMMUNAL_REFUSE_CHUTE, aspect_type=AspectType.PRESENCE, ), (51, 32): ElementMapping( - element=Element.COMMUNAL_STAIRS, + element=ElementType.COMMUNAL_STAIRS, aspect_type=AspectType.FINISH, ), (51, 33): ElementMapping( - element=Element.COMMUNAL_STORE_DOORS, + element=ElementType.COMMUNAL_STORE_DOORS, aspect_type=AspectType.MATERIAL, ), (51, 34): ElementMapping( - element=Element.COMMUNAL_STORE_ROOF, + element=ElementType.COMMUNAL_STORE_ROOF, aspect_type=AspectType.MATERIAL, ), (51, 35): ElementMapping( - element=Element.COMMUNAL_STORE_WALLS, + element=ElementType.COMMUNAL_STORE_WALLS, aspect_type=AspectType.MATERIAL, ), (51, 36): ElementMapping( - element=Element.COMMUNAL_WALKWAYS, + element=ElementType.COMMUNAL_WALKWAYS, aspect_type=AspectType.FINISH, ), (51, 37): ElementMapping( - element=Element.COMMUNAL_WARDEN_CALL_SYSTEM, + element=ElementType.COMMUNAL_WARDEN_CALL_SYSTEM, aspect_type=AspectType.PRESENCE, ), (51, 38): ElementMapping( - element=Element.COMMUNAL_TOILETS, + element=ElementType.COMMUNAL_TOILETS, aspect_type=AspectType.TYPE, ), (51, 39): ElementMapping( - element=Element.COMMUNAL_WET_RISER, + element=ElementType.COMMUNAL_WET_RISER, aspect_type=AspectType.PRESENCE, ), (51, 40): ElementMapping( - element=Element.COMMUNAL_PLUG_SOCKETS, + element=ElementType.COMMUNAL_PLUG_SOCKETS, aspect_type=AspectType.PRESENCE, ), (200, 1): ElementMapping( - element=Element.COMMUNAL_BOILER, + element=ElementType.COMMUNAL_BOILER, aspect_type=AspectType.TYPE, ), # Duplicate of (51, 9) - correct? (200, 2): ElementMapping( - element=Element.COMMUNAL_HEATING, + element=ElementType.COMMUNAL_HEATING, aspect_type=AspectType.TYPE, ), (200, 3): ElementMapping( - element=Element.COMMUNAL_ELECTRICS, + element=ElementType.COMMUNAL_ELECTRICS, aspect_type=AspectType.TYPE, ), (200, 4): ElementMapping( - element=Element.COMMUNAL_FIRE_ALARM, + element=ElementType.COMMUNAL_FIRE_ALARM, aspect_type=AspectType.TYPE, ), (200, 5): ElementMapping( - element=Element.COMMUNAL_LIFT, + element=ElementType.COMMUNAL_LIFT, aspect_type=AspectType.TYPE, ), (200, 6): ElementMapping( - element=Element.COMMUNAL_FLOOR_COVERING, + element=ElementType.COMMUNAL_FLOOR_COVERING, aspect_type=AspectType.MATERIAL, ), (200, 7): ElementMapping( - element=Element.COMMUNAL_KITCHEN, + element=ElementType.COMMUNAL_KITCHEN, aspect_type=AspectType.TYPE, ), (200, 8): ElementMapping( - element=Element.COMMUNAL_BATHROOM, + element=ElementType.COMMUNAL_BATHROOM, aspect_type=AspectType.TYPE, ), # Duplicate of (51, 4) - correct? (200, 9): ElementMapping( - element=Element.COMMUNAL_TOILETS, + element=ElementType.COMMUNAL_TOILETS, aspect_type=AspectType.TYPE, ), # Duplicate of (51, 38) - correct? (200, 10): ElementMapping( - element=Element.COMMUNAL_GATES, + element=ElementType.COMMUNAL_GATES, aspect_type=AspectType.TYPE, ), # ========================================================== # INTERNAL – HEATING # ========================================================== (50, 4): ElementMapping( - element=Element.HEATING_BOILER, + element=ElementType.HEATING_BOILER, aspect_type=AspectType.PRESENCE, ), # This is actually "Central heating boiler" - ok like this? (50, 5): ElementMapping( - element=Element.CENTRAL_HEATING, + element=ElementType.CENTRAL_HEATING, aspect_type=AspectType.EXTENT, ), (50, 6): ElementMapping( - element=Element.COLD_WATER_STORAGE, + element=ElementType.COLD_WATER_STORAGE, aspect_type=AspectType.PRESENCE, ), (50, 12): ElementMapping( - element=Element.HEATING_DISTRIBUTION, + element=ElementType.HEATING_DISTRIBUTION, aspect_type=AspectType.TYPE, ), (50, 19): ElementMapping( - element=Element.PROGRAMMABLE_HEATING, + element=ElementType.PROGRAMMABLE_HEATING, aspect_type=AspectType.TYPE, ), (50, 25): ElementMapping( - element=Element.HEATING_BOILER, + element=ElementType.HEATING_BOILER, aspect_type=AspectType.TYPE, ), (170, 1): ElementMapping( - element=Element.HEATING_BOILER, + element=ElementType.HEATING_BOILER, aspect_type=AspectType.TYPE, ), # Duplicate of (50,25) - correct? (170, 2): ElementMapping( - element=Element.HEATING_DISTRIBUTION, + element=ElementType.HEATING_DISTRIBUTION, aspect_type=AspectType.TYPE, ), # Duplicate of (50,12) - correct? (170, 3): ElementMapping( - element=Element.SECONDARY_HEATING, + element=ElementType.SECONDARY_HEATING, aspect_type=AspectType.TYPE, ), (170, 4): ElementMapping( - element=Element.COLD_WATER_STORAGE, + element=ElementType.COLD_WATER_STORAGE, aspect_type=AspectType.TYPE, ), (170, 5): ElementMapping( - element=Element.HOT_WATER_SYSTEM, + element=ElementType.HOT_WATER_SYSTEM, aspect_type=AspectType.TYPE, ), # ========================================================== # ELECTRICS # ========================================================== (50, 24): ElementMapping( - element=Element.INTERNAL_WIRING, + element=ElementType.INTERNAL_WIRING, aspect_type=AspectType.MATERIAL, ), (180, 1): ElementMapping( - element=Element.ELECTRICAL_WIRING, + element=ElementType.ELECTRICAL_WIRING, aspect_type=AspectType.WORK_REQUIRED, ), # Not certain about the AspectType - only example in the sample data is "Full Rewire" (180, 2): ElementMapping( - element=Element.CONSUMER_UNIT, + element=ElementType.CONSUMER_UNIT, aspect_type=AspectType.TYPE, ), (180, 3): ElementMapping( - element=Element.SMOKE_DETECTION, + element=ElementType.SMOKE_DETECTION, aspect_type=AspectType.TYPE, ), # Duplicate of (50, 21) - correct? (180, 4): ElementMapping( - element=Element.CARBON_MONOXIDE_DETECTION, + element=ElementType.CARBON_MONOXIDE_DETECTION, aspect_type=AspectType.TYPE, ), # Duplicate of (50, 2) - correct? # ========================================================== # HHSRS # ========================================================== (54, 1): ElementMapping( - element=Element.HHSRS_DAMP_AND_MOULD, + element=ElementType.HHSRS_DAMP_AND_MOULD, aspect_type=AspectType.RISK, ), (54, 4): ElementMapping( - element=Element.HHSRS_ASBESTOS_AND_MMF, + element=ElementType.HHSRS_ASBESTOS_AND_MMF, aspect_type=AspectType.RISK, ), (54, 15): ElementMapping( - element=Element.HHSRS_DOMESTIC_HYGIENE_PESTS_REFUSE, + element=ElementType.HHSRS_DOMESTIC_HYGIENE_PESTS_REFUSE, aspect_type=AspectType.RISK, ), (54, 29): ElementMapping( - element=Element.HHSRS_STRUCTURAL_COLLAPSE, + element=ElementType.HHSRS_STRUCTURAL_COLLAPSE, aspect_type=AspectType.RISK, ), } diff --git a/backend/condition/domain/mapping/peabody/peabody_mapper.py b/backend/condition/domain/mapping/peabody/peabody_mapper.py index dea07756..8c8a103b 100644 --- a/backend/condition/domain/mapping/peabody/peabody_mapper.py +++ b/backend/condition/domain/mapping/peabody/peabody_mapper.py @@ -1,6 +1,6 @@ from typing import Any, List, Optional -from backend.condition.domain.asset_condition import AssetCondition +from backend.condition.domain.element import Element from backend.condition.domain.mapping.element_mapping import ElementMapping from backend.condition.domain.mapping.peabody.peabody_element_map import ( PEABODY_ELEMENT_MAP, @@ -15,12 +15,12 @@ logger = setup_logger() class PeabodyMapper(Mapper): def map_asset_conditions_for_property( self, client_data: Any, survey_year: Optional[int] = None - ) -> List[AssetCondition]: + ) -> List[Element]: assert isinstance( client_data, PeabodyProperty ) # TODO: think of a better way to do this - mapped_assets: List[AssetCondition] = [] + mapped_assets: List[Element] = [] uprn: int = client_data.uprn for raw_asset in client_data.assets: @@ -36,7 +36,7 @@ class PeabodyMapper(Mapper): continue mapped_assets.append( - AssetCondition( + Element( uprn=uprn, element=element_mapping.element, aspect_type=element_mapping.aspect_type, diff --git a/backend/condition/domain/property_condition_survey.py b/backend/condition/domain/property_condition_survey.py new file mode 100644 index 00000000..6955e5fa --- /dev/null +++ b/backend/condition/domain/property_condition_survey.py @@ -0,0 +1,14 @@ +from dataclasses import dataclass +from typing import List +from datetime import date + +from backend.condition.domain.element import Element + + +@dataclass +class PropertyConditionSurvey: + uprn: int + elements: List[Element] + + date: date + source: str # TODO: make enum diff --git a/backend/condition/processor.py b/backend/condition/processor.py index 903c9f23..3ed0904a 100644 --- a/backend/condition/processor.py +++ b/backend/condition/processor.py @@ -1,7 +1,7 @@ from typing import Any, BinaryIO, List from datetime import datetime -from backend.condition.domain.asset_condition import AssetCondition +from backend.condition.domain.element import Element from backend.condition.domain.mapping.mapper import Mapper from backend.condition.parsing.parser import Parser from utils.logger import setup_logger @@ -22,7 +22,7 @@ def process_file(file_stream: BinaryIO, source_key: str) -> None: survey_year = datetime.now().year # TODO: get this from filepath or elsewhere - assets: List[AssetCondition] = [] + assets: List[Element] = [] for p in raw_properties: assets.extend(mapper.map_asset_conditions_for_property(p, survey_year)) diff --git a/backend/condition/tests/mapping/test_lbwf_mapper.py b/backend/condition/tests/mapping/test_lbwf_mapper.py index 907bd250..dab9c7a1 100644 --- a/backend/condition/tests/mapping/test_lbwf_mapper.py +++ b/backend/condition/tests/mapping/test_lbwf_mapper.py @@ -4,13 +4,13 @@ import pytest from datetime import date from backend.condition.domain.aspect_type import AspectType -from backend.condition.domain.element import Element +from backend.condition.domain.element_type import ElementType from backend.condition.domain.mapping.lbwf.lbwf_mapper import LbwfMapper from backend.condition.parsing.records.lbwf.lbwf_house import LbwfHouse from backend.condition.parsing.records.lbwf.lbwf_asset_condition import ( LbwfAssetCondition, ) -from backend.condition.domain.asset_condition import AssetCondition +from backend.condition.domain.element import Element def test_lbwf_mapper_maps_house(): @@ -219,10 +219,10 @@ def test_lbwf_mapper_maps_house(): survey_year = 2026 - expected_assets: List[AssetCondition] = [ - AssetCondition( + expected_assets: List[Element] = [ + Element( uprn=1, - element=Element.ACCESSIBLE_HOUSING_REGISTER, + element=ElementType.ACCESSIBLE_HOUSING_REGISTER, aspect_type=AspectType.CATEGORY, element_instance=None, value="General Needs", @@ -231,9 +231,9 @@ def test_lbwf_mapper_maps_house(): install_date=None, comments=None, ), - AssetCondition( + Element( uprn=1, - element=Element.FLOOR_LEVEL_FRONT_DOOR, + element=ElementType.FLOOR_LEVEL_FRONT_DOOR, aspect_type=AspectType.LOCATION, element_instance=None, value="Ground Floor", @@ -242,9 +242,9 @@ def test_lbwf_mapper_maps_house(): install_date=None, comments=None, ), - AssetCondition( + Element( uprn=1, - element=Element.ASBESTOS, + element=ElementType.ASBESTOS, aspect_type=AspectType.PRESENCE, element_instance=None, value="Yes", @@ -253,9 +253,9 @@ def test_lbwf_mapper_maps_house(): install_date=None, comments="Source of Data = ACT", ), - AssetCondition( + Element( uprn=1, - element=Element.HHSRS_ASBESTOS_AND_MMF, + element=ElementType.HHSRS_ASBESTOS_AND_MMF, aspect_type=AspectType.RISK, element_instance=None, value="Category 4 - Typical Risk", @@ -264,9 +264,9 @@ def test_lbwf_mapper_maps_house(): install_date=None, comments="Source of Data = ACT", ), - AssetCondition( + Element( uprn=1, - element=Element.BATHROOM, + element=ElementType.BATHROOM, aspect_type=AspectType.LOCATION, element_instance=None, value="Bathroom on Entrance Level in Property", @@ -275,9 +275,9 @@ def test_lbwf_mapper_maps_house(): install_date=None, comments="Source of Data = Codeman", ), - AssetCondition( + Element( uprn=1, - element=Element.CENTRAL_HEATING, + element=ElementType.CENTRAL_HEATING, aspect_type=AspectType.EXTENT, element_instance=None, value="No Central Heating in Property", @@ -286,9 +286,9 @@ def test_lbwf_mapper_maps_house(): install_date=None, comments="Source of Data = Codeman", ), - AssetCondition( + Element( uprn=1, - element=Element.HHSRS_FIRE, + element=ElementType.HHSRS_FIRE, aspect_type=AspectType.RISK, element_instance=None, value="Category 4 - Typical Risk", @@ -297,9 +297,9 @@ def test_lbwf_mapper_maps_house(): install_date=None, comments="Source of Data = Morgan Sindall", ), - AssetCondition( + Element( uprn=1, - element=Element.EXTERNAL_WALL, + element=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH, element_instance=1, value="Render or Pebbledash in External Area", @@ -308,9 +308,9 @@ def test_lbwf_mapper_maps_house(): install_date=date(2009, 4, 1), comments="Source of Data = Codeman", ), - AssetCondition( + Element( uprn=1, - element=Element.EXTERNAL_WALL, + element=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH, element_instance=2, value="Smooth Render Wall Finish 2 in External Area", @@ -322,7 +322,7 @@ def test_lbwf_mapper_maps_house(): ] # act - actual_assets: List[AssetCondition] = mapper.map_asset_conditions_for_property( + actual_assets: List[Element] = mapper.map_asset_conditions_for_property( lbwf_house, survey_year ) diff --git a/backend/condition/tests/mapping/test_peabody_mapper.py b/backend/condition/tests/mapping/test_peabody_mapper.py index 9997dfa8..75e03016 100644 --- a/backend/condition/tests/mapping/test_peabody_mapper.py +++ b/backend/condition/tests/mapping/test_peabody_mapper.py @@ -2,13 +2,13 @@ from datetime import datetime from typing import List from backend.condition.domain.aspect_type import AspectType -from backend.condition.domain.element import Element +from backend.condition.domain.element_type import ElementType from backend.condition.domain.mapping.peabody.peabody_mapper import PeabodyMapper from backend.condition.parsing.records.peabody.peabody_asset_condition import ( PeabodyAssetCondition, ) from backend.condition.parsing.records.peabody.peabody_property import PeabodyProperty -from backend.condition.domain.asset_condition import AssetCondition +from backend.condition.domain.element import Element def test_peabody_mapper_maps_property(): @@ -56,10 +56,10 @@ def test_peabody_mapper_maps_property(): ) mapper = PeabodyMapper() - expected_assets: List[AssetCondition] = [ - AssetCondition( + expected_assets: List[Element] = [ + Element( uprn=1, - element=Element.EXTERNAL_WINDOWS, + element=ElementType.EXTERNAL_WINDOWS, aspect_type=AspectType.MATERIAL, value="UPVC Double Glazed", quantity=8, @@ -69,9 +69,9 @@ def test_peabody_mapper_maps_property(): source_system=None, comments=None, ), - AssetCondition( + Element( uprn=1, - element=Element.EXTERNAL_DECORATION, + element=ElementType.EXTERNAL_DECORATION, aspect_type=AspectType.CONDITION, value="Normal", quantity=1, @@ -155,10 +155,10 @@ def test_wall_primary_and_secondary_wall_finish_map_correctly(): ) mapper = PeabodyMapper() - expected_assets: List[AssetCondition] = [ - AssetCondition( + expected_assets: List[Element] = [ + Element( uprn=1, - element=Element.EXTERNAL_WALLS, + element=ElementType.EXTERNAL_WALLS, aspect_type=AspectType.FINISH, value="Pointed", element_instance=1, @@ -169,9 +169,9 @@ def test_wall_primary_and_secondary_wall_finish_map_correctly(): source_system=None, comments=None, ), - AssetCondition( + Element( uprn=1, - element=Element.EXTERNAL_WALLS, + element=ElementType.EXTERNAL_WALLS, aspect_type=AspectType.FINISH, value="Pointing", element_instance=1, @@ -182,9 +182,9 @@ def test_wall_primary_and_secondary_wall_finish_map_correctly(): source_system=None, comments=None, ), - AssetCondition( + Element( uprn=1, - element=Element.EXTERNAL_WALLS, + element=ElementType.EXTERNAL_WALLS, aspect_type=AspectType.FINISH, value="Tile Hung", element_instance=1, From a0fa676230e5be1a34aac8d463a68dcca8090799 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 27 Jan 2026 13:53:09 +0000 Subject: [PATCH 41/50] =?UTF-8?q?Map=20peabody=20data=20to=20new=20structu?= =?UTF-8?q?re=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/condition/domain/element.py | 2 +- .../domain/mapping/lbwf/lbwf_mapper.py | 15 +- backend/condition/domain/mapping/mapper.py | 5 +- .../domain/mapping/peabody/peabody_mapper.py | 15 +- backend/condition/processor.py | 9 +- .../tests/mapping/test_lbwf_mapper.py | 18 +- .../tests/mapping/test_peabody_mapper.py | 169 ++++++++++-------- 7 files changed, 127 insertions(+), 106 deletions(-) diff --git a/backend/condition/domain/element.py b/backend/condition/domain/element.py index 7aca11fd..4a154815 100644 --- a/backend/condition/domain/element.py +++ b/backend/condition/domain/element.py @@ -7,6 +7,6 @@ from backend.condition.domain.element_type import ElementType @dataclass class Element: - element: ElementType + element_type: ElementType element_instance: int aspect_conditions: List[AspectCondition] diff --git a/backend/condition/domain/mapping/lbwf/lbwf_mapper.py b/backend/condition/domain/mapping/lbwf/lbwf_mapper.py index bb5f777d..fa61abf0 100644 --- a/backend/condition/domain/mapping/lbwf/lbwf_mapper.py +++ b/backend/condition/domain/mapping/lbwf/lbwf_mapper.py @@ -4,6 +4,7 @@ from backend.condition.domain.element import Element from backend.condition.domain.mapping.element_mapping import ElementMapping from backend.condition.domain.mapping.lbwf.lbwf_element_map import LBWF_ELEMENT_MAP from backend.condition.domain.mapping.mapper import Mapper +from backend.condition.domain.property_condition_survey import PropertyConditionSurvey from backend.condition.parsing.records.lbwf.lbwf_asset_condition import ( LbwfAssetCondition, ) @@ -16,16 +17,18 @@ logger = setup_logger() class LbwfMapper(Mapper): def map_asset_conditions_for_property( - self, client_data: Any, survey_year: Optional[int] = None - ) -> List[Element]: + self, client_property_data: Any, survey_year: Optional[int] = None + ) -> PropertyConditionSurvey: + raise NotImplementedError + assert isinstance( - client_data, LbwfHouse + client_property_data, LbwfHouse ) # TODO: think of a better way to do this mapped_assets: List[Element] = [] - uprn: int = client_data.uprn - for raw_asset in client_data.assets: + uprn: int = client_property_data.uprn + for raw_asset in client_property_data.assets: # Ignore metadata rows if raw_asset.element_code not in ["EICINSFREQ", "DECNTHMINC"]: try: @@ -41,7 +44,7 @@ class LbwfMapper(Mapper): mapped_assets.append( Element( uprn=uprn, - element=element_mapping.element, + element_type=element_mapping.element, aspect_type=element_mapping.aspect_type, value=raw_asset.attribute_code_description, quantity=raw_asset.quantity, diff --git a/backend/condition/domain/mapping/mapper.py b/backend/condition/domain/mapping/mapper.py index ace6ad73..3479668a 100644 --- a/backend/condition/domain/mapping/mapper.py +++ b/backend/condition/domain/mapping/mapper.py @@ -2,13 +2,14 @@ from abc import ABC, abstractmethod from typing import Any, List, Optional from backend.condition.domain.element import Element +from backend.condition.domain.property_condition_survey import PropertyConditionSurvey class Mapper(ABC): @abstractmethod def map_asset_conditions_for_property( - self, client_data: Any, survey_year: Optional[int] = None - ) -> List[Element]: + self, client_property_data: Any, survey_year: Optional[int] = None + ) -> PropertyConditionSurvey: # TODO: client_data should be properly typed pass diff --git a/backend/condition/domain/mapping/peabody/peabody_mapper.py b/backend/condition/domain/mapping/peabody/peabody_mapper.py index 8c8a103b..e9ee99a9 100644 --- a/backend/condition/domain/mapping/peabody/peabody_mapper.py +++ b/backend/condition/domain/mapping/peabody/peabody_mapper.py @@ -6,6 +6,7 @@ from backend.condition.domain.mapping.peabody.peabody_element_map import ( PEABODY_ELEMENT_MAP, ) from backend.condition.domain.mapping.mapper import Mapper +from backend.condition.domain.property_condition_survey import PropertyConditionSurvey from backend.condition.parsing.records.peabody.peabody_property import PeabodyProperty from utils.logger import setup_logger @@ -14,16 +15,18 @@ logger = setup_logger() class PeabodyMapper(Mapper): def map_asset_conditions_for_property( - self, client_data: Any, survey_year: Optional[int] = None - ) -> List[Element]: + self, client_property_data: Any, survey_year: Optional[int] = None + ) -> PropertyConditionSurvey: + raise NotImplementedError + assert isinstance( - client_data, PeabodyProperty + client_property_data, PeabodyProperty ) # TODO: think of a better way to do this mapped_assets: List[Element] = [] - uprn: int = client_data.uprn - for raw_asset in client_data.assets: + uprn: int = client_property_data.uprn + for raw_asset in client_property_data.assets: try: element_mapping: ElementMapping = PeabodyMapper._map_element( raw_asset.element_code, raw_asset.sub_element_code @@ -38,7 +41,7 @@ class PeabodyMapper(Mapper): mapped_assets.append( Element( uprn=uprn, - element=element_mapping.element, + element_type=element_mapping.element, aspect_type=element_mapping.aspect_type, value=raw_asset.material_or_answer, quantity=raw_asset.renewal_quantity, diff --git a/backend/condition/processor.py b/backend/condition/processor.py index 3ed0904a..3135d8a5 100644 --- a/backend/condition/processor.py +++ b/backend/condition/processor.py @@ -1,8 +1,8 @@ from typing import Any, BinaryIO, List from datetime import datetime -from backend.condition.domain.element import Element from backend.condition.domain.mapping.mapper import Mapper +from backend.condition.domain.property_condition_survey import PropertyConditionSurvey from backend.condition.parsing.parser import Parser from utils.logger import setup_logger from backend.condition.file_type import FileType, detect_file_type @@ -22,8 +22,11 @@ def process_file(file_stream: BinaryIO, source_key: str) -> None: survey_year = datetime.now().year # TODO: get this from filepath or elsewhere - assets: List[Element] = [] + property_condition_surveys: List[PropertyConditionSurvey] = [] + for p in raw_properties: - assets.extend(mapper.map_asset_conditions_for_property(p, survey_year)) + property_condition_surveys.push( + mapper.map_asset_conditions_for_property(p, survey_year) + ) print("done") # temp diff --git a/backend/condition/tests/mapping/test_lbwf_mapper.py b/backend/condition/tests/mapping/test_lbwf_mapper.py index dab9c7a1..8c92c029 100644 --- a/backend/condition/tests/mapping/test_lbwf_mapper.py +++ b/backend/condition/tests/mapping/test_lbwf_mapper.py @@ -222,7 +222,7 @@ def test_lbwf_mapper_maps_house(): expected_assets: List[Element] = [ Element( uprn=1, - element=ElementType.ACCESSIBLE_HOUSING_REGISTER, + element_type=ElementType.ACCESSIBLE_HOUSING_REGISTER, aspect_type=AspectType.CATEGORY, element_instance=None, value="General Needs", @@ -233,7 +233,7 @@ def test_lbwf_mapper_maps_house(): ), Element( uprn=1, - element=ElementType.FLOOR_LEVEL_FRONT_DOOR, + element_type=ElementType.FLOOR_LEVEL_FRONT_DOOR, aspect_type=AspectType.LOCATION, element_instance=None, value="Ground Floor", @@ -244,7 +244,7 @@ def test_lbwf_mapper_maps_house(): ), Element( uprn=1, - element=ElementType.ASBESTOS, + element_type=ElementType.ASBESTOS, aspect_type=AspectType.PRESENCE, element_instance=None, value="Yes", @@ -255,7 +255,7 @@ def test_lbwf_mapper_maps_house(): ), Element( uprn=1, - element=ElementType.HHSRS_ASBESTOS_AND_MMF, + element_type=ElementType.HHSRS_ASBESTOS_AND_MMF, aspect_type=AspectType.RISK, element_instance=None, value="Category 4 - Typical Risk", @@ -266,7 +266,7 @@ def test_lbwf_mapper_maps_house(): ), Element( uprn=1, - element=ElementType.BATHROOM, + element_type=ElementType.BATHROOM, aspect_type=AspectType.LOCATION, element_instance=None, value="Bathroom on Entrance Level in Property", @@ -277,7 +277,7 @@ def test_lbwf_mapper_maps_house(): ), Element( uprn=1, - element=ElementType.CENTRAL_HEATING, + element_type=ElementType.CENTRAL_HEATING, aspect_type=AspectType.EXTENT, element_instance=None, value="No Central Heating in Property", @@ -288,7 +288,7 @@ def test_lbwf_mapper_maps_house(): ), Element( uprn=1, - element=ElementType.HHSRS_FIRE, + element_type=ElementType.HHSRS_FIRE, aspect_type=AspectType.RISK, element_instance=None, value="Category 4 - Typical Risk", @@ -299,7 +299,7 @@ def test_lbwf_mapper_maps_house(): ), Element( uprn=1, - element=ElementType.EXTERNAL_WALL, + element_type=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH, element_instance=1, value="Render or Pebbledash in External Area", @@ -310,7 +310,7 @@ def test_lbwf_mapper_maps_house(): ), Element( uprn=1, - element=ElementType.EXTERNAL_WALL, + element_type=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH, element_instance=2, value="Smooth Render Wall Finish 2 in External Area", diff --git a/backend/condition/tests/mapping/test_peabody_mapper.py b/backend/condition/tests/mapping/test_peabody_mapper.py index 75e03016..63cd19c9 100644 --- a/backend/condition/tests/mapping/test_peabody_mapper.py +++ b/backend/condition/tests/mapping/test_peabody_mapper.py @@ -1,9 +1,10 @@ -from datetime import datetime -from typing import List +from datetime import datetime, date +from backend.condition.domain.aspect_condition import AspectCondition from backend.condition.domain.aspect_type import AspectType from backend.condition.domain.element_type import ElementType from backend.condition.domain.mapping.peabody.peabody_mapper import PeabodyMapper +from backend.condition.domain.property_condition_survey import PropertyConditionSurvey from backend.condition.parsing.records.peabody.peabody_asset_condition import ( PeabodyAssetCondition, ) @@ -56,40 +57,51 @@ def test_peabody_mapper_maps_property(): ) mapper = PeabodyMapper() - expected_assets: List[Element] = [ - Element( - uprn=1, - element=ElementType.EXTERNAL_WINDOWS, - aspect_type=AspectType.MATERIAL, - value="UPVC Double Glazed", - quantity=8, - install_date=None, - renewal_year=2036, - element_instance=None, - source_system=None, - comments=None, - ), - Element( - uprn=1, - element=ElementType.EXTERNAL_DECORATION, - aspect_type=AspectType.CONDITION, - value="Normal", - quantity=1, - install_date=None, - renewal_year=2029, - element_instance=None, - source_system=None, - comments=None, - ), - ] + expected_condition_survey = PropertyConditionSurvey( + uprn=1, + elements=[ + Element( + element_type=ElementType.EXTERNAL_WINDOWS, + element_instance=1, + aspect_conditions=[ + AspectCondition( + aspect_type=AspectType.MATERIAL, + aspect_instance=1, + value="UPVC Double Glazed", + quantity=8, + install_date=None, + renewal_year=2036, + comments=None, + ), + ], + ), + Element( + element_type=ElementType.EXTERNAL_DECORATION, + element_instance=1, + aspect_conditions=[ + AspectCondition( + aspect_type=AspectType.CONDITION, + aspect_instance=1, + value="Normal", + quantity=1, + install_date=None, + renewal_year=2029, + comments=None, + ) + ], + ), + ], + date=date(2000, 1, 1), # what should this be? + source="Peabody", + ) + # act - actual_assets = mapper.map_asset_conditions_for_property(peabody_property) + actual_condition_survey: PropertyConditionSurvey = ( + mapper.map_asset_conditions_for_property(peabody_property) + ) # assert - assert len(actual_assets) == len(expected_assets) - - for i, (actual, expected) in enumerate(zip(actual_assets, expected_assets)): - assert actual == expected, f"Mismatch at index {i}" + assert actual_condition_survey == expected_condition_survey def test_wall_primary_and_secondary_wall_finish_map_correctly(): @@ -155,52 +167,51 @@ def test_wall_primary_and_secondary_wall_finish_map_correctly(): ) mapper = PeabodyMapper() - expected_assets: List[Element] = [ - Element( - uprn=1, - element=ElementType.EXTERNAL_WALLS, - aspect_type=AspectType.FINISH, - value="Pointed", - element_instance=1, - aspect_instance=1, - quantity=65, - install_date=None, - renewal_year=2045, - source_system=None, - comments=None, - ), - Element( - uprn=1, - element=ElementType.EXTERNAL_WALLS, - aspect_type=AspectType.FINISH, - value="Pointing", - element_instance=1, - aspect_instance=1, - quantity=1, - install_date=None, - renewal_year=2069, - source_system=None, - comments=None, - ), - Element( - uprn=1, - element=ElementType.EXTERNAL_WALLS, - aspect_type=AspectType.FINISH, - value="Tile Hung", - element_instance=1, - aspect_instance=2, - quantity=8, - install_date=None, - renewal_year=2049, - source_system=None, - comments=None, - ), - ] + expected_condition_survey = PropertyConditionSurvey( + uprn=1, + elements=[ + Element( + element_type=ElementType.EXTERNAL_WALL, + element_instance=1, + aspect_conditions=[ + AspectCondition( + aspect_type=AspectType.FINISH, + aspect_instance=1, + value="Pointed", + quantity=65, + install_date=None, + renewal_year=2045, + comments=None, + ), + AspectCondition( + aspect_type=AspectType.FINISH, + aspect_instance=1, + value="Pointing", + quantity=1, + install_date=None, + renewal_year=2069, + comments=None, + ), + AspectCondition( + aspect_type=AspectType.FINISH, + aspect_instance=2, + value="Tile Hung", + quantity=8, + install_date=None, + renewal_year=2049, + comments=None, + ), + ], + ), + ], + date=date(2000, 1, 1), # what should this be? + source="Peabody", + ) + # act - actual_assets = mapper.map_asset_conditions_for_property(peabody_property) + actual_condition_survey: PropertyConditionSurvey = ( + mapper.map_asset_conditions_for_property(peabody_property) + ) # assert - assert len(actual_assets) == len(expected_assets) - - for i, (actual, expected) in enumerate(zip(actual_assets, expected_assets)): - assert actual == expected, f"Mismatch at index {i}" + assert actual_condition_survey == expected_condition_survey From 0bd5106cb4535ba46a657d8777ac310a53e4c563 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 27 Jan 2026 14:30:23 +0000 Subject: [PATCH 42/50] =?UTF-8?q?Map=20peabody=20data=20to=20new=20structu?= =?UTF-8?q?re=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mapping/peabody/peabody_element_map.py | 10 +-- .../domain/mapping/peabody/peabody_mapper.py | 63 ++++++++++++++----- 2 files changed, 52 insertions(+), 21 deletions(-) diff --git a/backend/condition/domain/mapping/peabody/peabody_element_map.py b/backend/condition/domain/mapping/peabody/peabody_element_map.py index 508b8968..62cb2fc3 100644 --- a/backend/condition/domain/mapping/peabody/peabody_element_map.py +++ b/backend/condition/domain/mapping/peabody/peabody_element_map.py @@ -63,8 +63,10 @@ PEABODY_ELEMENT_MAP = { element=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH ), (53, 30): ElementMapping( - element=ElementType.SECONDARY_WALL, aspect_type=AspectType.FINISH - ), # Should this be combined with primary wall, with different instance value? + element=ElementType.EXTERNAL_WALL, + aspect_type=AspectType.FINISH, + aspect_instance=2, + ), (53, 36): ElementMapping( element=ElementType.EXTERNAL_WALL, aspect_type=AspectType.INSULATION ), @@ -84,8 +86,8 @@ PEABODY_ELEMENT_MAP = { element=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH ), (120, 3): ElementMapping( - element=ElementType.PRIMARY_WALL, aspect_type=AspectType.INSULATION - ), # This code element code is actually "WALL" not "external wall" - correct? + element=ElementType.EXTERNAL_WALL, aspect_type=AspectType.INSULATION + ), # ========================================================== # EXTERNAL – ROOFS # ========================================================== diff --git a/backend/condition/domain/mapping/peabody/peabody_mapper.py b/backend/condition/domain/mapping/peabody/peabody_mapper.py index e9ee99a9..37bb3b55 100644 --- a/backend/condition/domain/mapping/peabody/peabody_mapper.py +++ b/backend/condition/domain/mapping/peabody/peabody_mapper.py @@ -1,6 +1,9 @@ from typing import Any, List, Optional +from datetime import date +from backend.condition.domain.aspect_condition import AspectCondition from backend.condition.domain.element import Element +from backend.condition.domain.element_type import ElementType from backend.condition.domain.mapping.element_mapping import ElementMapping from backend.condition.domain.mapping.peabody.peabody_element_map import ( PEABODY_ELEMENT_MAP, @@ -17,15 +20,12 @@ class PeabodyMapper(Mapper): def map_asset_conditions_for_property( self, client_property_data: Any, survey_year: Optional[int] = None ) -> PropertyConditionSurvey: - raise NotImplementedError - assert isinstance( client_property_data, PeabodyProperty ) # TODO: think of a better way to do this - mapped_assets: List[Element] = [] + mapped_elements: List[Element] = [] - uprn: int = client_property_data.uprn for raw_asset in client_property_data.assets: try: element_mapping: ElementMapping = PeabodyMapper._map_element( @@ -38,22 +38,51 @@ class PeabodyMapper(Mapper): ) continue - mapped_assets.append( - Element( - uprn=uprn, - element_type=element_mapping.element, - aspect_type=element_mapping.aspect_type, - value=raw_asset.material_or_answer, - quantity=raw_asset.renewal_quantity, - install_date=None, # Not available in peabody data - renewal_year=raw_asset.renewal_year, - element_instance=element_mapping.element_instance, - source_system=None, # Once we know the system name we'll set it here - comments=None, # Not available in peabody data + aspect_condition = AspectCondition( + aspect_type=element_mapping.aspect_type, + aspect_instance=element_mapping.aspect_instance or 1, + value=raw_asset.material_or_answer, + quantity=raw_asset.renewal_quantity, + install_date=None, # Not available in peabody data + renewal_year=raw_asset.renewal_year, + comments=None, # Not available in peabody data + ) + matching_element_type_instance: Optional[Element] = ( + PeabodyMapper._check_for_element_type_and_instance( + mapped_elements, + element_mapping.element, + element_mapping.element_instance or 1, ) ) - return mapped_assets + if not matching_element_type_instance: + mapped_elements.append( + Element( + element_type=element_mapping.element, + element_instance=element_mapping.element_instance or 1, + aspect_conditions=[aspect_condition], + ) + ) + else: + matching_element_type_instance.aspect_conditions.append( + aspect_condition + ) + + return PropertyConditionSurvey( + uprn=client_property_data.uprn, + elements=mapped_elements, + date=date(2000, 1, 1), # Temp. Not sure how to get this + source="Peabody", # TODO: Make this the system, not the client + ) + + @staticmethod + def _check_for_element_type_and_instance( + elements: List[Element], type: ElementType, instance: int + ) -> Optional[Element]: + for e in elements: + if e.element_type == type and e.element_instance == instance: + return e + return None @staticmethod def _map_element(element_code: int, sub_element_code: int) -> ElementMapping: From dc5b43d4539ebbda6588dafe5a7bae28e67def7f Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 27 Jan 2026 14:47:17 +0000 Subject: [PATCH 43/50] =?UTF-8?q?Map=20peabody=20data=20to=20new=20structu?= =?UTF-8?q?re=20=F0=9F=9F=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/mapping/peabody/peabody_mapper.py | 114 +++++++++++------- 1 file changed, 69 insertions(+), 45 deletions(-) diff --git a/backend/condition/domain/mapping/peabody/peabody_mapper.py b/backend/condition/domain/mapping/peabody/peabody_mapper.py index 37bb3b55..e052249b 100644 --- a/backend/condition/domain/mapping/peabody/peabody_mapper.py +++ b/backend/condition/domain/mapping/peabody/peabody_mapper.py @@ -1,4 +1,4 @@ -from typing import Any, List, Optional +from typing import Any, Dict, List, Optional, Tuple from datetime import date from backend.condition.domain.aspect_condition import AspectCondition @@ -24,57 +24,85 @@ class PeabodyMapper(Mapper): client_property_data, PeabodyProperty ) # TODO: think of a better way to do this - mapped_elements: List[Element] = [] + elements_by_key: dict[tuple[ElementType, int], Element] = {} for raw_asset in client_property_data.assets: - try: - element_mapping: ElementMapping = PeabodyMapper._map_element( - raw_asset.element_code, raw_asset.sub_element_code - ) - except: - logger.warning( - f"""Unrecognised Peabody Asset Element: {raw_asset.element} ({raw_asset.element_code}), - Sub-Element: {raw_asset.sub_element} ({raw_asset.sub_element_code}). Skipping record""" - ) - continue + element_mapping = PeabodyMapper._safe_map_element(raw_asset) - aspect_condition = AspectCondition( - aspect_type=element_mapping.aspect_type, - aspect_instance=element_mapping.aspect_instance or 1, - value=raw_asset.material_or_answer, - quantity=raw_asset.renewal_quantity, - install_date=None, # Not available in peabody data - renewal_year=raw_asset.renewal_year, - comments=None, # Not available in peabody data - ) - matching_element_type_instance: Optional[Element] = ( - PeabodyMapper._check_for_element_type_and_instance( - mapped_elements, - element_mapping.element, - element_mapping.element_instance or 1, - ) + aspect_condition = PeabodyMapper._build_aspect_condition( + raw_asset, element_mapping ) - if not matching_element_type_instance: - mapped_elements.append( - Element( - element_type=element_mapping.element, - element_instance=element_mapping.element_instance or 1, - aspect_conditions=[aspect_condition], - ) - ) - else: - matching_element_type_instance.aspect_conditions.append( - aspect_condition - ) + element_key = ( + element_mapping.element, + element_mapping.element_instance or 1, + ) + + PeabodyMapper._attach_aspect_condition_to_element( + elements_by_key, + element_key, + aspect_condition, + ) return PropertyConditionSurvey( uprn=client_property_data.uprn, - elements=mapped_elements, - date=date(2000, 1, 1), # Temp. Not sure how to get this + elements=list(elements_by_key.values()), + date=date(2000, 1, 1), # Temp - not sure how to get this source="Peabody", # TODO: Make this the system, not the client ) + @staticmethod + def _safe_map_element(raw_asset) -> Optional[ElementMapping]: + try: + return PeabodyMapper._map_element( + raw_asset.element_code, + raw_asset.sub_element_code, + ) + except KeyError: + logger.warning( + f"Unrecognised Peabody Asset Element: " + f"{raw_asset.element} ({raw_asset.element_code}), " + f"Sub-Element: {raw_asset.sub_element} ({raw_asset.sub_element_code}). " + "Skipping record" + ) + return None + + @staticmethod + def _map_element(element_code: int, sub_element_code: int) -> ElementMapping: + return PEABODY_ELEMENT_MAP[(element_code, sub_element_code)] + + @staticmethod + def _attach_aspect_condition_to_element( + elements_by_key: Dict[Tuple[ElementType, int], Element], + element_key: Tuple[ElementType, int], + aspect_condition: AspectCondition, + ) -> None: + element = elements_by_key.get(element_key) + + if element is None: + element = Element( + element_type=element_key[0], + element_instance=element_key[1], + aspect_conditions=[], + ) + elements_by_key[element_key] = element + + element.aspect_conditions.append(aspect_condition) + + @staticmethod + def _build_aspect_condition( + raw_asset, element_mapping: ElementMapping + ) -> AspectCondition: + return AspectCondition( + aspect_type=element_mapping.aspect_type, + aspect_instance=element_mapping.aspect_instance or 1, + value=raw_asset.material_or_answer, + quantity=raw_asset.renewal_quantity, + install_date=None, + renewal_year=raw_asset.renewal_year, + comments=None, + ) + @staticmethod def _check_for_element_type_and_instance( elements: List[Element], type: ElementType, instance: int @@ -83,7 +111,3 @@ class PeabodyMapper(Mapper): if e.element_type == type and e.element_instance == instance: return e return None - - @staticmethod - def _map_element(element_code: int, sub_element_code: int) -> ElementMapping: - return PEABODY_ELEMENT_MAP[(element_code, sub_element_code)] From 61caf8c495baaa7b4a7cdda4fed8e3027b4dc055 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 27 Jan 2026 14:48:05 +0000 Subject: [PATCH 44/50] =?UTF-8?q?Map=20peabody=20data=20to=20new=20structu?= =?UTF-8?q?re=20=F0=9F=9F=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../condition/domain/mapping/peabody/peabody_mapper.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/backend/condition/domain/mapping/peabody/peabody_mapper.py b/backend/condition/domain/mapping/peabody/peabody_mapper.py index e052249b..41b2ff39 100644 --- a/backend/condition/domain/mapping/peabody/peabody_mapper.py +++ b/backend/condition/domain/mapping/peabody/peabody_mapper.py @@ -102,12 +102,3 @@ class PeabodyMapper(Mapper): renewal_year=raw_asset.renewal_year, comments=None, ) - - @staticmethod - def _check_for_element_type_and_instance( - elements: List[Element], type: ElementType, instance: int - ) -> Optional[Element]: - for e in elements: - if e.element_type == type and e.element_instance == instance: - return e - return None From d6112f3dc8f45aabd63f789f475c09929bafb56d Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 27 Jan 2026 15:20:13 +0000 Subject: [PATCH 45/50] =?UTF-8?q?Map=20lbwf=20data=20to=20new=20structure?= =?UTF-8?q?=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/mapping/peabody/peabody_mapper.py | 2 +- .../tests/mapping/test_lbwf_mapper.py | 247 ++++++++++-------- 2 files changed, 141 insertions(+), 108 deletions(-) diff --git a/backend/condition/domain/mapping/peabody/peabody_mapper.py b/backend/condition/domain/mapping/peabody/peabody_mapper.py index 41b2ff39..7749b024 100644 --- a/backend/condition/domain/mapping/peabody/peabody_mapper.py +++ b/backend/condition/domain/mapping/peabody/peabody_mapper.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, Optional, Tuple from datetime import date from backend.condition.domain.aspect_condition import AspectCondition diff --git a/backend/condition/tests/mapping/test_lbwf_mapper.py b/backend/condition/tests/mapping/test_lbwf_mapper.py index 8c92c029..064e06f7 100644 --- a/backend/condition/tests/mapping/test_lbwf_mapper.py +++ b/backend/condition/tests/mapping/test_lbwf_mapper.py @@ -3,9 +3,11 @@ from xml.dom.minidom import Element import pytest from datetime import date +from backend.condition.domain.aspect_condition import AspectCondition from backend.condition.domain.aspect_type import AspectType from backend.condition.domain.element_type import ElementType from backend.condition.domain.mapping.lbwf.lbwf_mapper import LbwfMapper +from backend.condition.domain.property_condition_survey import PropertyConditionSurvey from backend.condition.parsing.records.lbwf.lbwf_house import LbwfHouse from backend.condition.parsing.records.lbwf.lbwf_asset_condition import ( LbwfAssetCondition, @@ -219,115 +221,146 @@ def test_lbwf_mapper_maps_house(): survey_year = 2026 - expected_assets: List[Element] = [ - Element( - uprn=1, - element_type=ElementType.ACCESSIBLE_HOUSING_REGISTER, - aspect_type=AspectType.CATEGORY, - element_instance=None, - value="General Needs", - quantity=1, - renewal_year=None, - install_date=None, - comments=None, - ), - Element( - uprn=1, - element_type=ElementType.FLOOR_LEVEL_FRONT_DOOR, - aspect_type=AspectType.LOCATION, - element_instance=None, - value="Ground Floor", - quantity=1, - renewal_year=None, - install_date=None, - comments=None, - ), - Element( - uprn=1, - element_type=ElementType.ASBESTOS, - aspect_type=AspectType.PRESENCE, - element_instance=None, - value="Yes", - quantity=None, - renewal_year=None, - install_date=None, - comments="Source of Data = ACT", - ), - Element( - uprn=1, - element_type=ElementType.HHSRS_ASBESTOS_AND_MMF, - aspect_type=AspectType.RISK, - element_instance=None, - value="Category 4 - Typical Risk", - quantity=None, - renewal_year=None, - install_date=None, - comments="Source of Data = ACT", - ), - Element( - uprn=1, - element_type=ElementType.BATHROOM, - aspect_type=AspectType.LOCATION, - element_instance=None, - value="Bathroom on Entrance Level in Property", - quantity=1, - renewal_year=None, - install_date=None, - comments="Source of Data = Codeman", - ), - Element( - uprn=1, - element_type=ElementType.CENTRAL_HEATING, - aspect_type=AspectType.EXTENT, - element_instance=None, - value="No Central Heating in Property", - quantity=1, - renewal_year=None, - install_date=None, - comments="Source of Data = Codeman", - ), - Element( - uprn=1, - element_type=ElementType.HHSRS_FIRE, - aspect_type=AspectType.RISK, - element_instance=None, - value="Category 4 - Typical Risk", - quantity=1, - renewal_year=None, - install_date=None, - comments="Source of Data = Morgan Sindall", - ), - Element( - uprn=1, - element_type=ElementType.EXTERNAL_WALL, - aspect_type=AspectType.FINISH, - element_instance=1, - value="Render or Pebbledash in External Area", - quantity=1, - renewal_year=2052, - install_date=date(2009, 4, 1), - comments="Source of Data = Codeman", - ), - Element( - uprn=1, - element_type=ElementType.EXTERNAL_WALL, - aspect_type=AspectType.FINISH, - element_instance=2, - value="Smooth Render Wall Finish 2 in External Area", - quantity=1, - renewal_year=2052, - install_date=date(2009, 4, 1), - comments="Source of Data = Codeman", - ), - ] + expected_condition_survey = PropertyConditionSurvey( + uprn=1, + elements=[ + Element( + element_type=ElementType.ACCESSIBLE_HOUSING_REGISTER, + element_instance=1, + aspect_conditions=[ + AspectCondition( + aspect_type=AspectType.CATEGORY, + aspect_instance=1, + value="General Needs", + quantity=1, + install_date=None, + renewal_year=None, + comments=None, + ) + ], + ), + Element( + element_type=ElementType.FLOOR_LEVEL_FRONT_DOOR, + element_instance=1, + aspect_conditions=[ + AspectCondition( + aspect_type=AspectType.LOCATION, + aspect_instance=1, + value="Ground Floor", + quantity=1, + install_date=None, + renewal_year=None, + comments=None, + ) + ], + ), + Element( + element_type=ElementType.ASBESTOS, + element_instance=1, + aspect_conditions=[ + AspectCondition( + aspect_type=AspectType.PRESENCE, + aspect_instance=1, + value="Yes", + quantity=None, + install_date=None, + renewal_year=None, + comments=None, + ) + ], + ), + Element( + element_type=ElementType.HHSRS_ASBESTOS_AND_MMF, + element_instance=1, + aspect_conditions=[ + AspectCondition( + aspect_type=AspectType.RISK, + aspect_instance=1, + value="Category 4 - Typical Risk", + quantity=None, + renewal_year=None, + comments="Source of Data = ACT", + ) + ], + ), + Element( + element_type=ElementType.BATHROOM, + element_instance=1, + aspect_conditions=[ + AspectCondition( + aspect_type=AspectType.LOCATION, + aspect_instance=1, + value="Bathroom on Entrance Level in Property", + quantity=1, + install_date=None, + renewal_year=None, + comments="Source of Data = Codeman", + ) + ], + ), + Element( + element_type=ElementType.CENTRAL_HEATING, + element_instance=1, + aspect_conditions=[ + AspectCondition( + aspect_type=AspectType.EXTENT, + aspect_instance=1, + value="No Central Heating in Property", + quantity=1, + install_date=None, + renewal_year=None, + comments="Source of Data = Codeman", + ) + ], + ), + Element( + element_type=ElementType.HHSRS_FIRE, + element_instance=1, + aspect_conditions=[ + AspectCondition( + aspect_type=AspectType.RISK, + aspect_instance=1, + value="Category 4 - Typical Risk", + quantity=1, + install_date=None, + renewal_year=None, + comments="Source of Data = Morgan Sindall", + ) + ], + ), + Element( + element_type=ElementType.EXTERNAL_WALL, + element_instance=1, + aspect_conditions=[ + AspectCondition( + aspect_type=AspectType.FINISH, + aspect_instance=1, + value="Render or Pebbledash in External Area", + quantity=1, + install_date=date(2009, 4, 1), + renewal_year=2052, + comments="Source of Data = Codeman", + ), + AspectCondition( + aspect_type=AspectType.FINISH, + aspect_instance=2, + value="Smooth Render Wall Finish 2 in External Area", + quantity=1, + install_date=date(2009, 4, 1), + renewal_year=2052, + comments="Source of Data = Codeman", + ), + ], + ), + ], + date=date(2000, 1, 1), # what should this be? + source="LBWF", + ) # act - actual_assets: List[Element] = mapper.map_asset_conditions_for_property( - lbwf_house, survey_year + actual_condition_survey: PropertyConditionSurvey = ( + mapper.map_asset_conditions_for_property(lbwf_house) ) # assert - assert len(actual_assets) == len(expected_assets) - - for i, (actual, expected) in enumerate(zip(actual_assets, expected_assets)): - assert actual == expected, f"Mismatch at index {i}" + assert actual_condition_survey == expected_condition_survey From 803484defd8319e1d893f23e65c53b7cd97f0a88 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 27 Jan 2026 15:59:06 +0000 Subject: [PATCH 46/50] =?UTF-8?q?Map=20lbwf=20data=20to=20new=20structure?= =?UTF-8?q?=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/mapping/lbwf/lbwf_element_map.py | 3 +- .../domain/mapping/lbwf/lbwf_mapper.py | 103 ++++++++++++------ .../domain/mapping/peabody/peabody_mapper.py | 7 +- backend/condition/tests/custom_asserts.py | 74 +++++++++++++ .../tests/mapping/test_lbwf_mapper.py | 12 +- .../tests/mapping/test_peabody_mapper.py | 5 +- 6 files changed, 161 insertions(+), 43 deletions(-) create mode 100644 backend/condition/tests/custom_asserts.py diff --git a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py index dfd9ca4e..96f21b84 100644 --- a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py +++ b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py @@ -175,7 +175,8 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { "EXTWALLFN2": ElementMapping( element=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH, - element_instance=2, + element_instance=1, + aspect_instance=2, ), "EXTWALLINS": ElementMapping( element=ElementType.EXTERNAL_WALL, diff --git a/backend/condition/domain/mapping/lbwf/lbwf_mapper.py b/backend/condition/domain/mapping/lbwf/lbwf_mapper.py index fa61abf0..f11133bf 100644 --- a/backend/condition/domain/mapping/lbwf/lbwf_mapper.py +++ b/backend/condition/domain/mapping/lbwf/lbwf_mapper.py @@ -1,6 +1,9 @@ -from typing import Any, List, Optional +from typing import Any, Dict, List, Optional, Tuple +from datetime import date +from backend.condition.domain.aspect_condition import AspectCondition from backend.condition.domain.element import Element +from backend.condition.domain.element_type import ElementType from backend.condition.domain.mapping.element_mapping import ElementMapping from backend.condition.domain.mapping.lbwf.lbwf_element_map import LBWF_ELEMENT_MAP from backend.condition.domain.mapping.mapper import Mapper @@ -19,51 +22,85 @@ class LbwfMapper(Mapper): def map_asset_conditions_for_property( self, client_property_data: Any, survey_year: Optional[int] = None ) -> PropertyConditionSurvey: - raise NotImplementedError - assert isinstance( client_property_data, LbwfHouse ) # TODO: think of a better way to do this - mapped_assets: List[Element] = [] + elements_by_key: dict[tuple[ElementType, int], Element] = {} - uprn: int = client_property_data.uprn for raw_asset in client_property_data.assets: - # Ignore metadata rows - if raw_asset.element_code not in ["EICINSFREQ", "DECNTHMINC"]: - try: - element_mapping: ElementMapping = LbwfMapper._map_element( - raw_asset.element_code - ) - except: - logger.warning( - f"Unrecognised LBWF Asset Element Code: {raw_asset.element_code}. Skipping record" - ) - continue + element_mapping = LbwfMapper._safe_map_element(raw_asset) - mapped_assets.append( - Element( - uprn=uprn, - element_type=element_mapping.element, - aspect_type=element_mapping.aspect_type, - value=raw_asset.attribute_code_description, - quantity=raw_asset.quantity, - install_date=raw_asset.install_date, - renewal_year=LbwfMapper._calculate_renewal_year( - raw_asset, survey_year - ), - element_instance=element_mapping.element_instance, - source_system=None, # Once we know the system name we'll set it here - comments=raw_asset.element_comments, - ) + aspect_condition = LbwfMapper._build_aspect_condition( + raw_asset, element_mapping, survey_year + ) + + element_key = ( + element_mapping.element, + element_mapping.element_instance or 1, + ) + + LbwfMapper._attach_aspect_condition_to_element( + elements_by_key, element_key, aspect_condition + ) + + return PropertyConditionSurvey( + uprn=client_property_data.uprn, + elements=list(elements_by_key.values()), + date=date(2000, 1, 1), # Temp - not sure how to get this + source="LBWF", # TODO: Make this the system, not the client + ) + + @staticmethod + def _safe_map_element(raw_asset: LbwfAssetCondition) -> Optional[ElementMapping]: + try: + return LbwfMapper._map_element(raw_asset.element_code) + except KeyError: + logger.warning( + logger.warning( + f"Unrecognised LBWF Asset Element: " + f"{raw_asset.element_code} ({raw_asset.element_code_description})). " + "Skipping record" ) - - return mapped_assets + ) + return None @staticmethod def _map_element(lbwf_element_code: str) -> ElementMapping: return LBWF_ELEMENT_MAP[lbwf_element_code] + @staticmethod + def _build_aspect_condition( + raw_asset, element_mapping: ElementMapping, survey_year: int + ) -> AspectCondition: + return AspectCondition( + aspect_type=element_mapping.aspect_type, + aspect_instance=element_mapping.aspect_instance or 1, + value=raw_asset.attribute_code_description, + quantity=raw_asset.quantity, + install_date=raw_asset.install_date, + renewal_year=LbwfMapper._calculate_renewal_year(raw_asset, survey_year), + comments=raw_asset.element_comments, + ) + + @staticmethod + def _attach_aspect_condition_to_element( + elements_by_key: Dict[Tuple[ElementType, int], Element], + element_key: Tuple[ElementType, int], + aspect_condition: AspectCondition, + ) -> None: + element = elements_by_key.get(element_key) + + if element is None: + element = Element( + element_type=element_key[0], + element_instance=element_key[1], + aspect_conditions=[], + ) + elements_by_key[element_key] = element + + element.aspect_conditions.append(aspect_condition) + @staticmethod def _calculate_renewal_year( lbwf_asset: LbwfAssetCondition, survey_year: Optional[int] diff --git a/backend/condition/domain/mapping/peabody/peabody_mapper.py b/backend/condition/domain/mapping/peabody/peabody_mapper.py index 7749b024..184b2898 100644 --- a/backend/condition/domain/mapping/peabody/peabody_mapper.py +++ b/backend/condition/domain/mapping/peabody/peabody_mapper.py @@ -10,6 +10,9 @@ from backend.condition.domain.mapping.peabody.peabody_element_map import ( ) from backend.condition.domain.mapping.mapper import Mapper from backend.condition.domain.property_condition_survey import PropertyConditionSurvey +from backend.condition.parsing.records.peabody.peabody_asset_condition import ( + PeabodyAssetCondition, +) from backend.condition.parsing.records.peabody.peabody_property import PeabodyProperty from utils.logger import setup_logger @@ -52,7 +55,7 @@ class PeabodyMapper(Mapper): ) @staticmethod - def _safe_map_element(raw_asset) -> Optional[ElementMapping]: + def _safe_map_element(raw_asset: PeabodyAssetCondition) -> Optional[ElementMapping]: try: return PeabodyMapper._map_element( raw_asset.element_code, @@ -98,7 +101,7 @@ class PeabodyMapper(Mapper): aspect_instance=element_mapping.aspect_instance or 1, value=raw_asset.material_or_answer, quantity=raw_asset.renewal_quantity, - install_date=None, + install_date=None, # Not available in peabody data renewal_year=raw_asset.renewal_year, comments=None, ) diff --git a/backend/condition/tests/custom_asserts.py b/backend/condition/tests/custom_asserts.py new file mode 100644 index 00000000..9e3abd7f --- /dev/null +++ b/backend/condition/tests/custom_asserts.py @@ -0,0 +1,74 @@ +from backend.condition.domain.property_condition_survey import PropertyConditionSurvey + + +class CustomAsserts: + def assert_property_condition_surveys_equal( + actual: PropertyConditionSurvey, + expected: PropertyConditionSurvey, + ) -> bool: + assert actual.uprn == expected.uprn, "UPRN differs" + assert actual.source == expected.source, "Source differs" + assert actual.date == expected.date, "Date differs" + + assert len(actual.elements) == len(expected.elements), ( + f"Expected {len(expected.elements)} elements, " + f"got {len(actual.elements)}" + ) + + for i, (actual_element, expected_element) in enumerate( + zip(actual.elements, expected.elements) + ): + assert actual_element.element_type == expected_element.element_type, ( + f"Element[{i}] type differs: " + f"{actual_element.element_type} != {expected_element.element_type}" + ) + assert ( + actual_element.element_instance == expected_element.element_instance + ), ( + f"Element[{i}] instance differs: " + f"{actual_element.element_instance} != {expected_element.element_instance}" + ) + + assert len(actual_element.aspect_conditions) == len( + expected_element.aspect_conditions + ), f"Element[{i}] aspect count differs" + + for j, (actual_aspect, expected_aspect) in enumerate( + zip( + actual_element.aspect_conditions, + expected_element.aspect_conditions, + ) + ): + prefix = f"Element[{i}].Aspect[{j}]" + + assert actual_aspect.aspect_type == expected_aspect.aspect_type, ( + f"{prefix}.aspect_type differs: " + f"{actual_aspect.aspect_type} != {expected_aspect.aspect_type}" + ) + assert ( + actual_aspect.aspect_instance == expected_aspect.aspect_instance + ), ( + f"{prefix}.aspect_instance differs: " + f"{actual_aspect.aspect_instance} != {expected_aspect.aspect_instance}" + ) + assert actual_aspect.value == expected_aspect.value, ( + f"{prefix}.value differs: " + f"{actual_aspect.value} != {expected_aspect.value}" + ) + assert actual_aspect.quantity == expected_aspect.quantity, ( + f"{prefix}.quantity differs: " + f"{actual_aspect.quantity} != {expected_aspect.quantity}" + ) + assert actual_aspect.install_date == expected_aspect.install_date, ( + f"{prefix}.install_date differs: " + f"{actual_aspect.install_date} != {expected_aspect.install_date}" + ) + assert actual_aspect.renewal_year == expected_aspect.renewal_year, ( + f"{prefix}.renewal_year differs: " + f"{actual_aspect.renewal_year} != {expected_aspect.renewal_year}" + ) + assert actual_aspect.comments == expected_aspect.comments, ( + f"{prefix}.comments differs: " + f"{actual_aspect.comments} != {expected_aspect.comments}" + ) + return True diff --git a/backend/condition/tests/mapping/test_lbwf_mapper.py b/backend/condition/tests/mapping/test_lbwf_mapper.py index 064e06f7..77890155 100644 --- a/backend/condition/tests/mapping/test_lbwf_mapper.py +++ b/backend/condition/tests/mapping/test_lbwf_mapper.py @@ -1,6 +1,3 @@ -from typing import List -from xml.dom.minidom import Element -import pytest from datetime import date from backend.condition.domain.aspect_condition import AspectCondition @@ -13,6 +10,7 @@ from backend.condition.parsing.records.lbwf.lbwf_asset_condition import ( LbwfAssetCondition, ) from backend.condition.domain.element import Element +from backend.condition.tests.custom_asserts import CustomAsserts def test_lbwf_mapper_maps_house(): @@ -265,7 +263,7 @@ def test_lbwf_mapper_maps_house(): quantity=None, install_date=None, renewal_year=None, - comments=None, + comments="Source of Data = ACT", ) ], ), @@ -359,8 +357,10 @@ def test_lbwf_mapper_maps_house(): # act actual_condition_survey: PropertyConditionSurvey = ( - mapper.map_asset_conditions_for_property(lbwf_house) + mapper.map_asset_conditions_for_property(lbwf_house, survey_year) ) # assert - assert actual_condition_survey == expected_condition_survey + assert CustomAsserts.assert_property_condition_surveys_equal( + actual_condition_survey, expected_condition_survey + ) diff --git a/backend/condition/tests/mapping/test_peabody_mapper.py b/backend/condition/tests/mapping/test_peabody_mapper.py index 63cd19c9..979258b0 100644 --- a/backend/condition/tests/mapping/test_peabody_mapper.py +++ b/backend/condition/tests/mapping/test_peabody_mapper.py @@ -10,6 +10,7 @@ from backend.condition.parsing.records.peabody.peabody_asset_condition import ( ) from backend.condition.parsing.records.peabody.peabody_property import PeabodyProperty from backend.condition.domain.element import Element +from backend.condition.tests.custom_asserts import CustomAsserts def test_peabody_mapper_maps_property(): @@ -214,4 +215,6 @@ def test_wall_primary_and_secondary_wall_finish_map_correctly(): ) # assert - assert actual_condition_survey == expected_condition_survey + assert CustomAsserts.assert_property_condition_surveys_equal( + actual_condition_survey, expected_condition_survey + ) From 32f9850a27e101503d819c7b3abda5d5eeb251f6 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 27 Jan 2026 16:08:20 +0000 Subject: [PATCH 47/50] minor fixes and TODO --- backend/condition/domain/element_type.py | 2 + .../domain/mapping/element_mapping.py | 4 +- .../domain/mapping/lbwf/lbwf_element_map.py | 240 ++++++------- .../domain/mapping/lbwf/lbwf_mapper.py | 2 +- .../mapping/peabody/peabody_element_map.py | 327 +++++++++--------- .../domain/mapping/peabody/peabody_mapper.py | 2 +- 6 files changed, 291 insertions(+), 286 deletions(-) diff --git a/backend/condition/domain/element_type.py b/backend/condition/domain/element_type.py index 32897895..bc2aa2d6 100644 --- a/backend/condition/domain/element_type.py +++ b/backend/condition/domain/element_type.py @@ -228,6 +228,8 @@ class ElementType(str, Enum): # HHSRS – ALL 29 HAZARDS # ========================================================== + # TODO: In order to group HHSRS, should there be a single HHSRS element type, and each of the below is an AspectType? + HHSRS_DAMP_AND_MOULD = "hhsrs_damp_and_mould" HHSRS_EXCESS_COLD = "hhsrs_excess_cold" HHSRS_EXCESS_HEAT = "hhsrs_excess_heat" diff --git a/backend/condition/domain/mapping/element_mapping.py b/backend/condition/domain/mapping/element_mapping.py index c93862c8..95fd08b9 100644 --- a/backend/condition/domain/mapping/element_mapping.py +++ b/backend/condition/domain/mapping/element_mapping.py @@ -1,13 +1,13 @@ from dataclasses import dataclass from typing import Optional -from xml.dom.minidom import Element from backend.condition.domain.aspect_type import AspectType +from backend.condition.domain.element_type import ElementType @dataclass(frozen=True) class ElementMapping: - element: Element + elementType: ElementType aspect_type: AspectType element_instance: Optional[int] = None aspect_instance: Optional[int] = None diff --git a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py index 96f21b84..a547fe5c 100644 --- a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py +++ b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py @@ -8,11 +8,11 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { # PROPERTY / GENERAL # ========================================================== "AHR_CAT": ElementMapping( - element=ElementType.ACCESSIBLE_HOUSING_REGISTER, + elementType=ElementType.ACCESSIBLE_HOUSING_REGISTER, aspect_type=AspectType.CATEGORY, ), "ASSETSAREA": ElementMapping( - element=ElementType.PROPERTY, + elementType=ElementType.PROPERTY, aspect_type=AspectType.AREA, ), # "DECNTHMINC": ElementMapping( @@ -20,302 +20,302 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { # aspect_type=AspectType.INCLUSION, # ), # Ignore this one "QUALITYSTD": ElementMapping( - element=ElementType.QUALITY_STANDARD, + elementType=ElementType.QUALITY_STANDARD, aspect_type=AspectType.TYPE, ), "EXTSTOREY": ElementMapping( - element=ElementType.PROPERTY, + elementType=ElementType.PROPERTY, aspect_type=AspectType.CONFIGURATION, ), "FLVL": ElementMapping( - element=ElementType.FLOOR_LEVEL_FRONT_DOOR, + elementType=ElementType.FLOOR_LEVEL_FRONT_DOOR, aspect_type=AspectType.LOCATION, ), "INTFLRLVL": ElementMapping( - element=ElementType.FLOOR_LEVEL, + elementType=ElementType.FLOOR_LEVEL, aspect_type=AspectType.LOCATION, ), "INTNSEINSL": ElementMapping( - element=ElementType.EXTERNAL_NOISE_INSULATION, # Maybe this shouldn't be "EXTERNAL_" + elementType=ElementType.EXTERNAL_NOISE_INSULATION, # Maybe this shouldn't be "EXTERNAL_" aspect_type=AspectType.ADEQUACY, ), "INTSTEPSFD": ElementMapping( - element=ElementType.STEPS_TO_FRONT_DOOR, + elementType=ElementType.STEPS_TO_FRONT_DOOR, aspect_type=AspectType.QUANTITY, ), # ========================================================== # ASBESTOS (NON-HHSRS RECORD) # ========================================================== "ASBESTOS": ElementMapping( - element=ElementType.ASBESTOS, + elementType=ElementType.ASBESTOS, aspect_type=AspectType.PRESENCE, ), # ========================================================== # INTERNAL – BATHROOMS & KITCHENS # ========================================================== "INTBTHRLOC": ElementMapping( - element=ElementType.BATHROOM, + elementType=ElementType.BATHROOM, aspect_type=AspectType.LOCATION, ), "INTBTHADEQ": ElementMapping( - element=ElementType.BATHROOM, + elementType=ElementType.BATHROOM, aspect_type=AspectType.ADEQUACY, ), "INTKITADEQ": ElementMapping( - element=ElementType.KITCHEN, + elementType=ElementType.KITCHEN, aspect_type=AspectType.ADEQUACY, ), "INTCKRLOC": ElementMapping( - element=ElementType.KITCHEN, + elementType=ElementType.KITCHEN, aspect_type=AspectType.LOCATION, ), "INTADDWCW": ElementMapping( - element=ElementType.ADDITIONAL_WC_OR_WHB, + elementType=ElementType.ADDITIONAL_WC_OR_WHB, aspect_type=AspectType.PRESENCE, ), "INTBTHREML": ElementMapping( - element=ElementType.BATHROOM_REMAINING_LIFE_SOURCE, + elementType=ElementType.BATHROOM_REMAINING_LIFE_SOURCE, aspect_type=AspectType.TYPE, ), "INTKITREML": ElementMapping( - element=ElementType.KITCHEN_REMAINING_LIFE_SOURCE, + elementType=ElementType.KITCHEN_REMAINING_LIFE_SOURCE, aspect_type=AspectType.TYPE, ), "INTTNTINST": ElementMapping( - element=ElementType.TENANT_INSTALLED_KITCHEN, + elementType=ElementType.TENANT_INSTALLED_KITCHEN, aspect_type=AspectType.TYPE, # Not certain about this aspect type - need more data ), # ========================================================== # INTERNAL – FIRE # ========================================================== "FRARISKRTG": ElementMapping( - element=ElementType.FIRE_RISK_ASSESSMENT, + elementType=ElementType.FIRE_RISK_ASSESSMENT, aspect_type=AspectType.RATING, ), "FRATYPE": ElementMapping( - element=ElementType.FIRE_RISK_ASSESSMENT, + elementType=ElementType.FIRE_RISK_ASSESSMENT, aspect_type=AspectType.TYPE, ), "FRAEVACSTR": ElementMapping( - element=ElementType.FIRE_RISK_ASSESSMENT, + elementType=ElementType.FIRE_RISK_ASSESSMENT, aspect_type=AspectType.STRATEGY, ), "INTSMKDET": ElementMapping( - element=ElementType.SMOKE_DETECTION, + elementType=ElementType.SMOKE_DETECTION, aspect_type=AspectType.PRESENCE, ), "INTCHEXTNT": ElementMapping( - element=ElementType.HEATING_SYSTEM, + elementType=ElementType.HEATING_SYSTEM, aspect_type=AspectType.EXTENT, ), # ========================================================== # HEATING & SERVICES # ========================================================== "INTCHEXTNT": ElementMapping( - element=ElementType.CENTRAL_HEATING, + elementType=ElementType.CENTRAL_HEATING, aspect_type=AspectType.EXTENT, ), "INTCHDIST": ElementMapping( - element=ElementType.HEATING_DISTRIBUTION, + elementType=ElementType.HEATING_DISTRIBUTION, aspect_type=AspectType.TYPE, ), "INTCHBLR": ElementMapping( - element=ElementType.HEATING_BOILER, + elementType=ElementType.HEATING_BOILER, aspect_type=AspectType.TYPE, ), "INTBOILERF": ElementMapping( - element=ElementType.BOILER_FUEL, + elementType=ElementType.BOILER_FUEL, aspect_type=AspectType.TYPE, ), "INTHTDISYS": ElementMapping( - element=ElementType.HEATING_SYSTEM, + elementType=ElementType.HEATING_SYSTEM, aspect_type=AspectType.DISTRIBUTION, ), "INTWTRHTNG": ElementMapping( - element=ElementType.WATER_HEATING, + elementType=ElementType.WATER_HEATING, aspect_type=AspectType.TYPE, ), "INTCOMHTG": ElementMapping( - element=ElementType.COMMUNITY_HEATING, + elementType=ElementType.COMMUNITY_HEATING, aspect_type=AspectType.TYPE, ), "INTELECTRC": ElementMapping( - element=ElementType.ELECTRICS, + elementType=ElementType.ELECTRICS, aspect_type=AspectType.WORK_REQUIRED, # Not certain about this aspect type - need more data ), "INTGASAVAI": ElementMapping( - element=ElementType.GAS_AVAILABLE, + elementType=ElementType.GAS_AVAILABLE, aspect_type=AspectType.PRESENCE, # Maybe should be AspectType.TYPE ? ), "INTHEATREC": ElementMapping( - element=ElementType.HEAT_RECOVERY_UNITS, + elementType=ElementType.HEAT_RECOVERY_UNITS, aspect_type=AspectType.PRESENCE, ), "INTHTIMP": ElementMapping( - element=ElementType.GAS_AVAILABLE, + elementType=ElementType.GAS_AVAILABLE, aspect_type=AspectType.WORK_REQUIRED, ), "INTPROGHTG": ElementMapping( - element=ElementType.PROGRAMMABLE_HEATING, + elementType=ElementType.PROGRAMMABLE_HEATING, aspect_type=AspectType.TYPE, # Should maybe be PRESENCE, but set to TYPE for consistency with Peabody data ), # ========================================================== # EXTERNAL – WALLS (INSTANCED) # ========================================================== "EXTWALLSTR": ElementMapping( - element=ElementType.EXTERNAL_WALL, + elementType=ElementType.EXTERNAL_WALL, aspect_type=AspectType.STRUCTURE, element_instance=1, ), "EXTWALLFN1": ElementMapping( - element=ElementType.EXTERNAL_WALL, + elementType=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH, element_instance=1, ), "EXTWALLFN2": ElementMapping( - element=ElementType.EXTERNAL_WALL, + elementType=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH, element_instance=1, aspect_instance=2, ), "EXTWALLINS": ElementMapping( - element=ElementType.EXTERNAL_WALL, + elementType=ElementType.EXTERNAL_WALL, aspect_type=AspectType.INSULATION, ), "EXTWALLSPL": ElementMapping( - element=ElementType.EXTERNAL_WALL, + elementType=ElementType.EXTERNAL_WALL, aspect_type=AspectType.CONDITION, ), "EXTDWNPTYP": ElementMapping( - element=ElementType.DOWNPIPES, + elementType=ElementType.DOWNPIPES, aspect_type=AspectType.MATERIAL, ), "EXTGUTRTYP": ElementMapping( - element=ElementType.GUTTERS, + elementType=ElementType.GUTTERS, aspect_type=AspectType.MATERIAL, ), # ========================================================== # EXTERNAL – ROOFS (INSTANCED) # ========================================================== "EXTRFSTR1": ElementMapping( - element=ElementType.ROOF, + elementType=ElementType.ROOF, aspect_type=AspectType.STRUCTURE, element_instance=1, ), "EXTRFSTR2": ElementMapping( - element=ElementType.ROOF, + elementType=ElementType.ROOF, aspect_type=AspectType.STRUCTURE, element_instance=2, ), "EXTRFSTR3": ElementMapping( - element=ElementType.ROOF, + elementType=ElementType.ROOF, aspect_type=AspectType.STRUCTURE, element_instance=3, ), "EXTROOF1": ElementMapping( - element=ElementType.ROOF, + elementType=ElementType.ROOF, aspect_type=AspectType.COVERING, element_instance=1, ), "EXTROOF2": ElementMapping( - element=ElementType.ROOF, + elementType=ElementType.ROOF, aspect_type=AspectType.COVERING, element_instance=2, ), "EXTROOF3": ElementMapping( - element=ElementType.ROOF, + elementType=ElementType.ROOF, aspect_type=AspectType.COVERING, element_instance=3, ), "EXTCHIMNEY": ElementMapping( - element=ElementType.CHIMNEY, + elementType=ElementType.CHIMNEY, aspect_type=AspectType.WORK_REQUIRED, ), "EXTFASOFBR": ElementMapping( - element=ElementType.FASCIA_SOFFIT_BARGEBOARDS, + elementType=ElementType.FASCIA_SOFFIT_BARGEBOARDS, aspect_type=AspectType.MATERIAL, ), "EXTGARROOF": ElementMapping( - element=ElementType.GARAGE_ROOF, + elementType=ElementType.GARAGE_ROOF, aspect_type=AspectType.MATERIAL, ), "EXTGARSTRF": ElementMapping( - element=ElementType.GARAGE_AND_STORE_ROOF, + elementType=ElementType.GARAGE_AND_STORE_ROOF, aspect_type=AspectType.MATERIAL, ), "EXTSTRROOF": ElementMapping( - element=ElementType.STORE_ROOF, + elementType=ElementType.STORE_ROOF, aspect_type=AspectType.MATERIAL, ), "INTLOFTINS": ElementMapping( - element=ElementType.LOFT_INSULATION, + elementType=ElementType.LOFT_INSULATION, aspect_type=AspectType.TYPE, ), # ========================================================== # EXTERNAL – DOORS & WINDOWS # ========================================================== "INTFRDOOR": ElementMapping( - element=ElementType.EXTERNAL_DOOR, + elementType=ElementType.EXTERNAL_DOOR, aspect_type=AspectType.TYPE, ), "INTFRDRFRR": ElementMapping( - element=ElementType.EXTERNAL_DOOR, + elementType=ElementType.EXTERNAL_DOOR, aspect_type=AspectType.FIRE_RATING, ), "EXTBKSDDR1": ElementMapping( - element=ElementType.EXTERNAL_DOOR, + elementType=ElementType.EXTERNAL_DOOR, aspect_type=AspectType.TYPE, element_instance=1, ), "EXTBKSDDR2": ElementMapping( - element=ElementType.EXTERNAL_DOOR, + elementType=ElementType.EXTERNAL_DOOR, aspect_type=AspectType.TYPE, element_instance=2, ), "INTWDWTYPE": ElementMapping( - element=ElementType.EXTERNAL_WINDOWS, + elementType=ElementType.EXTERNAL_WINDOWS, aspect_type=AspectType.TYPE, ), "EXTWNDWS1": ElementMapping( - element=ElementType.EXTERNAL_WINDOWS, + elementType=ElementType.EXTERNAL_WINDOWS, aspect_type=AspectType.TYPE, element_instance=1, ), "EXTWNDWS2": ElementMapping( - element=ElementType.EXTERNAL_WINDOWS, + elementType=ElementType.EXTERNAL_WINDOWS, aspect_type=AspectType.TYPE, element_instance=2, ), "EXTGARDOOR": ElementMapping( - element=ElementType.GARAGE_DOOR, + elementType=ElementType.GARAGE_DOOR, aspect_type=AspectType.MATERIAL, ), "EXTGARSTDR": ElementMapping( - element=ElementType.GARAGE_AND_STORE_DOOR, + elementType=ElementType.GARAGE_AND_STORE_DOOR, aspect_type=AspectType.MATERIAL, ), "EXTSTRDOOR": ElementMapping( - element=ElementType.STORE_DOOR, + elementType=ElementType.STORE_DOOR, aspect_type=AspectType.MATERIAL, ), "EXTGARWDWS": ElementMapping( - element=ElementType.GARAGE_WINDOWS, + elementType=ElementType.GARAGE_WINDOWS, aspect_type=AspectType.MATERIAL, ), "EXTSTRWDWS": ElementMapping( - element=ElementType.STORE_WINDOWS, + elementType=ElementType.STORE_WINDOWS, aspect_type=AspectType.MATERIAL, ), "EXTGARSTWD": ElementMapping( - element=ElementType.GARAGE_AND_STORE_WINDOWS, + elementType=ElementType.GARAGE_AND_STORE_WINDOWS, aspect_type=AspectType.MATERIAL, ), "EXTLINTELS": ElementMapping( - element=ElementType.LINTEL, + elementType=ElementType.LINTEL, aspect_type=AspectType.PRESENCE, ), "EXTPTFRDR1": ElementMapping( - element=ElementType.PATIO_FRENCH_DOOR, + elementType=ElementType.PATIO_FRENCH_DOOR, aspect_type=AspectType.MATERIAL, element_instance=1, ), @@ -323,217 +323,217 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { # EXTERNAL AREAS # ========================================================== "EXTBALCONY": ElementMapping( - element=ElementType.PRIVATE_BALCONY, + elementType=ElementType.PRIVATE_BALCONY, aspect_type=AspectType.PRESENCE, ), "EXTBPOINTG": ElementMapping( - element=ElementType.EXTERNAL_BRICKWORK_POINTING, + elementType=ElementType.EXTERNAL_BRICKWORK_POINTING, aspect_type=AspectType.PRESENCE, ), "EXTDRPKERB": ElementMapping( - element=ElementType.DROP_KERB, + elementType=ElementType.DROP_KERB, aspect_type=AspectType.PRESENCE, ), "EXTEXTDECS": ElementMapping( - element=ElementType.EXTERNAL_DECORATION, + elementType=ElementType.EXTERNAL_DECORATION, aspect_type=AspectType.PRESENCE, ), "EXTHARDSTD": ElementMapping( - element=ElementType.PATHS_AND_HARDSTANDINGS, + elementType=ElementType.PATHS_AND_HARDSTANDINGS, aspect_type=AspectType.MATERIAL, ), "EXTINTDWNP": ElementMapping( - element=ElementType.INTERNAL_DOWNPIPES_EXTERNAL_AREA, + elementType=ElementType.INTERNAL_DOWNPIPES_EXTERNAL_AREA, aspect_type=AspectType.MATERIAL, ), "EXTOUTBOH": ElementMapping( - element=ElementType.OUTBUILDING_OVERHAUL, + elementType=ElementType.OUTBUILDING_OVERHAUL, aspect_type=AspectType.TYPE, ), "EXTPARKING": ElementMapping( - element=ElementType.PARKING_AREAS, + elementType=ElementType.PARKING_AREAS, aspect_type=AspectType.PRESENCE, ), "EXTPCHCNPY": ElementMapping( - element=ElementType.PORCH_CANOPY, + elementType=ElementType.PORCH_CANOPY, aspect_type=AspectType.TYPE, ), "EXTSTRINSP": ElementMapping( - element=ElementType.EXTERNAL_STRUCTURAL_DEFECTS, + elementType=ElementType.EXTERNAL_STRUCTURAL_DEFECTS, aspect_type=AspectType.TYPE, # Need more sample data to know whether this is the correct aspect type ), "INTACCRAMP": ElementMapping( - element=ElementType.ACCESS_RAMP, + elementType=ElementType.ACCESS_RAMP, aspect_type=AspectType.TYPE, # # Need more sample data to know whether this is the correct aspect type ), # ====================== # FITNESS FOR HUMAN HABITATION # ====================== "FFHHDAMP": ElementMapping( - element=ElementType.FFHH_DAMP, + elementType=ElementType.FFHH_DAMP, aspect_type=AspectType.RISK, ), "FFHHHCWAT": ElementMapping( - element=ElementType.FFHH_HOT_AND_COLD_WATER, + elementType=ElementType.FFHH_HOT_AND_COLD_WATER, aspect_type=AspectType.RISK, ), "FFHHDRNWC": ElementMapping( - element=ElementType.FFHH_DRAINAGE_LAVATORIES, + elementType=ElementType.FFHH_DRAINAGE_LAVATORIES, aspect_type=AspectType.RISK, ), "FFHHNEGLC": ElementMapping( - element=ElementType.FFHH_NEGLECTED, + elementType=ElementType.FFHH_NEGLECTED, aspect_type=AspectType.RISK, ), "FFHHNONAT": ElementMapping( - element=ElementType.FFHH_NATURAL_LIGHT, + elementType=ElementType.FFHH_NATURAL_LIGHT, aspect_type=AspectType.RISK, ), "FFHHNOVEN": ElementMapping( - element=ElementType.FFHH_VENTILATION, + elementType=ElementType.FFHH_VENTILATION, aspect_type=AspectType.RISK, ), "FFHHPRPCK": ElementMapping( - element=ElementType.FFHH_FOOD_PREP_AND_WASHUP, + elementType=ElementType.FFHH_FOOD_PREP_AND_WASHUP, aspect_type=AspectType.RISK, ), "FFHHUNLAY": ElementMapping( - element=ElementType.FFHH_UNSAFE_LAYOUT, + elementType=ElementType.FFHH_UNSAFE_LAYOUT, aspect_type=AspectType.RISK, ), "FFHHUNSTA": ElementMapping( - element=ElementType.FFHH_UNSTABLE_BUILDING, + elementType=ElementType.FFHH_UNSTABLE_BUILDING, aspect_type=AspectType.RISK, ), # ========================================================== # HHSRS # ========================================================== "HHSRSDAMP": ElementMapping( - element=ElementType.HHSRS_DAMP_AND_MOULD, + elementType=ElementType.HHSRS_DAMP_AND_MOULD, aspect_type=AspectType.RISK, ), "HHSRSCOLD": ElementMapping( - element=ElementType.HHSRS_EXCESS_COLD, + elementType=ElementType.HHSRS_EXCESS_COLD, aspect_type=AspectType.RISK, ), "HHSRSHEAT": ElementMapping( - element=ElementType.HHSRS_EXCESS_HEAT, + elementType=ElementType.HHSRS_EXCESS_HEAT, aspect_type=AspectType.RISK, ), "HHSRSASB": ElementMapping( - element=ElementType.HHSRS_ASBESTOS_AND_MMF, + elementType=ElementType.HHSRS_ASBESTOS_AND_MMF, aspect_type=AspectType.RISK, ), "HHSRSBIOC": ElementMapping( - element=ElementType.HHSRS_BIOCIDES, + elementType=ElementType.HHSRS_BIOCIDES, aspect_type=AspectType.RISK, ), "HHSRSCO": ElementMapping( - element=ElementType.HHSRS_CARBON_MONOXIDE, + elementType=ElementType.HHSRS_CARBON_MONOXIDE, aspect_type=AspectType.RISK, ), "HHSRSNO2": ElementMapping( - element=ElementType.HHSRS_CARBON_MONOXIDE, + elementType=ElementType.HHSRS_CARBON_MONOXIDE, aspect_type=AspectType.RISK, ), # Duplicate of HHSRSCO; I think they relate to the same HHSRS hazard "HHSRSSO2": ElementMapping( - element=ElementType.HHSRS_CARBON_MONOXIDE, + elementType=ElementType.HHSRS_CARBON_MONOXIDE, aspect_type=AspectType.RISK, ), # Duplicate of HHSRSCO; I think they relate to the same HHSRS hazard "HHSRSLEAD": ElementMapping( - element=ElementType.HHSRS_LEAD, + elementType=ElementType.HHSRS_LEAD, aspect_type=AspectType.RISK, ), "HHSRSRADIA": ElementMapping( - element=ElementType.HHSRS_RADIATION, + elementType=ElementType.HHSRS_RADIATION, aspect_type=AspectType.RISK, ), "HHSRSFUEL": ElementMapping( - element=ElementType.HHSRS_UNCOMBUSTED_FUEL_GAS, + elementType=ElementType.HHSRS_UNCOMBUSTED_FUEL_GAS, aspect_type=AspectType.RISK, ), "HHSRSORGAN": ElementMapping( - element=ElementType.HHSRS_VOLATILE_ORGANIC_COMPOUNDS, + elementType=ElementType.HHSRS_VOLATILE_ORGANIC_COMPOUNDS, aspect_type=AspectType.RISK, ), "HHSRSCROWD": ElementMapping( - element=ElementType.HHSRS_CROWDING_AND_SPACE, + elementType=ElementType.HHSRS_CROWDING_AND_SPACE, aspect_type=AspectType.RISK, ), "HHSRSENTRY": ElementMapping( - element=ElementType.HHSRS_ENTRY_BY_INTRUDERS, + elementType=ElementType.HHSRS_ENTRY_BY_INTRUDERS, aspect_type=AspectType.RISK, ), "HHSRSLIGHT": ElementMapping( - element=ElementType.HHSRS_LIGHTING, + elementType=ElementType.HHSRS_LIGHTING, aspect_type=AspectType.RISK, ), "HHSRSNOISE": ElementMapping( - element=ElementType.HHSRS_NOISE, + elementType=ElementType.HHSRS_NOISE, aspect_type=AspectType.RISK, ), "HHSRSDOMES": ElementMapping( - element=ElementType.HHSRS_DOMESTIC_HYGIENE_PESTS_REFUSE, + elementType=ElementType.HHSRS_DOMESTIC_HYGIENE_PESTS_REFUSE, aspect_type=AspectType.RISK, ), "HHSRSFOOD": ElementMapping( - element=ElementType.HHSRS_FOOD_SAFETY, + elementType=ElementType.HHSRS_FOOD_SAFETY, aspect_type=AspectType.RISK, ), "HHSRSPERS": ElementMapping( - element=ElementType.HHSRS_PERSONAL_HYGIENE_SANITATION, + elementType=ElementType.HHSRS_PERSONAL_HYGIENE_SANITATION, aspect_type=AspectType.RISK, ), "HHSRSWATER": ElementMapping( - element=ElementType.HHSRS_WATER_SUPPLY, + elementType=ElementType.HHSRS_WATER_SUPPLY, aspect_type=AspectType.RISK, ), "HHSRSFBATH": ElementMapping( - element=ElementType.HHSRS_FALLS_ASSOCIATED_WITH_BATHS, + elementType=ElementType.HHSRS_FALLS_ASSOCIATED_WITH_BATHS, aspect_type=AspectType.RISK, ), "HHSRSFLEVE": ElementMapping( - element=ElementType.HHSRS_FALLS_ON_LEVEL_SURFACES, + elementType=ElementType.HHSRS_FALLS_ON_LEVEL_SURFACES, aspect_type=AspectType.RISK, ), "HHSRSFSTAI": ElementMapping( - element=ElementType.HHSRS_FALLS_ON_STAIRS, + elementType=ElementType.HHSRS_FALLS_ON_STAIRS, aspect_type=AspectType.RISK, ), "HHSRSFBETW": ElementMapping( - element=ElementType.HHSRS_FALLS_BETWEEN_LEVELS, + elementType=ElementType.HHSRS_FALLS_BETWEEN_LEVELS, aspect_type=AspectType.RISK, ), "HHSRSELEC": ElementMapping( - element=ElementType.HHSRS_ELECTRICAL_HAZARDS, + elementType=ElementType.HHSRS_ELECTRICAL_HAZARDS, aspect_type=AspectType.RISK, ), "HHSRSFIRE": ElementMapping( - element=ElementType.HHSRS_FIRE, + elementType=ElementType.HHSRS_FIRE, aspect_type=AspectType.RISK, ), "HHSRSFLAME": ElementMapping( - element=ElementType.HHSRS_FLAMES_HOT_SURFACES, + elementType=ElementType.HHSRS_FLAMES_HOT_SURFACES, aspect_type=AspectType.RISK, ), "HHSRSENTRP": ElementMapping( - element=ElementType.HHSRS_COLLISION_AND_ENTRAPMENT, + elementType=ElementType.HHSRS_COLLISION_AND_ENTRAPMENT, aspect_type=AspectType.RISK, ), "HHSRSEXPLO": ElementMapping( - element=ElementType.HHSRS_EXPLOSIONS, + elementType=ElementType.HHSRS_EXPLOSIONS, aspect_type=AspectType.RISK, ), "HHSRSSTRUC": ElementMapping( - element=ElementType.HHSRS_STRUCTURAL_COLLAPSE, + elementType=ElementType.HHSRS_STRUCTURAL_COLLAPSE, aspect_type=AspectType.RISK, ), "HHSRSCLOW": ElementMapping( - element=ElementType.HHSRS_COLLISION_AND_ENTRAPMENT, + elementType=ElementType.HHSRS_COLLISION_AND_ENTRAPMENT, aspect_type=AspectType.RISK, ), "HHSRSPOSI": ElementMapping( - element=ElementType.HHSRS_AMENITIES, + elementType=ElementType.HHSRS_AMENITIES, aspect_type=AspectType.RISK, ), } diff --git a/backend/condition/domain/mapping/lbwf/lbwf_mapper.py b/backend/condition/domain/mapping/lbwf/lbwf_mapper.py index f11133bf..09109ef9 100644 --- a/backend/condition/domain/mapping/lbwf/lbwf_mapper.py +++ b/backend/condition/domain/mapping/lbwf/lbwf_mapper.py @@ -36,7 +36,7 @@ class LbwfMapper(Mapper): ) element_key = ( - element_mapping.element, + element_mapping.elementType, element_mapping.element_instance or 1, ) diff --git a/backend/condition/domain/mapping/peabody/peabody_element_map.py b/backend/condition/domain/mapping/peabody/peabody_element_map.py index 62cb2fc3..2281a17c 100644 --- a/backend/condition/domain/mapping/peabody/peabody_element_map.py +++ b/backend/condition/domain/mapping/peabody/peabody_element_map.py @@ -7,660 +7,663 @@ PEABODY_ELEMENT_MAP = { # ========================================================== # PROPERTY / GENERAL # ========================================================== - (100, 1): ElementMapping(element=ElementType.PROPERTY, aspect_type=AspectType.TYPE), + (100, 1): ElementMapping( + elementType=ElementType.PROPERTY, aspect_type=AspectType.TYPE + ), # (100, 3): ElementMapping(element=Element.PROPERTY, aspect_type=AspectType.AGE), # (100, 14): ElementMapping(element="property", aspect_type="construction_type"), (50, 2): ElementMapping( - element=ElementType.CARBON_MONOXIDE_DETECTION, aspect_type=AspectType.TYPE + elementType=ElementType.CARBON_MONOXIDE_DETECTION, aspect_type=AspectType.TYPE ), - (50, 3): ElementMapping(element=ElementType.CCU, aspect_type=AspectType.TYPE), + (50, 3): ElementMapping(elementType=ElementType.CCU, aspect_type=AspectType.TYPE), (50, 7): ElementMapping( - element=ElementType.DISABLED_HOIST_TRACKING, aspect_type=AspectType.PRESENCE + elementType=ElementType.DISABLED_HOIST_TRACKING, aspect_type=AspectType.PRESENCE ), (50, 11): ElementMapping( - element=ElementType.HEAT_DETECTION, aspect_type=AspectType.TYPE + elementType=ElementType.HEAT_DETECTION, aspect_type=AspectType.TYPE ), (50, 21): ElementMapping( - element=ElementType.SMOKE_DETECTION, aspect_type=AspectType.TYPE + elementType=ElementType.SMOKE_DETECTION, aspect_type=AspectType.TYPE ), (50, 22): ElementMapping( - element=ElementType.STAIRLIFT, aspect_type=AspectType.PRESENCE + elementType=ElementType.STAIRLIFT, aspect_type=AspectType.PRESENCE ), (50, 26): ElementMapping( - element=ElementType.DISABLED_FACILITIES, aspect_type=AspectType.TYPE + elementType=ElementType.DISABLED_FACILITIES, aspect_type=AspectType.TYPE ), (100, 3): ElementMapping( - element=ElementType.PROPERTY, aspect_type=AspectType.AGE_BAND + elementType=ElementType.PROPERTY, aspect_type=AspectType.AGE_BAND ), (100, 14): ElementMapping( - element=ElementType.PROPERTY, aspect_type=AspectType.CONSTRUCTION_TYPE + elementType=ElementType.PROPERTY, aspect_type=AspectType.CONSTRUCTION_TYPE ), (100, 16): ElementMapping( - element=ElementType.PROPERTY, aspect_type=AspectType.CLASSIFICATION + elementType=ElementType.PROPERTY, aspect_type=AspectType.CLASSIFICATION ), (210, 2): ElementMapping( - element=ElementType.PASSENGER_LIFT, aspect_type=AspectType.TYPE + elementType=ElementType.PASSENGER_LIFT, aspect_type=AspectType.TYPE ), # ========================================================== # EXTERNAL – WALLS # ========================================================== (50, 16): ElementMapping( - element=ElementType.PARTY_WALL_FIRE_BREAK, aspect_type=AspectType.PRESENCE + elementType=ElementType.PARTY_WALL_FIRE_BREAK, aspect_type=AspectType.PRESENCE ), (53, 1): ElementMapping( - element=ElementType.BOUNDARY_WALLS, aspect_type=AspectType.PRESENCE + elementType=ElementType.BOUNDARY_WALLS, aspect_type=AspectType.PRESENCE ), (53, 4): ElementMapping( - element=ElementType.EXTERNAL_DECORATION, aspect_type=AspectType.PRESENCE + elementType=ElementType.EXTERNAL_DECORATION, aspect_type=AspectType.PRESENCE ), (53, 5): ElementMapping( - element=ElementType.EXTERNAL_NOISE_INSULATION, aspect_type=AspectType.ADEQUACY + elementType=ElementType.EXTERNAL_NOISE_INSULATION, + aspect_type=AspectType.ADEQUACY, ), (53, 14): ElementMapping( - element=ElementType.GARAGE_WALLS, aspect_type=AspectType.MATERIAL + elementType=ElementType.GARAGE_WALLS, aspect_type=AspectType.MATERIAL ), (53, 23): ElementMapping( - element=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH + elementType=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH ), (53, 30): ElementMapping( - element=ElementType.EXTERNAL_WALL, + elementType=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH, aspect_instance=2, ), (53, 36): ElementMapping( - element=ElementType.EXTERNAL_WALL, aspect_type=AspectType.INSULATION + elementType=ElementType.EXTERNAL_WALL, aspect_type=AspectType.INSULATION ), (53, 40): ElementMapping( - element=ElementType.SPANDREL_PANELS, aspect_type=AspectType.MATERIAL + elementType=ElementType.SPANDREL_PANELS, aspect_type=AspectType.MATERIAL ), (53, 41): ElementMapping( - element=ElementType.CLADDING, aspect_type=AspectType.MATERIAL + elementType=ElementType.CLADDING, aspect_type=AspectType.MATERIAL ), (100, 15): ElementMapping( - element=ElementType.EXTERNAL_DECORATION, aspect_type=AspectType.CONDITION + elementType=ElementType.EXTERNAL_DECORATION, aspect_type=AspectType.CONDITION ), (120, 1): ElementMapping( - element=ElementType.EXTERNAL_WALL, aspect_type=AspectType.STRUCTURE + elementType=ElementType.EXTERNAL_WALL, aspect_type=AspectType.STRUCTURE ), (120, 2): ElementMapping( - element=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH + elementType=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH ), (120, 3): ElementMapping( - element=ElementType.EXTERNAL_WALL, aspect_type=AspectType.INSULATION + elementType=ElementType.EXTERNAL_WALL, aspect_type=AspectType.INSULATION ), # ========================================================== # EXTERNAL – ROOFS # ========================================================== (50, 15): ElementMapping( - element=ElementType.LOFT_INSULATION, + elementType=ElementType.LOFT_INSULATION, aspect_type=AspectType.TYPE, ), (53, 2): ElementMapping( - element=ElementType.CHIMNEY, + elementType=ElementType.CHIMNEY, aspect_type=AspectType.PRESENCE, ), (53, 6): ElementMapping( - element=ElementType.FASCIA_SOFFIT_BARGEBOARDS, + elementType=ElementType.FASCIA_SOFFIT_BARGEBOARDS, aspect_type=AspectType.MATERIAL, ), (53, 7): ElementMapping( - element=ElementType.FLAT_ROOF_COVERING, + elementType=ElementType.FLAT_ROOF_COVERING, aspect_type=AspectType.MATERIAL, ), (53, 13): ElementMapping( - element=ElementType.GARAGE_ROOF, + elementType=ElementType.GARAGE_ROOF, aspect_type=AspectType.MATERIAL, ), (53, 15): ElementMapping( - element=ElementType.GUTTERS, + elementType=ElementType.GUTTERS, aspect_type=AspectType.MATERIAL, ), (53, 21): ElementMapping( - element=ElementType.PITCHED_ROOF_COVERING, + elementType=ElementType.PITCHED_ROOF_COVERING, aspect_type=AspectType.MATERIAL, ), (53, 22): ElementMapping( - element=ElementType.PORCH_CANOPY, + elementType=ElementType.PORCH_CANOPY, aspect_type=AspectType.TYPE, ), (53, 47): ElementMapping( - element=ElementType.ROOF, + elementType=ElementType.ROOF, aspect_type=AspectType.STRUCTURE, ), (110, 1): ElementMapping( - element=ElementType.ROOF, + elementType=ElementType.ROOF, aspect_type=AspectType.MATERIAL, element_instance=1, ), (110, 2): ElementMapping( - element=ElementType.ROOF, + elementType=ElementType.ROOF, aspect_type=AspectType.MATERIAL, element_instance=1, ), (110, 3): ElementMapping( - element=ElementType.CHIMNEY, + elementType=ElementType.CHIMNEY, aspect_type=AspectType.WORK_REQUIRED, ), (110, 4): ElementMapping( - element=ElementType.FASCIA, + elementType=ElementType.FASCIA, aspect_type=AspectType.MATERIAL, ), (110, 5): ElementMapping( - element=ElementType.SOFFIT, + elementType=ElementType.SOFFIT, aspect_type=AspectType.MATERIAL, ), (110, 6): ElementMapping( - element=ElementType.RAINWATER_GOODS, + elementType=ElementType.RAINWATER_GOODS, aspect_type=AspectType.MATERIAL, ), (110, 7): ElementMapping( - element=ElementType.LOFT_INSULATION, + elementType=ElementType.LOFT_INSULATION, aspect_type=AspectType.WORK_REQUIRED, # possibly not the right aspect type ), (110, 8): ElementMapping( - element=ElementType.PORCH_CANOPY, + elementType=ElementType.PORCH_CANOPY, aspect_type=AspectType.MATERIAL, ), # ========================================================== # EXTERNAL – DOORS & WINDOWS # ========================================================== (50, 8): ElementMapping( - element=ElementType.DOOR_ENTRY_HANDSET, + elementType=ElementType.DOOR_ENTRY_HANDSET, aspect_type=AspectType.PRESENCE, ), (53, 8): ElementMapping( - element=ElementType.FRONT_DOOR, + elementType=ElementType.FRONT_DOOR, aspect_type=AspectType.MATERIAL, ), (53, 12): ElementMapping( - element=ElementType.GARAGE_DOOR, + elementType=ElementType.GARAGE_DOOR, aspect_type=AspectType.MATERIAL, ), (53, 16): ElementMapping( - element=ElementType.LINTEL, + elementType=ElementType.LINTEL, aspect_type=AspectType.PRESENCE, ), (53, 19): ElementMapping( - element=ElementType.PATIO_FRENCH_DOOR, + elementType=ElementType.PATIO_FRENCH_DOOR, aspect_type=AspectType.MATERIAL, ), (53, 25): ElementMapping( - element=ElementType.REAR_DOOR, + elementType=ElementType.REAR_DOOR, aspect_type=AspectType.MATERIAL, ), (53, 29): ElementMapping( - element=ElementType.SECONDARY_GLAZING, + elementType=ElementType.SECONDARY_GLAZING, aspect_type=AspectType.PRESENCE, ), (53, 35): ElementMapping( - element=ElementType.STORE_DOOR, + elementType=ElementType.STORE_DOOR, aspect_type=AspectType.MATERIAL, ), (53, 38): ElementMapping( - element=ElementType.EXTERNAL_WINDOWS, + elementType=ElementType.EXTERNAL_WINDOWS, aspect_type=AspectType.TYPE, element_instance=1, ), (53, 39): ElementMapping( - element=ElementType.EXTERNAL_WINDOWS, + elementType=ElementType.EXTERNAL_WINDOWS, aspect_type=AspectType.TYPE, element_instance=2, ), (53, 43): ElementMapping( - element=ElementType.FRONT_DOOR, + elementType=ElementType.FRONT_DOOR, aspect_type=AspectType.TYPE, ), (130, 1): ElementMapping( - element=ElementType.EXTERNAL_WINDOWS, + elementType=ElementType.EXTERNAL_WINDOWS, aspect_type=AspectType.MATERIAL, ), (130, 2): ElementMapping( - element=ElementType.COMMUNAL_WINDOWS, + elementType=ElementType.COMMUNAL_WINDOWS, aspect_type=AspectType.MATERIAL, ), (140, 1): ElementMapping( - element=ElementType.MAIN_DOOR, + elementType=ElementType.MAIN_DOOR, aspect_type=AspectType.MATERIAL, ), (140, 2): ElementMapping( - element=ElementType.STORE_DOOR, + elementType=ElementType.STORE_DOOR, aspect_type=AspectType.MATERIAL, ), # Duplicate of (53, 35) (140, 3): ElementMapping( - element=ElementType.GARAGE_DOOR, + elementType=ElementType.GARAGE_DOOR, aspect_type=AspectType.MATERIAL, ), # Duplicate of (53, 12) (140, 4): ElementMapping( - element=ElementType.BLOCK_ENTRANCE_DOOR, + elementType=ElementType.BLOCK_ENTRANCE_DOOR, aspect_type=AspectType.MATERIAL, ), # ========================================================== # EXTERNAL AREAS # ========================================================== (53, 3): ElementMapping( - element=ElementType.DOWNPIPES, + elementType=ElementType.DOWNPIPES, aspect_type=AspectType.MATERIAL, ), (53, 9): ElementMapping( - element=ElementType.FRONT_FENCING, + elementType=ElementType.FRONT_FENCING, aspect_type=AspectType.MATERIAL, ), (53, 10): ElementMapping( - element=ElementType.FRONT_GATE, + elementType=ElementType.FRONT_GATE, aspect_type=AspectType.TYPE, ), (53, 17): ElementMapping( - element=ElementType.PARKING_AREAS, + elementType=ElementType.PARKING_AREAS, aspect_type=AspectType.MATERIAL, ), (53, 18): ElementMapping( - element=ElementType.PATHS_AND_HARDSTANDINGS, + elementType=ElementType.PATHS_AND_HARDSTANDINGS, aspect_type=AspectType.MATERIAL, ), (53, 24): ElementMapping( - element=ElementType.PRIVATE_BALCONY, + elementType=ElementType.PRIVATE_BALCONY, aspect_type=AspectType.PRESENCE, ), (53, 26): ElementMapping( - element=ElementType.REAR_FENCING, + elementType=ElementType.REAR_FENCING, aspect_type=AspectType.MATERIAL, ), (53, 27): ElementMapping( - element=ElementType.REAR_GATE, + elementType=ElementType.REAR_GATE, aspect_type=AspectType.TYPE, ), (53, 28): ElementMapping( - element=ElementType.RETAINING_WALLS, + elementType=ElementType.RETAINING_WALLS, aspect_type=AspectType.PRESENCE, ), (53, 31): ElementMapping( - element=ElementType.SIDE_FENCING, + elementType=ElementType.SIDE_FENCING, aspect_type=AspectType.MATERIAL, ), (53, 32): ElementMapping( - element=ElementType.SOIL_AND_VENT, + elementType=ElementType.SOIL_AND_VENT, aspect_type=AspectType.MATERIAL, ), (53, 34): ElementMapping( - element=ElementType.SOLAR_THERMALS, + elementType=ElementType.SOLAR_THERMALS, aspect_type=AspectType.PRESENCE, ), (53, 44): ElementMapping( - element=ElementType.GARAGE_STRUCTURE, + elementType=ElementType.GARAGE_STRUCTURE, aspect_type=AspectType.TYPE, ), (53, 45): ElementMapping( - element=ElementType.BALCONY_BALUSTRADE, + elementType=ElementType.BALCONY_BALUSTRADE, aspect_type=AspectType.MATERIAL, ), (150, 1): ElementMapping( - element=ElementType.BLOCK_ENTRANCE_DOOR, + elementType=ElementType.BLOCK_ENTRANCE_DOOR, aspect_type=AspectType.MATERIAL, ), (150, 2): ElementMapping( - element=ElementType.PATHS_AND_HARDSTANDINGS, + elementType=ElementType.PATHS_AND_HARDSTANDINGS, aspect_type=AspectType.MATERIAL, ), # Duplicate of (53, 18) - correct? (150, 3): ElementMapping( - element=ElementType.ROADS, + elementType=ElementType.ROADS, aspect_type=AspectType.MATERIAL, ), (150, 4): ElementMapping( - element=ElementType.BOUNDARY_WALLS, + elementType=ElementType.BOUNDARY_WALLS, aspect_type=AspectType.MATERIAL, ), (150, 5): ElementMapping( - element=ElementType.OUTBUILDINGS, + elementType=ElementType.OUTBUILDINGS, aspect_type=AspectType.TYPE, ), (150, 6): ElementMapping( - element=ElementType.GARAGE_STRUCTURE, + elementType=ElementType.GARAGE_STRUCTURE, aspect_type=AspectType.TYPE, ), # ========================================================== # INTERNAL – BATHROOMS & KITCHENS # ========================================================== (50, 1): ElementMapping( - element=ElementType.SECONDARY_TOILET, + elementType=ElementType.SECONDARY_TOILET, aspect_type=AspectType.PRESENCE, ), (50, 9): ElementMapping( - element=ElementType.BATHROOM_EXTRACTOR_FAN, + elementType=ElementType.BATHROOM_EXTRACTOR_FAN, aspect_type=AspectType.PRESENCE, ), (50, 9): ElementMapping( - element=ElementType.KITCHEN, + elementType=ElementType.KITCHEN, aspect_type=AspectType.TYPE, ), (50, 10): ElementMapping( - element=ElementType.KITCHEN_EXTRACTOR_FAN, + elementType=ElementType.KITCHEN_EXTRACTOR_FAN, aspect_type=AspectType.PRESENCE, ), (50, 13): ElementMapping( - element=ElementType.KITCHEN_SPACE_LAYOUT, + elementType=ElementType.KITCHEN_SPACE_LAYOUT, aspect_type=AspectType.ADEQUACY, ), (50, 14): ElementMapping( - element=ElementType.KITCHEN, + elementType=ElementType.KITCHEN, aspect_type=AspectType.TYPE, ), (50, 17): ElementMapping( - element=ElementType.BATHROOM, + elementType=ElementType.BATHROOM, aspect_type=AspectType.LOCATION, ), (50, 18): ElementMapping( - element=ElementType.BATHROOM, + elementType=ElementType.BATHROOM, aspect_type=AspectType.TYPE, ), # Actually "Primary bathroom type" - ok like this? (50, 20): ElementMapping( - element=ElementType.BATHROOM, + elementType=ElementType.BATHROOM, aspect_type=AspectType.TYPE, element_instance=2, ), # Actually "Secondary bathroom type" - ok like this? (160, 1): ElementMapping( - element=ElementType.KITCHEN, + elementType=ElementType.KITCHEN, aspect_type=AspectType.CONDITION, ), (160, 2): ElementMapping( - element=ElementType.KITCHEN_SPACE_LAYOUT, + elementType=ElementType.KITCHEN_SPACE_LAYOUT, aspect_type=AspectType.ADEQUACY, ), (190, 1): ElementMapping( - element=ElementType.BATHROOM, + elementType=ElementType.BATHROOM, aspect_type=AspectType.CONDITION, ), (190, 2): ElementMapping( - element=ElementType.SECONDARY_TOILET, + elementType=ElementType.SECONDARY_TOILET, aspect_type=AspectType.TYPE, ), # ========================================================== # COMMUNAL # ========================================================== (51, 1): ElementMapping( - element=ElementType.COMMUNAL_AERIAL, + elementType=ElementType.COMMUNAL_AERIAL, aspect_type=AspectType.PRESENCE, ), (51, 2): ElementMapping( - element=ElementType.COMMUNAL_AOV, + elementType=ElementType.COMMUNAL_AOV, aspect_type=AspectType.PRESENCE, ), (51, 3): ElementMapping( - element=ElementType.COMMUNAL_BALCONY_WALKWAY, + elementType=ElementType.COMMUNAL_BALCONY_WALKWAY, aspect_type=AspectType.PRESENCE, ), (51, 4): ElementMapping( - element=ElementType.COMMUNAL_BATHROOM, + elementType=ElementType.COMMUNAL_BATHROOM, aspect_type=AspectType.TYPE, ), (51, 5): ElementMapping( - element=ElementType.COMMUNAL_BIN_STORE_DOORS, + elementType=ElementType.COMMUNAL_BIN_STORE_DOORS, aspect_type=AspectType.PRESENCE, ), (51, 6): ElementMapping( - element=ElementType.COMMUNAL_BIN_STORE_ROOF, + elementType=ElementType.COMMUNAL_BIN_STORE_ROOF, aspect_type=AspectType.PRESENCE, ), (51, 7): ElementMapping( - element=ElementType.COMMUNAL_BIN_STORE_WALLS, + elementType=ElementType.COMMUNAL_BIN_STORE_WALLS, aspect_type=AspectType.MATERIAL, ), (51, 8): ElementMapping( - element=ElementType.COMMUNAL_BMS, + elementType=ElementType.COMMUNAL_BMS, aspect_type=AspectType.PRESENCE, ), (51, 9): ElementMapping( - element=ElementType.COMMUNAL_BOILER, + elementType=ElementType.COMMUNAL_BOILER, aspect_type=AspectType.TYPE, ), (51, 10): ElementMapping( - element=ElementType.COMMUNAL_BOOSTER_PUMP, + elementType=ElementType.COMMUNAL_BOOSTER_PUMP, aspect_type=AspectType.PRESENCE, ), (51, 11): ElementMapping( - element=ElementType.COMMUNAL_CCTV, + elementType=ElementType.COMMUNAL_CCTV, aspect_type=AspectType.PRESENCE, ), (51, 12): ElementMapping( - element=ElementType.COMMUNAL_CIRCULATION_SPACE, + elementType=ElementType.COMMUNAL_CIRCULATION_SPACE, aspect_type=AspectType.ADEQUACY, ), (51, 13): ElementMapping( - element=ElementType.COMMUNAL_COLD_WATER_STORAGE, + elementType=ElementType.COMMUNAL_COLD_WATER_STORAGE, aspect_type=AspectType.PRESENCE, ), (51, 14): ElementMapping( - element=ElementType.COMMUNAL_DOOR_ENTRY, + elementType=ElementType.COMMUNAL_DOOR_ENTRY, aspect_type=AspectType.SYSTEM, ), (51, 15): ElementMapping( - element=ElementType.COMMUNAL_DRY_RISER, + elementType=ElementType.COMMUNAL_DRY_RISER, aspect_type=AspectType.PRESENCE, ), (51, 16): ElementMapping( - element=ElementType.COMMUNAL_EMERGENCY_LIGHTING, + elementType=ElementType.COMMUNAL_EMERGENCY_LIGHTING, aspect_type=AspectType.PRESENCE, ), (51, 17): ElementMapping( - element=ElementType.COMMUNAL_EXTERNAL_DOORS, + elementType=ElementType.COMMUNAL_EXTERNAL_DOORS, aspect_type=AspectType.MATERIAL, ), (51, 19): ElementMapping( - element=ElementType.COMMUNAL_FIRE_ALARM, + elementType=ElementType.COMMUNAL_FIRE_ALARM, aspect_type=AspectType.TYPE, ), (51, 20): ElementMapping( - element=ElementType.COMMUNAL_INTERNAL_DECORATIONS, + elementType=ElementType.COMMUNAL_INTERNAL_DECORATIONS, aspect_type=AspectType.PRESENCE, ), (51, 21): ElementMapping( - element=ElementType.COMMUNAL_INTERNAL_DOORS, + elementType=ElementType.COMMUNAL_INTERNAL_DOORS, aspect_type=AspectType.MATERIAL, ), (51, 22): ElementMapping( - element=ElementType.COMMUNAL_INTERNAL_FLOOR, + elementType=ElementType.COMMUNAL_INTERNAL_FLOOR, aspect_type=AspectType.FINISH, ), (51, 23): ElementMapping( - element=ElementType.COMMUNAL_KITCHEN, + elementType=ElementType.COMMUNAL_KITCHEN, aspect_type=AspectType.TYPE, ), (51, 24): ElementMapping( - element=ElementType.COMMUNAL_LATERAL_MAINS, + elementType=ElementType.COMMUNAL_LATERAL_MAINS, aspect_type=AspectType.PRESENCE, ), (51, 25): ElementMapping( - element=ElementType.COMMUNAL_LIGHTING, + elementType=ElementType.COMMUNAL_LIGHTING, aspect_type=AspectType.PRESENCE, ), (51, 26): ElementMapping( - element=ElementType.COMMUNAL_LIGHTING_CONDUCTOR, + elementType=ElementType.COMMUNAL_LIGHTING_CONDUCTOR, aspect_type=AspectType.PRESENCE, ), (51, 27): ElementMapping( - element=ElementType.COMMUNAL_PASSENGER_LIFT, + elementType=ElementType.COMMUNAL_PASSENGER_LIFT, aspect_type=AspectType.TYPE, ), (51, 28): ElementMapping( - element=ElementType.COMMUNAL_ENTRANCE, + elementType=ElementType.COMMUNAL_ENTRANCE, aspect_type=AspectType.MATERIAL, element_instance=1, ), (51, 30): ElementMapping( - element=ElementType.COMMUNAL_ENTRANCE, + elementType=ElementType.COMMUNAL_ENTRANCE, aspect_type=AspectType.FINISH, element_instance=2, ), (51, 31): ElementMapping( - element=ElementType.COMMUNAL_SPRINKLER, + elementType=ElementType.COMMUNAL_SPRINKLER, aspect_type=AspectType.PRESENCE, ), (51, 29): ElementMapping( - element=ElementType.COMMUNAL_REFUSE_CHUTE, + elementType=ElementType.COMMUNAL_REFUSE_CHUTE, aspect_type=AspectType.PRESENCE, ), (51, 32): ElementMapping( - element=ElementType.COMMUNAL_STAIRS, + elementType=ElementType.COMMUNAL_STAIRS, aspect_type=AspectType.FINISH, ), (51, 33): ElementMapping( - element=ElementType.COMMUNAL_STORE_DOORS, + elementType=ElementType.COMMUNAL_STORE_DOORS, aspect_type=AspectType.MATERIAL, ), (51, 34): ElementMapping( - element=ElementType.COMMUNAL_STORE_ROOF, + elementType=ElementType.COMMUNAL_STORE_ROOF, aspect_type=AspectType.MATERIAL, ), (51, 35): ElementMapping( - element=ElementType.COMMUNAL_STORE_WALLS, + elementType=ElementType.COMMUNAL_STORE_WALLS, aspect_type=AspectType.MATERIAL, ), (51, 36): ElementMapping( - element=ElementType.COMMUNAL_WALKWAYS, + elementType=ElementType.COMMUNAL_WALKWAYS, aspect_type=AspectType.FINISH, ), (51, 37): ElementMapping( - element=ElementType.COMMUNAL_WARDEN_CALL_SYSTEM, + elementType=ElementType.COMMUNAL_WARDEN_CALL_SYSTEM, aspect_type=AspectType.PRESENCE, ), (51, 38): ElementMapping( - element=ElementType.COMMUNAL_TOILETS, + elementType=ElementType.COMMUNAL_TOILETS, aspect_type=AspectType.TYPE, ), (51, 39): ElementMapping( - element=ElementType.COMMUNAL_WET_RISER, + elementType=ElementType.COMMUNAL_WET_RISER, aspect_type=AspectType.PRESENCE, ), (51, 40): ElementMapping( - element=ElementType.COMMUNAL_PLUG_SOCKETS, + elementType=ElementType.COMMUNAL_PLUG_SOCKETS, aspect_type=AspectType.PRESENCE, ), (200, 1): ElementMapping( - element=ElementType.COMMUNAL_BOILER, + elementType=ElementType.COMMUNAL_BOILER, aspect_type=AspectType.TYPE, ), # Duplicate of (51, 9) - correct? (200, 2): ElementMapping( - element=ElementType.COMMUNAL_HEATING, + elementType=ElementType.COMMUNAL_HEATING, aspect_type=AspectType.TYPE, ), (200, 3): ElementMapping( - element=ElementType.COMMUNAL_ELECTRICS, + elementType=ElementType.COMMUNAL_ELECTRICS, aspect_type=AspectType.TYPE, ), (200, 4): ElementMapping( - element=ElementType.COMMUNAL_FIRE_ALARM, + elementType=ElementType.COMMUNAL_FIRE_ALARM, aspect_type=AspectType.TYPE, ), (200, 5): ElementMapping( - element=ElementType.COMMUNAL_LIFT, + elementType=ElementType.COMMUNAL_LIFT, aspect_type=AspectType.TYPE, ), (200, 6): ElementMapping( - element=ElementType.COMMUNAL_FLOOR_COVERING, + elementType=ElementType.COMMUNAL_FLOOR_COVERING, aspect_type=AspectType.MATERIAL, ), (200, 7): ElementMapping( - element=ElementType.COMMUNAL_KITCHEN, + elementType=ElementType.COMMUNAL_KITCHEN, aspect_type=AspectType.TYPE, ), (200, 8): ElementMapping( - element=ElementType.COMMUNAL_BATHROOM, + elementType=ElementType.COMMUNAL_BATHROOM, aspect_type=AspectType.TYPE, ), # Duplicate of (51, 4) - correct? (200, 9): ElementMapping( - element=ElementType.COMMUNAL_TOILETS, + elementType=ElementType.COMMUNAL_TOILETS, aspect_type=AspectType.TYPE, ), # Duplicate of (51, 38) - correct? (200, 10): ElementMapping( - element=ElementType.COMMUNAL_GATES, + elementType=ElementType.COMMUNAL_GATES, aspect_type=AspectType.TYPE, ), # ========================================================== # INTERNAL – HEATING # ========================================================== (50, 4): ElementMapping( - element=ElementType.HEATING_BOILER, + elementType=ElementType.HEATING_BOILER, aspect_type=AspectType.PRESENCE, ), # This is actually "Central heating boiler" - ok like this? (50, 5): ElementMapping( - element=ElementType.CENTRAL_HEATING, + elementType=ElementType.CENTRAL_HEATING, aspect_type=AspectType.EXTENT, ), (50, 6): ElementMapping( - element=ElementType.COLD_WATER_STORAGE, + elementType=ElementType.COLD_WATER_STORAGE, aspect_type=AspectType.PRESENCE, ), (50, 12): ElementMapping( - element=ElementType.HEATING_DISTRIBUTION, + elementType=ElementType.HEATING_DISTRIBUTION, aspect_type=AspectType.TYPE, ), (50, 19): ElementMapping( - element=ElementType.PROGRAMMABLE_HEATING, + elementType=ElementType.PROGRAMMABLE_HEATING, aspect_type=AspectType.TYPE, ), (50, 25): ElementMapping( - element=ElementType.HEATING_BOILER, + elementType=ElementType.HEATING_BOILER, aspect_type=AspectType.TYPE, ), (170, 1): ElementMapping( - element=ElementType.HEATING_BOILER, + elementType=ElementType.HEATING_BOILER, aspect_type=AspectType.TYPE, ), # Duplicate of (50,25) - correct? (170, 2): ElementMapping( - element=ElementType.HEATING_DISTRIBUTION, + elementType=ElementType.HEATING_DISTRIBUTION, aspect_type=AspectType.TYPE, ), # Duplicate of (50,12) - correct? (170, 3): ElementMapping( - element=ElementType.SECONDARY_HEATING, + elementType=ElementType.SECONDARY_HEATING, aspect_type=AspectType.TYPE, ), (170, 4): ElementMapping( - element=ElementType.COLD_WATER_STORAGE, + elementType=ElementType.COLD_WATER_STORAGE, aspect_type=AspectType.TYPE, ), (170, 5): ElementMapping( - element=ElementType.HOT_WATER_SYSTEM, + elementType=ElementType.HOT_WATER_SYSTEM, aspect_type=AspectType.TYPE, ), # ========================================================== # ELECTRICS # ========================================================== (50, 24): ElementMapping( - element=ElementType.INTERNAL_WIRING, + elementType=ElementType.INTERNAL_WIRING, aspect_type=AspectType.MATERIAL, ), (180, 1): ElementMapping( - element=ElementType.ELECTRICAL_WIRING, + elementType=ElementType.ELECTRICAL_WIRING, aspect_type=AspectType.WORK_REQUIRED, ), # Not certain about the AspectType - only example in the sample data is "Full Rewire" (180, 2): ElementMapping( - element=ElementType.CONSUMER_UNIT, + elementType=ElementType.CONSUMER_UNIT, aspect_type=AspectType.TYPE, ), (180, 3): ElementMapping( - element=ElementType.SMOKE_DETECTION, + elementType=ElementType.SMOKE_DETECTION, aspect_type=AspectType.TYPE, ), # Duplicate of (50, 21) - correct? (180, 4): ElementMapping( - element=ElementType.CARBON_MONOXIDE_DETECTION, + elementType=ElementType.CARBON_MONOXIDE_DETECTION, aspect_type=AspectType.TYPE, ), # Duplicate of (50, 2) - correct? # ========================================================== # HHSRS # ========================================================== (54, 1): ElementMapping( - element=ElementType.HHSRS_DAMP_AND_MOULD, + elementType=ElementType.HHSRS_DAMP_AND_MOULD, aspect_type=AspectType.RISK, ), (54, 4): ElementMapping( - element=ElementType.HHSRS_ASBESTOS_AND_MMF, + elementType=ElementType.HHSRS_ASBESTOS_AND_MMF, aspect_type=AspectType.RISK, ), (54, 15): ElementMapping( - element=ElementType.HHSRS_DOMESTIC_HYGIENE_PESTS_REFUSE, + elementType=ElementType.HHSRS_DOMESTIC_HYGIENE_PESTS_REFUSE, aspect_type=AspectType.RISK, ), (54, 29): ElementMapping( - element=ElementType.HHSRS_STRUCTURAL_COLLAPSE, + elementType=ElementType.HHSRS_STRUCTURAL_COLLAPSE, aspect_type=AspectType.RISK, ), } diff --git a/backend/condition/domain/mapping/peabody/peabody_mapper.py b/backend/condition/domain/mapping/peabody/peabody_mapper.py index 184b2898..92f1687f 100644 --- a/backend/condition/domain/mapping/peabody/peabody_mapper.py +++ b/backend/condition/domain/mapping/peabody/peabody_mapper.py @@ -37,7 +37,7 @@ class PeabodyMapper(Mapper): ) element_key = ( - element_mapping.element, + element_mapping.elementType, element_mapping.element_instance or 1, ) From 0d9ee79c40a60a32f94fba86c634a4bc13a64e6e Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 27 Jan 2026 16:25:21 +0000 Subject: [PATCH 48/50] adjust some element mappings for consistency between systems --- .../domain/mapping/lbwf/lbwf_element_map.py | 26 +++--- .../mapping/peabody/peabody_element_map.py | 80 ++++++++++++------- 2 files changed, 61 insertions(+), 45 deletions(-) diff --git a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py index a547fe5c..bf54c5bb 100644 --- a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py +++ b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py @@ -165,17 +165,14 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { "EXTWALLSTR": ElementMapping( elementType=ElementType.EXTERNAL_WALL, aspect_type=AspectType.STRUCTURE, - element_instance=1, ), "EXTWALLFN1": ElementMapping( elementType=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH, - element_instance=1, ), "EXTWALLFN2": ElementMapping( elementType=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH, - element_instance=1, aspect_instance=2, ), "EXTWALLINS": ElementMapping( @@ -200,32 +197,30 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { "EXTRFSTR1": ElementMapping( elementType=ElementType.ROOF, aspect_type=AspectType.STRUCTURE, - element_instance=1, ), "EXTRFSTR2": ElementMapping( elementType=ElementType.ROOF, aspect_type=AspectType.STRUCTURE, - element_instance=2, + aspect_instance=2, ), "EXTRFSTR3": ElementMapping( elementType=ElementType.ROOF, aspect_type=AspectType.STRUCTURE, - element_instance=3, + aspect_instance=3, ), "EXTROOF1": ElementMapping( elementType=ElementType.ROOF, - aspect_type=AspectType.COVERING, - element_instance=1, + aspect_type=AspectType.MATERIAL, ), "EXTROOF2": ElementMapping( elementType=ElementType.ROOF, - aspect_type=AspectType.COVERING, - element_instance=2, + aspect_type=AspectType.MATERIAL, + aspect_instance=2, ), "EXTROOF3": ElementMapping( elementType=ElementType.ROOF, - aspect_type=AspectType.COVERING, - element_instance=3, + aspect_type=AspectType.MATERIAL, + aspect_instance=3, ), "EXTCHIMNEY": ElementMapping( elementType=ElementType.CHIMNEY, @@ -265,12 +260,11 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { "EXTBKSDDR1": ElementMapping( elementType=ElementType.EXTERNAL_DOOR, aspect_type=AspectType.TYPE, - element_instance=1, ), "EXTBKSDDR2": ElementMapping( elementType=ElementType.EXTERNAL_DOOR, aspect_type=AspectType.TYPE, - element_instance=2, + aspect_instance=2, ), "INTWDWTYPE": ElementMapping( elementType=ElementType.EXTERNAL_WINDOWS, @@ -279,12 +273,11 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { "EXTWNDWS1": ElementMapping( elementType=ElementType.EXTERNAL_WINDOWS, aspect_type=AspectType.TYPE, - element_instance=1, ), "EXTWNDWS2": ElementMapping( elementType=ElementType.EXTERNAL_WINDOWS, aspect_type=AspectType.TYPE, - element_instance=2, + aspect_instance=2, ), "EXTGARDOOR": ElementMapping( elementType=ElementType.GARAGE_DOOR, @@ -317,7 +310,6 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { "EXTPTFRDR1": ElementMapping( elementType=ElementType.PATIO_FRENCH_DOOR, aspect_type=AspectType.MATERIAL, - element_instance=1, ), # ========================================================== # EXTERNAL AREAS diff --git a/backend/condition/domain/mapping/peabody/peabody_element_map.py b/backend/condition/domain/mapping/peabody/peabody_element_map.py index 2281a17c..ce344b9a 100644 --- a/backend/condition/domain/mapping/peabody/peabody_element_map.py +++ b/backend/condition/domain/mapping/peabody/peabody_element_map.py @@ -8,62 +8,81 @@ PEABODY_ELEMENT_MAP = { # PROPERTY / GENERAL # ========================================================== (100, 1): ElementMapping( - elementType=ElementType.PROPERTY, aspect_type=AspectType.TYPE + elementType=ElementType.PROPERTY, + aspect_type=AspectType.TYPE, ), # (100, 3): ElementMapping(element=Element.PROPERTY, aspect_type=AspectType.AGE), # (100, 14): ElementMapping(element="property", aspect_type="construction_type"), (50, 2): ElementMapping( - elementType=ElementType.CARBON_MONOXIDE_DETECTION, aspect_type=AspectType.TYPE + elementType=ElementType.CARBON_MONOXIDE_DETECTION, + aspect_type=AspectType.TYPE, + ), + (50, 3): ElementMapping( + elementType=ElementType.CCU, + aspect_type=AspectType.TYPE, ), - (50, 3): ElementMapping(elementType=ElementType.CCU, aspect_type=AspectType.TYPE), (50, 7): ElementMapping( - elementType=ElementType.DISABLED_HOIST_TRACKING, aspect_type=AspectType.PRESENCE + elementType=ElementType.DISABLED_HOIST_TRACKING, + aspect_type=AspectType.PRESENCE, ), (50, 11): ElementMapping( - elementType=ElementType.HEAT_DETECTION, aspect_type=AspectType.TYPE + elementType=ElementType.HEAT_DETECTION, + aspect_type=AspectType.TYPE, ), (50, 21): ElementMapping( - elementType=ElementType.SMOKE_DETECTION, aspect_type=AspectType.TYPE + elementType=ElementType.SMOKE_DETECTION, + aspect_type=AspectType.TYPE, ), (50, 22): ElementMapping( - elementType=ElementType.STAIRLIFT, aspect_type=AspectType.PRESENCE + elementType=ElementType.STAIRLIFT, + aspect_type=AspectType.PRESENCE, ), (50, 26): ElementMapping( - elementType=ElementType.DISABLED_FACILITIES, aspect_type=AspectType.TYPE + elementType=ElementType.DISABLED_FACILITIES, + aspect_type=AspectType.TYPE, ), (100, 3): ElementMapping( - elementType=ElementType.PROPERTY, aspect_type=AspectType.AGE_BAND + elementType=ElementType.PROPERTY, + aspect_type=AspectType.AGE_BAND, ), (100, 14): ElementMapping( - elementType=ElementType.PROPERTY, aspect_type=AspectType.CONSTRUCTION_TYPE + elementType=ElementType.PROPERTY, + aspect_type=AspectType.CONSTRUCTION_TYPE, ), (100, 16): ElementMapping( - elementType=ElementType.PROPERTY, aspect_type=AspectType.CLASSIFICATION + elementType=ElementType.PROPERTY, + aspect_type=AspectType.CLASSIFICATION, ), (210, 2): ElementMapping( - elementType=ElementType.PASSENGER_LIFT, aspect_type=AspectType.TYPE + elementType=ElementType.PASSENGER_LIFT, + aspect_type=AspectType.TYPE, ), # ========================================================== # EXTERNAL – WALLS # ========================================================== (50, 16): ElementMapping( - elementType=ElementType.PARTY_WALL_FIRE_BREAK, aspect_type=AspectType.PRESENCE + elementType=ElementType.PARTY_WALL_FIRE_BREAK, + aspect_type=AspectType.PRESENCE, ), (53, 1): ElementMapping( - elementType=ElementType.BOUNDARY_WALLS, aspect_type=AspectType.PRESENCE + elementType=ElementType.BOUNDARY_WALLS, + aspect_type=AspectType.PRESENCE, ), (53, 4): ElementMapping( - elementType=ElementType.EXTERNAL_DECORATION, aspect_type=AspectType.PRESENCE + elementType=ElementType.EXTERNAL_DECORATION, + aspect_type=AspectType.PRESENCE, ), (53, 5): ElementMapping( elementType=ElementType.EXTERNAL_NOISE_INSULATION, aspect_type=AspectType.ADEQUACY, ), (53, 14): ElementMapping( - elementType=ElementType.GARAGE_WALLS, aspect_type=AspectType.MATERIAL + elementType=ElementType.GARAGE_WALLS, + aspect_type=AspectType.MATERIAL, ), (53, 23): ElementMapping( - elementType=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH + elementType=ElementType.EXTERNAL_WALL, + aspect_type=AspectType.FINISH, ), (53, 30): ElementMapping( elementType=ElementType.EXTERNAL_WALL, @@ -71,25 +90,32 @@ PEABODY_ELEMENT_MAP = { aspect_instance=2, ), (53, 36): ElementMapping( - elementType=ElementType.EXTERNAL_WALL, aspect_type=AspectType.INSULATION + elementType=ElementType.EXTERNAL_WALL, + aspect_type=AspectType.INSULATION, ), (53, 40): ElementMapping( - elementType=ElementType.SPANDREL_PANELS, aspect_type=AspectType.MATERIAL + elementType=ElementType.SPANDREL_PANELS, + aspect_type=AspectType.MATERIAL, ), (53, 41): ElementMapping( - elementType=ElementType.CLADDING, aspect_type=AspectType.MATERIAL + elementType=ElementType.CLADDING, + aspect_type=AspectType.MATERIAL, ), (100, 15): ElementMapping( - elementType=ElementType.EXTERNAL_DECORATION, aspect_type=AspectType.CONDITION + elementType=ElementType.EXTERNAL_DECORATION, + aspect_type=AspectType.CONDITION, ), (120, 1): ElementMapping( - elementType=ElementType.EXTERNAL_WALL, aspect_type=AspectType.STRUCTURE + elementType=ElementType.EXTERNAL_WALL, + aspect_type=AspectType.STRUCTURE, ), (120, 2): ElementMapping( - elementType=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH + elementType=ElementType.EXTERNAL_WALL, + aspect_type=AspectType.FINISH, ), (120, 3): ElementMapping( - elementType=ElementType.EXTERNAL_WALL, aspect_type=AspectType.INSULATION + elementType=ElementType.EXTERNAL_WALL, + aspect_type=AspectType.INSULATION, ), # ========================================================== # EXTERNAL – ROOFS @@ -133,12 +159,11 @@ PEABODY_ELEMENT_MAP = { (110, 1): ElementMapping( elementType=ElementType.ROOF, aspect_type=AspectType.MATERIAL, - element_instance=1, ), (110, 2): ElementMapping( elementType=ElementType.ROOF, aspect_type=AspectType.MATERIAL, - element_instance=1, + aspect_instance=1, ), (110, 3): ElementMapping( elementType=ElementType.CHIMNEY, @@ -202,12 +227,11 @@ PEABODY_ELEMENT_MAP = { (53, 38): ElementMapping( elementType=ElementType.EXTERNAL_WINDOWS, aspect_type=AspectType.TYPE, - element_instance=1, ), (53, 39): ElementMapping( elementType=ElementType.EXTERNAL_WINDOWS, aspect_type=AspectType.TYPE, - element_instance=2, + aspect_instance=2, ), (53, 43): ElementMapping( elementType=ElementType.FRONT_DOOR, From 60241f947e6991559d7d4f2adfc929dc2becc02a Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 27 Jan 2026 16:57:59 +0000 Subject: [PATCH 49/50] add readme --- backend/condition/README.md | 75 +++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 backend/condition/README.md diff --git a/backend/condition/README.md b/backend/condition/README.md new file mode 100644 index 00000000..140d4585 --- /dev/null +++ b/backend/condition/README.md @@ -0,0 +1,75 @@ +# Condition Data Processor + +The Condition Data Processor performs the following steps: + +- **Extract** + - Ingest client Condition Survey data files (currently from local files; future support planned for S3 and internal survey sources) + - Parse input files into Data Transfer Objects (DTOs) + +- **Transform** + - Map source data into the internal domain data model + +- **Load** + - Persist transformed data into the ARA database (not yet implemented) + +The processor currently supports file formats provided by **Peabody** and **LBWF**. + +--- + +## Running Locally + +The `local_runner` script allows the processor to be executed in a local environment. + +1. Copy a sample input file into the `sample_data/` directory. +2. Update `local_runner.py` as required, specifically the definitions of: + - `lbwf_path` + - `peabody_path` + - `file_paths` +3. Run `local_runner.py`. + Breakpoints may be added and the script run in debug mode if required. + +--- + +## Known Data Issues + +Some inconsistencies exist in the source datasets, primarily involving multiple representations of the same logical element within a single file. In these cases, assumptions have been made in order to normalise the data into the internal domain model. + +### Peabody Data – Wall Finish Mapping + +In the original Peabody sample dataset, multiple Element/Sub-Element combinations correspond to wall finishes: + +| Element_Code | Element | Sub_Element_Code | Sub_Element | +|--------------|----------|------------------|-----------------------| +| 53 | External | 23 | Primary Wall Finish | +| 53 | External | 30 | Secondary Wall Finish | +| 120 | WALLS | 2 | Wall Finish | + +A single property may contain records for all three combinations, and each combination may appear multiple times. + +For example, the property at **55 Burnaby Street, London** contains entries for all three of the above combinations. However, it contains only a single entry for *“WALLS: Wall structure”*, indicating that the property has only one structure rather than multiple. + +This pattern is also observed in other sampled properties. Based on this, the following assumption is applied: + +- “Secondary” refers to a secondary **finish**, not a secondary **wall**. + +As a result: +- The property is mapped to a single Wall element. +- That Wall element is assigned three Finish aspects: + - Two with `aspect_instance = 1` + - One with `aspect_instance = 2` + +This means that the combination of +`UPRN / ElementType / ElementInstance / AspectType / AspectInstance` +is **not guaranteed to be unique**. + +### LBWF Data – Wall Finish Mapping + +In the LBWF dataset, the following element codes map to wall finishes: + +- `EXTWALLFN1` +- `EXTWALLFN2` + +These are similarly mapped as multiple instances of the **Finish** aspect for a single Wall element. + +--- + From 751032e6669857ce75e6af9621a7949462a4c17d Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Wed, 28 Jan 2026 09:54:33 +0000 Subject: [PATCH 50/50] fixes so it actually runs --- .../domain/mapping/lbwf/lbwf_mapper.py | 7 +++++ backend/condition/parsing/lbwf_parser.py | 31 +++++++++++-------- backend/condition/processor.py | 2 +- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/backend/condition/domain/mapping/lbwf/lbwf_mapper.py b/backend/condition/domain/mapping/lbwf/lbwf_mapper.py index 09109ef9..60c8b1ac 100644 --- a/backend/condition/domain/mapping/lbwf/lbwf_mapper.py +++ b/backend/condition/domain/mapping/lbwf/lbwf_mapper.py @@ -29,8 +29,15 @@ class LbwfMapper(Mapper): elements_by_key: dict[tuple[ElementType, int], Element] = {} for raw_asset in client_property_data.assets: + if raw_asset.element_code in ["DECNTHMINC", "EICINSFREQ"]: + # skip metadata rows + continue + element_mapping = LbwfMapper._safe_map_element(raw_asset) + if not element_mapping: + continue + aspect_condition = LbwfMapper._build_aspect_condition( raw_asset, element_mapping, survey_year ) diff --git a/backend/condition/parsing/lbwf_parser.py b/backend/condition/parsing/lbwf_parser.py index 63512c41..14d2efe4 100644 --- a/backend/condition/parsing/lbwf_parser.py +++ b/backend/condition/parsing/lbwf_parser.py @@ -3,18 +3,23 @@ from openpyxl import Workbook, load_workbook from collections import defaultdict from backend.condition.parsing.parser import Parser -from backend.condition.parsing.records.lbwf.lbwf_asset_condition import LbwfAssetCondition +from backend.condition.parsing.records.lbwf.lbwf_asset_condition import ( + LbwfAssetCondition, +) from backend.condition.parsing.records.lbwf.lbwf_house import LbwfHouse from backend.condition.utils.date_utils import normalise_date from utils.logger import setup_logger logger = setup_logger() + class LbwfParser(Parser): def parse(self, file_stream: BinaryIO) -> Any: wb: Workbook = load_workbook(file_stream) - address_to_uprn_map: Dict[str, int] = LbwfParser._generate_address_to_uprn_dict(wb) + address_to_uprn_map: Dict[str, int] = LbwfParser._generate_address_to_uprn_dict( + wb + ) assets = self._parse_assets(wb) houses = self._parse_houses(wb, address_to_uprn_map) @@ -82,7 +87,6 @@ class LbwfParser(Parser): for house in houses: house.assets = assets_by_ref.get(house.reference, []) - @staticmethod def _map_row_to_house_record( row: Any | Tuple[object | None, ...], @@ -100,8 +104,8 @@ class LbwfParser(Parser): house=row[header_indexes["HOSUE"]], fail_decency=row[header_indexes["Fail Decency"]], assets=[], - ) - + ) + @staticmethod def _map_row_to_asset_record( row: Any | Tuple[object | None, ...], @@ -119,7 +123,9 @@ class LbwfParser(Parser): element_code=row[header_indexes["ELEMENT CODE"]], element_code_description=row[header_indexes["ELEMENT CODE DESCRIPTION"]], attribute_code=row[header_indexes["ATTRIBUTE CODE"]], - attribute_code_description=row[header_indexes["ATTRIBUTE CODE DESCRIPTION"]], + attribute_code_description=row[ + header_indexes["ATTRIBUTE CODE DESCRIPTION"] + ], element_date_value=row[header_indexes["ELEMENT DATE VALUE"]], element_numerical_value=row[header_indexes["ELEMENT NUMERIC VALUE"]], element_text_value=row[header_indexes["ELEMENT TEXT VALUE"]], @@ -128,7 +134,6 @@ class LbwfParser(Parser): remaining_life=row[header_indexes["REMAINING LIFE"]], element_comments=row[header_indexes["ELEMENT COMMENTS"]], ) - @staticmethod def _generate_address_to_uprn_dict(wb: Workbook) -> Dict[str, int | None]: @@ -158,10 +163,9 @@ class LbwfParser(Parser): return mapping - @staticmethod def _get_column_indexes_by_name( - headers: Tuple[object | None, ...] + headers: Tuple[object | None, ...], ) -> Dict[str, int]: index: Dict[str, int] = {} @@ -170,13 +174,14 @@ class LbwfParser(Parser): index[header] = i return index - + @staticmethod - def _get_uprn_from_address(address: str, address_to_uprn_map: Dict[str, int]) -> int | None: + def _get_uprn_from_address( + address: str, address_to_uprn_map: Dict[str, int] + ) -> int | None: pseudo_name = address.split(",")[0] if pseudo_name.lower() in (k.lower() for k in address_to_uprn_map.keys()): return address_to_uprn_map[pseudo_name.upper()] - + return None - diff --git a/backend/condition/processor.py b/backend/condition/processor.py index 3135d8a5..3cbff498 100644 --- a/backend/condition/processor.py +++ b/backend/condition/processor.py @@ -25,7 +25,7 @@ def process_file(file_stream: BinaryIO, source_key: str) -> None: property_condition_surveys: List[PropertyConditionSurvey] = [] for p in raw_properties: - property_condition_surveys.push( + property_condition_surveys.append( mapper.map_asset_conditions_for_property(p, survey_year) )