Merge branch 'main' into feature/automate-categorisation-of-works

This commit is contained in:
Daniel Roth 2026-02-11 17:23:04 +00:00
commit e0857ab7a2
10 changed files with 76 additions and 29 deletions

View file

@ -1,5 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="PROJECT_PROFILE" value="Default" />
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>

View file

@ -69,24 +69,24 @@ def app():
Property UPRN
"""
data_folder = "/Users/khalimconn-kowlessar/Documents/hestia/Customers/Aspire"
data_filename = "ASPIRE ASSET LIST.xlsx"
sheet_name = "Asset List"
postcode_column = "Postcode"
data_folder = "/Users/khalimconn-kowlessar/Documents/hestia/Customers/West Kent"
data_filename = "West Kent Asset List.xlsx"
sheet_name = "Sheet1"
postcode_column = "POSTCODE"
address1_column = None
address1_method = "house_number_extraction"
fulladdress_column = "Address"
fulladdress_column = "ADDRESS"
address_cols_to_concat = []
missing_postcodes_method = None
landlord_year_built = None
landlord_os_uprn = None
landlord_property_type = "Property Type"
landlord_property_type = "PROPERTY TYPE"
landlord_built_form = None
landlord_wall_construction = None
landlord_roof_construction = None
landlord_wall_construction = "wall combined"
landlord_roof_construction = "HEATING SYSTEM"
landlord_heating_system = None
landlord_existing_pv = None
landlord_property_id = "LLUPRN"
landlord_property_id = "UPRN"
landlord_sap = None
outcomes_filename = None
outcomes_sheetname = None

View file

@ -308,6 +308,18 @@ ROOF_CONSTRUCTION_MAPPINGS = {
'Flat: No Insulation': 'flat uninsulated',
'AnotherDwellingAbove: Unknown, PitchedNormalLoftAccess: 250mm': 'another dwelling above',
'PitchedNormalLoftAccess: 175mm': 'pitched insulated',
'AnotherDwellingAbove: 300mm': 'another dwelling above'
'AnotherDwellingAbove: 300mm': 'another dwelling above',
'Another dwelling above, As built': 'another dwelling above',
'Pitched (slates or tiles) no loft access, 400mm+': 'pitched insulated',
'Pitched (slates or tiles) access to loft, 400mm+': 'pitched insulated',
'Pitched (slates or tiles) access to loft, 300mm': 'pitched insulated',
'Pitched (slates or tiles) access to loft, 75mm': 'pitched less than 100mm insulation',
'Pitched (slates or tiles) no loft access, 300mm': 'pitched insulated',
'Pitched (slates or tiles) access to loft, 270mm': 'pitched insulated',
'Pitched (slates or tiles) access to loft, 100mm': 'pitched insulated',
'Pitched (slates or tiles) no loft access, 200mm': 'pitched insulated',
'Pitched (slates or tiles) access to loft, 200mm': 'pitched insulated',
'Pitched (slates or tiles) access to loft, 50mm': 'pitched less than 100mm insulation'
}

View file

@ -363,6 +363,12 @@ WALL_CONSTRUCTION_MAPPINGS = {
'Timber Frame, As Built': 'timber frame unknown insulation',
'Solid Brick, Internal Insulation': 'insulated solid brick',
'Granite or Whinstone, As Built': 'granite or whinstone unknown insulation',
'Solid Brick, External': 'insulated solid brick'
'Solid Brick, External': 'insulated solid brick',
'Cavity, Filled cavity': 'filled cavity',
'Solid Brick, As built': 'solid brick unknown insulation',
'System built, As built': 'system built unknown insulation',
'Timber frame, As built': 'timber frame unknown insulation',
'Cavity, As built': 'cavity unknown insulation'
}

View file

@ -220,7 +220,7 @@ def get_data(
searcher.find_property(skip_os=True)
# Check if we have a flat or appartment
if searcher.newest_epc is None and uprn is None:
if not searcher.newest_epc and uprn is None:
# Try again:
if SearchEpc.get_house_number(address=str(house_number), postcode=postcode) is None:
# Backup
@ -252,12 +252,12 @@ def get_data(
searcher.find_property(skip_os=True)
# As a final resort, we estimate the EPC
if property_type is not None and searcher.newest_epc is None:
if property_type is not None and not searcher.newest_epc:
searcher.ordnance_survey_client.property_type = property_type
searcher.ordnance_survey_client.built_form = built_form
searcher.find_property(skip_os=True)
if searcher.newest_epc is None:
if not searcher.newest_epc:
no_epc.append(home[row_id_name])
continue

View file

@ -1,3 +1,4 @@
from typing import Iterator
from backend.addresses.Address import Address
@ -12,6 +13,9 @@ class Addresses:
def __len__(self) -> int:
return len(self._addresses)
def __iter__(self) -> Iterator[Address]:
return iter(self._addresses)
@classmethod
def from_plan_input(cls, plan_input: list[dict], body) -> "Addresses":
addresses = []

View file

@ -74,6 +74,8 @@ class RoofAttributes(Definitions):
"insulation_thickness",
]
NODATA_NULLS = ["insulation_thickness", "thermal_transmittance", "thermal_transmittance_unit"]
def __init__(self, description: str):
"""
:param description: Description of the roof.
@ -153,6 +155,10 @@ class RoofAttributes(Definitions):
if self.nodata:
for key in self.DEFAULT_KEYS:
result[key] = False
# Insulation thickness, thermal transmittance and thermal transmittance unit are set to None for nodata
# cases
for k in self.NODATA_NULLS:
result[k] = None
return result
description = self.description

View file

@ -26,11 +26,10 @@ class TestRoofAttributes:
def test_empty_str(self):
# Test initialization with an empty description
assert RoofAttributes('').process() == {
'thermal_transmittance': False, 'thermal_transmittance_unit': False, 'is_pitched': False,
'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': False,
'is_roof_room': False, 'is_loft': False, 'is_flat': False, 'is_thatched': False, 'is_at_rafters': False,
'is_assumed': False, 'has_dwelling_above': False, 'is_valid': False, 'insulation_thickness': False
'is_assumed': False, 'has_dwelling_above': False, 'is_valid': False, 'insulation_thickness': None
}
assert set(list(RoofAttributes('').process().values())) == {False}
def test_clean_roof(self):
result = RoofAttributes('Pitched, 270 mm loft insulation').process()
@ -92,15 +91,6 @@ class TestRoofAttributes:
with pytest.raises(ValueError):
RoofAttributes('nonsense string').process()
def test_clean_roof_no_description(self):
roof = RoofAttributes('').process()
assert roof == {
'thermal_transmittance': False, 'thermal_transmittance_unit': False, 'is_pitched': False,
'is_roof_room': False, 'is_loft': False, 'is_flat': False, 'is_thatched': False,
'is_at_rafters': False, 'is_assumed': False, 'has_dwelling_above': False, 'is_valid': False,
'insulation_thickness': False
}
def test_clean_roof_edge_cases(self):
# Insulation thickness edge case
assert RoofAttributes('Pitched, 99999 mm loft insulation').process()['insulation_thickness'] == "99999"

View file

@ -59,9 +59,9 @@ class RoofRecommendations:
# Extract the insulation thickness from the roof, which is used throughout this method
self.insulation_thickness = convert_thickness_to_numeric(
self.property.roof["insulation_thickness"],
self.property.roof["is_pitched"],
self.property.roof["is_flat"]
string_thickness=self.property.roof["insulation_thickness"],
is_pitched=self.property.roof["is_pitched"],
is_flat=self.property.roof["is_flat"]
)
@classmethod
@ -300,6 +300,10 @@ class RoofRecommendations:
):
return False
if self.property.roof["original_description"] is None:
# There is no description so we cannot make an assessment
return False
return True
@staticmethod

View file

@ -8,6 +8,30 @@ from recommendations.tests.test_data.materials import materials
class TestRoofRecommendations:
def test_null_roof_description(self):
epc_record = EPCRecord()
epc_record.prepared_epc = {
"county": "Cambridgeshire",
}
property_instance = Property(id=0, address="fake", postcode="fake", epc_record=epc_record)
property_instance.age_band = "F"
property_instance.insulation_floor_area = 100
property_instance.roof = {
'original_description': None,
'clean_description': None,
'thermal_transmittance': None,
'thermal_transmittance_unit': None,
'is_pitched': True, 'is_roof_room': False, 'is_loft': False, 'is_flat': False, 'is_thatched': False,
'is_at_rafters': False, 'is_assumed': True, 'has_dwelling_above': False, 'is_valid': True,
'insulation_thickness': 'none', 'roof_thermal_transmittance': None, 'roof_insulation_thickness': None
}
property_instance.already_installed = []
roof_recommender = RoofRecommendations(property_instance=property_instance, materials=materials)
roof_recommender.recommend(phase=0)
assert not roof_recommender.recommendations
def test_loft_insulation_recommendation_no_insulation(self):
epc_record = EPCRecord()
epc_record.prepared_epc = {