diff --git a/backend/Property.py b/backend/Property.py index 77415d0e..584e1442 100644 --- a/backend/Property.py +++ b/backend/Property.py @@ -502,43 +502,12 @@ class Property: output["low_energy_lighting_ending"] = 100 output["lighting_energy_eff_ending"] = "Very Good" - if recommendation["type"] == "windows_glazing": - is_secondary_glazing = recommendation["is_secondary_glazing"] - output["multi_glaze_proportion_ending"] = 100 - if output["windows_energy_eff_ending"] not in ["Average", "Good", "Very Good"]: - output["windows_energy_eff_ending"] = "Average" if not is_secondary_glazing else "Good" - - if output["glazing_type_ending"] == "multiple": - pass - elif output["glazing_type_ending"] == "single": - output["glazing_type_ending"] = ( - "secondary" if is_secondary_glazing else "double" - ) - elif output["glazing_type_ending"] == "double": - output["glazing_type_ending"] = ( - "multiple" if is_secondary_glazing else "double" - ) - elif output["glazing_type_ending"] == "secondary": - output["glazing_type_ending"] = ( - "secondary" if is_secondary_glazing else "multiple" - ) - elif output["glazing_type_ending"] in ["triple", "high performance"]: - output["glazing_type_ending"] = "multiple" - else: - raise ValueError("Invalid glazing type - implement me") - - if is_secondary_glazing: - output["glazed_type_ending"] = "secondary glazing" - else: - output["glazed_type_ending"] = ( - "double glazing installed during or after 2002" - ) - if recommendation["type"] in [ "heating", "hot_water_tank_insulation", "heating_control", "secondary_heating", "internal_wall_insulation", "external_wall_insulation", "cavity_wall_insulation", "cylinder_thermostat", "loft_insulation", "room_roof_insulation", "flat_roof_insulation", - "solid_floor_insulation", "suspended_floor_insulation", "mixed_glazing" + "solid_floor_insulation", "suspended_floor_insulation", "mixed_glazing", + "windows_glazing" ]: # We update the data, as defined in the recommendaton for prefix in ["walls", "roof", "floor"]: diff --git a/backend/app/plan/schemas.py b/backend/app/plan/schemas.py index c08cdefc..6f0f6327 100644 --- a/backend/app/plan/schemas.py +++ b/backend/app/plan/schemas.py @@ -58,7 +58,7 @@ NON_INVASIVE_SPECIFIC_MEASURES = [ # such as "external_wall_insulation", "internal_wall_insulation", "cavity_wall_insulation" MEASURE_MAP = { "wall_insulation": [ - "internal_wall_insulation", "external_wall_insulation", "cavity_wall_insulation", "cavity_extract_and_refill" + "internal_wall_insulation", "external_wall_insulation", "cavity_wall_insulation", ], "roof_insulation": ["loft_insulation", "flat_roof_insulation", "room_roof_insulation"], "floor_insulation": ["suspended_floor_insulation", "solid_floor_insulation"], diff --git a/backend/tests/test_property.py b/backend/tests/test_property.py index 43149791..78f08f3c 100644 --- a/backend/tests/test_property.py +++ b/backend/tests/test_property.py @@ -1,9 +1,11 @@ +from datetime import datetime import pandas as pd import pytest from unittest.mock import Mock from backend.Property import Property from etl.epc_clean.EpcClean import EpcClean from etl.epc.Record import EPCRecord +from etl.bill_savings.KwhData import KwhData # Define some test data mock_epc_response = { @@ -17,12 +19,13 @@ mock_epc_response = { "built-form": "Detached", "inspection-date": "2023-06-01", 'lodgement-datetime': '2023-06-01 20:29:01', + 'lodgement-date': '2023-06-01', "some-other-key": "some-value", "roof-description": "pitched, no insulation", "walls-description": "Walls Description", - "windows-description": "Windows Description", - "mainheat-description": "Main Heating Description", - "hotwater-description": "Hot Water Description", + "windows-description": "Fully double glazed", + "mainheat-description": "Boiler and radiators, mains gas", + "hotwater-description": "From main system", "transaction-type": "rental", "lighting-description": "Good Lighting Efficiency", "energy-consumption-current": "50", @@ -39,7 +42,10 @@ mock_epc_response = { "total-floor-area": 100, "construction-age-band": "England and Wales: 1967-1975", "floor-description": "Floor Description", - "floor-level": "Ground" + "floor-level": "Ground", + "lighting-cost-current": 123, + "heating-cost-current": 800, + "hot-water-cost-current": 200 }, { "lmk-key": 2, @@ -49,12 +55,13 @@ mock_epc_response = { "built-form": "Detached", "inspection-date": "2023-05-01", 'lodgement-datetime': '2023-05-01 20:29:01', + 'lodgement-date': '2023-05-01', "some-other-key": "some-other-value", "roof-description": "Roof Description", "walls-description": "Walls Description", - "windows-description": "Windows Description", - "mainheat-description": "Main Heating Description", - "hotwater-description": "Hot Water Description", + "windows-description": "Fully double glazed", + "mainheat-description": "Boiler and radiators, mains gas", + "hotwater-description": "From main system", "transaction-type": "rental", "lighting-description": "Good Lighting Efficiency", "energy-consumption-current": "50", @@ -71,98 +78,10 @@ mock_epc_response = { "total-floor-area": 100, "construction-age-band": "England and Wales: 1967-1975", "floor-description": "Floor Description", - "floor-level": "Ground" - } - ] -} - -mock_epc_response_dupe = { - 'rows': [ - { - "lmk-key": 1, - "uprn": 1, - "number-habitable-rooms": 5, - "property-type": "House", - 'inspection-date': '2023-06-01', - 'lodgement-datetime': '2023-06-01 20:29:01', - 'some-other-key': 'some-value', 'roof-description': 'Roof Description', - 'walls-description': 'Walls Description', 'windows-description': 'Windows Description', - 'mainheat-description': 'Main Heating Description', 'hotwater-description': 'Hot Water Description', - "transaction-type": "rental", - "lighting-description": "Good Lighting Efficiency", - "energy-consumption-current": "50", - "co2-emissions-current": "123", - "mechanical-ventilation": "natural", - 'photo-supply': 0, - "solar-water-heating-flag": "N", - "wind-turbine-count": 0, - "extension-count": 0, - "heat-loss-corridor": "no corridor", - "unheated-corridor-length": 0, - "mains-gas-flag": "Y", - "floor-height": 2.5, - "total-floor-area": 100, - "construction-age-band": "England and Wales: 1967-1975", - "floor-description": "Floor Description", - "floor-level": "Ground" - }, - { - "lmk-key": 2, - "uprn": 2, - "number-habitable-rooms": 5, - "property-type": "House", - 'inspection-date': '2023-05-01', - 'lodgement-datetime': '2023-05-01 20:29:01', - 'some-other-key': 'some-other-value', - 'roof-description': 'Roof Description', 'walls-description': 'Walls Description', - 'windows-description': 'Windows Description', 'mainheat-description': 'Main Heating Description', - 'hotwater-description': 'Hot Water Description', - "transaction-type": "rental", - "lighting-description": "Good Lighting Efficiency", - "energy-consumption-current": "50", - "co2-emissions-current": "123", - "mechanical-ventilation": "natural", - 'photo-supply': 0, - "solar-water-heating-flag": "N", - "wind-turbine-count": 0, - "extension-count": 0, - "heat-loss-corridor": "no corridor", - "unheated-corridor-length": 0, - "mains-gas-flag": "Y", - "floor-height": 2.5, - "total-floor-area": 100, - "construction-age-band": "England and Wales: 1967-1975", - "floor-description": "Floor Description", - "floor-level": "Ground" - }, - { - "lmk-key": 3, - "uprn": 3, - "number-habitable-rooms": 5, - "property-type": "House", - 'inspection-date': '2023-06-01', - 'lodgement-datetime': '2023-06-01 20:29:01', - 'some-other-key': 'duplicate-date', - 'roof-description': 'Roof Description', - 'walls-description': 'Walls Description', 'windows-description': 'Windows Description', - 'mainheat-description': 'Main Heating Description', 'hotwater-description': 'Hot Water Description', - "transaction-type": "rental", - "lighting-description": "Good Lighting Efficiency", - "energy-consumption-current": "50", - "co2-emissions-current": "123", - "mechanical-ventilation": "natural", - 'photo-supply': 0, - "solar-water-heating-flag": "N", - "wind-turbine-count": 0, - "extension-count": 0, - "heat-loss-corridor": "no corridor", - "unheated-corridor-length": 0, - "mains-gas-flag": "Y", - "floor-height": 2.5, - "total-floor-area": 100, - "construction-age-band": "England and Wales: 1967-1975", - "floor-description": "Floor Description", - "floor-level": "Ground" + "floor-level": "Ground", + "lighting-cost-current": 123, + "heating-cost-current": 800, + "hot-water-cost-current": 200 } ] } @@ -170,34 +89,14 @@ mock_epc_response_dupe = { class TestProperty: - @pytest.fixture(autouse=True) - def mock_photo_supply_lookup(self): - return pd.DataFrame( - [ - dict( - tenure="rental (social)", - built_form="Detached", - property_type="House", - construction_age_band="England and Wales: 1967-1975", - is_flat=False, - is_pitched=True, - is_roof_room=False, - floor_area_decile=2, - photo_supply_median=40 - ) - ] - ) - - @pytest.fixture(autouse=True) - def mock_floor_area_decile_thresholds(self): - return pd.DataFrame( - {"floor_area_decile_thresholds": [0, 10, 30, 50]} - ) - @pytest.fixture(autouse=True) def property_instance(self, mock_cleaner): epc_record = EPCRecord() - epc_record.prepared_epc = mock_epc_response["rows"][0] + prepared_epc = mock_epc_response["rows"][0].copy() + # Replace hyphens with underscores + prepared_epc = {k.replace("-", "_"): v for k, v in prepared_epc.items()} + epc_record.prepared_epc = prepared_epc + epc_record.uprn = prepared_epc["uprn"] property_instance = Property(id=1, postcode="AB12CD", address="Test Address", epc_record=epc_record) property_instance.number_of_floors = 2 @@ -206,27 +105,6 @@ class TestProperty: property_instance.floor_height = 2.5 return property_instance - @pytest.fixture(autouse=True) - def property_instance_dupe_data(self): - epc_record = EPCRecord() - epc_record.prepared_epc = mock_epc_response_dupe["rows"][0] - property_instance_dupe_data = Property(id=2, postcode="AB12CD", address="Test Address", epc_record=epc_record) - return property_instance_dupe_data - - # @pytest.fixture - # def mock_epc_client(self): - # mock_epc_client = Mock(spec=EpcClient(auth_token="mocked_auth_token")) - # mock_epc_client.domestic.search.return_value = mock_epc_response.copy() - # mock_epc_client.auth_token = "mocked_auth_token" - # return mock_epc_client - # - # @pytest.fixture - # def mock_epc_client_dupe_data(self): - # mock_epc_client_dupe_data = Mock(spec=EpcClient(auth_token="mocked_auth_token")) - # mock_epc_client_dupe_data.domestic.search.return_value = mock_epc_response_dupe.copy() - # mock_epc_client_dupe_data.auth_token = "mocked_auth_token" - # return mock_epc_client_dupe_data - @pytest.fixture def mock_cleaner(self): lighting_averages = [ @@ -270,15 +148,59 @@ class TestProperty: "is_roof_room": False} ], "walls-description": [walls_data], - "windows-description": [{"original_description": "Windows Description"}], - "mainheat-description": [{"original_description": "Main Heating Description"}], - "hotwater-description": [{"original_description": "Hot Water Description"}], + "windows-description": [ + {'original_description': 'Fully double glazed', 'has_glazing': True, 'glazing_coverage': 'full', + 'glazing_type': 'double', 'no_data': False} + ], + "mainheat-description": [ + { + 'original_description': 'Boiler and radiators, mains gas', 'has_radiators': True, + 'has_fan_coil_units': False, + 'has_pipes_in_screed_above_insulation': False, 'has_pipes_in_insulated_timber_floor': False, + 'has_pipes_in_concrete_slab': False, 'has_boiler': True, 'has_air_source_heat_pump': False, + 'has_room_heaters': False, 'has_electric_storage_heaters': False, 'has_warm_air': False, + 'has_electric_underfloor_heating': False, 'has_electric_ceiling_heating': False, + 'has_community_scheme': False, + 'has_ground_source_heat_pump': False, 'has_no_system_present': False, + 'has_portable_electric_heaters': False, + 'has_water_source_heat_pump': False, 'has_electric': False, 'has_mains_gas': True, + 'has_wood_logs': False, + 'has_coal': False, 'has_oil': False, 'has_wood_pellets': False, 'has_anthracite': False, + 'has_dual_fuel_mineral_and_wood': False, 'has_smokeless_fuel': False, 'has_lpg': False, + 'has_assumed': False, + 'has_electricaire': False, 'has_assumed_for_most_rooms': False, 'has_underfloor_heating': False, + "has_electric_heat_pumps": False, + "has_micro-cogeneration": False + } + ], + "hotwater-description": [ + {'original_description': 'From main system', 'heater_type': None, 'system_type': 'from main system', + 'thermostat_characteristics': None, 'heating_scope': None, 'energy_recovery': None, + 'tariff_type': None, + 'extra_features': None, 'chp_systems': None, 'distribution_system': None, 'no_system_present': None, + 'assumed': False, "appliance": None} + ], "lighting-description": [{"original_description": "Good Lighting Efficiency"}], "floor-description": [ {"original_description": "Floor Description", "is_suspended": True, "another_property_below": False}] } return mock_cleaner + @pytest.fixture + def kwh_client(self): + kwh_client = KwhData(bucket="retrofit-data-dev", read_consumption_data=False) + # We fix this pricing table for these tests + kwh_client.retail_price_comparison = pd.DataFrame( + [ + { + "Date": datetime.today().strftime("%Y-%m-%d"), + 'Average standard variable tariff (Large legacy suppliers)': 1 + } + ] + ) + kwh_client.retail_price_comparison["Date"] = pd.to_datetime(kwh_client.retail_price_comparison["Date"]) + return kwh_client + def test_init(self): epc_record = EPCRecord() epc_record.prepared_epc = {"uprn": 1} @@ -292,13 +214,26 @@ class TestProperty: inst3 = Property(4, "AB12CD", "Test Address", epc_record=epc_record) assert inst3.data == {"uprn": 1} - def test_get_components( - self, property_instance, mock_cleaner, mock_photo_supply_lookup, mock_floor_area_decile_thresholds + def test_set_features( + self, property_instance, mock_cleaner, kwh_client, ): - property_instance.get_components( + kwh_predictions = { + "heating_kwh_predictions": pd.DataFrame( + [ + {"id": property_instance.uprn, "predictions": 12000} + ] + ), + "hotwater_kwh_predictions": pd.DataFrame( + [ + {"id": property_instance.uprn, "predictions": 3000} + ] + ), + } + + property_instance.set_features( mock_cleaner.cleaned, - photo_supply_lookup=mock_photo_supply_lookup, - floor_area_decile_thresholds=mock_floor_area_decile_thresholds + kwh_client, + kwh_predictions ) # Verify that the components are set correctly @@ -318,9 +253,32 @@ class TestProperty: "is_sandstone_or_limestone": False, "is_granite_or_whinstone": False, } - assert property_instance.windows == {"original_description": "Windows Description"} - assert property_instance.main_heating == {"original_description": "Main Heating Description"} - assert property_instance.hotwater == {"original_description": "Hot Water Description"} + assert property_instance.windows == { + 'original_description': 'Fully double glazed', 'has_glazing': True, 'glazing_coverage': 'full', + 'glazing_type': 'double', 'no_data': False + } + assert property_instance.main_heating == { + 'original_description': 'Boiler and radiators, mains gas', 'has_radiators': True, + 'has_fan_coil_units': False, 'has_pipes_in_screed_above_insulation': False, + 'has_pipes_in_insulated_timber_floor': False, 'has_pipes_in_concrete_slab': False, 'has_boiler': True, + 'has_air_source_heat_pump': False, 'has_room_heaters': False, 'has_electric_storage_heaters': False, + 'has_warm_air': False, 'has_electric_underfloor_heating': False, 'has_electric_ceiling_heating': False, + 'has_community_scheme': False, 'has_ground_source_heat_pump': False, 'has_no_system_present': False, + 'has_portable_electric_heaters': False, 'has_water_source_heat_pump': False, 'has_electric': False, + 'has_mains_gas': True, 'has_wood_logs': False, 'has_coal': False, 'has_oil': False, + 'has_wood_pellets': False, 'has_anthracite': False, 'has_dual_fuel_mineral_and_wood': False, + 'has_smokeless_fuel': False, 'has_lpg': False, 'has_assumed': False, 'has_electricaire': False, + 'has_assumed_for_most_rooms': False, 'has_underfloor_heating': False, 'has_electric_heat_pumps': False, + 'has_micro-cogeneration': False + } + + assert property_instance.hotwater == { + 'original_description': 'From main system', 'heater_type': None, + 'system_type': 'from main system', 'thermostat_characteristics': None, + 'heating_scope': None, 'energy_recovery': None, 'tariff_type': None, + 'extra_features': None, 'chp_systems': None, 'distribution_system': None, + 'no_system_present': None, 'assumed': False, 'appliance': None + } assert property_instance.wall_type == "cavity" @@ -330,11 +288,24 @@ class TestProperty: # Verify that ValueError is raised when EpcClean doesn't contain cleaned data with pytest.raises(ValueError, match="Cleaner does not contain cleaned data"): - property_instance.get_components(mock_cleaner.cleaned, pd.DataFrame(), pd.DataFrame()) + property_instance.set_features(mock_cleaner.cleaned, pd.DataFrame(), pd.DataFrame()) def test_get_components_no_attributes( - self, property_instance, mock_cleaner, mock_photo_supply_lookup, mock_floor_area_decile_thresholds + self, property_instance, mock_cleaner, kwh_client ): + kwh_predictions = { + "heating_kwh_predictions": pd.DataFrame( + [ + {"id": property_instance.uprn, "predictions": 12000} + ] + ), + "hotwater_kwh_predictions": pd.DataFrame( + [ + {"id": property_instance.uprn, "predictions": 3000} + ] + ), + } + # Modify the mock cleaner to have no attributes for a specific description mock_cleaner.cleaned = { "roof-description": [] @@ -351,23 +322,45 @@ class TestProperty: "is_sandstone_or_limestone": False, "is_granite_or_whinstone": False, } - property_instance.floor = { "is_suspended": False, "another_property_below": False, "is_solid": True } + property_instance.main_heating = { + 'original_description': 'Boiler and radiators, mains gas', 'has_radiators': True, + 'has_fan_coil_units': False, 'has_pipes_in_screed_above_insulation': False, + 'has_pipes_in_insulated_timber_floor': False, 'has_pipes_in_concrete_slab': False, 'has_boiler': True, + 'has_air_source_heat_pump': False, 'has_room_heaters': False, 'has_electric_storage_heaters': False, + 'has_warm_air': False, 'has_electric_underfloor_heating': False, 'has_electric_ceiling_heating': False, + 'has_community_scheme': False, 'has_ground_source_heat_pump': False, 'has_no_system_present': False, + 'has_portable_electric_heaters': False, 'has_water_source_heat_pump': False, 'has_electric': False, + 'has_mains_gas': True, 'has_wood_logs': False, 'has_coal': False, 'has_oil': False, + 'has_wood_pellets': False, 'has_anthracite': False, 'has_dual_fuel_mineral_and_wood': False, + 'has_smokeless_fuel': False, 'has_lpg': False, 'has_assumed': False, 'has_electricaire': False, + 'has_assumed_for_most_rooms': False, 'has_underfloor_heating': False, 'has_electric_heat_pumps': False, + 'has_micro-cogeneration': False + } + property_instance.hotwater = { + 'original_description': 'From main system', 'heater_type': None, 'system_type': 'from main system', + 'thermostat_characteristics': None, 'heating_scope': None, 'energy_recovery': None, + 'tariff_type': None, + 'extra_features': None, 'chp_systems': None, 'distribution_system': None, 'no_system_present': None, + 'assumed': False, "appliance": None + } # Assert backup cleaning has been applied - property_instance.get_components( - mock_cleaner.cleaned, mock_photo_supply_lookup, mock_floor_area_decile_thresholds + property_instance.set_features( + mock_cleaner.cleaned, + kwh_client, + kwh_predictions ) assert property_instance.roof["clean_description"] == "Pitched, no insulation" assert property_instance.roof["is_pitched"] def test_get_components_multiple_attributes( - self, property_instance, mock_cleaner, mock_photo_supply_lookup, mock_floor_area_decile_thresholds + self, property_instance, mock_cleaner, kwh_client ): # This shouldn't happen - it would mean a cleaning error property_instance.data["roof-description"] = "Roof Description" @@ -378,13 +371,27 @@ class TestProperty: ] } + kwh_predictions = { + "heating_kwh_predictions": pd.DataFrame( + [ + {"id": property_instance.uprn, "predictions": 12000} + ] + ), + "hotwater_kwh_predictions": pd.DataFrame( + [ + {"id": property_instance.uprn, "predictions": 3000} + ] + ), + } + # Verify that ValueError is raised when multiple attributes are found with pytest.raises(ValueError, match="Either No attributes or multiple found for roof-description"): - property_instance.get_components(cleaned, mock_photo_supply_lookup, mock_floor_area_decile_thresholds) + property_instance.set_features(cleaned, kwh_client, kwh_predictions) def test_set_spatial(self): epc_record = EPCRecord() epc_record.prepared_epc = mock_epc_response["rows"][0] + epc_record.uprn = mock_epc_response["rows"][0]["uprn"] prop = Property(1, postcode="AB12CD", address="Test Address", epc_record=epc_record) spatial1 = pd.DataFrame([{ @@ -418,6 +425,7 @@ class TestProperty: # floor, so we should set floor_level to 0 epc_record = EPCRecord() epc_record.prepared_epc = {'floor-level': '01', 'property-type': 'Flat'} + epc_record.uprn = 1 prop = Property(1, postcode="AB12CD", address="Test Address", epc_record=epc_record) prop.floor = { 'original_description': 'Solid, no insulation (assumed)', 'clean_description': 'Solid, no insulation', diff --git a/etl/epc_clean/tests/test_data/test_floor_attributes_cases.py b/etl/epc_clean/tests/test_data/test_floor_attributes_cases.py index 280e7459..81ec7a32 100644 --- a/etl/epc_clean/tests/test_data/test_floor_attributes_cases.py +++ b/etl/epc_clean/tests/test_data/test_floor_attributes_cases.py @@ -367,7 +367,7 @@ clean_floor_cases = [ 'thermal_transmittance_unit': None, 'is_assumed': True, 'is_to_unheated_space': True, 'is_to_external_air': False, 'is_suspended': False, 'is_solid': False, 'insulation_thickness': 'none', "another_property_below": False}, {'original_description': "Average thermal transmittance 1.10 W/m+é-¦K", 'thermal_transmittance': 1.1, - 'thermal_transmittance_unit': 'w/m+é-¦k', 'is_assumed': False, + 'thermal_transmittance_unit': 'w/m-¦k', 'is_assumed': False, 'is_to_unheated_space': False, 'is_to_external_air': False, 'is_suspended': False, 'is_solid': False, 'another_property_below': False, 'insulation_thickness': None}, { diff --git a/etl/epc_clean/tests/test_data/test_mainheat_attributes_cases.py b/etl/epc_clean/tests/test_data/test_mainheat_attributes_cases.py index 51fc3416..16acdd37 100644 --- a/etl/epc_clean/tests/test_data/test_mainheat_attributes_cases.py +++ b/etl/epc_clean/tests/test_data/test_mainheat_attributes_cases.py @@ -1658,9 +1658,9 @@ mainheat_cases = [ 'has_air_source_heat_pump': False, 'has_room_heaters': False, 'has_electric_storage_heaters': False, 'has_warm_air': False, 'has_electric_underfloor_heating': False, 'has_electric_ceiling_heating': False, 'has_community_scheme': False, 'has_ground_source_heat_pump': False, 'has_no_system_present': False, - 'has_portable_electric_heaters': False, 'has_water_source_heat_pump': False, 'has_electric_heat_pump': False, + 'has_portable_electric_heaters': True, 'has_water_source_heat_pump': False, 'has_electric_heat_pump': False, 'has_micro-cogeneration': False, 'has_solar_assisted_heat_pump': False, 'has_exhaust_source_heat_pump': False, - 'has_community_heat_pump': False, 'has_portable_electric_heating': True, 'has_electric': True, + 'has_community_heat_pump': False, 'has_electric': True, 'has_mains_gas': False, 'has_wood_logs': False, 'has_coal': False, 'has_oil': False, 'has_wood_pellets': False, 'has_anthracite': False, 'has_dual_fuel_mineral_and_wood': False, 'has_smokeless_fuel': False, 'has_lpg': False, 'has_b30k': False, 'has_assumed': True, 'has_electricaire': False, 'has_assumed_for_most_rooms': True, diff --git a/etl/epc_clean/tests/test_data/test_wall_attributes_cases.py b/etl/epc_clean/tests/test_data/test_wall_attributes_cases.py index 96c545c1..507449ab 100644 --- a/etl/epc_clean/tests/test_data/test_wall_attributes_cases.py +++ b/etl/epc_clean/tests/test_data/test_wall_attributes_cases.py @@ -1,5 +1,5 @@ wall_cases = [ - {'original_description': 'Average thermal transmittance -4.67 W/m-¦K', 'thermal_transmittance': -4.67, + {'original_description': 'Average thermal transmittance -4.67 W/m-¦K', 'thermal_transmittance': 4.67, 'thermal_transmittance_unit': 'w/m-¦k', 'is_cavity_wall': False, 'is_filled_cavity': False, 'is_solid_brick': False, 'is_system_built': False, 'is_timber_frame': False, 'is_granite_or_whinstone': False, 'is_as_built': False, 'is_cob': False, 'is_assumed': False, 'is_sandstone_or_limestone': False, @@ -692,7 +692,7 @@ wall_cases = [ 'is_cob': False, 'is_assumed': True, 'is_sandstone_or_limestone': False, 'insulation_thickness': 'none', 'external_insulation': False, 'internal_insulation': False}, {'original_description': 'Average thermal transmittance 1.60 W/m+é-¦K', - 'thermal_transmittance': 1.6, 'thermal_transmittance_unit': 'w/m+é-¦k', 'is_cavity_wall': False, + 'thermal_transmittance': 1.6, 'thermal_transmittance_unit': 'w/m-¦k', 'is_cavity_wall': False, 'is_filled_cavity': False, 'is_solid_brick': False, 'is_system_built': False, 'is_timber_frame': False, 'is_granite_or_whinstone': False, 'is_as_built': False, 'is_cob': False, 'is_assumed': False, 'is_sandstone_or_limestone': False, 'insulation_thickness': None, 'external_insulation': False, diff --git a/etl/epc_clean/tests/test_mainheat_attributes.py b/etl/epc_clean/tests/test_mainheat_attributes.py index f175e821..d79c271a 100644 --- a/etl/epc_clean/tests/test_mainheat_attributes.py +++ b/etl/epc_clean/tests/test_mainheat_attributes.py @@ -11,10 +11,6 @@ class TestMainHeatAttributes: floor_attr = MainHeatAttributes(valid_description) assert floor_attr.description == valid_description.lower() - # Test initialization with an empty description - with pytest.raises(ValueError): - MainHeatAttributes('') - # Test initialization with a description that contains none of the keywords with pytest.raises(ValueError): MainHeatAttributes('description without keywords') @@ -38,7 +34,6 @@ class TestMainHeatAttributes: def test_invalid_description(self): # Test that invalid descriptions raise a ValueError invalid_descriptions = [ - "", "invalid description", "description with no known heating data_types", ] diff --git a/etl/epc_clean/tests/test_wall_attributes.py b/etl/epc_clean/tests/test_wall_attributes.py index 01a60615..970dbd98 100644 --- a/etl/epc_clean/tests/test_wall_attributes.py +++ b/etl/epc_clean/tests/test_wall_attributes.py @@ -16,7 +16,7 @@ class TestWallAttributes: description = 'average thermal transmittance -4.67 w/m-¦k' wa = wall_attr(description) result = wa.process() - assert result['thermal_transmittance'] == -4.67 + assert result['thermal_transmittance'] == 4.67 assert result['thermal_transmittance_unit'] == 'w/m-¦k' def test_wall_types(self, wall_attr): diff --git a/recommendations/RoofRecommendations.py b/recommendations/RoofRecommendations.py index fbd99d67..2a77a3a5 100644 --- a/recommendations/RoofRecommendations.py +++ b/recommendations/RoofRecommendations.py @@ -446,20 +446,7 @@ class RoofRecommendations: _, new_u_value = calculate_u_value_uplift(u_value, part_u_value) new_u_value = math.ceil(new_u_value * 100.0) / 100.0 - # If I have a lowest U value and my new u value is higher than that but lower than the - # diminishing returns threshold, it can be considered - - # If I have a lowest U value and my new u value is lower than the lowest value, it's - # further into the diminishing returns threshold and can shouldn't be - - # if is_diminishing_returns( - # recommendations, new_u_value, lowest_selected_u_value, self.DIMINISHING_RETURNS_U_VALUE - # ): - # continue - # We allow a small tolerance for error so we don't discount the recommendation entirely - # if new_u_value <= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE: - # lowest_selected_u_value = update_lowest_selected_u_value(lowest_selected_u_value, new_u_value) estimated_cost = ( cost_per_unit * self.property.insulation_floor_area if @@ -504,7 +491,7 @@ class RoofRecommendations: "type": "room_roof_insulation", "description": "Insulate room in roof at rafters and re-decorate", "starting_u_value": u_value, - "new_u_value": None, + "new_u_value": new_u_value, "sap_points": sap_points, "simulation_config": simulation_config, "description_simulation": { diff --git a/recommendations/WindowsRecommendations.py b/recommendations/WindowsRecommendations.py index 235d9ee2..cd1b982b 100644 --- a/recommendations/WindowsRecommendations.py +++ b/recommendations/WindowsRecommendations.py @@ -48,6 +48,15 @@ class WindowsRecommendations: if not any(x in measures for x in MEASURE_MAP["windows"]): return + if self.property.windows["glazing_type"] in ["triple", "high performance"]: + # We don't make any recommendations in this case. The property already has outstanding glazing + return + + if self.property.windows["has_glazing"] & ( + self.property.windows["glazing_coverage"] == "full" + ): + return + # If the property is in a conservation area or is a listed building, it becomes more difficult to install # double glazing. Therefore, we don't recommend it. It is still possible but is not practical as it # requires planning permission and might require a more expensive window type, such as timber. @@ -67,11 +76,6 @@ class WindowsRecommendations: if not number_of_windows: raise ValueError("Number of windows not specified") - if self.property.windows["has_glazing"] & ( - self.property.windows["glazing_coverage"] == "full" - ): - return - if windows_area is not None: # TODO - we don't have a price for this so we can't recommend it print("We have windows area, we should use this data for our recommendations!!!") @@ -122,6 +126,98 @@ class WindowsRecommendations: ". Secondary glazing recommended due to conservation area status" ) + # Set up the simulation config + if self.property.windows["glazing_type"] == "multiple": + glazing_type_ending = "multiple" + glazed_type_ending = ( + "secondary glazing" if is_secondary_glazing else "double glazing installed during or after 2002" + ) + windows_energy_eff = "Good" + new_windows_description = "Multiple glazing throughout" + + elif self.property.windows["glazing_type"] == "single": + # We will only recommend either secondary or double glazing + glazing_type_ending = ( + "secondary" if is_secondary_glazing else "double" + ) + glazed_type_ending = ( + "secondary glazing" if is_secondary_glazing else "double glazing installed during or after 2002" + ) + + if is_secondary_glazing: + windows_energy_eff = "Good" + new_windows_description = "Full secondary glazing" + else: + windows_energy_eff = "Average" + new_windows_description = "Fully double glazed" + + elif self.property.windows["glazing_type"] == "double": + glazing_type_ending = ( + "multiple" if is_secondary_glazing else "double" + ) + + # We set glazed type depending on which window type is more prevalent. Since there is already double + # glazing in place, if we're recommending more double glazing, we set the glazed type to double glazing + # otherwise, if we're recommending secondary glazing and the proportion of glazing in place already that + # is double is less than 50% we set the glazed type to secondary glazing + + if not is_secondary_glazing: + glazed_type_ending = "double glazing installed during or after 2002" + new_windows_description = "Fully double glazed" + windows_energy_eff = "Average" + else: + if self.property.data["multi-glaze-proportion"] < 50: + glazed_type_ending = "secondary glazing" + else: + glazed_type_ending = "double glazing installed during or after 2002" + + new_windows_description = "Multiple glazing throughout" + windows_energy_eff = "Good" + + elif self.property.windows["glazing_type"] == "secondary": + glazing_type_ending = ( + "secondary" if is_secondary_glazing else "multiple" + ) + windows_energy_eff = "Good" + # This is the opposite. If there is secondary glazing in place, and we're recommending double + # we set glazed_type_ending, depending on the proportion of glazing in place + if is_secondary_glazing: + glazed_type_ending = "secondary glazing" + new_windows_description = "Full secondary glazing" + else: + if self.property.data["multi-glaze-proportion"] < 50: + glazed_type_ending = "double glazing installed during or after 2002" + else: + glazed_type_ending = "secondary glazing" + new_windows_description = "Multiple glazing throughout" + + else: + raise ValueError("Invalid glazing type - implement me") + + if (self.property.data["windows-energy-eff"] in ["Good", "Very Good"]) and (windows_energy_eff == "Average"): + windows_energy_eff = self.property.data["windows-energy-eff"] + + windows_ending_config = WindowAttributes(new_windows_description).process() + + windows_simulation_config = check_simulation_difference( + new_config=windows_ending_config, old_config=self.property.windows, prefix="windows_" + ) + + simulation_config = { + **windows_simulation_config, + "multi_glaze_proportion_ending": 100, + "windows_energy_eff_ending": windows_energy_eff, + "glazing_type_ending": glazing_type_ending, + "glazed_type_ending": glazed_type_ending, + } + + description_simulation = { + "multi-glaze-proportion": 100, + "windows-energy-eff": windows_energy_eff, + "windows-description": new_windows_description, + "glazed-type": glazed_type_ending, + } + self.recommendation = [ { "phase": phase, @@ -134,13 +230,8 @@ class WindowsRecommendations: "already_installed": already_installed, **cost_result, "is_secondary_glazing": is_secondary_glazing, - # TODO: Make this condition on is_secondary_glazing - "description_simulation": { - "multi-glaze-proportion": 100, - "windows-energy-eff": "Average", - "windows-description": "Fully double glazed", - "glazed-type": "double glazing installed during or after 2002", - } + "description_simulation": description_simulation, + "simulation_config": simulation_config, } ] diff --git a/recommendations/tests/test_air_source_heat_pump.py b/recommendations/tests/test_air_source_heat_pump.py deleted file mode 100644 index 0d69b10d..00000000 --- a/recommendations/tests/test_air_source_heat_pump.py +++ /dev/null @@ -1,944 +0,0 @@ -import pandas as pd -import msgpack -from datetime import datetime - -from utils.s3 import read_dataframe_from_s3_parquet, read_from_s3 -from backend.Property import Property -from recommendations.HeatingRecommender import HeatingRecommender -from recommendations.Recommendations import Recommendations -from etl.epc.Record import EPCRecord -from etl.solar.SolarPhotoSupply import SolarPhotoSupply -from backend.ml_models.api import ModelApi - - -def find_examples(): - """ Some scrappy helper code to find EPC examples""" - # Let's look for some testing data, where the only thing different pre and post is the installation of an - # air source heat pump - data = read_dataframe_from_s3_parquet( - bucket_name="retrofit-data-dev", - file_key="sap_change_model/2024-03-24-15-51-13/dataset_no_cleaning.parquet" - ) - - # Firstly, take records where before there was no air source heat pump and afterwards there was - data = data[ - data["has_air_source_heat_pump_ending"] & ~data["has_air_source_heat_pump"] - ] - - # Start with a property that has a boiler - data = data[data["has_boiler"]] - - static_columns = [ - # Walls - 'walls_thermal_transmittance_ending', - 'is_filled_cavity_ending', - 'is_park_home_ending', - 'walls_insulation_thickness_ending', - 'external_insulation_ending', - 'internal_insulation_ending', - # Floors - # 'floor_thermal_transmittance_ending', # Don't subset on this, because it changes based on floor area - 'floor_insulation_thickness_ending', - # Roof - 'roof_thermal_transmittance_ending', - 'is_at_rafters_ending', - 'roof_insulation_thickness_ending', - # Hot water - air source heat pump will shange the hot water system (probably from whatever it was -> main) - # 'heater_type_ending', - # 'system_type_ending', - # 'thermostat_characteristics_ending', - # 'heating_scope_ending', - # 'energy_recovery_ending', - # 'hotwater_tariff_type_ending', - # 'extra_features_ending', - # 'chp_systems_ending', - # 'distribution_system_ending', - # 'no_system_present_ending', - # 'appliance_ending', - # Heating - Will change when installing an ASHP - # 'has_radiators_ending', - # 'has_fan_coil_units_ending', - # 'has_pipes_in_screed_above_insulation_ending', - # 'has_pipes_in_insulated_timber_floor_ending', - # 'has_pipes_in_concrete_slab_ending', - # 'has_boiler_ending', - # 'has_air_source_heat_pump_ending', # We want the air source heat pump to change - # 'has_room_heaters_ending', - # 'has_electric_storage_heaters_ending', - # 'has_warm_air_ending', - # 'has_electric_underfloor_heating_ending', - # 'has_electric_ceiling_heating_ending', - # 'has_community_scheme_ending', - # 'has_ground_source_heat_pump_ending', - # 'has_no_system_present_ending', - # 'has_portable_electric_heaters_ending', - # 'has_water_source_heat_pump_ending', - # 'has_electric_heat_pump_ending', - # 'has_micro-cogeneration_ending', - # 'has_solar_assisted_heat_pump_ending', - # 'has_exhaust_source_heat_pump_ending', - # 'has_community_heat_pump_ending', - # 'has_electric_ending', - # 'has_mains_gas_ending', - # 'has_wood_logs_ending', 'has_coal_ending', 'has_oil_ending', - # 'has_wood_pellets_ending', 'has_anthracite_ending', 'has_dual_fuel_mineral_and_wood_ending', - # 'has_smokeless_fuel_ending', 'has_lpg_ending', 'has_b30k_ending', 'has_electricaire_ending', - # 'has_assumed_for_most_rooms_ending', 'has_underfloor_heating_ending', - # 'thermostatic_control_ending', - # 'charging_system_ending', - # 'switch_system_ending', - # 'no_control_ending', - # 'dhw_control_ending', - # 'community_heating_ending', - # 'multiple_room_thermostats_ending', - # 'auxiliary_systems_ending', - # 'trvs_ending', - # 'rate_control_ending', - # Window - 'glazing_type_ending', - # Fuel - could change with ASHP - # 'fuel_type_ending', - # 'main-fuel_tariff_type_ending', - # 'is_community_ending', - # 'no_individual_heating_or_community_network_ending', - # 'complex_fuel_type_ending', - - 'mechanical_ventilation_ending', 'secondheat_description_ending', 'glazed_type_ending', - 'multi_glaze_proportion_ending', 'low_energy_lighting_ending', 'number_open_fireplaces_ending', - 'solar_water_heating_flag_ending', - 'photo_supply_ending', - 'energy_tariff_ending', - 'extension_count_ending', - 'total_floor_area_ending', - # 'hot_water_energy_eff_ending', - 'floor_energy_eff_ending', - 'windows_energy_eff_ending', - 'walls_energy_eff_ending', - 'sheating_energy_eff_ending', - 'roof_energy_eff_ending', - # 'mainheat_energy_eff_ending', - # 'mainheatc_energy_eff_ending', - 'lighting_energy_eff_ending', - 'number_habitable_rooms_ending', - 'number_heated_rooms_ending', - ] - - for col in static_columns: - - base_starting = col.split("_ending")[0] - if base_starting + "_starting" in data.columns: - starting_col = base_starting + "_starting" - else: - starting_col = base_starting - # Filter - print("Column: %s" % col) - print("Starting size: %s" % data.shape[0]) - data = data[data[starting_col] == data[col]] - print("Ending size: %s" % data.shape[0]) - - z = data[['uprn', col, starting_col]] - - # Great example UPRNs - # 100030969273 - # 10034685399 - Completely transforms the heating and hot water systems in the home (goes from oil -> electricity) - # 100091200828 - goes from a liquid petroleum gas boiler to ashp - - # Look for starting with a gas boiler - data[ - data["has_boiler"] & data["has_radiators"] & data["has_mains_gas"] & ~data["has_boiler_ending"] - ] - - # UPRN: 100011776843 - - -class TestAirSourceHeatPump: - - def test_eligible(self): - # This tests a house, which will be suitable for an air source heat pump - epc_record = EPCRecord() - epc_record.prepared_epc = { - "county": "Broxbourne", - "mainheat-energy-eff": "Good", - "hot-water-energy-eff": "Good", - "mainheatc-energy-eff": "Good", - "number-heated-rooms": 5, - "property-type": "House", - "built-form": "Semi-Detached" - } - - property_instance = Property(id=0, address="fake", postcode="fake", epc_record=epc_record) - property_instance.main_heating = { - 'original_description': 'Boiler and radiators, mains gas', - "clean_description": "Boiler and radiators, mains gas", - 'has_radiators': True, - 'has_fan_coil_units': False, 'has_pipes_in_screed_above_insulation': False, - 'has_pipes_in_insulated_timber_floor': False, 'has_pipes_in_concrete_slab': False, 'has_boiler': True, - 'has_air_source_heat_pump': False, - 'has_room_heaters': False, 'has_electric_storage_heaters': False, - 'has_warm_air': False, - 'has_electric_underfloor_heating': False, - 'has_electric_ceiling_heating': False, 'has_community_scheme': False, - 'has_ground_source_heat_pump': False, 'has_no_system_present': False, - 'has_portable_electric_heaters': False, - 'has_water_source_heat_pump': False, 'has_electric': False, - 'has_mains_gas': True, 'has_wood_logs': False, - 'has_coal': False, 'has_oil': False, 'has_wood_pellets': False, - 'has_anthracite': False, - 'has_dual_fuel_mineral_and_wood': False, 'has_smokeless_fuel': False, - 'has_lpg': False, 'has_assumed': False, - 'has_electricaire': False, 'has_assumed_for_most_rooms': False, - 'has_underfloor_heating': False, - "has_electric_heat_pumps": False, - "has_micro-cogeneration": False - } - property_instance.main_fuel = { - 'original_description': 'mains gas (not community)', 'fuel_type': 'mains gas', - 'tariff_type': None, - 'is_community': False, 'no_individual_heating_or_community_network': False, - 'complex_fuel_type': None - } - property_instance.hotwater = { - 'original_description': 'From main system', - 'clean_description': 'From main system', - 'heater_type': None, - 'system_type': 'from main system', - 'thermostat_characteristics': None, 'heating_scope': None, - 'energy_recovery': None, 'tariff_type': None, - 'extra_features': None, 'chp_systems': None, 'distribution_system': None, - 'no_system_present': None, - 'assumed': False, "appliance": None - } - property_instance.main_heating_controls = { - 'original_description': 'Programmer, room thermostat and TRVs', - 'thermostatic_control': 'room thermostat', 'charging_system': None, 'switch_system': 'programmer', - 'no_control': None, 'dhw_control': None, 'community_heating': None, 'multiple_room_thermostats': False, - 'auxiliary_systems': None, 'trvs': 'trvs', 'rate_control': None - - } - - recommender = HeatingRecommender(property_instance=property_instance) - - assert not recommender.heating_recommendations - - recommender.recommend(phase=0) - - assert recommender.recommendation is None - - def test_air_source_heat_pump_gas_boiler_starting(self): - starting_epc = { - 'low-energy-fixed-light-count': '', 'address': '430 Gidlow Lane', 'uprn-source': 'Energy Assessor', - 'floor-height': '2.62', 'heating-cost-potential': '599', 'unheated-corridor-length': '', - 'hot-water-cost-potential': '67', 'construction-age-band': 'England and Wales: 1950-1966', - 'potential-energy-rating': 'C', 'mainheat-energy-eff': 'Good', 'windows-env-eff': 'Good', - 'lighting-energy-eff': 'Very Good', 'environment-impact-potential': '72', - 'glazed-type': 'double glazing installed during or after 2002', 'heating-cost-current': '913', - 'address3': '', 'mainheatcont-description': 'Programmer, no room thermostat', 'sheating-energy-eff': 'N/A', - 'property-type': 'House', 'local-authority-label': 'Wigan', 'fixed-lighting-outlets-count': '9', - 'energy-tariff': 'Single', 'mechanical-ventilation': 'natural', 'hot-water-cost-current': '210', - 'county': '', 'postcode': 'WN6 8RG', 'solar-water-heating-flag': 'N', 'constituency': 'E14001039', - 'co2-emissions-potential': '2.6', 'number-heated-rooms': '4', - 'floor-description': 'Solid, no insulation (assumed)', 'energy-consumption-potential': '180', - 'local-authority': 'E08000010', 'built-form': 'Mid-Terrace', 'number-open-fireplaces': '0', - 'windows-description': 'Fully double glazed', 'glazed-area': 'Normal', 'inspection-date': '2022-02-15', - 'mains-gas-flag': 'Y', 'co2-emiss-curr-per-floor-area': '78', 'address1': '430 Gidlow Lane', - 'heat-loss-corridor': '', 'flat-storey-count': '', 'constituency-label': 'Wigan', - 'roof-energy-eff': 'Very Poor', 'total-floor-area': '80.0', 'building-reference-number': '10002334112', - 'environment-impact-current': '38', 'co2-emissions-current': '6.2', - 'roof-description': 'Pitched, no insulation (assumed)', 'floor-energy-eff': 'N/A', - 'number-habitable-rooms': '4', 'address2': '', 'hot-water-env-eff': 'Poor', 'posttown': 'WIGAN', - 'mainheatc-energy-eff': 'Very Poor', 'main-fuel': 'mains gas (not community)', - 'lighting-env-eff': 'Very Good', 'windows-energy-eff': 'Good', 'floor-env-eff': 'N/A', - 'sheating-env-eff': 'N/A', 'lighting-description': 'Low energy lighting in all fixed outlets', - 'roof-env-eff': 'Very Poor', 'walls-energy-eff': 'Average', 'photo-supply': '0.0', - 'lighting-cost-potential': '67', 'mainheat-env-eff': 'Good', 'multi-glaze-proportion': '100', - 'main-heating-controls': '', 'lodgement-datetime': '2022-02-23 16:39:41', 'flat-top-storey': '', - 'current-energy-rating': 'E', 'secondheat-description': 'Room heaters, mains gas', - 'walls-env-eff': 'Average', 'transaction-type': 'ECO assessment', 'uprn': '100011776843', - 'current-energy-efficiency': '45', 'energy-consumption-current': '441', - 'mainheat-description': 'Boiler and radiators, mains gas', 'lighting-cost-current': '67', - 'lodgement-date': '2022-02-23', 'extension-count': '1', 'mainheatc-env-eff': 'Very Poor', - 'lmk-key': '46cb404438a6d88ddff8965cab8b3027ec15c32d93e0b6a5f0381a5109b9bb0d', 'wind-turbine-count': '0', - 'tenure': 'Owner-occupied', 'floor-level': '', 'potential-energy-efficiency': '77', - 'hot-water-energy-eff': 'Poor', 'low-energy-lighting': '100', - 'walls-description': 'Cavity wall, filled cavity', - 'hotwater-description': 'From main system, no cylinder thermostat' - } - - ending_epc = { - 'low-energy-fixed-light-count': '', 'address': '430 Gidlow Lane', 'uprn-source': 'Energy Assessor', - 'floor-height': '2.62', 'heating-cost-potential': '803', 'unheated-corridor-length': '', - 'hot-water-cost-potential': '292', 'construction-age-band': 'England and Wales: 1950-1966', - 'potential-energy-rating': 'C', 'mainheat-energy-eff': 'Very Good', 'windows-env-eff': 'Good', - 'lighting-energy-eff': 'Very Good', 'environment-impact-potential': '78', - 'glazed-type': 'double glazing installed during or after 2002', 'heating-cost-current': '861', - 'address3': '', 'mainheatcont-description': 'Time and temperature zone control', - 'sheating-energy-eff': 'N/A', 'property-type': 'House', 'local-authority-label': 'Wigan', - 'fixed-lighting-outlets-count': '9', 'energy-tariff': 'Single', 'mechanical-ventilation': 'natural', - 'hot-water-cost-current': '434', 'county': '', 'postcode': 'WN6 8RG', 'solar-water-heating-flag': 'N', - 'constituency': 'E14001039', 'co2-emissions-potential': '2.0', 'number-heated-rooms': '4', - 'floor-description': 'Solid, no insulation (assumed)', 'energy-consumption-potential': '147', - 'local-authority': 'E08000010', 'built-form': 'Mid-Terrace', 'number-open-fireplaces': '0', - 'windows-description': 'Fully double glazed', 'glazed-area': 'Normal', 'inspection-date': '2022-05-11', - 'mains-gas-flag': 'Y', 'co2-emiss-curr-per-floor-area': '43', 'address1': '430 Gidlow Lane', - 'heat-loss-corridor': '', 'flat-storey-count': '', 'constituency-label': 'Wigan', - 'roof-energy-eff': 'Very Poor', 'total-floor-area': '80.0', 'building-reference-number': '10002334112', - 'environment-impact-current': '63', 'co2-emissions-current': '3.4', - 'roof-description': 'Pitched, no insulation (assumed)', 'floor-energy-eff': 'N/A', - 'number-habitable-rooms': '4', 'address2': '', 'hot-water-env-eff': 'Poor', 'posttown': 'WIGAN', - 'mainheatc-energy-eff': 'Very Good', 'main-fuel': 'electricity (not community)', - 'lighting-env-eff': 'Very Good', 'windows-energy-eff': 'Good', 'floor-env-eff': 'N/A', - 'sheating-env-eff': 'N/A', 'lighting-description': 'Low energy lighting in all fixed outlets', - 'roof-env-eff': 'Very Poor', 'walls-energy-eff': 'Average', 'photo-supply': '0.0', - 'lighting-cost-potential': '67', 'mainheat-env-eff': 'Very Good', 'multi-glaze-proportion': '100', - 'main-heating-controls': '', 'lodgement-datetime': '2022-06-06 13:01:20', 'flat-top-storey': '', - 'current-energy-rating': 'E', 'secondheat-description': 'Room heaters, mains gas', - 'walls-env-eff': 'Average', 'transaction-type': 'ECO assessment', 'uprn': '100011776843', - 'current-energy-efficiency': '53', 'energy-consumption-current': '252', - 'mainheat-description': 'Air source heat pump, radiators, electric', 'lighting-cost-current': '67', - 'lodgement-date': '2022-06-06', 'extension-count': '1', 'mainheatc-env-eff': 'Very Good', - 'lmk-key': '672d5947f3d4a55d97255af71651d6127a939418fa66a687070af77e0ba90df2', 'wind-turbine-count': '0', - 'tenure': 'Owner-occupied', 'floor-level': '', 'potential-energy-efficiency': '70', - 'hot-water-energy-eff': 'Very Poor', 'low-energy-lighting': '100', - 'walls-description': 'Cavity wall, filled cavity', 'hotwater-description': 'From main system' - } - - # differences = [] - # for k, v in ending_epc.items(): - # if v != starting_epc[k]: - # differences.append( - # { - # "variable": k, - # "starting_value": starting_epc[k], - # "ending_value": v - # } - # ) - # differences = pd.DataFrame(differences) - # - # diffs = differences[ - # differences["variable"].isin( - # [ - # "mainheat-energy-eff", - # "mainheatcont-description", - # "mainheatc-energy-eff", - # "main-fuel", - # "mainheat-env-eff", - # "mainheat-description", - # "hot-water-energy-eff", - # "hotwater-description" - # ] - # ) - # ] - - cleaning_data = read_dataframe_from_s3_parquet( - bucket_name="retrofit-data-dev", file_key="sap_change_model/cleaning_dataset.parquet", - ) - - cleaned = read_from_s3( - s3_file_name="cleaned_epc_data/cleaned.bson", - bucket_name="retrofit-data-dev" - ) - cleaned = msgpack.unpackb(cleaned, raw=False) - - photo_supply_lookup, floor_area_decile_thresholds = SolarPhotoSupply.load(bucket="retrofit-data-dev") - - epc = EPCRecord( - epc_records={ - 'original_epc': starting_epc, - 'full_sap_epc': {}, - 'old_data': [] - }, - run_mode="newdata", - cleaning_data=cleaning_data - ) - - home = Property( - id=0, - address="", - postcode="", - epc_record=epc, - already_installed={}, - non_invasive_recommendations={}, - ) - home.in_conservation_area = False - home.is_listed = False - home.is_heritage = False - home.restricted_measures = True - home.get_components( - cleaned=cleaned, - photo_supply_lookup=photo_supply_lookup, - floor_area_decile_thresholds=floor_area_decile_thresholds - ) - - recommender = HeatingRecommender(property_instance=home) - recommender.recommend_air_source_heat_pump(phase=0, has_cavity_or_loft_recommendations=False) - - # Patch - for this property, the hot water energy efficiency is very poor. it's not clear why this is, - # but we insert this for this test - recommender.heating_recommendations[0]["simulation_config"]["hot_water_energy_eff_ending"] = "Very Poor" - - property_recommendations = Recommendations.insert_temp_recommendation_id([recommender.heating_recommendations]) - - assert len(recommender.heating_recommendations) == 1 - - home.create_base_difference_epc_record(cleaned_lookup=cleaned) - home.adjust_difference_record_with_recommendations( - property_recommendations, [] - ) - - scoring_data = pd.DataFrame(home.recommendations_scoring_data).drop( - columns=["rdsap_change", "heat_demand_change", "carbon_change", "sap_ending", "heat_demand_ending", - "carbon_ending"] - ) - - model_api = ModelApi(portfolio_id="ashp-test", timestamp=datetime.now().isoformat()) - model_api.MODEL_PREFIXES = ["sap_change_predictions"] - - predictions_dict = model_api.predict_all( - df=scoring_data, - bucket="retrofit-data-dev", - prediction_buckets={ - "sap_change_predictions": "retrofit-sap-predictions-dev", - } - ) - assert predictions_dict["sap_change_predictions"]["predictions"].values[0] == 52.2 - - def test_air_source_heat_pump_gas_boiler_starting_2(self): - """ - This property seems to have miniscule movement in SAP - just 2 poins - :return: - """ - - starting_epc = { - 'low-energy-fixed-light-count': '', 'address': '31 Whinney Hill Park', 'uprn-source': 'Energy Assessor', - 'floor-height': '2.3', 'heating-cost-potential': '394', 'unheated-corridor-length': '', - 'hot-water-cost-potential': '48', 'construction-age-band': 'England and Wales: 1967-1975', - 'potential-energy-rating': 'B', 'mainheat-energy-eff': 'Good', 'windows-env-eff': 'Average', - 'lighting-energy-eff': 'Good', 'environment-impact-potential': '87', - 'glazed-type': 'double glazing, unknown install date', 'heating-cost-current': '487', 'address3': '', - 'mainheatcont-description': 'Programmer, room thermostat and TRVs', 'sheating-energy-eff': 'N/A', - 'property-type': 'Bungalow', 'local-authority-label': 'Calderdale', 'fixed-lighting-outlets-count': '5', - 'energy-tariff': 'Single', 'mechanical-ventilation': 'natural', 'hot-water-cost-current': '86', - 'county': '', 'postcode': 'HD6 2PX', 'solar-water-heating-flag': 'N', 'constituency': 'E14000614', - 'co2-emissions-potential': '0.8', 'number-heated-rooms': '2', - 'floor-description': 'Solid, no insulation (assumed)', 'energy-consumption-potential': '105', - 'local-authority': 'E08000033', 'built-form': 'End-Terrace', 'number-open-fireplaces': '0', - 'windows-description': 'Fully double glazed', 'glazed-area': 'Normal', 'inspection-date': '2021-11-25', - 'mains-gas-flag': 'Y', 'co2-emiss-curr-per-floor-area': '56', 'address1': '31 Whinney Hill Park', - 'heat-loss-corridor': '', 'flat-storey-count': '', 'constituency-label': 'Calder Valley', - 'roof-energy-eff': 'Good', 'total-floor-area': '44.0', 'building-reference-number': '10001772583', - 'environment-impact-current': '62', 'co2-emissions-current': '2.5', - 'roof-description': 'Pitched, 250 mm loft insulation', 'floor-energy-eff': 'N/A', - 'number-habitable-rooms': '2', 'address2': '', 'hot-water-env-eff': 'Good', 'posttown': 'BRIGHOUSE', - 'mainheatc-energy-eff': 'Good', 'main-fuel': 'mains gas (not community)', 'lighting-env-eff': 'Good', - 'windows-energy-eff': 'Average', 'floor-env-eff': 'N/A', 'sheating-env-eff': 'N/A', - 'lighting-description': 'Low energy lighting in 60% of fixed outlets', 'roof-env-eff': 'Good', - 'walls-energy-eff': 'Average', 'photo-supply': '0.0', 'lighting-cost-potential': '40', - 'mainheat-env-eff': 'Good', 'multi-glaze-proportion': '100', 'main-heating-controls': '', - 'lodgement-datetime': '2021-11-25 11:39:35', 'flat-top-storey': '', 'current-energy-rating': 'D', - 'secondheat-description': 'Room heaters, electric', 'walls-env-eff': 'Average', - 'transaction-type': 'rental', 'uprn': '100051304421', 'current-energy-efficiency': '62', - 'energy-consumption-current': '322', 'mainheat-description': 'Boiler and radiators, mains gas', - 'lighting-cost-current': '56', 'lodgement-date': '2021-11-25', 'extension-count': '0', - 'mainheatc-env-eff': 'Good', 'lmk-key': '077f70657e9c3f1f0ce5392798398398616b159493b2a8ca2338961596631c27', - 'wind-turbine-count': '0', 'tenure': 'Rented (social)', 'floor-level': '', - 'potential-energy-efficiency': '86', 'hot-water-energy-eff': 'Good', 'low-energy-lighting': '60', - 'walls-description': 'Cavity wall, filled cavity', 'hotwater-description': 'From main system' - } - - ending_epc = { - 'low-energy-fixed-light-count': '', 'address': '31 Whinney Hill Park', - 'uprn-source': 'Energy Assessor', 'floor-height': '2.3', 'heating-cost-potential': '277', - 'unheated-corridor-length': '', 'hot-water-cost-potential': '266', - 'construction-age-band': 'England and Wales: 1967-1975', 'potential-energy-rating': 'B', - 'mainheat-energy-eff': 'Very Good', 'windows-env-eff': 'Average', 'lighting-energy-eff': 'Good', - 'environment-impact-potential': '90', 'glazed-type': 'double glazing, unknown install date', - 'heating-cost-current': '331', 'address3': '', - 'mainheatcont-description': 'Programmer and room thermostat', 'sheating-energy-eff': 'N/A', - 'property-type': 'Bungalow', 'local-authority-label': 'Calderdale', - 'fixed-lighting-outlets-count': '5', 'energy-tariff': 'Single', - 'mechanical-ventilation': 'natural', 'hot-water-cost-current': '404', 'county': '', - 'postcode': 'HD6 2PX', 'solar-water-heating-flag': 'N', 'constituency': 'E14000614', - 'co2-emissions-potential': '0.7', 'number-heated-rooms': '2', - 'floor-description': 'Solid, no insulation (assumed)', 'energy-consumption-potential': '92', - 'local-authority': 'E08000033', 'built-form': 'End-Terrace', 'number-open-fireplaces': '0', - 'windows-description': 'Fully double glazed', 'glazed-area': 'Normal', - 'inspection-date': '2021-11-25', 'mains-gas-flag': 'Y', 'co2-emiss-curr-per-floor-area': '48', - 'address1': '31 Whinney Hill Park', 'heat-loss-corridor': '', 'flat-storey-count': '', - 'constituency-label': 'Calder Valley', 'roof-energy-eff': 'Good', 'total-floor-area': '44.0', - 'building-reference-number': '10001772583', 'environment-impact-current': '68', - 'co2-emissions-current': '2.1', 'roof-description': 'Pitched, 250 mm loft insulation', - 'floor-energy-eff': 'N/A', 'number-habitable-rooms': '2', 'address2': '', - 'hot-water-env-eff': 'Poor', 'posttown': 'BRIGHOUSE', 'mainheatc-energy-eff': 'Average', - 'main-fuel': 'electricity (not community)', 'lighting-env-eff': 'Good', - 'windows-energy-eff': 'Average', 'floor-env-eff': 'N/A', 'sheating-env-eff': 'N/A', - 'lighting-description': 'Low energy lighting in 60% of fixed outlets', 'roof-env-eff': 'Good', - 'walls-energy-eff': 'Average', 'photo-supply': '0.0', 'lighting-cost-potential': '40', - 'mainheat-env-eff': 'Very Good', 'multi-glaze-proportion': '100', 'main-heating-controls': '', - 'lodgement-datetime': '2022-03-23 16:06:21', 'flat-top-storey': '', 'current-energy-rating': 'D', - 'secondheat-description': 'Room heaters, electric', 'walls-env-eff': 'Average', - 'transaction-type': 'rental', 'uprn': '100051304421', 'current-energy-efficiency': '64', - 'energy-consumption-current': '283', - 'mainheat-description': 'Air source heat pump, radiators, electric', - 'lighting-cost-current': '57', 'lodgement-date': '2022-03-23', 'extension-count': '0', - 'mainheatc-env-eff': 'Average', - 'lmk-key': '6296248141447b53426a40f1c39da17dad5f4786485db55ee38737891111a4d4', - 'wind-turbine-count': '0', 'tenure': 'Rented (social)', 'floor-level': '', - 'potential-energy-efficiency': '89', 'hot-water-energy-eff': 'Very Poor', - 'low-energy-lighting': '60', 'walls-description': 'Cavity wall, filled cavity', - 'hotwater-description': 'From main system' - } - - # differences = [] - # for k, v in ending_epc.items(): - # if v != starting_epc[k]: - # differences.append( - # { - # "variable": k, - # "starting_value": starting_epc[k], - # "ending_value": v - # } - # ) - # differences = pd.DataFrame(differences) - # - # diffs = differences[ - # differences["variable"].isin( - # [ - # "mainheat-energy-eff", - # "mainheatcont-description", - # "mainheatc-energy-eff", - # "main-fuel", - # "mainheat-env-eff", - # "mainheat-description", - # "hot-water-energy-eff", - # "hotwater-description" - # ] - # ) - # ] - - cleaning_data = read_dataframe_from_s3_parquet( - bucket_name="retrofit-data-dev", file_key="sap_change_model/cleaning_dataset.parquet", - ) - - cleaned = read_from_s3( - s3_file_name="cleaned_epc_data/cleaned.bson", - bucket_name="retrofit-data-dev" - ) - cleaned = msgpack.unpackb(cleaned, raw=False) - - photo_supply_lookup, floor_area_decile_thresholds = SolarPhotoSupply.load(bucket="retrofit-data-dev") - - epc = EPCRecord( - epc_records={ - 'original_epc': starting_epc, - 'full_sap_epc': {}, - 'old_data': [] - }, - run_mode="newdata", - cleaning_data=cleaning_data - ) - - home = Property( - id=0, - address="", - postcode="", - epc_record=epc, - already_installed={}, - non_invasive_recommendations={}, - ) - home.in_conservation_area = False - home.is_listed = False - home.is_heritage = False - home.restricted_measures = True - home.get_components( - cleaned=cleaned, - photo_supply_lookup=photo_supply_lookup, - floor_area_decile_thresholds=floor_area_decile_thresholds - ) - - recommender = HeatingRecommender(property_instance=home) - recommender.recommend_air_source_heat_pump(phase=0, has_cavity_or_loft_recommendations=False) - property_recommendations = Recommendations.insert_temp_recommendation_id([recommender.heating_recommendations]) - - assert len(recommender.heating_recommendations) == 1 - - home.create_base_difference_epc_record(cleaned_lookup=cleaned) - home.adjust_difference_record_with_recommendations( - property_recommendations, [] - ) - - scoring_data = pd.DataFrame(home.recommendations_scoring_data).drop( - columns=["rdsap_change", "heat_demand_change", "carbon_change", "sap_ending", "heat_demand_ending", - "carbon_ending"] - ) - - model_api = ModelApi(portfolio_id="ashp-test", timestamp=datetime.now().isoformat()) - model_api.MODEL_PREFIXES = ["sap_change_predictions"] - - predictions_dict = model_api.predict_all( - df=scoring_data, - bucket="retrofit-data-dev", - prediction_buckets={ - "sap_change_predictions": "retrofit-sap-predictions-dev", - } - ) - assert predictions_dict["sap_change_predictions"]["predictions"].values[0] == 69.3 - - # In actuality with this property, the heating controls get downgraded, so we test a manual patch of this - patched_simulation_config = { - 'mainheat_energy_eff_ending': "Very Good", - 'hot_water_energy_eff_ending': 'Very Poor', - 'has_boiler_ending': False, - 'has_air_source_heat_pump_ending': True, - 'has_electric_ending': True, - 'has_mains_gas_ending': False, - 'fuel_type_ending': 'electricity', - 'trvs_ending': None, - "mainheatc_energy_eff_ending": 'Average' - } - - # PATCHING - property_recommendations_patch = Recommendations.insert_temp_recommendation_id( - [recommender.heating_recommendations] - ) - property_recommendations_patch[0][0]["simulation_config"] = patched_simulation_config - - home.create_base_difference_epc_record(cleaned_lookup=cleaned) - home.adjust_difference_record_with_recommendations( - property_recommendations_patch, [] - ) - - scoring_data_patch = pd.DataFrame(home.recommendations_scoring_data).drop( - columns=["rdsap_change", "heat_demand_change", "carbon_change", "sap_ending", "heat_demand_ending", - "carbon_ending"] - ) - - model_api = ModelApi(portfolio_id="ashp-test", timestamp=datetime.now().isoformat()) - model_api.MODEL_PREFIXES = ["sap_change_predictions"] - - predictions_dict_patch = model_api.predict_all( - df=scoring_data_patch, - bucket="retrofit-data-dev", - prediction_buckets={ - "sap_change_predictions": "retrofit-sap-predictions-dev", - } - ) - # The error is only 0.3, so the model is working - assert predictions_dict_patch["sap_change_predictions"]["predictions"].values[0] == 64.3 - assert ending_epc["current-energy-efficiency"] == '64' - - def test_air_source_heat_pump_lpg_boiler(self): - starting_epc = { - 'low-energy-fixed-light-count': '', 'address': 'Holly Lodge, The Drive, Perry', - 'uprn-source': 'Energy Assessor', 'floor-height': '2.8', 'heating-cost-potential': '1628', - 'unheated-corridor-length': '', 'hot-water-cost-potential': '175', - 'construction-age-band': 'England and Wales: 1950-1966', 'potential-energy-rating': 'D', - 'mainheat-energy-eff': 'Poor', 'windows-env-eff': 'Average', 'lighting-energy-eff': 'Average', - 'environment-impact-potential': '70', 'glazed-type': 'double glazing, unknown install date', - 'heating-cost-current': '2158', 'address3': 'Perry', - 'mainheatcont-description': 'No time or thermostatic control of room temperature', - 'sheating-energy-eff': 'N/A', 'property-type': 'Bungalow', 'local-authority-label': 'Huntingdonshire', - 'fixed-lighting-outlets-count': '12', 'energy-tariff': 'Single', 'mechanical-ventilation': 'natural', - 'hot-water-cost-current': '257', 'county': 'Cambridgeshire', 'postcode': 'PE28 0SX', - 'solar-water-heating-flag': 'N', 'constituency': 'E14000757', 'co2-emissions-potential': '3.3', - 'number-heated-rooms': '5', 'floor-description': 'Solid, no insulation (assumed)', - 'energy-consumption-potential': '128', 'local-authority': 'E07000011', 'built-form': 'Semi-Detached', - 'number-open-fireplaces': '0', 'windows-description': 'Fully double glazed', 'glazed-area': 'Normal', - 'inspection-date': '2023-08-31', 'mains-gas-flag': 'N', 'co2-emiss-curr-per-floor-area': '51', - 'address1': 'Holly Lodge', 'heat-loss-corridor': '', 'flat-storey-count': '', - 'constituency-label': 'Huntingdon', 'roof-energy-eff': 'Good', 'total-floor-area': '117.0', - 'building-reference-number': '10005199915', 'environment-impact-current': '50', - 'co2-emissions-current': '5.9', 'roof-description': 'Pitched, 270 mm loft insulation', - 'floor-energy-eff': 'N/A', 'number-habitable-rooms': '5', 'address2': 'The Drive', - 'hot-water-env-eff': 'Good', 'posttown': 'HUNTINGDON', 'mainheatc-energy-eff': 'Very Poor', - 'main-fuel': 'LPG (not community)', 'lighting-env-eff': 'Average', 'windows-energy-eff': 'Average', - 'floor-env-eff': 'N/A', 'sheating-env-eff': 'N/A', - 'lighting-description': 'Low energy lighting in 33% of fixed outlets', 'roof-env-eff': 'Good', - 'walls-energy-eff': 'Average', 'photo-supply': '0.0', 'lighting-cost-potential': '166', - 'mainheat-env-eff': 'Good', 'multi-glaze-proportion': '100', 'main-heating-controls': '', - 'lodgement-datetime': '2023-10-30 13:46:54', 'flat-top-storey': '', 'current-energy-rating': 'F', - 'secondheat-description': 'Room heaters, electric', 'walls-env-eff': 'Average', - 'transaction-type': 'ECO assessment', 'uprn': '100091200828', 'current-energy-efficiency': '32', - 'energy-consumption-current': '243', 'mainheat-description': 'Boiler and radiators, LPG', - 'lighting-cost-current': '277', 'lodgement-date': '2023-10-30', 'extension-count': '0', - 'mainheatc-env-eff': 'Very Poor', - 'lmk-key': 'f1d3bd4b8b50bc9b006231ccb158537c408523b748b3f4ef7e98cd03b144afa5', 'wind-turbine-count': '0', - 'tenure': 'Owner-occupied', 'floor-level': '', 'potential-energy-efficiency': '56', - 'hot-water-energy-eff': 'Poor', 'low-energy-lighting': '33', - 'walls-description': 'Cavity wall, filled cavity', 'hotwater-description': 'From main system' - } - - ending_epc = { - 'low-energy-fixed-light-count': '', 'address': 'Holly Lodge, The Drive, Perry', - 'uprn-source': 'Energy Assessor', 'floor-height': '2.8', 'heating-cost-potential': '917', - 'unheated-corridor-length': '', 'hot-water-cost-potential': '328', - 'construction-age-band': 'England and Wales: 1950-1966', 'potential-energy-rating': 'A', - 'mainheat-energy-eff': 'Very Good', 'windows-env-eff': 'Average', 'lighting-energy-eff': 'Average', - 'environment-impact-potential': '96', 'glazed-type': 'double glazing, unknown install date', - 'heating-cost-current': '1098', 'address3': 'Perry', - 'mainheatcont-description': 'Programmer, TRVs and bypass', 'sheating-energy-eff': 'N/A', - 'property-type': 'Bungalow', 'local-authority-label': 'Huntingdonshire', - 'fixed-lighting-outlets-count': '12', 'energy-tariff': 'Single', 'mechanical-ventilation': 'natural', - 'hot-water-cost-current': '328', 'county': 'Cambridgeshire', 'postcode': 'PE28 0SX', - 'solar-water-heating-flag': 'N', 'constituency': 'E14000757', 'co2-emissions-potential': '0.3', - 'number-heated-rooms': '5', 'floor-description': 'Solid, no insulation (assumed)', - 'energy-consumption-potential': '16', 'local-authority': 'E07000011', 'built-form': 'Semi-Detached', - 'number-open-fireplaces': '0', 'windows-description': 'Fully double glazed', 'glazed-area': 'Normal', - 'inspection-date': '2023-10-05', 'mains-gas-flag': 'N', 'co2-emiss-curr-per-floor-area': '6', - 'address1': 'Holly Lodge', 'heat-loss-corridor': '', 'flat-storey-count': '', - 'constituency-label': 'Huntingdon', 'roof-energy-eff': 'Good', 'total-floor-area': '117.0', - 'building-reference-number': '10005199915', 'environment-impact-current': '92', - 'co2-emissions-current': '0.7', 'roof-description': 'Pitched, 270 mm loft insulation', - 'floor-energy-eff': 'N/A', 'number-habitable-rooms': '5', 'address2': 'The Drive', - 'hot-water-env-eff': 'Very Good', 'posttown': 'HUNTINGDON', 'mainheatc-energy-eff': 'Average', - 'main-fuel': 'electricity (not community)', 'lighting-env-eff': 'Average', 'windows-energy-eff': 'Average', - 'floor-env-eff': 'N/A', 'sheating-env-eff': 'N/A', - 'lighting-description': 'Low energy lighting in 33% of fixed outlets', 'roof-env-eff': 'Good', - 'walls-energy-eff': 'Average', 'photo-supply': '', 'lighting-cost-potential': '166', - 'mainheat-env-eff': 'Very Good', 'multi-glaze-proportion': '100', 'main-heating-controls': '', - 'lodgement-datetime': '2023-11-01 16:29:16', 'flat-top-storey': '', 'current-energy-rating': 'A', - 'secondheat-description': 'Room heaters, electric', 'walls-env-eff': 'Average', - 'transaction-type': 'ECO assessment', 'uprn': '100091200828', 'current-energy-efficiency': '92', - 'energy-consumption-current': '37', 'mainheat-description': 'Air source heat pump, radiators, electric', - 'lighting-cost-current': '277', 'lodgement-date': '2023-11-01', 'extension-count': '0', - 'mainheatc-env-eff': 'Average', - 'lmk-key': 'cb7f2838b727907767c8c2a385cd22f722b1e4745463391d910d228e52124515', 'wind-turbine-count': '0', - 'tenure': 'Owner-occupied', 'floor-level': '', 'potential-energy-efficiency': '95', - 'hot-water-energy-eff': 'Good', 'low-energy-lighting': '33', - 'walls-description': 'Cavity wall, filled cavity', 'hotwater-description': 'From main system' - } - - cleaning_data = read_dataframe_from_s3_parquet( - bucket_name="retrofit-data-dev", file_key="sap_change_model/cleaning_dataset.parquet", - ) - - cleaned = read_from_s3( - s3_file_name="cleaned_epc_data/cleaned.bson", - bucket_name="retrofit-data-dev" - ) - cleaned = msgpack.unpackb(cleaned, raw=False) - - photo_supply_lookup, floor_area_decile_thresholds = SolarPhotoSupply.load(bucket="retrofit-data-dev") - - epc = EPCRecord( - epc_records={ - 'original_epc': starting_epc, - 'full_sap_epc': {}, - 'old_data': [] - }, - run_mode="newdata", - cleaning_data=cleaning_data - ) - - home = Property( - id=0, - address="", - postcode="", - epc_record=epc, - already_installed={}, - non_invasive_recommendations={}, - ) - home.in_conservation_area = False - home.is_listed = False - home.is_heritage = False - home.restricted_measures = True - home.get_components( - cleaned=cleaned, - photo_supply_lookup=photo_supply_lookup, - floor_area_decile_thresholds=floor_area_decile_thresholds - ) - - recommender = HeatingRecommender(property_instance=home) - recommender.recommend_air_source_heat_pump(phase=0, has_cavity_or_loft_recommendations=False) - property_recommendations = Recommendations.insert_temp_recommendation_id([recommender.heating_recommendations]) - - assert len(recommender.heating_recommendations) == 1 - - home.create_base_difference_epc_record(cleaned_lookup=cleaned) - home.adjust_difference_record_with_recommendations( - property_recommendations, [] - ) - - scoring_data = pd.DataFrame(home.recommendations_scoring_data).drop( - columns=["rdsap_change", "heat_demand_change", "carbon_change", "sap_ending", "heat_demand_ending", - "carbon_ending"] - ) - - model_api = ModelApi(portfolio_id="ashp-test", timestamp=datetime.now().isoformat()) - model_api.MODEL_PREFIXES = ["sap_change_predictions"] - - predictions_dict = model_api.predict_all( - df=scoring_data, - bucket="retrofit-data-dev", - prediction_buckets={ - "sap_change_predictions": "retrofit-sap-predictions-dev", - } - ) - # We predict a huge uplift but not quite as much as the EPC, due to some distinct differences between our - # recommendation and the EPC - assert predictions_dict["sap_change_predictions"]["predictions"].values[0] == 81.3 - assert ending_epc['current-energy-efficiency'] == '92' - - # PATCH - # We patch the simulation config, to reflect the ending EPC, to see if we get the ending EPC's config - patched_simulation_config = { - 'mainheat_energy_eff_ending': "Very Good", - 'hot_water_energy_eff_ending': 'Good', - 'has_boiler_ending': False, - 'has_air_source_heat_pump_ending': True, - 'has_electric_ending': True, - 'has_lpg_ending': False, - 'fuel_type_ending': 'electricity', - 'switch_system_ending': 'programmer', - 'no_control_ending': None, - 'auxiliary_systems_ending': 'bypass', - 'trvs_ending': 'trvs', - "mainheatc_energy_eff_ending": 'Average' - } - - # PATCHING - property_recommendations_patch = Recommendations.insert_temp_recommendation_id( - [recommender.heating_recommendations] - ) - property_recommendations_patch[0][0]["simulation_config"] = patched_simulation_config - - home.create_base_difference_epc_record(cleaned_lookup=cleaned) - home.adjust_difference_record_with_recommendations( - property_recommendations_patch, [] - ) - - scoring_data_patch = pd.DataFrame(home.recommendations_scoring_data).drop( - columns=["rdsap_change", "heat_demand_change", "carbon_change", "sap_ending", "heat_demand_ending", - "carbon_ending"] - ) - - model_api = ModelApi(portfolio_id="ashp-test", timestamp=datetime.now().isoformat()) - model_api.MODEL_PREFIXES = ["sap_change_predictions"] - - predictions_dict_patch = model_api.predict_all( - df=scoring_data_patch, - bucket="retrofit-data-dev", - prediction_buckets={ - "sap_change_predictions": "retrofit-sap-predictions-dev", - } - ) - - assert predictions_dict_patch["sap_change_predictions"]["predictions"].values[0] == 88.9 - # We still underpredict but the improvement is notable - - def test_offgrid(self): - """ - We test on a property we've worked with before, where we compare two options - a) Upgrading to a boiler - b) Upgrading to a heat pump - :return: - """ - - starting_epc = { - 'low-energy-fixed-light-count': '', 'address': '6 Beech Road', 'uprn-source': 'Energy Assessor', - 'floor-height': '2.4', 'heating-cost-potential': '612', 'unheated-corridor-length': '', - 'hot-water-cost-potential': '123', 'construction-age-band': 'England and Wales: 1930-1949', - 'potential-energy-rating': 'B', 'mainheat-energy-eff': 'Very Poor', 'windows-env-eff': 'Good', - 'lighting-energy-eff': 'Good', 'environment-impact-potential': '87', - 'glazed-type': 'double glazing installed during or after 2002', 'heating-cost-current': '2278', - 'address3': '', 'mainheatcont-description': 'Appliance thermostats', 'sheating-energy-eff': 'N/A', - 'property-type': 'House', 'local-authority-label': 'Dudley', 'fixed-lighting-outlets-count': '9', - 'energy-tariff': 'Single', 'mechanical-ventilation': 'natural', 'hot-water-cost-current': '604', - 'county': '', 'postcode': 'DY1 4BP', 'solar-water-heating-flag': 'N', 'constituency': 'E14000671', - 'co2-emissions-potential': '1.0', 'number-heated-rooms': '4', - 'floor-description': 'Solid, no insulation (assumed)', 'energy-consumption-potential': '93', - 'local-authority': 'E08000027', 'built-form': 'End-Terrace', 'number-open-fireplaces': '0', - 'windows-description': 'Fully double glazed', 'glazed-area': 'Normal', 'inspection-date': '2024-03-13', - 'mains-gas-flag': 'Y', 'co2-emiss-curr-per-floor-area': '83', 'address1': '6 Beech Road', - 'heat-loss-corridor': '', 'flat-storey-count': '', 'constituency-label': 'Dudley North', - 'roof-energy-eff': 'Very Poor', 'total-floor-area': '60.0', 'building-reference-number': '10005780080', - 'environment-impact-current': '41', 'co2-emissions-current': '5.0', - 'roof-description': 'Pitched, 12 mm loft insulation', 'floor-energy-eff': 'N/A', - 'number-habitable-rooms': '4', 'address2': '', 'hot-water-env-eff': 'Poor', 'posttown': 'DUDLEY', - 'mainheatc-energy-eff': 'Good', 'main-fuel': 'electricity (not community)', 'lighting-env-eff': 'Good', - 'windows-energy-eff': 'Good', 'floor-env-eff': 'N/A', 'sheating-env-eff': 'N/A', - 'lighting-description': 'Low energy lighting in 67% of fixed outlets', 'roof-env-eff': 'Very Poor', - 'walls-energy-eff': 'Average', 'photo-supply': '0.0', 'lighting-cost-potential': '113', - 'mainheat-env-eff': 'Poor', 'multi-glaze-proportion': '100', 'main-heating-controls': '', - 'lodgement-datetime': '2024-03-13 11:29:11', 'flat-top-storey': '', 'current-energy-rating': 'F', - 'secondheat-description': 'None', 'walls-env-eff': 'Average', 'transaction-type': 'rental', - 'uprn': '90055152', 'current-energy-efficiency': '32', 'energy-consumption-current': '491', - 'mainheat-description': 'Room heaters, electric', 'lighting-cost-current': '113', - 'lodgement-date': '2024-03-13', 'extension-count': '1', 'mainheatc-env-eff': 'Good', - 'lmk-key': '78ddf851b660e599a0894924d0e6b503980f5e0ad1aa711f8411718dc2989c44', 'wind-turbine-count': '0', - 'tenure': 'Rented (social)', 'floor-level': '', 'potential-energy-efficiency': '87', - 'hot-water-energy-eff': 'Very Poor', 'low-energy-lighting': '67', - 'walls-description': 'Cavity wall, filled cavity', - 'hotwater-description': 'Electric immersion, standard tariff' - } - - cleaning_data = read_dataframe_from_s3_parquet( - bucket_name="retrofit-data-dev", file_key="sap_change_model/cleaning_dataset.parquet", - ) - - cleaned = read_from_s3( - s3_file_name="cleaned_epc_data/cleaned.bson", - bucket_name="retrofit-data-dev" - ) - cleaned = msgpack.unpackb(cleaned, raw=False) - - photo_supply_lookup, floor_area_decile_thresholds = SolarPhotoSupply.load(bucket="retrofit-data-dev") - - epc = EPCRecord( - epc_records={ - 'original_epc': starting_epc, - 'full_sap_epc': {}, - 'old_data': [] - }, - run_mode="newdata", - cleaning_data=cleaning_data - ) - - home = Property( - id=0, - address="", - postcode="", - epc_record=epc, - already_installed={}, - non_invasive_recommendations={}, - ) - home.in_conservation_area = False - home.is_listed = False - home.is_heritage = False - home.restricted_measures = True - home.get_components( - cleaned=cleaned, - photo_supply_lookup=photo_supply_lookup, - floor_area_decile_thresholds=floor_area_decile_thresholds - ) - - recommender = HeatingRecommender(property_instance=home) - recommender.recommend_air_source_heat_pump(phase=0, has_cavity_or_loft_recommendations=False) - recommender.recommend_boiler_upgrades(phase=0, system_change=True, exising_room_heaters=False) - - assert len(recommender.heating_recommendations) == 3 - - property_recommendations = Recommendations.insert_temp_recommendation_id([recommender.heating_recommendations]) - - home.create_base_difference_epc_record(cleaned_lookup=cleaned) - home.adjust_difference_record_with_recommendations( - property_recommendations, [] - ) - - scoring_data = pd.DataFrame(home.recommendations_scoring_data).drop( - columns=["rdsap_change", "heat_demand_change", "carbon_change", "sap_ending", "heat_demand_ending", - "carbon_ending"] - ) - - model_api = ModelApi(portfolio_id="ashp-test", timestamp=datetime.now().isoformat()) - model_api.MODEL_PREFIXES = ["sap_change_predictions"] - - predictions_dict = model_api.predict_all( - df=scoring_data, - bucket="retrofit-data-dev", - prediction_buckets={ - "sap_change_predictions": "retrofit-sap-predictions-dev", - } - ) - - # The ASHP isn't better under SAP, compared to a gas boiler with good heat controls - assert predictions_dict["sap_change_predictions"]["predictions"].tolist() == [66.9, 65.5, 65.9] diff --git a/recommendations/tests/test_costs.py b/recommendations/tests/test_costs.py index 402e38eb..74a210c1 100644 --- a/recommendations/tests/test_costs.py +++ b/recommendations/tests/test_costs.py @@ -18,10 +18,9 @@ class TestCosts: "description": "cwi", "depth": 75, "thermal_conductivity": 0.037, - "prime_cost": 5.17, - "material_cost": 5.62, - "labour_cost": 1.125, + "total_cost": 14, "labour_hours_per_unit": 0.065, + "is_installer_quote": True } cwi_results = costs.cavity_wall_insulation( @@ -29,12 +28,7 @@ class TestCosts: material=cwi_material, ) - assert cwi_results == { - 'total': 1065.0661223512907, 'subtotal': 887.5551019594088, 'vat': 177.51102039188177, - 'contingency': 63.396792997100626, 'preliminaries': 63.396792997100626, 'material': 539.0166061175574, - 'profit': 126.79358599420125, 'labour_hours': 6.234177828761786, 'labour_cost': 94.95132385344874, - 'labour_days': 0.38963611429761164 - } + assert cwi_results == {'total': 1342.7459938871539, 'labour_hours': 8, 'labour_days': 1} def test_loft_insulation(self): mock_property = Mock() @@ -47,22 +41,17 @@ class TestCosts: "description": "Crown Loft Roll 44 glass fibre roll", "depth": 270, "thermal_conductivity": 0.044, - "prime_cost": None, - "material_cost": 5.91938, - "labour_cost": 1.96, - "labour_hours_per_unit": 0.11 + "total_cost": 11, + "labour_hours_per_unit": 0.11, + "is_installer_quote": True, } - loft_results = costs.loft_insulation( + loft_results = costs.loft_and_flat_insulation( floor_area=33.5, material=loft_material, ) - assert loft_results == { - 'total': 639.4133610000001, 'subtotal': 532.8444675000001, 'vat': 106.56889350000002, - 'contingency': 71.045929, 'preliminaries': 35.5229645, 'material': 297.448845, 'profit': 71.045929, - 'labour_hours': 3.685, 'labour_cost': 57.7808, 'labour_days': 0.460625 - } + assert loft_results == {'total': 368.5, 'labour_hours': 8, 'labour_days': 1} def test_internal_wall_insulation(self): mock_property = Mock() @@ -71,87 +60,6 @@ class TestCosts: } costs = Costs(mock_property) - iwi_non_insulation_materials = [ - {'type': 'iwi_wall_demolition', - 'description': 'Solid & Dry Lined walls: Hack of wall finishes with chipping hammer; plaster to walls.', - 'depth': 0.0, 'depth_unit': 0.0, 'cost_unit': 'gbp_per_m2', 'thermal_conductivity': 0.0, - 'thermal_conductivity_unit': 0.0, 'prime_material_cost': 0.0, 'material_cost': 0.0, 'labour_cost': 10.27, - 'labour_hours_per_unit': 0.33, 'plant_cost': 1.28, 'total_cost': 11.55, 'link': 'SPONs', 'Notes': 0.0}, - {'type': 'iwi_wall_demolition', - 'description': 'Stud walls: Remove wall linings including battening behind; plasterboard and skim', - 'depth': 0.0, 'depth_unit': 0.0, 'cost_unit': 'gbp_per_m2', 'thermal_conductivity': 0.0, - 'thermal_conductivity_unit': 0.0, 'prime_material_cost': 0.0, 'material_cost': 0.0, 'labour_cost': 6.23, - 'labour_hours_per_unit': 0.2, 'plant_cost': 1.25, 'total_cost': 7.48, 'link': 'SPONs', 'Notes': 0.0}, - {'type': 'iwi_wall_demolition', - 'description': 'Lathe and Plaster walls: Remove wall linings including battening behind; wood lath and ' - 'plaster', - 'depth': 0.0, 'depth_unit': 0.0, 'cost_unit': 'gbp_per_m2', 'thermal_conductivity': 0.0, - 'thermal_conductivity_unit': 0.0, 'prime_material_cost': 0.0, 'material_cost': 0.0, 'labour_cost': 6.85, - 'labour_hours_per_unit': 0.22, 'plant_cost': 2.09, 'total_cost': 8.94, 'link': 'SPONs', 'Notes': 0.0}, - {'Notes': "", - 'cost_unit': "", - 'depth': "", - 'depth_unit': "", - 'description': 'Visqueen High Performance Vapour Barrier', - 'labour_cost': 0.48, - 'labour_hours_per_unit': 0.02, - 'link': 'SPONs', - 'material_cost': 1.21, - 'plant_cost': 0, - 'prime_material_cost': 0.58, - 'thermal_conductivity': "", - 'thermal_conductivity_unit': "", - 'total_cost': 1.69, - 'type': 'iwi_vapour_barrier'}, - {'Notes': "", - 'cost_unit': "", - 'depth': "", - 'depth_unit': "", - 'description': 'Plaster; one coat Thistle board finish or other equal; steel trowelled; 3 mm thick work ' - 'to walls or ceilings; one coat; to plasterboard base; over 600mm wide', - 'labour_cost': 6.58, - 'labour_hours_per_unit': 0.25, - 'link': "", - 'material_cost': 0.06, - 'plant_cost': 0, - 'prime_material_cost': 0.0, - 'thermal_conductivity': "", - 'thermal_conductivity_unit': "", - 'total_cost': 6.64, - 'type': 'iwi_redecoration'}, - {'Notes': "", - 'cost_unit': "", - 'depth': "", - 'depth_unit': "", - 'description': 'Two coats emulsion paint on plaster, over 40mm girth; 3.5m - ' - '5m high', - 'labour_cost': 0.0, - 'labour_hours_per_unit': 0.21, - 'link': "", - 'material_cost': 0.41, - 'plant_cost': 0, - 'prime_material_cost': "", - 'thermal_conductivity': "", - 'thermal_conductivity_unit': "", - 'total_cost': 4.34, - 'type': 'iwi_redecoration'}, - {'Notes': "", - 'cost_unit': "", - 'depth': "", - 'depth_unit': "", - 'description': 'Fitting existing softwood skirting or architrave to new ' - 'frames; 150mm high', - 'labour_cost': 4.87, - 'labour_hours_per_unit': 0.01, - 'link': "", - 'material_cost': 4.86, - 'plant_cost': 0, - 'prime_material_cost': "", - 'thermal_conductivity': "", - 'thermal_conductivity_unit': "", - 'total_cost': 4.88, - 'type': 'iwi_redecoration'} - ] iwi_material = { "type": "internal_wall_insulation", @@ -161,26 +69,19 @@ class TestCosts: "cost_unit": "gbp_per_m2", "thermal_conductivity": 0.022, "thermal_conductivity_unit": "watt_per_meter_kelvin", - "prime_material_cost": "", - "material_cost": 11.68, - "labour_cost": 3.12, "labour_hours_per_unit": 0.18, - "plant_cost": "", - "total_cost": 14.8, - "link": "SPONs" + "total_cost": 200, + "link": "link", + "is_installer_quote": True } - iwi_results = costs.internal_wall_insulation( + iwi_results = costs.solid_wall_insulation( wall_area=95.9104281347967, material=iwi_material, - non_insulation_materials=iwi_non_insulation_materials ) assert iwi_results == { - 'total': 6880.2304726777775, 'subtotal': 5733.525393898148, 'vat': 1146.7050787796295, - 'contingency': 764.470052519753, 'preliminaries': 382.2350262598765, 'material': 1747.488000615996, - 'profit': 764.470052519753, 'labour_hours': 88.23759388401297, 'labour_days': 2.757424808875405, - 'labour_cost': 1927.1602026551818 + 'total': 19182.085626959342, 'labour_hours': 17.263877064263404, 'labour_days': 0.5394961582582314 } def test_suspended_floor_insulation(self): @@ -201,7 +102,8 @@ class TestCosts: 'total_cost': 13.46, 'link': 'SPONs', 'Notes': 'Spons did not contain labour costs so we use values for similar insulations. ' 'We use the ' - 'same values as in Crown loft roll 44, since it is also an insulation roll' + 'same values as in Crown loft roll 44, since it is also an insulation roll', + "is_installer_quote": False } sus_floor_non_insulation_materials = [ @@ -256,7 +158,7 @@ class TestCosts: 'depth': 100.0, 'depth_unit': 'mm', 'cost_unit': 'gbp_per_m2', 'thermal_conductivity': 0.033, 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'prime_material_cost': 0, 'material_cost': 12.02, 'labour_cost': 4.4, 'labour_hours_per_unit': 0.19, 'plant_cost': 0, - 'total_cost': 16.42, 'link': 'SPONs', 'Notes': 0 + 'total_cost': 16.42, 'link': 'SPONs', 'Notes': 0, "is_installer_quote": False } sol_floor_non_insulation_materials = [ @@ -342,81 +244,18 @@ class TestCosts: ewi_material = { 'type': 'external_wall_insulation', 'description': 'Ecotherm Eco-Versal PIR Insulation Board', 'depth': 150.0, 'depth_unit': 'mm', 'cost_unit': 'gbp_per_m2', 'thermal_conductivity': 0.022, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'prime_material_cost': 23.53, - 'material_cost': 34.62, 'labour_cost': 33.06, 'labour_hours_per_unit': 1.4, 'plant_cost': 0, - 'total_cost': 67.68, 'link': 'SPONs', 'Notes': 0 + 'thermal_conductivity_unit': 'watt_per_meter_kelvin', + 'labour_hours_per_unit': 1.4, + 'total_cost': 300, 'link': 'SPONs', 'Notes': 0, "is_installer_quote": True } - ewi_non_insulation_materials = [ - {'type': 'ewi_wall_demolition', - 'description': 'Solid & Dry Lined walls: Hack of wall finishes with chipping ' - 'hammer; plaster to walls.', - 'depth': 0, 'depth_unit': 0, 'cost_unit': 'gbp_per_m2', - 'thermal_conductivity': 0, 'thermal_conductivity_unit': 0, - 'prime_material_cost': 0, 'material_cost': 0, 'labour_cost': 10.27, - 'labour_hours_per_unit': 0.33, 'plant_cost': 1.28, 'total_cost': 11.55, - 'link': 'SPONs', 'Notes': 0}, {'type': 'ewi_wall_demolition', - 'description': 'Stud walls: Remove wall linings ' - 'including battening behind; ' - 'plasterboard and skim', - 'depth': 0, 'depth_unit': 0, - 'cost_unit': 'gbp_per_m2', - 'thermal_conductivity': 0, - 'thermal_conductivity_unit': 0, - 'prime_material_cost': 0, 'material_cost': 0, - 'labour_cost': 6.23, 'labour_hours_per_unit': 0.2, - 'plant_cost': 1.25, 'total_cost': 7.48, - 'link': 'SPONs', 'Notes': 0}, - {'type': 'ewi_wall_demolition', - 'description': 'Lathe and Plaster walls: Remove wall linings including battening ' - 'behind; wood lath and plaster', - 'depth': 0, 'depth_unit': 0, 'cost_unit': 'gbp_per_m2', - 'thermal_conductivity': 0, 'thermal_conductivity_unit': 0, - 'prime_material_cost': 0, 'material_cost': 0, 'labour_cost': 6.85, - 'labour_hours_per_unit': 0.22, 'plant_cost': 2.09, 'total_cost': 8.94, - 'link': 'SPONs', 'Notes': 0}, {'type': 'ewi_wall_preparation', - 'description': 'Clean and prepare surfaces, ' - 'one coat Keim dilution, ' - 'one coat primer and two coats ' - 'of Keim Ecosil paint; Brick or ' - 'block walls; over 300 mm girth', - 'depth': 0, 'depth_unit': 0, 'cost_unit': 0, - 'thermal_conductivity': 0, - 'thermal_conductivity_unit': 0, - 'prime_material_cost': 0, 'material_cost': 7.3, - 'labour_cost': 5.62, 'labour_hours_per_unit': 0.3, - 'plant_cost': 0, 'total_cost': 12.92, - 'link': 'SPONs', - 'Notes': 'This work covers the preparation and ' - 'priming of the wall before insulating'}, - {'type': 'ewi_wall_redecoration', - 'description': 'EPS insulation fixed with adhesive to SFS structure (measured ' - 'separately) with horizontal PVC intermediate track and vertical ' - 'T-spines; with glassfibre mesh reinforcement embedded in Sto ' - 'Armat Classic Basecoat Render and Stolit K 1.5 Decorative ' - 'Topcoat Render (white)', - 'depth': 0, 'depth_unit': 0, 'cost_unit': 0, 'thermal_conductivity': 0, - 'thermal_conductivity_unit': 0, 'prime_material_cost': 0, 'material_cost': 0, - 'labour_cost': 0, 'labour_hours_per_unit': 0, 'plant_cost': 0, - 'total_cost': 69.94, 'link': 'SPONs', - 'Notes': 'This material in SPONs is for 70mm EPS insulation, which comes in at a ' - 'cost of 99.17 per meter square. This includes the cost of insulation. ' - 'To get the costing for just the works and not the insulation, ' - 'we subtract the cost of EPS insulation, using Ravathem 75mm insulation ' - 'as an example, which costs £29.23 per meter square, giving us the cost ' - 'of the remaining works without insulation. This material gives us a ' - 'cost for basecoat, mesh application and a render finish'}] - ewi_results = costs.external_wall_insulation( + ewi_results = costs.solid_wall_insulation( wall_area=95.9104281347967, material=ewi_material, - non_insulation_materials=ewi_non_insulation_materials ) assert ewi_results == { - 'total': 15047.078622131372, 'subtotal': 12539.232185109477, 'vat': 2507.8464370218953, - 'contingency': 808.9827216199662, 'preliminaries': 2022.4568040499155, 'material': 4020.565147410677, - 'profit': 1617.9654432399325, 'labour_hours': 187.02533486285358, 'labour_days': 5.8445417144641745, - 'labour_cost': 3921.5600094613983 + 'total': 28773.12844043901, 'labour_hours': 134.2745993887154, 'labour_days': 4.196081230897356 } def test_flat_roof_insulation(self): @@ -426,120 +265,47 @@ class TestCosts: } costs = Costs(mock_property) - flat_roof_material = {'id': 1225, 'type': 'flat_roof_insulation', - 'description': 'Kingspan Thermaroof TR21 zero OPD ' - 'urethene insulation board', - 'depth': 100.0, 'depth_unit': 'mm', 'cost': None, - 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.04, - 'r_value_unit': 'square_meter_kelvin_per_watt', - 'thermal_conductivity': 0.025, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', - 'link': 'SPONs', - 'created_at': "now", 'is_active': True, - 'prime_material_cost': None, 'material_cost': 50.95, - 'labour_cost': 10.66, 'labour_hours_per_unit': 0.48, - 'plant_cost': 0.0, 'total_cost': 61.61, - 'notes': "SPONs didn't have a labour hours so we use " - "0.48 which is similar to other materials"} + flat_roof_material = { + 'id': 1225, 'type': 'flat_roof_insulation', + 'description': 'Kingspan Thermaroof TR21 zero OPD ' + 'urethene insulation board', + 'depth': 100.0, 'depth_unit': 'mm', 'cost': None, + 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.04, + 'r_value_unit': 'square_meter_kelvin_per_watt', + 'thermal_conductivity': 0.025, + 'thermal_conductivity_unit': 'watt_per_meter_kelvin', + 'link': 'SPONs', + 'created_at': "now", 'is_active': True, + 'prime_material_cost': None, 'material_cost': 50.95, + 'labour_cost': 10.66, 'labour_hours_per_unit': 0.48, + 'plant_cost': 0.0, 'total_cost': 61.61, + 'notes': "SPONs didn't have a labour hours so we use " + "0.48 which is similar to other materials", + "is_installer_quote": False + } - flat_roof_non_insulation_materials = [ - {'id': 17, 'type': 'mechanical_ventilation', 'description': 'Mechanical Extract Ventilation', 'depth': None, - 'depth_unit': None, 'cost': 500, 'cost_unit': 'gbp_per_unit', 'r_value_per_mm': None, 'r_value_unit': None, - 'thermal_conductivity': None, 'thermal_conductivity_unit': None, 'link': None, - 'created_at': datetime.datetime(2023, 10, 18, 16, 39, 9, 827188), 'is_active': True, - 'prime_material_cost': None, - 'material_cost': None, 'labour_cost': None, 'labour_hours_per_unit': None, 'plant_cost': None, - 'total_cost': None, - 'notes': None}, - {'id': 1221, 'type': 'flat_roof_preparation', - 'description': 'clean surface to receive new damp-proof membrane', - 'depth': 0.0, 'depth_unit': None, 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': None, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, - 'thermal_conductivity_unit': None, - 'link': 'SPONs', 'created_at': datetime.datetime(2023, 12, 4, 20, 1, 49, 298076), 'is_active': True, - 'prime_material_cost': None, 'material_cost': 0.0, 'labour_cost': 4.36, 'labour_hours_per_unit': 0.14, - 'plant_cost': 0.0, 'total_cost': 4.36, - 'notes': 'This data is based on concrete however forms a decent baseline for a Bituminous Felt flat roof'}, - {'id': 1223, 'type': 'flat_roof_preparation', - 'description': 'One coat primer; on wood surfaces before fixing; General surfaces; over 300 mm girth', - 'depth': 0.0, 'depth_unit': None, 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': None, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, - 'thermal_conductivity_unit': None, - 'link': 'SPONs', 'created_at': datetime.datetime(2023, 12, 4, 20, 1, 49, 298076), 'is_active': True, - 'prime_material_cost': None, 'material_cost': 2.49, 'labour_cost': 1.5, 'labour_hours_per_unit': 0.08, - 'plant_cost': 0.0, 'total_cost': 3.99, 'notes': 'SPONs data gives us a baseline for a wood surface'}, - {'id': 1224, 'type': 'flat_roof_vapour_barrier', 'description': 'Visqueen High Performance Vapour Barrier', - 'depth': 0.0, 'depth_unit': None, 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': None, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, - 'thermal_conductivity_unit': None, - 'link': 'SPONs', 'created_at': datetime.datetime(2023, 12, 4, 20, 1, 49, 298076), 'is_active': True, - 'prime_material_cost': 0.58, 'material_cost': 1.21, 'labour_cost': 0.48, 'labour_hours_per_unit': 0.02, - 'plant_cost': 0.0, 'total_cost': 1.69, 'notes': None}, - {'id': 1234, 'type': 'flat_roof_waterproofing', - 'description': '20 mm thick two coat coverings; felt isolating membrane; to concrete (or ' - 'timber) base; flat or to falls or slopes not exceeding 10° from horizontal', - 'depth': 0.0, 'depth_unit': None, 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': None, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, - 'thermal_conductivity_unit': None, 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 12, 4, 20, 1, 49, 298076), 'is_active': True, - 'prime_material_cost': None, 'material_cost': 0.0, 'labour_cost': 0.0, - 'labour_hours_per_unit': 0.5, 'plant_cost': 0.0, 'total_cost': 31.13, 'notes': None} - ] - - flat_roof_floor_results = costs.flat_roof_insulation( + flat_roof_floor_results = costs.loft_and_flat_insulation( floor_area=33.5, material=flat_roof_material, - non_insulation_materials=flat_roof_non_insulation_materials ) - assert flat_roof_floor_results == {'total': 5325.327767999999, 'subtotal': 4437.773139999999, - 'vat': 887.5546279999999, 'contingency': 459.07998, - 'preliminaries': 306.05332, 'material': 1830.775, 'profit': 612.10664, - 'labour_hours': 24.79, 'labour_days': 1.549375, 'labour_cost': 186.9032} + assert flat_roof_floor_results == { + 'total': 2063.935, 'subtotal': 1719.9458333333334, 'vat': 343.9891666666665, 'labour_hours': 8, + 'labour_days': 1 + } assert costs.labour_adjustment_factor == 0.88 - # Mock property instance for regional tests - @pytest.fixture(params=[ - ("Northamptonshire", "East Midlands", 7927.44), - ("Greater London Authority", "Inner London", 10475.0), - ("Adur", "South East England", 8333.32), - ("Bournemouth", "South West England", 8452), - ("Basildon", "East of England", 7895.44), - ("Birmingham", "West Midlands", 7706.2), - ("County Durham", "North East England", 8113.96), - ("Allerdale", "North West England", 6481.68), - ("York", "Yorkshire and the Humber", 8243.6), - ("Cardiff", "Wales", 7595.32), - ("Glasgow City", "Scotland", 7871.88), - ("Belfast", "Northern Ireland", 8504.36) - ]) - def mock_property_with_region(self, request): - county, region, expected_cost = request.param - mock_property = Mock() - mock_property.data = {"county": county} - return mock_property, region, expected_cost - # Test for different wattages - @pytest.mark.parametrize("wattage, expected_cost", [ - (3000, 5945.58), - (4000, 7927.44), - (5000, 9909.3), - (6000, 11891.16), + @pytest.mark.parametrize("n_panels, expected_cost", [ + (7, 4055.0), + (10, 4540.0), + (12, 4863.0), + (15, 5707.0), ]) - def test_solar_pv_different_wattages(self, wattage, expected_cost): + def test_solar_pv_different_wattages(self, n_panels, expected_cost): mock_property = Mock() mock_property.data = {"county": "Mansfield"} costs = Costs(mock_property) - result = costs.solar_pv(wattage) - assert result['total'] == pytest.approx(expected_cost, rel=0.01) - - def test_solar_pv_regional_variation(self, mock_property_with_region): - # Test for regional cost variations - property_instance, expected_region, expected_cost = mock_property_with_region - costs = Costs(property_instance) - - assert costs.region == expected_region - - result = costs.solar_pv(4000) # Testing with a fixed wattage of 4000 + result = costs.solar_pv(n_panels) assert result['total'] == pytest.approx(expected_cost, rel=0.01) diff --git a/recommendations/tests/test_data/materials.py b/recommendations/tests/test_data/materials.py index 187d1401..194971e9 100644 --- a/recommendations/tests/test_data/materials.py +++ b/recommendations/tests/test_data/materials.py @@ -1,965 +1,327 @@ import datetime materials = [ - {'id': 17, 'type': 'mechanical_ventilation', 'description': 'Mechanical Extract Ventilation', 'depth': None, - 'depth_unit': None, 'cost': 500, 'cost_unit': 'gbp_per_unit', 'r_value_per_mm': None, 'r_value_unit': None, - 'thermal_conductivity': None, 'thermal_conductivity_unit': None, 'link': None, - 'created_at': datetime.datetime(2023, 10, 18, 16, 39, 9, 827188), 'is_active': True, 'prime_material_cost': None, - 'material_cost': None, 'labour_cost': None, 'labour_hours_per_unit': None, 'plant_cost': None, 'total_cost': None, - 'notes': None}, - {'id': 1221, 'type': 'flat_roof_preparation', 'description': 'clean surface to receive new damp-proof membrane', - 'depth': 0.0, 'depth_unit': None, 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': None, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, 'thermal_conductivity_unit': None, - 'link': 'SPONs', 'created_at': datetime.datetime(2023, 12, 4, 20, 1, 49, 298076), 'is_active': True, - 'prime_material_cost': None, 'material_cost': 0.0, 'labour_cost': 4.36, 'labour_hours_per_unit': 0.14, - 'plant_cost': 0.0, 'total_cost': 4.36, - 'notes': 'This data is based on concrete however forms a decent baseline for a Bituminous Felt flat roof'}, - {'id': 1223, 'type': 'flat_roof_preparation', - 'description': 'One coat primer; on wood surfaces before fixing; General surfaces; over 300 mm girth', - 'depth': 0.0, 'depth_unit': None, 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': None, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, 'thermal_conductivity_unit': None, - 'link': 'SPONs', 'created_at': datetime.datetime(2023, 12, 4, 20, 1, 49, 298076), 'is_active': True, - 'prime_material_cost': None, 'material_cost': 2.49, 'labour_cost': 1.5, 'labour_hours_per_unit': 0.08, - 'plant_cost': 0.0, 'total_cost': 3.99, 'notes': 'SPONs data gives us a baseline for a wood surface'}, - {'id': 1224, 'type': 'flat_roof_vapour_barrier', 'description': 'Visqueen High Performance Vapour Barrier', - 'depth': 0.0, 'depth_unit': None, 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': None, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, 'thermal_conductivity_unit': None, - 'link': 'SPONs', 'created_at': datetime.datetime(2023, 12, 4, 20, 1, 49, 298076), 'is_active': True, - 'prime_material_cost': 0.58, 'material_cost': 1.21, 'labour_cost': 0.48, 'labour_hours_per_unit': 0.02, - 'plant_cost': 0.0, 'total_cost': 1.69, 'notes': None}, {'id': 1225, 'type': 'flat_roof_insulation', - 'description': 'Kingspan Thermaroof TR21 zero OPD ' - 'urethene insulation board', - 'depth': 100.0, 'depth_unit': 'mm', 'cost': None, - 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.04, - 'r_value_unit': 'square_meter_kelvin_per_watt', - 'thermal_conductivity': 0.025, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', - 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 12, 4, 20, 1, 49, - 298076), 'is_active': True, - 'prime_material_cost': None, 'material_cost': 50.95, - 'labour_cost': 10.66, 'labour_hours_per_unit': 0.48, - 'plant_cost': 0.0, 'total_cost': 61.61, - 'notes': "SPONs didn't have a labour hours so we use " - "0.48 which is similar to other materials"}, - {'id': 1226, 'type': 'flat_roof_insulation', 'description': 'Ravatherm XPS × 500 SL', 'depth': 100.0, - 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.03125, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.032, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 12, 4, 20, 1, 49, 298076), 'is_active': True, 'prime_material_cost': None, - 'material_cost': 22.14, 'labour_cost': 10.66, 'labour_hours_per_unit': 0.48, 'plant_cost': 0.0, 'total_cost': 32.8, - 'notes': None}, - {'id': 1227, 'type': 'flat_roof_insulation', 'description': 'Ravatherm XPS × 500 SL', 'depth': 120.0, - 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.03125, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.032, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', - 'link': 'https://www.panelsystems.co.uk/product/floormate-ravatherm-sb?attribute_pa_group=floormate-500a' - '&attribute_pa_product-name=ravatherm-xps-x-500-sl&attribute_pa_length=1250&attribute_pa_width=600' - '&attribute_pa_thickness=120&attribute_pa_unit-of-sale=pack-3-brds&attribute_pa_min-order-qty=10&gclid' - '=CjwKCAiAjrarBhAWEiwA2qWdCKJK2iqlzUZ-mBFOfCLy2f5TldAbOj7G3LrvYw5JLaigplJAajzYpRoCtB8QAvD_BwE', - 'created_at': datetime.datetime(2023, 12, 4, 20, 1, 49, 298076), 'is_active': True, 'prime_material_cost': None, - 'material_cost': 26.187656, 'labour_cost': 10.66, 'labour_hours_per_unit': 0.48, 'plant_cost': 0.0, - 'total_cost': 36.847656, - 'notes': "SPONs didn't have this thickness, so the material price is based on the fact that on the link, " - "the 120mm thickness is 18% more expensive per board than the 100mm thickness"}, - {'id': 1228, 'type': 'flat_roof_insulation', 'description': 'Ravatherm XPS × 500 SL', 'depth': 140.0, - 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.03125, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.032, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', - 'link': 'https://www.panelsystems.co.uk/product/floormate-ravatherm-sb?attribute_pa_group=floormate-500a' - '&attribute_pa_product-name=ravatherm-xps-x-500-sl&attribute_pa_length=1250&attribute_pa_width=600' - '&attribute_pa_thickness=120&attribute_pa_unit-of-sale=pack-3-brds&attribute_pa_min-order-qty=10&gclid' - '=CjwKCAiAjrarBhAWEiwA2qWdCKJK2iqlzUZ-mBFOfCLy2f5TldAbOj7G3LrvYw5JLaigplJAajzYpRoCtB8QAvD_BwE', - 'created_at': datetime.datetime(2023, 12, 4, 20, 1, 49, 298076), 'is_active': True, 'prime_material_cost': None, - 'material_cost': 31.114737, 'labour_cost': 10.66, 'labour_hours_per_unit': 0.48, 'plant_cost': 0.0, - 'total_cost': 41.77474, - 'notes': "SPONs didn't have this thickness, so the material price is based on the fact that on the link, " - "the 140mm thickness is 40% more expensive per board than the 100mm thickness"}, - {'id': 1229, 'type': 'flat_roof_insulation', 'description': 'Foamglas T3+ Flat Roof Insulation', 'depth': 100.0, - 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.027777778, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.036, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 12, 4, 20, 1, 49, 298076), 'is_active': True, 'prime_material_cost': 95.83, - 'material_cost': 109.09, 'labour_cost': 30.7, 'labour_hours_per_unit': 1.3, 'plant_cost': 0.0, - 'total_cost': 139.79, 'notes': None}, - {'id': 1230, 'type': 'flat_roof_insulation', 'description': 'Foamglas T4+ Flat Roof Insulation', 'depth': 100.0, - 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.024390243, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.041, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 12, 4, 20, 1, 49, 298076), 'is_active': True, 'prime_material_cost': 63.89, - 'material_cost': 76.19, 'labour_cost': 28.34, 'labour_hours_per_unit': 1.2, 'plant_cost': 0.0, - 'total_cost': 104.53, 'notes': None}, - {'id': 1231, 'type': 'flat_roof_insulation', 'description': 'Ecotherm Eco-Versal General Purpose Insulation Board', - 'depth': 100.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 12, 4, 20, 1, 49, 298076), 'is_active': True, 'prime_material_cost': 15.12, - 'material_cost': 25.96, 'labour_cost': 30.7, 'labour_hours_per_unit': 1.3, 'plant_cost': 0.0, 'total_cost': 56.66, - 'notes': None}, - {'id': 1232, 'type': 'flat_roof_insulation', 'description': 'Ecotherm Eco-Versal General Purpose Insulation Board', - 'depth': 120.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 12, 4, 20, 1, 49, 298076), 'is_active': True, 'prime_material_cost': 20.16, - 'material_cost': 34.613335, 'labour_cost': 30.7, 'labour_hours_per_unit': 1.3, 'plant_cost': 0.0, - 'total_cost': 65.31333, - 'notes': "SPONs didn't have this thickness, so the material price is based on the fact that on the link, " - "the 120mm thickness is 33% more expensive than the 100mm thickness"}, - {'id': 1233, 'type': 'flat_roof_insulation', 'description': 'Ecotherm Eco-Versal General Purpose Insulation Board', - 'depth': 150.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 12, 4, 20, 1, 49, 298076), 'is_active': True, 'prime_material_cost': 23.53, - 'material_cost': 34.62, 'labour_cost': 33.06, 'labour_hours_per_unit': 1.4, 'plant_cost': 0.0, 'total_cost': 67.68, - 'notes': None}, {'id': 1234, 'type': 'flat_roof_waterproofing', - 'description': '20 mm thick two coat coverings; felt isolating membrane; to concrete (or ' - 'timber) base; flat or to falls or slopes not exceeding 10° from horizontal', - 'depth': 0.0, 'depth_unit': None, 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': None, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, - 'thermal_conductivity_unit': None, 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 12, 4, 20, 1, 49, 298076), 'is_active': True, - 'prime_material_cost': None, 'material_cost': 0.0, 'labour_cost': 0.0, - 'labour_hours_per_unit': 0.5, 'plant_cost': 0.0, 'total_cost': 31.13, 'notes': None}, - {'id': 1109, 'type': 'cavity_wall_insulation', 'description': 'Expanded Polystyrene Beads cavity wall insulation', - 'depth': 75.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.030303031, + {'id': 1997, 'type': 'cavity_wall_insulation', 'description': 'Imperial Bead cavity wall insulation', 'depth': 75.0, + 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.030303031, 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.033, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', - 'link': 'https://www.styrene.co.uk/downloads/Datasheets/Stylite_Cavity_Loose_Fill_Insulation_Datasheet_v20211.pdf', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None, - 'material_cost': 18.875, 'labour_cost': 1.125, 'labour_hours_per_unit': 0.065, 'plant_cost': 0.0, - 'total_cost': 20.0, - 'notes': "It is hard to find materials online. To price this, we've used this article: " - "https://www.greenmatch.co.uk/blog/cavity-wall-insulation-cost It puts EPS beads at around £22 per " - "meter squared, blowing wool insulation at £18 per meter squared and Polyurethane Foam at £26 per meter " - "squared, when taking the most pessimistic prices. These rates have been used to adjust the price of " - "the mineral wool insulation to give us the other forms of insulation"}, - {'id': 1110, 'type': 'cavity_wall_insulation', 'description': 'Injected Polyurthane Foam cavity wall insulation', - 'depth': 75.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.030303031, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.033, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', - 'link': 'https://www.foaminstall.co.uk/wp-content/uploads/2017/04/Lapolla-Cavity-Fill-BBA-certificate-sheet1.pdf', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None, - 'material_cost': 22.875, 'labour_cost': 1.125, 'labour_hours_per_unit': 0.065, 'plant_cost': 0.0, - 'total_cost': 24.0, 'notes': None}, - {'id': 1111, 'type': 'loft_insulation', 'description': 'Crown Loft Roll 44 glass fibre roll', 'depth': 100.0, - 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.022727273, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.044, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 2.03, - 'material_cost': 2.1, 'labour_cost': 1.56, 'labour_hours_per_unit': 0.09, 'plant_cost': 0.0, 'total_cost': 3.66, - 'notes': None}, - {'id': 1112, 'type': 'loft_insulation', 'description': 'Crown Loft Roll 44 glass fibre roll', 'depth': 150.0, - 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.022727273, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.044, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 3.06, - 'material_cost': 3.16, 'labour_cost': 1.78, 'labour_hours_per_unit': 0.1, 'plant_cost': 0.0, 'total_cost': 4.94, - 'notes': None}, - {'id': 1113, 'type': 'loft_insulation', 'description': 'Crown Loft Roll 44 glass fibre roll', 'depth': 170.0, - 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.022727273, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.044, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', - 'link': 'https://insulation4less.co.uk/products/knauf-170mm-combi-cut?variant=31671561257013&dfw_tracker=77750' - '-31671561257013&utm_source=google&utm_medium=shopping&utm_campaign=shoptimised&gad_source=1&gclid' - '=CjwKCAiAx_GqBhBQEiwAlDNAZi1LiTWKVn0W1vktOYAPPQU3hss5Tq2qNn6GNhodCQoRD_tvqCLdxhoCKnIQAvD_BwE', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None, - 'material_cost': 3.81938, 'labour_cost': 1.71304, 'labour_hours_per_unit': 0.11, 'plant_cost': 0.0, - 'total_cost': 5.53242, - 'notes': "We don't have a 170mm in SPONs so the material cost is based on the fact that the 170mm insulation is " - "87.4% of the cost of the 200mm insulation"}, - {'id': 1114, 'type': 'loft_insulation', 'description': 'Crown Loft Roll 44 glass fibre roll', 'depth': 200.0, - 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.022727273, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.044, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 4.25, - 'material_cost': 4.37, 'labour_cost': 1.96, 'labour_hours_per_unit': 0.11, 'plant_cost': 0.0, 'total_cost': 6.33, - 'notes': None}, - {'id': 1115, 'type': 'loft_insulation', 'description': 'Crown Loft Roll 44 glass fibre roll', 'depth': 270.0, - 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.022727273, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.044, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None, - 'material_cost': 5.91938, 'labour_cost': 1.96, 'labour_hours_per_unit': 0.11, 'plant_cost': 0.0, - 'total_cost': 7.87938, 'notes': 'This is the 100mm product + the 170mm product'}, - {'id': 1116, 'type': 'loft_insulation', 'description': 'Crown Loft Roll 44 glass fibre roll', 'depth': 300.0, - 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.022727273, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.044, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None, - 'material_cost': 6.47, 'labour_cost': 1.96, 'labour_hours_per_unit': 0.11, 'plant_cost': 0.0, 'total_cost': 8.43, - 'notes': 'This is the 100mm product + the 200mm product'}, - {'id': 1117, 'type': 'loft_insulation', 'description': 'Isover Mineral Wool Modular Roll', 'depth': 100.0, - 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.023255814, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.043, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 1.99, - 'material_cost': 2.05, 'labour_cost': 1.6, 'labour_hours_per_unit': 0.09, 'plant_cost': 0.0, 'total_cost': 3.65, - 'notes': None}, - {'id': 1118, 'type': 'loft_insulation', 'description': 'Isover Mineral Wool Modular Roll', 'depth': 150.0, - 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.023255814, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.043, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 2.96, - 'material_cost': 3.05, 'labour_cost': 1.78, 'labour_hours_per_unit': 0.1, 'plant_cost': 0.0, 'total_cost': 4.83, - 'notes': None}, - {'id': 1119, 'type': 'loft_insulation', 'description': 'Isover Mineral Wool Modular Roll', 'depth': 170.0, - 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.023255814, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.043, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', - 'link': 'https://flooringwarehousedirect.co.uk/product/isover-spacesaver-roll-170mm-x-1160mm-x-7-03m-8-15m2/', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None, - 'material_cost': 3.8706238, 'labour_cost': 2.281361, 'labour_hours_per_unit': 0.12816635, 'plant_cost': 0.0, - 'total_cost': 6.1519847, - 'notes': "We don't have a 170mm in SPONs so the material cost is based on the fact that the 170mm insulation is " - "85.4% of the cost of the 200mm insulation"}, - {'id': 1120, 'type': 'loft_insulation', 'description': 'Isover Mineral Wool Modular Roll', 'depth': 200.0, - 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.023255814, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.043, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 4.4, - 'material_cost': 4.53, 'labour_cost': 2.67, 'labour_hours_per_unit': 0.15, 'plant_cost': 0.0, 'total_cost': 7.2, - 'notes': None}, - {'id': 1121, 'type': 'loft_insulation', 'description': 'Isover Mineral Wool Modular Roll', 'depth': 270.0, - 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.023255814, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.043, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None, - 'material_cost': 5.920624, 'labour_cost': 2.67, 'labour_hours_per_unit': 0.15, 'plant_cost': 0.0, - 'total_cost': 8.590624, 'notes': 'This is the 100mm product + the 170mm product'}, - {'id': 1122, 'type': 'loft_insulation', 'description': 'Isover Mineral Wool Modular Roll', 'depth': 300.0, - 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.023255814, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.043, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None, - 'material_cost': 6.58, 'labour_cost': 2.67, 'labour_hours_per_unit': 0.15, 'plant_cost': 0.0, 'total_cost': 9.25, - 'notes': 'This is the 100mm product + the 200mm product'}, - {'id': 1123, 'type': 'loft_insulation', 'description': 'Isover Acoustic Partition Roll', 'depth': 100.0, - 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.023255814, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.043, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 5.93, - 'material_cost': 6.4, 'labour_cost': 2.67, 'labour_hours_per_unit': 0.15, 'plant_cost': 0.0, 'total_cost': 9.07, - 'notes': 'This provides acoustic insulation as well'}, - {'id': 1124, 'type': 'loft_insulation', 'description': 'Isover Acoustic Partition Roll', 'depth': 300.0, - 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.023255814, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.043, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 17.79, - 'material_cost': 19.2, 'labour_cost': 2.67, 'labour_hours_per_unit': 0.15, 'plant_cost': 0.0, 'total_cost': 21.87, - 'notes': 'This provides acoustic insulation as well'}, - {'id': 1125, 'type': 'loft_insulation', 'description': 'Thermafleece EcoRoll Insulation', 'depth': 300.0, - 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.025641026, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.039, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None, - 'material_cost': 24.78, 'labour_cost': 2.67, 'labour_hours_per_unit': 0.15, 'plant_cost': 0.0, 'total_cost': 27.45, - 'notes': 'This material is based on installing 3 layers of the 100mm product'}, - {'id': 1126, 'type': 'loft_insulation', 'description': 'Thermafleece EcoRoll Insulation', 'depth': 280.0, - 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.025641026, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.039, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None, - 'material_cost': 23.36, 'labour_cost': 3.12, 'labour_hours_per_unit': 0.18, 'plant_cost': 0.0, 'total_cost': 26.48, - 'notes': 'This material is based on installed 2 layers of the 140mm product'}, - {'id': 1127, 'type': 'iwi_wall_demolition', - 'description': 'Solid & Dry Lined walls: Hack of wall finishes with chipping hammer; plaster to walls.', - 'depth': 0.0, 'depth_unit': None, 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': None, + 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SCIS', + 'created_at': datetime.datetime(2024, 9, 24, 13, 42, 52, 584553), 'is_active': True, 'prime_material_cost': None, + 'material_cost': 0.0, 'labour_cost': 0.0, 'labour_hours_per_unit': 0.0, 'plant_cost': 0.0, 'total_cost': 14.21, + 'notes': None, 'is_installer_quote': True}, + {'id': 1998, 'type': 'mechanical_ventilation', 'description': 'Mechanical Extract Ventilation', 'depth': 0.0, + 'depth_unit': None, 'cost': None, 'cost_unit': 'gbp_per_unit', 'r_value_per_mm': None, 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, 'thermal_conductivity_unit': None, - 'link': 'SPONs', 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, - 'prime_material_cost': None, 'material_cost': 0.0, 'labour_cost': 10.27, 'labour_hours_per_unit': 0.33, - 'plant_cost': 1.28, 'total_cost': 11.55, 'notes': None}, {'id': 1128, 'type': 'iwi_wall_demolition', - 'description': 'Stud walls: Remove wall linings ' - 'including battening behind; ' - 'plasterboard and skim', - 'depth': 0.0, 'depth_unit': None, 'cost': None, - 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': None, - 'r_value_unit': 'square_meter_kelvin_per_watt', - 'thermal_conductivity': None, - 'thermal_conductivity_unit': None, 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, - 244907), - 'is_active': True, 'prime_material_cost': None, - 'material_cost': 0.0, 'labour_cost': 6.23, - 'labour_hours_per_unit': 0.2, 'plant_cost': 1.25, - 'total_cost': 7.48, 'notes': None}, - {'id': 1129, 'type': 'iwi_wall_demolition', - 'description': 'Lathe and Plaster walls: Remove wall linings including battening behind; wood lath and plaster', - 'depth': 0.0, 'depth_unit': None, 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': None, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, 'thermal_conductivity_unit': None, - 'link': 'SPONs', 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, - 'prime_material_cost': None, 'material_cost': 0.0, 'labour_cost': 6.85, 'labour_hours_per_unit': 0.22, - 'plant_cost': 2.09, 'total_cost': 8.94, 'notes': None}, - {'id': 1130, 'type': 'internal_wall_insulation', 'description': 'Foamglas Grade F Wall Insulation Slabs', - 'depth': 60.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.02631579, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.038, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 41.69, - 'material_cost': 53.33, 'labour_cost': 29.52, 'labour_hours_per_unit': 1.25, 'plant_cost': 0.0, - 'total_cost': 82.85, 'notes': None}, - {'id': 1131, 'type': 'internal_wall_insulation', 'description': 'Foamglas Grade F Wall Insulation Slabs', - 'depth': 100.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.02631579, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.038, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 86.86, - 'material_cost': 99.85, 'labour_cost': 29.52, 'labour_hours_per_unit': 1.25, 'plant_cost': 0.0, - 'total_cost': 129.37, 'notes': None}, - {'id': 1132, 'type': 'internal_wall_insulation', 'description': 'Foamglas Grade F Wall Insulation Slabs', - 'depth': 150.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.02631579, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.038, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, - 'prime_material_cost': 130.29, 'material_cost': 144.58, 'labour_cost': 29.52, 'labour_hours_per_unit': 1.25, - 'plant_cost': 0.0, 'total_cost': 174.1, 'notes': None}, - {'id': 1133, 'type': 'internal_wall_insulation', 'description': 'Ecotherm Eco-Versal PIR Insulation Board', - 'depth': 30.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547, + 'link': 'SCIS', 'created_at': datetime.datetime(2024, 9, 24, 13, 42, 52, 584553), 'is_active': True, + 'prime_material_cost': None, 'material_cost': 0.0, 'labour_cost': 0.0, 'labour_hours_per_unit': 0.0, + 'plant_cost': 0.0, 'total_cost': 535.5, 'notes': None, 'is_installer_quote': True}, + {'id': 2015, 'type': 'loft_insulation', 'description': 'Knauf Loft Roll 44 glass fibre roll', 'depth': 100.0, + 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.022727273, + 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.044, + 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SCIS', + 'created_at': datetime.datetime(2024, 9, 24, 13, 42, 52, 584553), 'is_active': True, 'prime_material_cost': None, + 'material_cost': 0.0, 'labour_cost': 0.0, 'labour_hours_per_unit': 0.09, 'plant_cost': 0.0, 'total_cost': 14.95, + 'notes': 'This is a placeholder cost until SCIS gives us a breakdown by thickness', 'is_installer_quote': True}, + {'id': 2016, 'type': 'loft_insulation', 'description': 'Knauf Loft Roll 44 glass fibre roll', 'depth': 200.0, + 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.022727273, + 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.044, + 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SCIS', + 'created_at': datetime.datetime(2024, 9, 24, 13, 42, 52, 584553), 'is_active': True, 'prime_material_cost': None, + 'material_cost': 0.0, 'labour_cost': 0.0, 'labour_hours_per_unit': 0.11, 'plant_cost': 0.0, 'total_cost': 15.525, + 'notes': 'This is a placeholder cost until SCIS gives us a breakdown by thickness', 'is_installer_quote': True}, + {'id': 2017, 'type': 'loft_insulation', 'description': 'Knauf Loft Roll 44 glass fibre roll', 'depth': 270.0, + 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.022727273, + 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.044, + 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SCIS', + 'created_at': datetime.datetime(2024, 9, 24, 13, 42, 52, 584553), 'is_active': True, 'prime_material_cost': None, + 'material_cost': 0.0, 'labour_cost': 0.0, 'labour_hours_per_unit': 0.11, 'plant_cost': 0.0, 'total_cost': 16.1, + 'notes': 'This is a placeholder cost until SCIS gives us a breakdown by thickness', 'is_installer_quote': True}, + {'id': 2018, 'type': 'loft_insulation', 'description': 'Knauf Loft Roll 44 glass fibre roll', 'depth': 300.0, + 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.022727273, + 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.044, + 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SCIS', + 'created_at': datetime.datetime(2024, 9, 24, 13, 42, 52, 584553), 'is_active': True, 'prime_material_cost': None, + 'material_cost': 0.0, 'labour_cost': 0.0, 'labour_hours_per_unit': 0.11, 'plant_cost': 0.0, 'total_cost': 16.53, + 'notes': 'This is a placeholder cost until SCIS gives us a breakdown by thickness', 'is_installer_quote': True}, + {'id': 2039, 'type': 'internal_wall_insulation', 'description': 'SWIP EcoBatt', 'depth': 95.0, 'depth_unit': 'mm', + 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.03125, 'r_value_unit': 'square_meter_kelvin_per_watt', + 'thermal_conductivity': 0.032, 'thermal_conductivity_unit': None, 'link': 'SCIS', + 'created_at': datetime.datetime(2024, 9, 24, 13, 42, 52, 584553), 'is_active': True, 'prime_material_cost': None, + 'material_cost': 0.0, 'labour_cost': 0.0, 'labour_hours_per_unit': 2.1, 'plant_cost': 0.0, 'total_cost': 244.8, + 'notes': 'We are awaiting further breakdown of costs by thickness and finishes', 'is_installer_quote': False}, + {'id': 2074, 'type': 'suspended_floor_insulation', 'description': 'Q-bot underfloor insulation', 'depth': 50.0, + 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547, 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 6.16, - 'material_cost': 16.73, 'labour_cost': 28.34, 'labour_hours_per_unit': 1.2, 'plant_cost': 0.0, 'total_cost': 45.07, - 'notes': None}, - {'id': 1134, 'type': 'internal_wall_insulation', 'description': 'Ecotherm Eco-Versal PIR Insulation Board', - 'depth': 50.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547, + 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SCIS', + 'created_at': datetime.datetime(2024, 9, 24, 13, 42, 52, 584553), 'is_active': True, 'prime_material_cost': None, + 'material_cost': 0.0, 'labour_cost': 0.0, 'labour_hours_per_unit': 1.63, 'plant_cost': 0.0, 'total_cost': 75.0, + 'notes': 'Linearly interpolated based on Qbot costs', 'is_installer_quote': True}, + {'id': 2075, 'type': 'suspended_floor_insulation', 'description': 'Q-bot underfloor insulation', 'depth': 75.0, + 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547, 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 8.46, - 'material_cost': 19.1, 'labour_cost': 28.34, 'labour_hours_per_unit': 1.2, 'plant_cost': 0.0, 'total_cost': 47.44, - 'notes': None}, - {'id': 1135, 'type': 'internal_wall_insulation', 'description': 'Ecotherm Eco-Versal PIR Insulation Board', - 'depth': 100.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547, + 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SCIS', + 'created_at': datetime.datetime(2024, 9, 24, 13, 42, 52, 584553), 'is_active': True, 'prime_material_cost': None, + 'material_cost': 0.0, 'labour_cost': 0.0, 'labour_hours_per_unit': 1.63, 'plant_cost': 0.0, 'total_cost': 93.75, + 'notes': 'Linearly interpolated based on Qbot costs', 'is_installer_quote': True}, + {'id': 2076, 'type': 'suspended_floor_insulation', 'description': 'Q-bot underfloor insulation', 'depth': 100.0, + 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547, 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 15.12, - 'material_cost': 25.96, 'labour_cost': 30.7, 'labour_hours_per_unit': 1.3, 'plant_cost': 0.0, 'total_cost': 56.66, - 'notes': None}, - {'id': 1136, 'type': 'internal_wall_insulation', 'description': 'Kingspan Kooltherm K18 insulated plasterboard', - 'depth': 37.5, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.04761905, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.021, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None, - 'material_cost': 26.86, 'labour_cost': 5.21, 'labour_hours_per_unit': 0.23, 'plant_cost': 0.0, 'total_cost': 32.07, - 'notes': None}, - {'id': 1137, 'type': 'internal_wall_insulation', 'description': 'Kingspan Kooltherm K18 insulated plasterboard', - 'depth': 42.5, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.04761905, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.021, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None, - 'material_cost': 17.37, 'labour_cost': 5.21, 'labour_hours_per_unit': 0.23, 'plant_cost': 0.0, 'total_cost': 22.58, - 'notes': None}, - {'id': 1138, 'type': 'internal_wall_insulation', 'description': 'Kingspan Kooltherm K18 insulated plasterboard', - 'depth': 52.5, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.04761905, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.021, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None, - 'material_cost': 21.74, 'labour_cost': 5.79, 'labour_hours_per_unit': 0.25, 'plant_cost': 0.0, 'total_cost': 27.53, - 'notes': None}, - {'id': 1139, 'type': 'internal_wall_insulation', 'description': 'Kingspan Kooltherm K18 insulated plasterboard', - 'depth': 62.5, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.04761905, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.021, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None, - 'material_cost': 19.3, 'labour_cost': 5.79, 'labour_hours_per_unit': 0.25, 'plant_cost': 0.0, 'total_cost': 25.09, - 'notes': None}, - {'id': 1140, 'type': 'internal_wall_insulation', 'description': 'Kingspan Kooltherm K18 insulated plasterboard', - 'depth': 72.5, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.04761905, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.021, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None, - 'material_cost': 23.15, 'labour_cost': 5.79, 'labour_hours_per_unit': 0.25, 'plant_cost': 0.0, 'total_cost': 28.94, - 'notes': None}, - {'id': 1141, 'type': 'iwi_vapour_barrier', 'description': 'Visqueen High Performance Vapour Barrier', 'depth': 0.0, + 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SCIS', + 'created_at': datetime.datetime(2024, 9, 24, 13, 42, 52, 584553), 'is_active': True, 'prime_material_cost': None, + 'material_cost': 0.0, 'labour_cost': 0.0, 'labour_hours_per_unit': 1.63, 'plant_cost': 0.0, 'total_cost': 112.5, + 'notes': 'Linearly interpolated based on Qbot costs', 'is_installer_quote': True}, + {'id': 2077, 'type': 'suspended_floor_insulation', 'description': 'Q-bot underfloor insulation', 'depth': 125.0, + 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547, + 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022, + 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SCIS', + 'created_at': datetime.datetime(2024, 9, 24, 13, 42, 52, 584553), 'is_active': True, 'prime_material_cost': None, + 'material_cost': 0.0, 'labour_cost': 0.0, 'labour_hours_per_unit': 1.63, 'plant_cost': 0.0, 'total_cost': 112.5, + 'notes': 'Linearly interpolated based on Qbot costs', 'is_installer_quote': True}, + {'id': 2078, 'type': 'suspended_floor_insulation', 'description': 'Q-bot underfloor insulation', 'depth': 150.0, + 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547, + 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022, + 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SCIS', + 'created_at': datetime.datetime(2024, 9, 24, 13, 42, 52, 584553), 'is_active': True, 'prime_material_cost': None, + 'material_cost': 0.0, 'labour_cost': 0.0, 'labour_hours_per_unit': 1.63, 'plant_cost': 0.0, 'total_cost': 150.0, + 'notes': 'Linearly interpolated based on Qbot costs', 'is_installer_quote': True}, + {'id': 2079, 'type': 'solid_floor_demolition', 'description': 'Removal of carpet and underfelt', 'depth': 0.0, 'depth_unit': None, 'cost': None, 'cost_unit': None, 'r_value_per_mm': None, 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, 'thermal_conductivity_unit': None, - 'link': 'SPONs', 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, - 'prime_material_cost': 0.58, 'material_cost': 1.21, 'labour_cost': 0.48, 'labour_hours_per_unit': 0.02, - 'plant_cost': 0.0, 'total_cost': 1.69, 'notes': None}, {'id': 1142, 'type': 'iwi_redecoration', - 'description': 'Plaster; one coat Thistle board finish ' - 'or other equal; steel trowelled; 3 mm ' - 'thick work to walls or ceilings; one ' - 'coat; to plasterboard base; over 600mm ' - 'wide', - 'depth': 0.0, 'depth_unit': None, 'cost': None, - 'cost_unit': None, 'r_value_per_mm': None, - 'r_value_unit': 'square_meter_kelvin_per_watt', - 'thermal_conductivity': None, - 'thermal_conductivity_unit': None, 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, - 244907), 'is_active': True, - 'prime_material_cost': None, 'material_cost': 0.06, - 'labour_cost': 6.58, 'labour_hours_per_unit': 0.25, - 'plant_cost': 0.0, 'total_cost': 6.64, 'notes': None}, - {'id': 1143, 'type': 'iwi_redecoration', - 'description': 'Two coats emulsion paint on plaster, over 40mm girth; 3.5m - 5m high', 'depth': 0.0, - 'depth_unit': None, 'cost': None, 'cost_unit': None, 'r_value_per_mm': None, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, 'thermal_conductivity_unit': None, - 'link': 'SPONs', 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, - 'prime_material_cost': None, 'material_cost': 0.41, 'labour_cost': 3.93, 'labour_hours_per_unit': 0.21, - 'plant_cost': 0.0, 'total_cost': 4.34, 'notes': None}, {'id': 1144, 'type': 'iwi_redecoration', - 'description': 'Fitting existing softwood skirting or ' - 'architrave to new frames; 150mm high', - 'depth': 0.0, 'depth_unit': None, 'cost': None, - 'cost_unit': None, 'r_value_per_mm': None, - 'r_value_unit': 'square_meter_kelvin_per_watt', - 'thermal_conductivity': None, - 'thermal_conductivity_unit': None, 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, - 244907), 'is_active': True, - 'prime_material_cost': None, 'material_cost': 0.01, - 'labour_cost': 4.87, 'labour_hours_per_unit': 0.12, - 'plant_cost': 0.0, 'total_cost': 4.88, 'notes': None}, - {'id': 1145, 'type': 'suspended_floor_demolition', 'description': 'Removal of carpet and underfelt', 'depth': 0.0, - 'depth_unit': None, 'cost': None, 'cost_unit': None, 'r_value_per_mm': None, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, 'thermal_conductivity_unit': None, - 'link': 'SPONs', 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, + 'link': 'SPONs', 'created_at': datetime.datetime(2024, 9, 24, 13, 42, 52, 584553), 'is_active': True, 'prime_material_cost': None, 'material_cost': 0.0, 'labour_cost': 3.32, 'labour_hours_per_unit': 0.11, 'plant_cost': 0.0, 'total_cost': 3.32, 'notes': 'We ignore the plant cost that is in SPONs because we assume the carpet is not scrapped and therefore ' - 'there is no need for a skip'}, - {'id': 1146, 'type': 'suspended_floor_demolition', - 'description': 'Remove boarding; withdraw nails; set aside for reuse; ground level', 'depth': 0.0, - 'depth_unit': None, 'cost': None, 'cost_unit': None, 'r_value_per_mm': None, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, 'thermal_conductivity_unit': None, - 'link': 'SPONs', 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, - 'prime_material_cost': None, 'material_cost': 0.0, 'labour_cost': 9.34, 'labour_hours_per_unit': 0.3, - 'plant_cost': 0.0, 'total_cost': 9.34, 'notes': None}, - {'id': 1147, 'type': 'suspended_floor_vapour_barrier', 'description': 'Visqueen High Performance Vapour Barrier', + 'there is no need for a skip', + 'is_installer_quote': False}, {'id': 2080, 'type': 'solid_floor_preparation', + 'description': 'clean surface of concrete to receive new damp-proof membrane', + 'depth': 0.0, 'depth_unit': None, 'cost': None, 'cost_unit': None, + 'r_value_per_mm': None, 'r_value_unit': 'square_meter_kelvin_per_watt', + 'thermal_conductivity': None, 'thermal_conductivity_unit': None, 'link': None, + 'created_at': datetime.datetime(2024, 9, 24, 13, 42, 52, 584553), 'is_active': True, + 'prime_material_cost': None, 'material_cost': 0.0, 'labour_cost': 4.36, + 'labour_hours_per_unit': 0.14, 'plant_cost': 0.0, 'total_cost': 4.36, 'notes': None, + 'is_installer_quote': False}, {'id': 2081, 'type': 'solid_floor_preparation', + 'description': 'Clean out crack to form a ' + '20mm×20mm groove and fill with ' + 'cement: mortar mixed with bonding ' + 'agent', + 'depth': 0.0, 'depth_unit': None, 'cost': None, + 'cost_unit': None, 'r_value_per_mm': None, + 'r_value_unit': 'square_meter_kelvin_per_watt', + 'thermal_conductivity': None, + 'thermal_conductivity_unit': None, 'link': None, + 'created_at': datetime.datetime(2024, 9, 24, 13, 42, + 52, 584553), + 'is_active': True, 'prime_material_cost': None, + 'material_cost': 6.91, 'labour_cost': 18.99, + 'labour_hours_per_unit': 0.61, 'plant_cost': 0.16, + 'total_cost': 26.06, + 'notes': 'This step is the assessment and repair ' + 'of any damage to the concrete floor such ' + 'as filling cracks or levelling uneven ' + 'areas', + 'is_installer_quote': False}, + {'id': 2082, 'type': 'solid_floor_vapour_barrier', 'description': 'Visqueen High Performance Vapour Barrier', 'depth': 0.0, 'depth_unit': None, 'cost': None, 'cost_unit': None, 'r_value_per_mm': None, 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, 'thermal_conductivity_unit': None, - 'link': 'SPONs', 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, + 'link': 'SPONs', 'created_at': datetime.datetime(2024, 9, 24, 13, 42, 52, 584553), 'is_active': True, 'prime_material_cost': 0.58, 'material_cost': 1.21, 'labour_cost': 0.48, 'labour_hours_per_unit': 0.02, - 'plant_cost': 0.0, 'total_cost': 1.69, 'notes': None}, - {'id': 1148, 'type': 'suspended_floor_insulation', 'description': 'Thermafleece CosyWool Roll', 'depth': 50.0, - 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.025641026, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.039, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None, - 'material_cost': 4.24, 'labour_cost': 1.56, 'labour_hours_per_unit': 0.09, 'plant_cost': 0.0, 'total_cost': 5.8, - 'notes': 'Spons did not contain labour costs so we use values for similar insulations. We use the same values as ' - 'in Crown loft roll 44, since it is also an insulation roll'}, - {'id': 1149, 'type': 'suspended_floor_insulation', 'description': 'Thermafleece CosyWool Roll', 'depth': 75.0, - 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.025641026, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.039, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None, - 'material_cost': 6.31, 'labour_cost': 1.56, 'labour_hours_per_unit': 0.09, 'plant_cost': 0.0, 'total_cost': 7.87, - 'notes': 'Spons did not contain labour costs so we use values for similar insulations. We use the same values as ' - 'in Crown loft roll 44, since it is also an insulation roll'}, - {'id': 1150, 'type': 'suspended_floor_insulation', 'description': 'Thermafleece CosyWool Roll', 'depth': 100.0, - 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.025641026, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.039, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None, - 'material_cost': 8.26, 'labour_cost': 1.56, 'labour_hours_per_unit': 0.1, 'plant_cost': 0.0, 'total_cost': 9.82, - 'notes': 'Spons did not contain labour costs so we use values for similar insulations. We use the same values as ' - 'in Crown loft roll 44, since it is also an insulation roll'}, - {'id': 1151, 'type': 'suspended_floor_insulation', 'description': 'Thermafleece CosyWool Roll', 'depth': 140.0, - 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.025641026, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.039, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None, - 'material_cost': 11.68, 'labour_cost': 1.78, 'labour_hours_per_unit': 0.1, 'plant_cost': 0.0, 'total_cost': 13.46, - 'notes': 'Spons did not contain labour costs so we use values for similar insulations. We use the same values as ' - 'in Crown loft roll 44, since it is also an insulation roll'}, - {'id': 1152, 'type': 'suspended_floor_insulation', - 'description': 'Thermafleece TF35 high density wool insulating batts', 'depth': 50.0, 'depth_unit': 'mm', - 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.028571429, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.035, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None, - 'material_cost': 6.63, 'labour_cost': 1.56, 'labour_hours_per_unit': 0.09, 'plant_cost': 0.0, 'total_cost': 8.19, - 'notes': 'Spons did not contain labour costs so we use values for similar insulations. We use the same values as ' - 'in Crown loft roll 44, since it is also an insulation roll'}, - {'id': 1153, 'type': 'suspended_floor_insulation', - 'description': 'Thermafleece TF35 high density wool insulating batts', 'depth': 75.0, 'depth_unit': 'mm', - 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.028571429, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.035, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None, - 'material_cost': 10.31, 'labour_cost': 1.56, 'labour_hours_per_unit': 0.09, 'plant_cost': 0.0, 'total_cost': 11.87, - 'notes': 'Spons did not contain labour costs so we use values for similar insulations. We use the same values as ' - 'in Crown loft roll 44, since it is also an insulation roll'}, - {'id': 1154, 'type': 'suspended_floor_insulation', - 'description': 'Ecotherm Eco-Versal General Purpose Insulation Board', 'depth': 30.0, 'depth_unit': 'mm', - 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 6.16, - 'material_cost': 16.73, 'labour_cost': 28.34, 'labour_hours_per_unit': 1.2, 'plant_cost': 0.0, 'total_cost': 45.07, - 'notes': None}, {'id': 1155, 'type': 'suspended_floor_insulation', - 'description': 'Ecotherm Eco-Versal General Purpose Insulation Board', 'depth': 50.0, - 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, - 'prime_material_cost': 8.46, 'material_cost': 19.1, 'labour_cost': 28.34, - 'labour_hours_per_unit': 1.2, 'plant_cost': 0.0, 'total_cost': 47.44, 'notes': None}, - {'id': 1156, 'type': 'suspended_floor_insulation', - 'description': 'Ecotherm Eco-Versal General Purpose Insulation Board', 'depth': 100.0, 'depth_unit': 'mm', - 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 15.12, - 'material_cost': 25.96, 'labour_cost': 30.7, 'labour_hours_per_unit': 1.3, 'plant_cost': 0.0, 'total_cost': 56.66, - 'notes': None}, {'id': 1157, 'type': 'suspended_floor_insulation', - 'description': 'Ecotherm Eco-Versal General Purpose Insulation Board', 'depth': 150.0, - 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, - 'prime_material_cost': 23.53, 'material_cost': 34.62, 'labour_cost': 33.06, - 'labour_hours_per_unit': 1.4, 'plant_cost': 0.0, 'total_cost': 67.68, 'notes': None}, - {'id': 1158, 'type': 'suspended_floor_insulation', 'description': 'Crown Loft Roll 44 glass fibre roll', - 'depth': 100.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_unit', 'r_value_per_mm': 0.022727273, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.044, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 2.03, - 'material_cost': 2.1, 'labour_cost': 1.56, 'labour_hours_per_unit': 0.09, 'plant_cost': 0.0, 'total_cost': 3.66, - 'notes': None}, - {'id': 1159, 'type': 'suspended_floor_insulation', 'description': 'Crown Loft Roll 44 glass fibre roll', - 'depth': 150.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_unit', 'r_value_per_mm': 0.022727273, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.044, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 3.06, - 'material_cost': 3.16, 'labour_cost': 1.78, 'labour_hours_per_unit': 0.1, 'plant_cost': 0.0, 'total_cost': 4.94, - 'notes': None}, - {'id': 1160, 'type': 'suspended_floor_insulation', 'description': 'Crown Loft Roll 44 glass fibre roll', - 'depth': 200.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_unit', 'r_value_per_mm': 0.022727273, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.044, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 4.25, - 'material_cost': 4.37, 'labour_cost': 1.96, 'labour_hours_per_unit': 0.11, 'plant_cost': 0.0, 'total_cost': 6.33, - 'notes': None}, - {'id': 1161, 'type': 'suspended_floor_insulation', 'description': 'Isover Mineral Wool Modular Roll', - 'depth': 100.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_unit', 'r_value_per_mm': 0.023255814, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.043, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 1.99, - 'material_cost': 2.05, 'labour_cost': 1.6, 'labour_hours_per_unit': 0.09, 'plant_cost': 0.0, 'total_cost': 3.65, - 'notes': None}, - {'id': 1162, 'type': 'suspended_floor_insulation', 'description': 'Isover Mineral Wool Modular Roll', - 'depth': 150.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_unit', 'r_value_per_mm': 0.023255814, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.043, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 2.96, - 'material_cost': 3.05, 'labour_cost': 1.78, 'labour_hours_per_unit': 0.1, 'plant_cost': 0.0, 'total_cost': 4.83, - 'notes': None}, - {'id': 1163, 'type': 'suspended_floor_insulation', 'description': 'Isover Mineral Wool Modular Roll', - 'depth': 200.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_unit', 'r_value_per_mm': 0.023255814, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.043, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 4.4, - 'material_cost': 4.53, 'labour_cost': 2.67, 'labour_hours_per_unit': 0.15, 'plant_cost': 0.0, 'total_cost': 7.2, - 'notes': None}, - {'id': 1164, 'type': 'suspended_floor_insulation', 'description': 'Isover Acoustic Partition Roll', 'depth': 25.0, - 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_unit', 'r_value_per_mm': 0.025641026, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.039, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 1.67, - 'material_cost': 2.01, 'labour_cost': 1.43, 'labour_hours_per_unit': 0.08, 'plant_cost': 0.0, 'total_cost': 3.44, - 'notes': None}, - {'id': 1165, 'type': 'suspended_floor_insulation', 'description': 'Isover Acoustic Partition Roll', 'depth': 50.0, - 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_unit', 'r_value_per_mm': 0.025641026, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.039, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 2.74, - 'material_cost': 3.11, 'labour_cost': 1.6, 'labour_hours_per_unit': 0.09, 'plant_cost': 0.0, 'total_cost': 4.71, - 'notes': None}, - {'id': 1166, 'type': 'suspended_floor_insulation', 'description': 'Isover Acoustic Partition Roll', 'depth': 75.0, - 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_unit', 'r_value_per_mm': 0.023255814, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.043, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 4.57, - 'material_cost': 5.01, 'labour_cost': 1.78, 'labour_hours_per_unit': 0.1, 'plant_cost': 0.0, 'total_cost': 6.79, - 'notes': None}, - {'id': 1167, 'type': 'suspended_floor_insulation', 'description': 'Isover Acoustic Partition Roll', 'depth': 100.0, - 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_unit', 'r_value_per_mm': 0.023255814, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.043, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 5.93, - 'material_cost': 6.4, 'labour_cost': 2.67, 'labour_hours_per_unit': 0.15, 'plant_cost': 0.0, 'total_cost': 9.07, - 'notes': None}, - {'id': 1168, 'type': 'suspended_floor_insulation', 'description': 'Kay-Cel Expanded Polystyrene Board', - 'depth': 25.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.030303031, + 'plant_cost': 0.0, 'total_cost': 1.69, 'notes': None, 'is_installer_quote': False}, + {'id': 2083, 'type': 'solid_floor_insulation', 'description': 'Kay-Cel Expanded Polystyrene Board', 'depth': 25.0, + 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.030303031, 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.033, 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None, + 'created_at': datetime.datetime(2024, 9, 24, 13, 42, 52, 584553), 'is_active': True, 'prime_material_cost': None, 'material_cost': 3.88, 'labour_cost': 3.24, 'labour_hours_per_unit': 0.14, 'plant_cost': 0.0, 'total_cost': 7.12, - 'notes': None}, - {'id': 1169, 'type': 'suspended_floor_insulation', 'description': 'Kay-Cel Expanded Polystyrene Board', - 'depth': 50.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.030303031, + 'notes': None, 'is_installer_quote': False}, + {'id': 2084, 'type': 'solid_floor_insulation', 'description': 'Kay-Cel Expanded Polystyrene Board', 'depth': 50.0, + 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.030303031, 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.033, 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None, + 'created_at': datetime.datetime(2024, 9, 24, 13, 42, 52, 584553), 'is_active': True, 'prime_material_cost': None, 'material_cost': 6.62, 'labour_cost': 3.71, 'labour_hours_per_unit': 0.16, 'plant_cost': 0.0, 'total_cost': 10.33, - 'notes': None}, - {'id': 1170, 'type': 'suspended_floor_insulation', 'description': 'Kay-Cel Expanded Polystyrene Board', - 'depth': 75.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.030303031, + 'notes': None, 'is_installer_quote': False}, + {'id': 2085, 'type': 'solid_floor_insulation', 'description': 'Kay-Cel Expanded Polystyrene Board', 'depth': 75.0, + 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.030303031, 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.033, 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None, + 'created_at': datetime.datetime(2024, 9, 24, 13, 42, 52, 584553), 'is_active': True, 'prime_material_cost': None, 'material_cost': 9.3, 'labour_cost': 4.17, 'labour_hours_per_unit': 0.18, 'plant_cost': 0.0, 'total_cost': 13.47, - 'notes': None}, - {'id': 1171, 'type': 'suspended_floor_insulation', 'description': 'Kay-Cel Expanded Polystyrene Board', - 'depth': 100.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.030303031, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.033, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None, - 'material_cost': 12.02, 'labour_cost': 4.4, 'labour_hours_per_unit': 0.19, 'plant_cost': 0.0, 'total_cost': 16.42, - 'notes': None}, {'id': 1172, 'type': 'suspended_floor_insulation', - 'description': 'Kingspan Thermafloor TF70 High Performance Rigid Floor Insulation', 'depth': 50.0, - 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, - 'prime_material_cost': None, 'material_cost': 10.36, 'labour_cost': 4.06, - 'labour_hours_per_unit': 0.18, 'plant_cost': 0.0, 'total_cost': 14.42, 'notes': None}, - {'id': 1173, 'type': 'suspended_floor_insulation', + 'notes': None, 'is_installer_quote': False}, {'id': 2086, 'type': 'solid_floor_insulation', + 'description': 'Kingspan Thermafloor TF70 High Performance Rigid ' + 'Floor Insulation', + 'depth': 50.0, 'depth_unit': 'mm', 'cost': None, + 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547, + 'r_value_unit': 'square_meter_kelvin_per_watt', + 'thermal_conductivity': 0.022, + 'thermal_conductivity_unit': 'watt_per_meter_kelvin', + 'link': 'SPONs', + 'created_at': datetime.datetime(2024, 9, 24, 13, 42, 52, 584553), + 'is_active': True, 'prime_material_cost': None, + 'material_cost': 10.36, 'labour_cost': 4.06, + 'labour_hours_per_unit': 0.18, 'plant_cost': 0.0, + 'total_cost': 14.42, 'notes': None, 'is_installer_quote': False}, + {'id': 2087, 'type': 'solid_floor_insulation', 'description': 'Kingspan Thermafloor TF70 High Performance Rigid Floor Insulation', 'depth': 75.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547, 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022, 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None, + 'created_at': datetime.datetime(2024, 9, 24, 13, 42, 52, 584553), 'is_active': True, 'prime_material_cost': None, 'material_cost': 15.35, 'labour_cost': 4.06, 'labour_hours_per_unit': 0.18, 'plant_cost': 0.0, 'total_cost': 19.41, - 'notes': None}, {'id': 1174, 'type': 'suspended_floor_insulation', - 'description': 'Kingspan Thermafloor TF70 High Performance Rigid Floor Insulation', - 'depth': 100.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', - 'r_value_per_mm': 0.045454547, 'r_value_unit': 'square_meter_kelvin_per_watt', - 'thermal_conductivity': 0.022, 'thermal_conductivity_unit': 'watt_per_meter_kelvin', - 'link': 'SPONs', 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), - 'is_active': True, 'prime_material_cost': None, 'material_cost': 19.17, 'labour_cost': 4.06, - 'labour_hours_per_unit': 0.18, 'plant_cost': 0.0, 'total_cost': 23.23, 'notes': None}, - {'id': 1175, 'type': 'suspended_floor_insulation', - 'description': 'Kingspan Thermafloor TF70 High Performance Rigid Floor Insulation', 'depth': 125.0, - 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None, - 'material_cost': 26.59, 'labour_cost': 4.06, 'labour_hours_per_unit': 0.18, 'plant_cost': 0.0, 'total_cost': 30.65, - 'notes': None}, {'id': 1176, 'type': 'suspended_floor_insulation', - 'description': 'Kingspan Thermafloor TF70 High Performance Rigid Floor Insulation', - 'depth': 150.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', - 'r_value_per_mm': 0.045454547, 'r_value_unit': 'square_meter_kelvin_per_watt', - 'thermal_conductivity': 0.022, 'thermal_conductivity_unit': 'watt_per_meter_kelvin', - 'link': 'SPONs', 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), - 'is_active': True, 'prime_material_cost': None, 'material_cost': 31.13, 'labour_cost': 4.64, - 'labour_hours_per_unit': 0.2, 'plant_cost': 0.0, 'total_cost': 35.77, 'notes': None}, - {'id': 1177, 'type': 'suspended_floor_redecoration', 'description': 'refix floorboards previously set aside', - 'depth': 0.0, 'depth_unit': None, 'cost': None, 'cost_unit': None, 'r_value_per_mm': None, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, 'thermal_conductivity_unit': None, - 'link': 'SPONs', 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, - 'prime_material_cost': None, 'material_cost': 1.54, 'labour_cost': 24.98, 'labour_hours_per_unit': 0.74, - 'plant_cost': 0.0, 'total_cost': 26.52, 'notes': None}, - {'id': 1178, 'type': 'suspended_floor_redecoration', 'description': 'Fitting carpet', 'depth': 0.0, - 'depth_unit': None, 'cost': None, 'cost_unit': None, 'r_value_per_mm': None, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, 'thermal_conductivity_unit': None, - 'link': 'SPONs', 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, - 'prime_material_cost': None, 'material_cost': 0.0, 'labour_cost': 6.59, 'labour_hours_per_unit': 0.37, - 'plant_cost': 0.0, 'total_cost': 6.59, - 'notes': 'SPONs does not have data on re-fitting the carpet so we use the data in Fitted carpeting; Gradus woven ' - 'polypropylene tufted loop\n\n as a baseline. We assume re-use of carpets, therefore we need just ' - 'labour rates'}, - {'id': 1179, 'type': 'solid_floor_demolition', 'description': 'Removal of carpet and underfelt', 'depth': 0.0, - 'depth_unit': None, 'cost': None, 'cost_unit': None, 'r_value_per_mm': None, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, 'thermal_conductivity_unit': None, - 'link': 'SPONs', 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, - 'prime_material_cost': None, 'material_cost': 0.0, 'labour_cost': 3.32, 'labour_hours_per_unit': 0.11, - 'plant_cost': 0.0, 'total_cost': 3.32, - 'notes': 'We ignore the plant cost that is in SPONs because we assume the carpet is not scrapped and therefore ' - 'there is no need for a skip'}, - {'id': 1180, 'type': 'solid_floor_preparation', - 'description': 'clean surface of concrete to receive new damp-proof membrane', 'depth': 0.0, 'depth_unit': None, - 'cost': None, 'cost_unit': None, 'r_value_per_mm': None, 'r_value_unit': 'square_meter_kelvin_per_watt', - 'thermal_conductivity': None, 'thermal_conductivity_unit': None, 'link': None, - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None, - 'material_cost': 0.0, 'labour_cost': 4.36, 'labour_hours_per_unit': 0.14, 'plant_cost': 0.0, 'total_cost': 4.36, - 'notes': None}, {'id': 1181, 'type': 'solid_floor_preparation', - 'description': 'Clean out crack to form a 20mm×20mm groove and fill with cement: mortar mixed ' - 'with bonding agent', - 'depth': 0.0, 'depth_unit': None, 'cost': None, 'cost_unit': None, 'r_value_per_mm': None, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, - 'thermal_conductivity_unit': None, 'link': None, - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, - 'prime_material_cost': None, 'material_cost': 6.91, 'labour_cost': 18.99, - 'labour_hours_per_unit': 0.61, 'plant_cost': 0.16, 'total_cost': 26.06, - 'notes': 'This step is the assessment and repair of any damage to the concrete floor such as ' - 'filling cracks or levelling uneven areas'}, - {'id': 1182, 'type': 'solid_floor_vapour_barrier', 'description': 'Visqueen High Performance Vapour Barrier', - 'depth': 0.0, 'depth_unit': None, 'cost': None, 'cost_unit': None, 'r_value_per_mm': None, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, 'thermal_conductivity_unit': None, - 'link': 'SPONs', 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, - 'prime_material_cost': 0.58, 'material_cost': 1.21, 'labour_cost': 0.48, 'labour_hours_per_unit': 0.02, - 'plant_cost': 0.0, 'total_cost': 1.69, 'notes': None}, - {'id': 1183, 'type': 'solid_floor_insulation', 'description': 'Kay-Cel Expanded Polystyrene Board', 'depth': 25.0, - 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.030303031, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.033, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None, - 'material_cost': 3.88, 'labour_cost': 3.24, 'labour_hours_per_unit': 0.14, 'plant_cost': 0.0, 'total_cost': 7.12, - 'notes': None}, - {'id': 1184, 'type': 'solid_floor_insulation', 'description': 'Kay-Cel Expanded Polystyrene Board', 'depth': 50.0, - 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.030303031, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.033, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None, - 'material_cost': 6.62, 'labour_cost': 3.71, 'labour_hours_per_unit': 0.16, 'plant_cost': 0.0, 'total_cost': 10.33, - 'notes': None}, - {'id': 1185, 'type': 'solid_floor_insulation', 'description': 'Kay-Cel Expanded Polystyrene Board', 'depth': 75.0, - 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.030303031, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.033, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None, - 'material_cost': 9.3, 'labour_cost': 4.17, 'labour_hours_per_unit': 0.18, 'plant_cost': 0.0, 'total_cost': 13.47, - 'notes': None}, {'id': 1186, 'type': 'solid_floor_insulation', - 'description': 'Kingspan Thermafloor TF70 High Performance Rigid Floor Insulation', 'depth': 50.0, - 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, - 'prime_material_cost': None, 'material_cost': 10.36, 'labour_cost': 4.06, - 'labour_hours_per_unit': 0.18, 'plant_cost': 0.0, 'total_cost': 14.42, 'notes': None}, - {'id': 1187, 'type': 'solid_floor_insulation', - 'description': 'Kingspan Thermafloor TF70 High Performance Rigid Floor Insulation', 'depth': 75.0, - 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None, - 'material_cost': 15.35, 'labour_cost': 4.06, 'labour_hours_per_unit': 0.18, 'plant_cost': 0.0, 'total_cost': 19.41, - 'notes': None}, {'id': 1188, 'type': 'solid_floor_insulation', - 'description': 'Ecotherm Eco-Versal General Purpose Insulation Board', 'depth': 30.0, - 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, - 'prime_material_cost': 6.16, 'material_cost': 16.73, 'labour_cost': 28.34, - 'labour_hours_per_unit': 1.2, 'plant_cost': 0.0, 'total_cost': 45.07, 'notes': None}, - {'id': 1189, 'type': 'solid_floor_insulation', + 'notes': None, 'is_installer_quote': False}, {'id': 2088, 'type': 'solid_floor_insulation', + 'description': 'Ecotherm Eco-Versal General Purpose Insulation ' + 'Board', + 'depth': 30.0, 'depth_unit': 'mm', 'cost': None, + 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547, + 'r_value_unit': 'square_meter_kelvin_per_watt', + 'thermal_conductivity': 0.022, + 'thermal_conductivity_unit': 'watt_per_meter_kelvin', + 'link': 'SPONs', + 'created_at': datetime.datetime(2024, 9, 24, 13, 42, 52, 584553), + 'is_active': True, 'prime_material_cost': 6.16, + 'material_cost': 16.73, 'labour_cost': 28.34, + 'labour_hours_per_unit': 1.2, 'plant_cost': 0.0, 'total_cost': 45.07, + 'notes': None, 'is_installer_quote': False}, + {'id': 2089, 'type': 'solid_floor_insulation', 'description': 'Ecotherm Eco-Versal General Purpose Insulation Board', 'depth': 50.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547, 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022, 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 8.46, + 'created_at': datetime.datetime(2024, 9, 24, 13, 42, 52, 584553), 'is_active': True, 'prime_material_cost': 8.46, 'material_cost': 19.1, 'labour_cost': 28.34, 'labour_hours_per_unit': 1.2, 'plant_cost': 0.0, 'total_cost': 47.44, - 'notes': None}, {'id': 1190, 'type': 'solid_floor_insulation', - 'description': 'Ecotherm Eco-Versal General Purpose Insulation Board', 'depth': 60.0, - 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', - 'link': 'https://londonbuildingsupplies.co.uk/products/60mm--ecotherm-eco-versal-general' - '-purpose-pir-insulation-board---2.4m-x-1.2m-x-60mm.html', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, - 'prime_material_cost': None, 'material_cost': 24.081198, 'labour_cost': 28.34, - 'labour_hours_per_unit': 1.2, 'plant_cost': 0.0, 'total_cost': 52.421196, - 'notes': "This material isn't in SPONs but checking online, is around 92% of the cost of the " - "100mm"}, - {'id': 1191, 'type': 'solid_floor_insulation', + 'notes': None, 'is_installer_quote': False}, {'id': 2090, 'type': 'solid_floor_insulation', + 'description': 'Ecotherm Eco-Versal General Purpose Insulation ' + 'Board', + 'depth': 60.0, 'depth_unit': 'mm', 'cost': None, + 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547, + 'r_value_unit': 'square_meter_kelvin_per_watt', + 'thermal_conductivity': 0.022, + 'thermal_conductivity_unit': 'watt_per_meter_kelvin', + 'link': 'https://londonbuildingsupplies.co.uk/products/60mm--ecotherm-eco-versal-general-purpose-pir-insulation-board---2.4m-x-1.2m-x-60mm.html', + 'created_at': datetime.datetime(2024, 9, 24, 13, 42, 52, 584553), + 'is_active': True, 'prime_material_cost': None, + 'material_cost': 24.081198, 'labour_cost': 28.34, + 'labour_hours_per_unit': 1.2, 'plant_cost': 0.0, + 'total_cost': 52.421196, + 'notes': "This material isn't in SPONs but checking online, " + "is around 92% of the cost of the 100mm", + 'is_installer_quote': False}, + {'id': 2091, 'type': 'solid_floor_insulation', 'description': 'Ecotherm Eco-Versal General Purpose Insulation Board', 'depth': 70.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547, 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022, 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'https://londonbuildingsupplies.co.uk/products/70mm--ecotherm-eco-versal-general-purpose-pir-insulation' '-board---2.4m-x-1.2m-x-70mm.html', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None, + 'created_at': datetime.datetime(2024, 9, 24, 13, 42, 52, 584553), 'is_active': True, 'prime_material_cost': None, 'material_cost': 27.089088, 'labour_cost': 28.34, 'labour_hours_per_unit': 1.2, 'plant_cost': 0.0, 'total_cost': 55.42909, 'notes': "This material isn't in SPONs but checking online, is around 104% of the cost of the 100mm (more " - "expensive than 100mm)"}, - {'id': 1192, 'type': 'solid_floor_insulation', - 'description': 'Ecotherm Eco-Versal General Purpose Insulation Board', 'depth': 100.0, 'depth_unit': 'mm', - 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 15.12, - 'material_cost': 25.96, 'labour_cost': 30.7, 'labour_hours_per_unit': 1.3, 'plant_cost': 0.0, 'total_cost': 56.66, - 'notes': None}, - {'id': 1193, 'type': 'solid_floor_insulation', 'description': 'Ravatherm XPS X 500 SL Polystyrene Foam', + "expensive than 100mm)", + 'is_installer_quote': False}, {'id': 2092, 'type': 'solid_floor_insulation', + 'description': 'Ecotherm Eco-Versal General Purpose Insulation Board', + 'depth': 100.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', + 'r_value_per_mm': 0.045454547, 'r_value_unit': 'square_meter_kelvin_per_watt', + 'thermal_conductivity': 0.022, 'thermal_conductivity_unit': 'watt_per_meter_kelvin', + 'link': 'SPONs', 'created_at': datetime.datetime(2024, 9, 24, 13, 42, 52, 584553), + 'is_active': True, 'prime_material_cost': 15.12, 'material_cost': 25.96, + 'labour_cost': 30.7, 'labour_hours_per_unit': 1.3, 'plant_cost': 0.0, + 'total_cost': 56.66, 'notes': None, 'is_installer_quote': False}, + {'id': 2093, 'type': 'solid_floor_insulation', 'description': 'Ravatherm XPS X 500 SL Polystyrene Foam', 'depth': 50.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.032258064, 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.031, 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None, + 'created_at': datetime.datetime(2024, 9, 24, 13, 42, 52, 584553), 'is_active': True, 'prime_material_cost': None, 'material_cost': 11.07, 'labour_cost': 10.66, 'labour_hours_per_unit': 0.46, 'plant_cost': 0.0, 'total_cost': 21.73, 'notes': "In Spons, the thermal conductivity is 0.033 however the datasheet indicates it's 0.32: " "https://ravagobuildingsolutions.com/uk/wp-content/uploads/sites/30/2022/08/ravatherm-xps-x-500-sl-tds" - "-version-1-20210901.pdf"}, - {'id': 1194, 'type': 'solid_floor_insulation', 'description': 'Ravatherm XPS X 500 SL Polystyrene Foam', + "-version-1-20210901.pdf", + 'is_installer_quote': False}, + {'id': 2094, 'type': 'solid_floor_insulation', 'description': 'Ravatherm XPS X 500 SL Polystyrene Foam', 'depth': 75.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.03125, 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.032, 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None, + 'created_at': datetime.datetime(2024, 9, 24, 13, 42, 52, 584553), 'is_active': True, 'prime_material_cost': None, 'material_cost': 16.28, 'labour_cost': 10.66, 'labour_hours_per_unit': 0.46, 'plant_cost': 0.0, - 'total_cost': 26.94, 'notes': None}, {'id': 1195, 'type': 'solid_floor_redecoration', - 'description': 'Screeded beds; protection to compressible formwork ' - 'exceeding 600mm wide', - 'depth': 0.0, 'depth_unit': None, 'cost': None, 'cost_unit': None, - 'r_value_per_mm': None, 'r_value_unit': 'square_meter_kelvin_per_watt', - 'thermal_conductivity': None, 'thermal_conductivity_unit': None, - 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), - 'is_active': True, 'prime_material_cost': 9.6, 'material_cost': 9.89, - 'labour_cost': 2.67, 'labour_hours_per_unit': 0.15, 'plant_cost': 0.0, - 'total_cost': 12.56, - 'notes': 'This is the screed layer, placed on top of the insulation'}, - {'id': 1196, 'type': 'solid_floor_redecoration', 'description': 'Fitting carpet', 'depth': 0.0, 'depth_unit': None, + 'total_cost': 26.94, 'notes': None, 'is_installer_quote': False}, {'id': 2095, 'type': 'solid_floor_redecoration', + 'description': 'Screeded beds; protection to ' + 'compressible formwork ' + 'exceeding 600mm wide', + 'depth': 0.0, 'depth_unit': None, 'cost': None, + 'cost_unit': None, 'r_value_per_mm': None, + 'r_value_unit': 'square_meter_kelvin_per_watt', + 'thermal_conductivity': None, + 'thermal_conductivity_unit': None, + 'link': 'SPONs', + 'created_at': datetime.datetime(2024, 9, 24, 13, + 42, 52, 584553), + 'is_active': True, 'prime_material_cost': 9.6, + 'material_cost': 9.89, 'labour_cost': 2.67, + 'labour_hours_per_unit': 0.15, + 'plant_cost': 0.0, 'total_cost': 12.56, + 'notes': 'This is the screed layer, ' + 'placed on top of the insulation', + 'is_installer_quote': False}, + {'id': 2096, 'type': 'solid_floor_redecoration', 'description': 'Fitting carpet', 'depth': 0.0, 'depth_unit': None, 'cost': None, 'cost_unit': None, 'r_value_per_mm': None, 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, 'thermal_conductivity_unit': None, 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None, + 'created_at': datetime.datetime(2024, 9, 24, 13, 42, 52, 584553), 'is_active': True, 'prime_material_cost': None, 'material_cost': 0.0, 'labour_cost': 6.59, 'labour_hours_per_unit': 0.37, 'plant_cost': 0.0, 'total_cost': 6.59, 'notes': 'SPONs does not have data on re-fitting the carpet so we use the data in Fitted carpeting; Gradus woven ' 'polypropylene tufted loop\n\n as a baseline. We assume re-use of carpets, therefore we need just ' - 'labour rates'}, - {'id': 1197, 'type': 'solid_floor_redecoration', - 'description': 'Fitting existing softwood skirting or architrave to new frames; 150mm high', 'depth': 0.0, - 'depth_unit': None, 'cost': None, 'cost_unit': None, 'r_value_per_mm': None, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, 'thermal_conductivity_unit': None, - 'link': 'SPONs', 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, - 'prime_material_cost': None, 'material_cost': 0.01, 'labour_cost': 4.87, 'labour_hours_per_unit': 0.12, - 'plant_cost': 0.0, 'total_cost': 4.88, 'notes': None}, {'id': 1198, 'type': 'ewi_wall_demolition', - 'description': 'Solid & Dry Lined walls: Hack of wall ' - 'finishes with chipping hammer; plaster ' - 'to walls.', - 'depth': 0.0, 'depth_unit': None, 'cost': None, - 'cost_unit': None, 'r_value_per_mm': None, - 'r_value_unit': 'square_meter_kelvin_per_watt', - 'thermal_conductivity': None, - 'thermal_conductivity_unit': None, 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, - 244907), 'is_active': True, - 'prime_material_cost': None, 'material_cost': 0.0, - 'labour_cost': 10.27, 'labour_hours_per_unit': 0.33, - 'plant_cost': 1.28, 'total_cost': 11.55, 'notes': None}, - {'id': 1199, 'type': 'ewi_wall_demolition', - 'description': 'Stud walls: Remove wall linings including battening behind; plasterboard and skim', 'depth': 0.0, - 'depth_unit': None, 'cost': None, 'cost_unit': None, 'r_value_per_mm': None, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, 'thermal_conductivity_unit': None, - 'link': 'SPONs', 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, - 'prime_material_cost': None, 'material_cost': 0.0, 'labour_cost': 6.23, 'labour_hours_per_unit': 0.2, - 'plant_cost': 1.25, 'total_cost': 7.48, 'notes': None}, {'id': 1200, 'type': 'ewi_wall_demolition', - 'description': 'Lathe and Plaster walls: Remove wall ' - 'linings including battening behind; ' - 'wood lath and plaster', - 'depth': 0.0, 'depth_unit': None, 'cost': None, - 'cost_unit': None, 'r_value_per_mm': None, - 'r_value_unit': 'square_meter_kelvin_per_watt', - 'thermal_conductivity': None, - 'thermal_conductivity_unit': None, 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, - 244907), - 'is_active': True, 'prime_material_cost': None, - 'material_cost': 0.0, 'labour_cost': 6.85, - 'labour_hours_per_unit': 0.22, 'plant_cost': 2.09, - 'total_cost': 8.94, 'notes': None}, - {'id': 1201, 'type': 'ewi_wall_preparation', - 'description': 'Clean and prepare surfaces, one coat Keim dilution, one coat primer and two coats of Keim Ecosil ' - 'paint; Brick or block walls; over 300 mm girth', - 'depth': 0.0, 'depth_unit': None, 'cost': None, 'cost_unit': None, 'r_value_per_mm': None, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, 'thermal_conductivity_unit': None, - 'link': 'SPONs', 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, - 'prime_material_cost': None, 'material_cost': 7.3, 'labour_cost': 5.62, 'labour_hours_per_unit': 0.3, - 'plant_cost': 0.0, 'total_cost': 12.92, - 'notes': 'This work covers the preparation and priming of the wall before insulating'}, - {'id': 1202, 'type': 'external_wall_insulation', 'description': 'Ecotherm Eco-Versal PIR Insulation Board', - 'depth': 30.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 6.16, - 'material_cost': 16.73, 'labour_cost': 28.34, 'labour_hours_per_unit': 1.2, 'plant_cost': 0.0, 'total_cost': 45.07, - 'notes': None}, - {'id': 1203, 'type': 'external_wall_insulation', 'description': 'Ecotherm Eco-Versal PIR Insulation Board', - 'depth': 50.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 8.46, - 'material_cost': 19.1, 'labour_cost': 28.34, 'labour_hours_per_unit': 1.2, 'plant_cost': 0.0, 'total_cost': 47.44, - 'notes': None}, - {'id': 1204, 'type': 'external_wall_insulation', 'description': 'Ecotherm Eco-Versal PIR Insulation Board', - 'depth': 100.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 15.12, - 'material_cost': 25.96, 'labour_cost': 30.7, 'labour_hours_per_unit': 1.3, 'plant_cost': 0.0, 'total_cost': 56.66, - 'notes': None}, - {'id': 1205, 'type': 'external_wall_insulation', 'description': 'Ecotherm Eco-Versal PIR Insulation Board', - 'depth': 150.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 23.53, - 'material_cost': 34.62, 'labour_cost': 33.06, 'labour_hours_per_unit': 1.4, 'plant_cost': 0.0, 'total_cost': 67.68, - 'notes': None}, - {'id': 1206, 'type': 'external_wall_insulation', 'description': 'Foamglas Grade F Wall Insulation Slabs', - 'depth': 60.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.02631579, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.038, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 41.69, - 'material_cost': 53.33, 'labour_cost': 29.52, 'labour_hours_per_unit': 1.25, 'plant_cost': 0.0, - 'total_cost': 82.85, 'notes': None}, - {'id': 1207, 'type': 'external_wall_insulation', 'description': 'Foamglas Grade F Wall Insulation Slabs', - 'depth': 100.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.02631579, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.038, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': 86.86, - 'material_cost': 99.85, 'labour_cost': 29.52, 'labour_hours_per_unit': 1.25, 'plant_cost': 0.0, - 'total_cost': 129.37, 'notes': None}, - {'id': 1208, 'type': 'external_wall_insulation', 'description': 'Foamglas Grade F Wall Insulation Slabs', - 'depth': 150.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.02631579, - 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.038, - 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, - 'prime_material_cost': 130.29, 'material_cost': 144.58, 'labour_cost': 29.52, 'labour_hours_per_unit': 1.25, - 'plant_cost': 0.0, 'total_cost': 174.1, 'notes': None}, {'id': 1209, 'type': 'ewi_wall_redecoration', - 'description': 'EPS insulation fixed with adhesive to ' - 'SFS structure (measured separately) ' - 'with horizontal PVC intermediate track ' - 'and vertical T-spines; with glassfibre ' - 'mesh reinforcement embedded in Sto ' - 'Armat Classic Basecoat Render and ' - 'Stolit K 1.5 Decorative Topcoat Render ' - '(white)', - 'depth': 0.0, 'depth_unit': None, 'cost': None, - 'cost_unit': None, 'r_value_per_mm': None, - 'r_value_unit': 'square_meter_kelvin_per_watt', - 'thermal_conductivity': None, - 'thermal_conductivity_unit': None, 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, - 244907), - 'is_active': True, 'prime_material_cost': None, - 'material_cost': 0.0, 'labour_cost': 0.0, - 'labour_hours_per_unit': 0.0, 'plant_cost': 0.0, - 'total_cost': 69.94, - 'notes': 'This material in SPONs is for 70mm EPS ' - 'insulation, which comes in at a cost of 99.17 ' - 'per meter square. This includes the cost of ' - 'insulation. To get the costing for just the ' - 'works and not the insulation, we subtract the ' - 'cost of EPS insulation, using Ravathem 75mm ' - 'insulation as an example, which costs £29.23 ' - 'per meter square, giving us the cost of the ' - 'remaining works without insulation. This ' - 'material gives us a cost for basecoat, ' - 'mesh application and a render finish'}, - {'id': 1210, 'type': 'low_energy_lighting_installation', 'description': 'Installation of fittings and cost of bub', + 'labour rates', + 'is_installer_quote': False}, {'id': 2097, 'type': 'solid_floor_redecoration', + 'description': 'Fitting existing softwood skirting or architrave to new frames; ' + '150mm high', + 'depth': 0.0, 'depth_unit': None, 'cost': None, 'cost_unit': None, + 'r_value_per_mm': None, 'r_value_unit': 'square_meter_kelvin_per_watt', + 'thermal_conductivity': None, 'thermal_conductivity_unit': None, 'link': 'SPONs', + 'created_at': datetime.datetime(2024, 9, 24, 13, 42, 52, 584553), 'is_active': True, + 'prime_material_cost': None, 'material_cost': 0.01, 'labour_cost': 4.87, + 'labour_hours_per_unit': 0.12, 'plant_cost': 0.0, 'total_cost': 4.88, 'notes': None, + 'is_installer_quote': False}, {'id': 2132, 'type': 'external_wall_insulation', + 'description': 'EWI Pro EPS external wall ' + 'insulation system with Brick Slip ' + 'finish', + 'depth': 150.0, 'depth_unit': 'mm', 'cost': None, + 'cost_unit': 'gbp_per_m2', + 'r_value_per_mm': 0.02631579, + 'r_value_unit': 'square_meter_kelvin_per_watt', + 'thermal_conductivity': 0.038, + 'thermal_conductivity_unit': 'watt_per_meter_kelvin', + 'link': 'SCIS', + 'created_at': datetime.datetime(2024, 9, 24, 13, 42, + 52, 584553), + 'is_active': True, 'prime_material_cost': None, + 'material_cost': 0.0, 'labour_cost': 0.0, + 'labour_hours_per_unit': 0.0, 'plant_cost': 0.0, + 'total_cost': 298.35, + 'notes': 'This is the quoted value from SCIS', + 'is_installer_quote': True}, + {'id': 2133, 'type': 'low_energy_lighting_installation', 'description': 'Installation of fittings and cost of bub', 'depth': 0.0, 'depth_unit': None, 'cost': None, 'cost_unit': 'gbp_per_unit', 'r_value_per_mm': None, 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, 'thermal_conductivity_unit': None, 'link': 'https://www.checkatrade.com/blog/cost-guides/cost-install-downlights/ ' 'https://www.hamuch.com/cost/led-spot-light#:~:text=It%20costs%20an%20average%20of,' 'will%20drive%20up%20the%20cost.', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None, - 'material_cost': 20.0, 'labour_cost': 15.0, 'labour_hours_per_unit': 0.8, 'plant_cost': 0.0, 'total_cost': 66.0, + 'created_at': datetime.datetime(2024, 9, 24, 13, 42, 52, 584553), 'is_active': True, 'prime_material_cost': None, + 'material_cost': 20.0, 'labour_cost': 15.0, 'labour_hours_per_unit': 0.8, 'plant_cost': 0.0, 'total_cost': 35.0, 'notes': 'We estimate the unit economics from the checkatrade article. We assume that the average job consists ' 'of installing 6 lights based on the hamuch article. We use the median value of 400 for a job of 6 ' - 'lights'}, - {'id': 1235, 'type': 'windows_glazing', - 'description': 'uPVC windows; Profile 22 or other equal and approved; reinforced where appropriate with ' - 'aluminium alloy; in refurbishment work, including standard ironmongery; sills and factory glazed ' - 'with low-e 24 mm double glazing; removing existing windows and fixing new in position; including ' - 'lugs plugged and screwed to brickwork or blockwork; Casement/fixed light; including vents; ' - 'e.p.d.m. glazing gaskets and weather seals; 1770 mm × 1200 mm; ref P312WW', - 'depth': 0.0, 'depth_unit': None, 'cost': None, 'cost_unit': 'gbp_per_unit', 'r_value_per_mm': None, + 'lights', + 'is_installer_quote': False}, + {'id': 2147, 'type': 'flat_roof_insulation', 'description': 'Ecotherm Eco-Versal General Purpose Insulation Board', + 'depth': 150.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547, + 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022, + 'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SCIS', + 'created_at': datetime.datetime(2024, 9, 24, 13, 42, 52, 584553), 'is_active': True, 'prime_material_cost': None, + 'material_cost': 0.0, 'labour_cost': 0.0, 'labour_hours_per_unit': 0.0, 'plant_cost': 0.0, 'total_cost': 195.0, + 'notes': 'Rough estimate based on a quote from Nic on 30th May, but the cost is just a rough estimate', + 'is_installer_quote': True}, + {'id': 2149, 'type': 'windows_glazing', 'description': 'REHAU PVCu Casement Windows', 'depth': 0.0, + 'depth_unit': None, 'cost': None, 'cost_unit': 'gbp_per_unit', 'r_value_per_mm': None, 'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, 'thermal_conductivity_unit': None, - 'link': 'SPONs', - 'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), - 'is_active': True, 'prime_material_cost': 176.55, - 'material_cost': 182.25, 'labour_cost': 163.36, 'labour_hours_per_unit': 6.5, 'plant_cost': 0.0, - 'total_cost': 345.61, - 'notes': 'This is the cost of removal of existing windows and installation of new windows. This is a casement ' - 'style window, which is the most common but also the cheapest style. In the cost estimation framework, ' - 'we can inflate prices for different finishes, to be conservative on price.'} + 'link': 'SCIS', 'created_at': datetime.datetime(2024, 9, 24, 13, 42, 52, 584553), 'is_active': True, + 'prime_material_cost': None, 'material_cost': 0.0, 'labour_cost': 0.0, 'labour_hours_per_unit': 0.0, + 'plant_cost': 0.0, 'total_cost': 1140.0, 'notes': None, 'is_installer_quote': True} ] diff --git a/recommendations/tests/test_fireplace_recommendations.py b/recommendations/tests/test_fireplace_recommendations.py index f21d6bc3..7eb55b21 100644 --- a/recommendations/tests/test_fireplace_recommendations.py +++ b/recommendations/tests/test_fireplace_recommendations.py @@ -40,7 +40,7 @@ class TestFirepaceRecommendations: assert recommender.recommendation assert recommender.recommendation[0]["type"] == "sealing_open_fireplace" - assert recommender.recommendation[0]["total"] == 300 + assert recommender.recommendation[0]["total"] == 235 def test_multiple_fireplaces(self): epc_record = EPCRecord() @@ -59,4 +59,4 @@ class TestFirepaceRecommendations: assert recommender.recommendation assert recommender.recommendation[0]["type"] == "sealing_open_fireplace" - assert recommender.recommendation[0]["total"] == 900 + assert recommender.recommendation[0]["total"] == 235 * 3 diff --git a/recommendations/tests/test_floor_recommendations.py b/recommendations/tests/test_floor_recommendations.py index 555f9a27..17f1f82e 100644 --- a/recommendations/tests/test_floor_recommendations.py +++ b/recommendations/tests/test_floor_recommendations.py @@ -5,13 +5,18 @@ from unittest.mock import Mock from recommendations.FloorRecommendations import FloorRecommendations from recommendations.tests.test_data.materials import materials from backend.Property import Property +from etl.epc.Record import EPCRecord +# import inspect +# +# file_path = inspect.getfile(lambda: None) # with open( -# os.path.abspath(os.path.dirname(__file__)) + "/recommendations/tests/test_data/input_properties.pkl", "rb" +# os.path.abspath(os.path.dirname(file_path)) + "/recommendations/tests/test_data/input_properties.pkl", "rb" # ) as f: # input_properties = pickle.load(f) + class TestFloorRecommendations: @pytest.fixture @@ -59,6 +64,7 @@ class TestFloorRecommendations: input_properties[2].floor_type = "suspended" input_properties[2].number_of_floors = 1 input_properties[2].floor_level = 0 + input_properties[2].already_installed = [] recommender = FloorRecommendations(property_instance=input_properties[2], materials=materials) assert recommender.estimated_u_value is None @@ -71,8 +77,8 @@ class TestFloorRecommendations: assert types == {"suspended_floor_insulation"} - assert len(recommender.recommendations) == 6 - assert recommender.recommendations[0]["total"] == 4925.205 + assert len(recommender.recommendations) == 1 + assert recommender.recommendations[0]["total"] == 4687.5 assert recommender.recommendations[0]["new_u_value"] == 0.21 def test_uvalue_0_12(self, input_properties): @@ -108,6 +114,7 @@ class TestFloorRecommendations: input_properties[4].floor_type = "solid" input_properties[4].number_of_floors = 1 input_properties[4].floor_level = 0 + input_properties[4].already_installed = [] # In this case, we have no county, so in this case, it should yse the local-authority-label if possible input_properties[4].data["county"] = "" @@ -146,123 +153,131 @@ class TestFloorRecommendations: assert recommender.estimated_u_value is None assert not recommender.recommendations - # def test_exposed_floor_no_insulation(self): - # input_property = Property(id=1, postcode="F4k3 2", address1="223 fake street", epc_client=Mock()) - # input_property.floor = { - # 'original_description': 'To unheated space, no insulation (assumed)', - # 'clean_description': 'To unheated space, no insulation', 'thermal_transmittance': None, - # 'thermal_transmittance_unit': None, 'is_assumed': True, 'is_to_unheated_space': True, - # 'is_to_external_air': False, 'is_suspended': False, 'is_solid': False, 'another_property_below': False, - # 'insulation_thickness': 'none' - # } - # input_property.age_band = "L" - # input_property.set_floor_type() - # input_property.data = {"floor-level": 0, "property-type": "House"} - # input_property.floor_area = 100 - # input_property.number_of_floors = 1 - # - # recommender = FloorRecommendations( - # property_instance=input_property, - # materials=materials - # ) - # - # assert not recommender.recommendations - # - # recommender.recommend() - # - # # Because of age band L, this should have a u-value of 0.22 to begin with and no recommendation - # assert not len(recommender.recommendations) - # assert recommender.estimated_u_value == 0.22 - # - # # Now with an older age band - # - # input_property2 = Property(id=1, postcode="F4k3 2", address1="223 fake street", epc_client=Mock()) - # input_property2.floor = { - # 'original_description': 'To unheated space, no insulation (assumed)', - # 'clean_description': 'To unheated space, no insulation', 'thermal_transmittance': None, - # 'thermal_transmittance_unit': None, 'is_assumed': True, 'is_to_unheated_space': True, - # 'is_to_external_air': False, 'is_suspended': False, 'is_solid': False, 'another_property_below': False, - # 'insulation_thickness': 'none' - # } - # input_property2.age_band = "D" - # input_property2.set_floor_type() - # input_property2.data = {"floor-level": 0, "property-type": "House"} - # input_property2.floor_area = 100 - # input_property2.number_of_floors = 1 - # - # recommender2 = FloorRecommendations( - # property_instance=input_property2, - # materials=materials - # ) - # - # assert not recommender2.recommendations - # - # recommender2.recommend() - # - # assert len(recommender2.recommendations) == 1 - # - # assert recommender2.recommendations[0]["new_u_value"] == 0.23 - # assert recommender2.recommendations[0]["starting_u_value"] == 1.2 - # assert recommender2.recommendations[0]["cost"] == 1500 - # - # def test_exposed_floor_below_average_insulated(self): - # input_property3 = Property(id=1, postcode="F4k3 2", address1="223 fake street", epc_client=Mock()) - # input_property3.floor = { - # 'original_description': 'To unheated space, below average insulation (assumed)', - # 'clean_description': 'To unheated space, below average insulation', 'thermal_transmittance': None, - # 'thermal_transmittance_unit': None, 'is_assumed': True, 'is_to_unheated_space': True, - # 'is_to_external_air': False, 'is_suspended': False, 'is_solid': False, 'another_property_below': False, - # 'insulation_thickness': 'below average' - # } - # input_property3.age_band = "C" - # input_property3.set_floor_type() - # input_property3.data = {"floor-level": 0, "property-type": "House"} - # input_property3.floor_area = 100 - # input_property3.number_of_floors = 1 - # - # recommender3 = FloorRecommendations( - # property_instance=input_property3, - # materials=materials - # ) - # - # assert not recommender3.recommendations - # - # recommender3.recommend() - # - # assert recommender3.estimated_u_value == 0.5 - # - # assert len(recommender3.recommendations) == 1 - # - # assert recommender3.recommendations[0]["new_u_value"] == 0.22 - # assert recommender3.recommendations[0]["starting_u_value"] == 0.5 - # assert recommender3.recommendations[0]["cost"] == 1100 - # assert recommender3.recommendations[0]["parts"][0]["depths"] == [100] - # - # # With average insulation, no recommendations - # - # input_property4 = Property(id=1, postcode="F4k3 2", address1="223 fake street", epc_client=Mock()) - # input_property4.floor = { - # 'original_description': 'To unheated space, insulated (assumed)', - # 'clean_description': 'To unheated space, insulated', 'thermal_transmittance': None, - # 'thermal_transmittance_unit': None, 'is_assumed': True, 'is_to_unheated_space': True, - # 'is_to_external_air': False, 'is_suspended': False, 'is_solid': False, 'another_property_below': False, - # 'insulation_thickness': 'average' - # } - # input_property4.age_band = "C" - # input_property4.set_floor_type() - # input_property4.data = {"floor-level": 0, "property-type": "House"} - # input_property4.floor_area = 100 - # input_property4.number_of_floors = 1 - # - # recommender4 = FloorRecommendations( - # property_instance=input_property4, - # materials=materials - # ) - # - # assert not recommender4.recommendations - # - # recommender4.recommend() - # - # assert recommender4.estimated_u_value is None - # - # assert len(recommender4.recommendations) == 0 + def test_exposed_floor_no_insulation(self): + epc_record = EPCRecord() + epc_record.prepared_epc = {"county": "Greater London", "floor-level": 0, "property-type": "House"} + epc_record.full_sap_epc = {} + + input_property = Property(id=1, postcode="F4k3 2", address="223 fake street", epc_record=epc_record) + input_property.floor = { + 'original_description': 'To unheated space, no insulation (assumed)', + 'clean_description': 'To unheated space, no insulation', 'thermal_transmittance': None, + 'thermal_transmittance_unit': None, 'is_assumed': True, 'is_to_unheated_space': True, + 'is_to_external_air': False, 'is_suspended': False, 'is_solid': False, 'another_property_below': False, + 'insulation_thickness': 'none' + } + input_property.age_band = "L" + input_property.set_floor_type() + input_property.floor_area = 100 + input_property.number_of_floors = 1 + + recommender = FloorRecommendations( + property_instance=input_property, + materials=materials + ) + + assert not recommender.recommendations + + recommender.recommend() + + # Because of age band L, this should have a u-value of 0.22 to begin with and no recommendation + assert not len(recommender.recommendations) + assert recommender.estimated_u_value == 0.22 + + # Now with an older age band + epc_record2 = EPCRecord() + epc_record2.prepared_epc = {"county": "Greater London", "floor-level": 0, "property-type": "House"} + epc_record2.full_sap_epc = {} + + input_property2 = Property(id=1, postcode="F4k3 2", address="223 fake street", epc_record=epc_record2) + input_property2.floor = { + 'original_description': 'To unheated space, no insulation (assumed)', + 'clean_description': 'To unheated space, no insulation', 'thermal_transmittance': None, + 'thermal_transmittance_unit': None, 'is_assumed': True, 'is_to_unheated_space': True, + 'is_to_external_air': False, 'is_suspended': False, 'is_solid': False, 'another_property_below': False, + 'insulation_thickness': 'none' + } + input_property2.age_band = "D" + input_property2.set_floor_type() + input_property2.insulation_floor_area = 100 + input_property2.number_of_floors = 1 + + recommender2 = FloorRecommendations( + property_instance=input_property2, + materials=materials + ) + + assert not recommender2.recommendations + + recommender2.recommend() + + assert len(recommender2.recommendations) == 1 + + assert recommender2.recommendations[0]["new_u_value"] == 0.24 + assert recommender2.recommendations[0]["starting_u_value"] == 1.2 + assert recommender2.recommendations[0]["total"] == 9375 + + def test_exposed_floor_below_average_insulated(self): + epc_record3 = EPCRecord() + epc_record3.prepared_epc = {"county": "Greater London", "floor-level": 0, "property-type": "House"} + epc_record3.full_sap_epc = {} + input_property3 = Property(id=1, postcode="F4k3 2", address="223 fake street", epc_record=epc_record3) + input_property3.floor = { + 'original_description': 'To unheated space, below average insulation (assumed)', + 'clean_description': 'To unheated space, below average insulation', 'thermal_transmittance': None, + 'thermal_transmittance_unit': None, 'is_assumed': True, 'is_to_unheated_space': True, + 'is_to_external_air': False, 'is_suspended': False, 'is_solid': False, 'another_property_below': False, + 'insulation_thickness': 'below average' + } + input_property3.age_band = "C" + input_property3.set_floor_type() + input_property3.insulation_floor_area = 100 + input_property3.number_of_floors = 1 + + recommender3 = FloorRecommendations( + property_instance=input_property3, + materials=materials + ) + + assert not recommender3.recommendations + + recommender3.recommend() + + assert recommender3.estimated_u_value == 0.5 + + assert len(recommender3.recommendations) == 1 + + assert recommender3.recommendations[0]["new_u_value"] == 0.24 + assert recommender3.recommendations[0]["starting_u_value"] == 0.5 + assert recommender3.recommendations[0]["total"] == 7500 + assert recommender3.recommendations[0]["parts"][0]["depth"] == 50 + + # With average insulation, no recommendations + epc_record4 = EPCRecord() + epc_record4.prepared_epc = {"county": "Greater London", "floor-level": 0, "property-type": "House"} + epc_record4.full_sap_epc = {} + input_property4 = Property(id=1, postcode="F4k3 2", address="223 fake street", epc_record=epc_record4) + input_property4.floor = { + 'original_description': 'To unheated space, insulated (assumed)', + 'clean_description': 'To unheated space, insulated', 'thermal_transmittance': None, + 'thermal_transmittance_unit': None, 'is_assumed': True, 'is_to_unheated_space': True, + 'is_to_external_air': False, 'is_suspended': False, 'is_solid': False, 'another_property_below': False, + 'insulation_thickness': 'average' + } + input_property4.age_band = "C" + input_property4.set_floor_type() + input_property4.insulation_floor_area = 100 + input_property4.number_of_floors = 1 + + recommender4 = FloorRecommendations( + property_instance=input_property4, + materials=materials + ) + + assert not recommender4.recommendations + + recommender4.recommend() + + assert recommender4.estimated_u_value is None + + assert len(recommender4.recommendations) == 0 diff --git a/recommendations/tests/test_lighting_recommendations.py b/recommendations/tests/test_lighting_recommendations.py index 45213d70..96440c01 100644 --- a/recommendations/tests/test_lighting_recommendations.py +++ b/recommendations/tests/test_lighting_recommendations.py @@ -41,8 +41,12 @@ class TestLightingRecommendations: assert len(lr.recommendation) == 1 assert lr.recommendation == [ - {'parts': [], 'type': 'low_energy_lighting', 'description': 'Install low energy lighting in 4 outlets', - 'starting_u_value': None, 'new_u_value': None, 'sap_points': 0.4, 'total': 240.24, - 'subtotal': 200.20000000000002, 'vat': 40.040000000000006, 'contingency': 14.3, 'preliminaries': 14.3, - 'material': 80.0, 'profit': 28.6, 'labour_hours': 3.2, 'labour_days': 0.4, 'labour_cost': 63.0} + {'phase': 0, 'parts': [], 'type': 'low_energy_lighting', + 'description': 'Install low energy lighting in 4 outlets', 'starting_u_value': None, 'new_u_value': None, + 'already_installed': False, 'sap_points': 0.4, 'kwh_savings': 219.0, 'co2_equivalent_savings': 0.035478, + 'description_simulation': {'lighting-energy-eff': 'Very Good', + 'lighting-description': 'Low energy lighting in all fixed outlets', + 'low-energy-lighting': 100}, 'total': 240.24, 'subtotal': 200.20000000000002, + 'vat': 40.040000000000006, 'contingency': 14.3, 'preliminaries': 14.3, 'material': 80.0, 'profit': 28.6, + 'labour_hours': 3.2, 'labour_days': 0.4, 'labour_cost': 63.0, 'survey': False} ] diff --git a/recommendations/tests/test_recommendation_utils.py b/recommendations/tests/test_recommendation_utils.py index 559a51b2..c42655eb 100644 --- a/recommendations/tests/test_recommendation_utils.py +++ b/recommendations/tests/test_recommendation_utils.py @@ -88,8 +88,8 @@ class TestRecommendationUtils: def test_get_roof_u_value_case_3(self): inputs = { - 'original_description': 'Room-in-roof, 200 mm insulation at rafters', - 'clean_description': 'Room-in-roof, 200 mm insulation at rafters', + 'original_description': 'Room-in-roof, insulated at rafters', + 'clean_description': 'Room-in-roof, insulated at rafters', 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': False, @@ -101,12 +101,12 @@ class TestRecommendationUtils: 'is_assumed': False, 'has_dwelling_above': False, 'is_valid': True, - 'insulation_thickness': '200', + 'insulation_thickness': 'average', 'age_band': "J" } u_value = recommendation_utils.get_roof_u_value(**inputs) - assert u_value == 0.21, f"Expected 0.21, but got {u_value}" + assert u_value == 0.4, f"Expected 0.4, but got {u_value}" def test_get_roof_u_value_case_4(self): inputs = { @@ -179,8 +179,8 @@ class TestRecommendationUtils: def test_get_roof_u_value_case_7(self): # Test case where the roof has a room in it inputs = { - 'original_description': 'Pitched, room-in-roof, 100mm insulation', - 'clean_description': 'Pitched, room-in-roof, 100mm insulation', + 'original_description': 'Pitched, room-in-roof, above average insulation', + 'clean_description': 'Pitched, room-in-roof, above average insulation', 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': True, @@ -192,12 +192,12 @@ class TestRecommendationUtils: 'is_assumed': False, 'has_dwelling_above': False, 'is_valid': True, - 'insulation_thickness': '100', + 'insulation_thickness': 'above average', 'age_band': "J" } u_value = recommendation_utils.get_roof_u_value(**inputs) - assert u_value == 0.40, f"Expected 0.40, but got {u_value}" + assert u_value == 0.16, f"Expected 0.16, but got {u_value}" def test_get_roof_u_value_case_8(self): # Test case where there is a dwelling above the roof, U-value should be 0 @@ -437,7 +437,6 @@ def test_estimate_windows(): construction_age_band="England and Wales: 1976-1982", floor_area=37, number_habitable_rooms=2, - extension_count=0, ) assert windows_case_1 == 4, f"Expected 4 windows, got {windows_case_1}" @@ -450,7 +449,6 @@ def test_estimate_windows(): construction_age_band="England and Wales: 1950-1966", floor_area=69, number_habitable_rooms=4, - extension_count=0, ) assert windows_case_2 == 6, f"Expected 6 windows, got {windows_case_2}" @@ -463,7 +461,6 @@ def test_estimate_windows(): construction_age_band="England and Wales: 1967-1975", floor_area=56, number_habitable_rooms=3, - extension_count=0, ) assert windows_case_3 == 5, f"Expected 5 windows, got {windows_case_3}" @@ -476,7 +473,6 @@ def test_estimate_windows(): construction_age_band="England and Wales: 1967-1975", floor_area=77.28, number_habitable_rooms=4, - extension_count=0, ) assert windows_case_4 == 7, f"Expected 7 windows, got {windows_case_4}" @@ -489,7 +485,6 @@ def test_estimate_windows(): construction_age_band="England and Wales: 1950-1966", floor_area=88.4, number_habitable_rooms=5, - extension_count=0, ) assert windows_case_5 == 12, f"Expected 12 windows, got {windows_case_5}" @@ -502,7 +497,6 @@ def test_estimate_windows(): construction_age_band="", floor_area=100, number_habitable_rooms=3, - extension_count=0, ) assert windows_case_6 == 5, f"Expected 5 windows, got {windows_case_6}" @@ -514,7 +508,6 @@ def test_estimate_windows(): construction_age_band="England and Wales: 1967-1975", floor_area=85, number_habitable_rooms=4, - extension_count=0, ) assert windows_case_7 == 10, f"Expected 10 windows, got {windows_case_7}" @@ -526,7 +519,6 @@ def test_estimate_windows(): construction_age_band="", floor_area=50, number_habitable_rooms=3, - extension_count=0, ) assert windows_case_8 == 5, f"Expected 5 windows, got {windows_case_8}" diff --git a/recommendations/tests/test_roof_recommendations.py b/recommendations/tests/test_roof_recommendations.py index 3d555a4f..139975bd 100644 --- a/recommendations/tests/test_roof_recommendations.py +++ b/recommendations/tests/test_roof_recommendations.py @@ -28,9 +28,10 @@ class TestRoofRecommendations: assert not roof_recommender.recommendations - roof_recommender.recommend() + roof_recommender.recommend(phase=0) - assert len(roof_recommender.recommendations) + assert len(roof_recommender.recommendations) == 1 + assert roof_recommender.recommendations[0]["parts"][0]["depth"] == 300 def test_loft_insulation_recommendation_50mm_insulation(self): epc_record = EPCRecord() @@ -52,13 +53,14 @@ class TestRoofRecommendations: assert not roof_recommender2.recommendations - roof_recommender2.recommend() + roof_recommender2.recommend(phase=0) assert len(roof_recommender2.recommendations) == 1 - assert roof_recommender2.recommendations[0]["total"] == 1936.9206000000004 + assert roof_recommender2.recommendations[0]["total"] == 1610.0000000000002 assert roof_recommender2.recommendations[0]["new_u_value"] == 0.14 assert roof_recommender2.recommendations[0]["starting_u_value"] == 0.68 + assert roof_recommender2.recommendations[0]["parts"][0]["depth"] == 270 epc_record = EPCRecord() epc_record.prepared_epc = {"county": "Greater London Authority"} @@ -79,7 +81,7 @@ class TestRoofRecommendations: assert not roof_recommender3.recommendations - roof_recommender3.recommend() + roof_recommender3.recommend(phase=0) assert roof_recommender3.recommendations assert len(roof_recommender3.recommendations) == 1 @@ -105,14 +107,14 @@ class TestRoofRecommendations: assert not roof_recommender4.recommendations - roof_recommender4.recommend() + roof_recommender4.recommend(phase=0) - assert len(roof_recommender4.recommendations) == 4 + assert len(roof_recommender4.recommendations) == 1 - assert roof_recommender4.recommendations[0]["total"] == 1128.744 - assert roof_recommender4.recommendations[0]["new_u_value"] == 0.15 + assert roof_recommender4.recommendations[0]["total"] == 1552.5 + assert roof_recommender4.recommendations[0]["new_u_value"] == 0.13 assert roof_recommender4.recommendations[0]["starting_u_value"] == 0.3 - assert roof_recommender4.recommendations[0]["parts"][0]["depth"] == 150 + assert roof_recommender4.recommendations[0]["parts"][0]["depth"] == 200 epc_record = EPCRecord() epc_record.prepared_epc = {"county": "Somerset"} @@ -133,12 +135,11 @@ class TestRoofRecommendations: assert not roof_recommender5.recommendations - roof_recommender5.recommend() + roof_recommender5.recommend(phase=0) - # The 150mm insulation should be selected, since there it already 150mm assert roof_recommender5.recommendations - assert len(roof_recommender5.recommendations) == 4 - assert roof_recommender5.recommendations[0]["parts"][0]["depth"] == 150 + assert len(roof_recommender5.recommendations) == 1 + assert roof_recommender5.recommendations[0]["parts"][0]["depth"] == 200 def test_loft_insulation_recommendation_270mm_insulation(self): # We shouldn't recommend anything in this case @@ -161,127 +162,121 @@ class TestRoofRecommendations: assert not roof_recommender6.recommendations - roof_recommender6.recommend() + roof_recommender6.recommend(phase=0) assert len(roof_recommender6.recommendations) == 0 - # def test_uninsulated_room_in_roof(self): - # property_instance7 = Property(id=0, address1="fake", postcode="fake", epc_client=Mock()) - # property_instance7.age_band = "F" - # property_instance7.insulation_floor_area = 100 - # property_instance7.roof = { - # 'original_description': 'Roof room(s), no insulation (assumed)', - # 'clean_description': 'Roof room(s), no insulation', - # 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': False, - # 'is_roof_room': True, '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' - # } - # - # property_instance7.pitched_roof_area = 110 - # property_instance7.data = {"county": "Southampton"} - # - # roof_recommender7 = RoofRecommendations(property_instance=property_instance7, materials=materials) - # - # assert not roof_recommender7.recommendations - # - # roof_recommender7.recommend() - # - # # Even though we have 3 depths, we only end with 1 due to diminishin returns - # assert len(roof_recommender7.recommendations) == 1 - # - # assert roof_recommender7.recommendations[0]["parts"][0]["depths"] == [270] - # - # assert roof_recommender7.recommendations[0]["new_u_value"] == 0.14 - # assert roof_recommender7.recommendations[0]["starting_u_value"] == 0.8 - # assert roof_recommender7.recommendations[0]["description"] == \ - # "Insulate your room roof with 270mm of Example room roof insulation" - # - # def test_ceiling_insulated_room_in_roof(self): - # property_instance8 = Property(id=8, address1="fake", postcode="fake", epc_client=Mock()) - # property_instance8.age_band = "F" - # property_instance8.insulation_floor_area = 100 - # property_instance8.roof = { - # 'original_description': 'Roof room(s), ceiling insulated', - # 'clean_description': 'Roof room(s), ceiling insulated', - # 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': False, - # 'is_roof_room': True, 'is_loft': False, 'is_flat': False, 'is_thatched': False, - # 'is_at_rafters': False, - # 'is_assumed': False, 'has_dwelling_above': False, 'is_valid': True, - # 'insulation_thickness': 'average' - # } - # - # property_instance8.pitched_roof_area = 110 - # - # roof_recommender8 = RoofRecommendations(property_instance=property_instance8, materials=materials) - # - # assert not roof_recommender8.recommendations - # - # roof_recommender8.recommend() - # - # # No recommendations in this case - # assert not roof_recommender8.recommendations - # - # def test_insulated_room_in_roof(self): - # property_instance9 = Property(id=9, address1="fake", postcode="fake", epc_client=Mock()) - # property_instance9.age_band = "F" - # property_instance9.insulation_floor_area = 100 - # property_instance9.roof = { - # 'original_description': 'Roof room(s), insulated (assumed)', - # 'clean_description': 'Roof room(s), insulated', - # 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': False, - # 'is_roof_room': True, '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': 'average' - # } - # - # property_instance9.pitched_roof_area = 110 - # property_instance9.data = {"county": "Rutland"} - # - # roof_recommender9 = RoofRecommendations(property_instance=property_instance9, materials=materials) - # - # assert not roof_recommender9.recommendations - # - # roof_recommender9.recommend() - # - # # No recommendations in this case - # assert not roof_recommender9.recommendations - # - # def test_limited_insulated_room_in_roof(self): - # property_instance10 = Property(id=10, address1="fake", postcode="fake", epc_client=Mock()) - # property_instance10.age_band = "F" - # property_instance10.insulation_floor_area = 100 - # property_instance10.roof = { - # 'original_description': 'Roof room(s), limited insulation (assumed)', - # 'clean_description': 'Roof room(s), limited insulation', - # 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': False, - # 'is_roof_room': True, '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': 'below average' - # } - # - # property_instance10.pitched_roof_area = 110 - # property_instance10.data = {"county": "Westmorland"} - # - # roof_recommender10 = RoofRecommendations(property_instance=property_instance10, materials=materials) - # - # assert not roof_recommender10.recommendations - # - # roof_recommender10.recommend() - # - # assert len(roof_recommender10.recommendations) == 2 - # - # assert roof_recommender10.recommendations[0]["parts"][0]["depths"] == [220] - # assert roof_recommender10.recommendations[1]["parts"][0]["depths"] == [270] - # - # assert roof_recommender10.recommendations[0]["new_u_value"] == 0.16 - # assert roof_recommender10.recommendations[1]["new_u_value"] == 0.14 - # - # assert roof_recommender10.recommendations[0]["starting_u_value"] == 0.8 - # assert roof_recommender10.recommendations[1]["starting_u_value"] == 0.8 - # - # assert roof_recommender10.recommendations[0]["description"] == \ - # "Insulate your room roof with 220mm of Example room roof insulation" - # assert roof_recommender10.recommendations[1]["description"] == \ - # "Insulate your room roof with 270mm of Example room roof insulation" + def test_uninsulated_room_in_roof(self): + epc_record = EPCRecord() + epc_record.prepared_epc = {"county": "Southampton", "roof-energy-eff": "Very Poor"} + property_instance7 = Property(id=0, address="fake", postcode="fake", epc_record=epc_record) + property_instance7.age_band = "F" + property_instance7.insulation_floor_area = 100 + property_instance7.roof = { + 'original_description': 'Roof room(s), no insulation (assumed)', + 'clean_description': 'Roof room(s), no insulation', + 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': False, + 'is_roof_room': True, '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' + } + + property_instance7.pitched_roof_area = 110 + + roof_recommender7 = RoofRecommendations(property_instance=property_instance7, materials=materials) + + assert not roof_recommender7.recommendations + + roof_recommender7.recommend(phase=0) + + assert len(roof_recommender7.recommendations) == 1 + assert roof_recommender7.recommendations[0]["new_u_value"] == 0.23 + assert roof_recommender7.recommendations[0]["starting_u_value"] == 1.5 + assert roof_recommender7.recommendations[0]["description"] == "Insulate room in roof at rafters and re-decorate" + + def test_ceiling_insulated_room_in_roof(self): + epc_record = EPCRecord() + epc_record.prepared_epc = {"county": "Southampton", "roof-energy-eff": "Very Poor"} + property_instance8 = Property(id=8, address="fake", postcode="fake", epc_record=epc_record) + property_instance8.age_band = "F" + property_instance8.insulation_floor_area = 100 + property_instance8.roof = { + 'original_description': 'Roof room(s), ceiling insulated', + 'clean_description': 'Roof room(s), ceiling insulated', + 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': False, + 'is_roof_room': True, 'is_loft': False, 'is_flat': False, 'is_thatched': False, + 'is_at_rafters': False, + 'is_assumed': False, 'has_dwelling_above': False, 'is_valid': True, + 'insulation_thickness': 'average' + } + + property_instance8.pitched_roof_area = 110 + + roof_recommender8 = RoofRecommendations(property_instance=property_instance8, materials=materials) + + assert not roof_recommender8.recommendations + + roof_recommender8.recommend(phase=0) + + # No recommendations in this case + assert not roof_recommender8.recommendations + + def test_insulated_room_in_roof(self): + epc_record = EPCRecord() + epc_record.prepared_epc = {"county": "Southampton", "roof-energy-eff": "Very Poor"} + property_instance9 = Property(id=9, address="fake", postcode="fake", epc_record=epc_record) + property_instance9.age_band = "F" + property_instance9.insulation_floor_area = 100 + property_instance9.roof = { + 'original_description': 'Roof room(s), insulated (assumed)', + 'clean_description': 'Roof room(s), insulated', + 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': False, + 'is_roof_room': True, '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': 'average' + } + + property_instance9.pitched_roof_area = 110 + property_instance9.data = {"county": "Rutland"} + + roof_recommender9 = RoofRecommendations(property_instance=property_instance9, materials=materials) + + assert not roof_recommender9.recommendations + + roof_recommender9.recommend(phase=0) + + # No recommendations in this case + assert not roof_recommender9.recommendations + + def test_limited_insulated_room_in_roof(self): + epc_record = EPCRecord() + epc_record.prepared_epc = {"county": "Westmorland", "roof-energy-eff": "Poor"} + property_instance10 = Property(id=10, address="fake", postcode="fake", epc_record=epc_record) + property_instance10.age_band = "F" + property_instance10.insulation_floor_area = 100 + property_instance10.roof = { + 'original_description': 'Roof room(s), limited insulation (assumed)', + 'clean_description': 'Roof room(s), limited insulation', + 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': False, + 'is_roof_room': True, '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': 'below average' + } + + property_instance10.pitched_roof_area = 110 + + roof_recommender10 = RoofRecommendations(property_instance=property_instance10, materials=materials) + + assert not roof_recommender10.recommendations + + roof_recommender10.recommend(phase=0) + + assert len(roof_recommender10.recommendations) == 1 + + assert roof_recommender10.recommendations[0]["new_u_value"] == 0.19 + + assert roof_recommender10.recommendations[0]["starting_u_value"] == 0.68 + + assert (roof_recommender10.recommendations[0]["description"] == + 'Insulate room in roof at rafters and re-decorate') def test_flat_no_insulation(self): epc_record = EPCRecord() @@ -302,12 +297,12 @@ class TestRoofRecommendations: assert not roof_recommender11.recommendations - roof_recommender11.recommend() + roof_recommender11.recommend(phase=0) assert len(roof_recommender11.recommendations) == 1 assert roof_recommender11.recommendations[0]["parts"][0]["depth"] == 150 - assert roof_recommender11.recommendations[0]["total"] == 4380.84324 + assert roof_recommender11.recommendations[0]["total"] == 6532.5 assert roof_recommender11.recommendations[0]["new_u_value"] == 0.14 assert roof_recommender11.recommendations[0]["starting_u_value"] == 2.3 assert roof_recommender11.recommendations[0]["description"] == \ @@ -334,7 +329,7 @@ class TestRoofRecommendations: assert not roof_recommender12.recommendations - roof_recommender12.recommend() + roof_recommender12.recommend(phase=0) assert not roof_recommender12.recommendations @@ -358,13 +353,13 @@ class TestRoofRecommendations: assert not roof_recommender13.recommendations - roof_recommender13.recommend() + roof_recommender13.recommend(phase=0) assert len(roof_recommender13.recommendations) == 1 assert roof_recommender13.recommendations[0]["parts"][0]["depth"] == 150 - assert roof_recommender13.recommendations[0]["total"] == 5199.969120000002 + assert roof_recommender13.recommendations[0]["total"] == 7800 assert roof_recommender13.recommendations[0]["new_u_value"] == 0.14 assert roof_recommender13.recommendations[0]["starting_u_value"] == 2.3 @@ -390,6 +385,6 @@ class TestRoofRecommendations: assert not roof_recommender14.recommendations - roof_recommender14.recommend() + roof_recommender14.recommend(phase=0) assert not roof_recommender14.recommendations diff --git a/recommendations/tests/test_solar_pv_recommendations.py b/recommendations/tests/test_solar_pv_recommendations.py index fbbfe3a1..05349f9c 100644 --- a/recommendations/tests/test_solar_pv_recommendations.py +++ b/recommendations/tests/test_solar_pv_recommendations.py @@ -50,360 +50,64 @@ class TestSolarPvRecommendations: epc_record = EPCRecord() epc_record.prepared_epc = {"property-type": "House", "photo-supply": None, "county": "Huntingdonshire"} property_instance_valid_all = Property(id=1, address="", postcode="", epc_record=epc_record) - property_instance_valid_all.solar_pv_roof_area = 20 - property_instance_valid_all.solar_pv_percentage = 40 + property_instance_valid_all.roof_area = 40 + property_instance_valid_all.number_of_floors = 2 property_instance_valid_all.roof = {"is_flat": True} + property_instance_valid_all.solar_panel_configuration = { + "panel_performance": pd.DataFrame( + [ + { + "panneled_roof_area": 20, + "n_panels": 10, + "array_wattage": 4000, + "initial_ac_kwh_per_year": 3800 + } + ] + ) + } + return property_instance_valid_all def test_invalid_property_type(self, property_instance_invalid_type): solar_pv = SolarPvRecommendations(property_instance_invalid_type) - solar_pv.recommend() + solar_pv.recommend(phase=0) assert not solar_pv.recommendation def test_invalid_roof_type(self, property_instance_invalid_roof): solar_pv = SolarPvRecommendations(property_instance_invalid_roof) - solar_pv.recommend() + solar_pv.recommend(phase=0) assert not solar_pv.recommendation def test_existing_solar_pv(self, property_instance_has_solar_pv): solar_pv = SolarPvRecommendations(property_instance_has_solar_pv) - solar_pv.recommend() + solar_pv.recommend(phase=0) assert not solar_pv.recommendation def test_valid_all_conditions(self, property_instance_valid_all): solar_pv = SolarPvRecommendations(property_instance_valid_all) - solar_pv.recommend() + solar_pv.recommend(phase=0) assert solar_pv.recommendation == [ { - 'parts': [], - 'type': 'solar_pv', - 'description': 'Install a 4 kilowatt-peak (kWp) solar photovoltaic (PV) panel system on the roof', - 'starting_u_value': None, - 'new_u_value': None, - 'sap_points': None, - 'total': 8527.0752, - 'subtotal': 7105.896, - 'vat': 1421.1791999999996, - 'labour_hours': 72, - 'labour_days': 2, - 'photo_supply': 4000 + 'phase': 0, 'parts': [], 'type': 'solar_pv', + 'description': 'Install a 4.0 kilowatt-peak (kWp) solar photovoltaic (PV) panel system on 50% the ' + 'roof.', + 'starting_u_value': None, 'new_u_value': None, 'sap_points': None, 'already_installed': False, + 'total': 4850.0, 'subtotal': 4041.666666666667, 'vat': 808.333333333333, 'labour_hours': 48, + 'labour_days': 2, 'photo_supply': 50.0, 'has_battery': False, 'initial_ac_kwh_per_year': 3800, + 'description_simulation': {'photo-supply': 50.0} + }, + { + 'phase': 0, 'parts': [], 'type': 'solar_pv', + 'description': 'Install a 4.0 kilowatt-peak (kWp) ' + 'solar photovoltaic (PV) panel system ' + 'on 50% the roof, with a battery ' + 'storage system.', + 'starting_u_value': None, 'new_u_value': None, + 'sap_points': None, 'already_installed': False, + 'total': 7550.0, 'subtotal': 6291.666666666667, + 'vat': 1258.333333333333, 'labour_hours': 48, + 'labour_days': 2, 'photo_supply': 50.0, + 'has_battery': True, 'initial_ac_kwh_per_year': 3800, + 'description_simulation': {'photo-supply': 50.0} } ] - - def test_model(self): - """ - This function tests the recommendation engine, in conjunction with the model - :return: - """ - - starting_epc = { - 'low-energy-fixed-light-count': '', 'address': '27 Cromwell Street', 'uprn-source': 'Energy Assessor', - 'floor-height': '2.5', 'heating-cost-potential': '443', 'unheated-corridor-length': '', - 'hot-water-cost-potential': '53', 'construction-age-band': 'England and Wales: before 1900', - 'potential-energy-rating': 'B', 'mainheat-energy-eff': 'Good', 'windows-env-eff': 'Average', - 'lighting-energy-eff': 'Very Poor', 'environment-impact-potential': '85', - 'glazed-type': 'double glazing installed before 2002', 'heating-cost-current': '904', 'address3': '', - 'mainheatcont-description': 'Programmer, room thermostat and TRVs', 'sheating-energy-eff': 'N/A', - 'property-type': 'House', 'local-authority-label': 'West Lindsey', 'fixed-lighting-outlets-count': '10', - 'energy-tariff': 'Single', 'mechanical-ventilation': 'natural', 'hot-water-cost-current': '79', - 'county': 'Lincolnshire', 'postcode': 'DN21 1DH', 'solar-water-heating-flag': 'N', - 'constituency': 'E14000707', 'co2-emissions-potential': '1.5', 'number-heated-rooms': '5', - 'floor-description': 'Suspended, no insulation (assumed)', 'energy-consumption-potential': '92', - 'local-authority': 'E07000142', 'built-form': 'Mid-Terrace', 'number-open-fireplaces': '0', - 'windows-description': 'Fully double glazed', 'glazed-area': 'Normal', 'inspection-date': '2021-11-17', - 'mains-gas-flag': 'Y', 'co2-emiss-curr-per-floor-area': '61', 'address1': '27 Cromwell Street', - 'heat-loss-corridor': '', 'flat-storey-count': '', 'constituency-label': 'Gainsborough', - 'roof-energy-eff': 'Very Poor', 'total-floor-area': '89.0', 'building-reference-number': '10001989430', - 'environment-impact-current': '47', 'co2-emissions-current': '5.4', - 'roof-description': 'Pitched, no insulation (assumed)', 'floor-energy-eff': 'N/A', - 'number-habitable-rooms': '5', 'address2': '', 'hot-water-env-eff': 'Good', 'posttown': 'GAINSBOROUGH', - 'mainheatc-energy-eff': 'Good', 'main-fuel': 'mains gas (not community)', 'lighting-env-eff': 'Very Poor', - 'windows-energy-eff': 'Average', 'floor-env-eff': 'N/A', 'sheating-env-eff': 'N/A', - 'lighting-description': 'No low energy lighting', 'roof-env-eff': 'Very Poor', - 'walls-energy-eff': 'Very Poor', 'photo-supply': '0.0', 'lighting-cost-potential': '72', - 'mainheat-env-eff': 'Good', 'multi-glaze-proportion': '100', 'main-heating-controls': '', - 'lodgement-datetime': '2021-12-01 10:12:23', 'flat-top-storey': '', 'current-energy-rating': 'E', - 'secondheat-description': 'Room heaters, mains gas', 'walls-env-eff': 'Very Poor', - 'transaction-type': 'ECO assessment', 'uprn': '100030949912', 'current-energy-efficiency': '54', - 'energy-consumption-current': '346', 'mainheat-description': 'Boiler and radiators, mains gas', - 'lighting-cost-current': '144', 'lodgement-date': '2021-12-01', 'extension-count': '2', - 'mainheatc-env-eff': 'Good', 'lmk-key': '3ec5533af02ec78361c1f9bea8dd2e878c2c6fa6cf59e5cc505c3eeb038e0f91', - 'wind-turbine-count': '0', 'tenure': 'Owner-occupied', 'floor-level': '', - 'potential-energy-efficiency': '86', 'hot-water-energy-eff': 'Good', 'low-energy-lighting': '0', - 'walls-description': 'Solid brick, as built, no insulation (assumed)', - 'hotwater-description': 'From main system' - } - - ending_epc = { - 'low-energy-fixed-light-count': '', 'address': '27 Cromwell Street', 'uprn-source': 'Energy Assessor', - 'floor-height': '2.5', 'heating-cost-potential': '443', 'unheated-corridor-length': '', - 'hot-water-cost-potential': '53', 'construction-age-band': 'England and Wales: before 1900', - 'potential-energy-rating': 'B', 'mainheat-energy-eff': 'Good', 'windows-env-eff': 'Average', - 'lighting-energy-eff': 'Very Poor', 'environment-impact-potential': '86', - 'glazed-type': 'double glazing installed before 2002', 'heating-cost-current': '904', 'address3': '', - 'mainheatcont-description': 'Programmer, room thermostat and TRVs', 'sheating-energy-eff': 'N/A', - 'property-type': 'House', 'local-authority-label': 'West Lindsey', 'fixed-lighting-outlets-count': '10', - 'energy-tariff': 'Single', 'mechanical-ventilation': 'natural', 'hot-water-cost-current': '79', - 'county': 'Lincolnshire', 'postcode': 'DN21 1DH', 'solar-water-heating-flag': 'N', - 'constituency': 'E14000707', 'co2-emissions-potential': '1.4', 'number-heated-rooms': '5', - 'floor-description': 'Suspended, no insulation (assumed)', 'energy-consumption-potential': '84', - 'local-authority': 'E07000142', 'built-form': 'Mid-Terrace', 'number-open-fireplaces': '0', - 'windows-description': 'Fully double glazed', 'glazed-area': 'Normal', 'inspection-date': '2021-12-21', - 'mains-gas-flag': 'Y', 'co2-emiss-curr-per-floor-area': '49', 'address1': '27 Cromwell Street', - 'heat-loss-corridor': '', 'flat-storey-count': '', 'constituency-label': 'Gainsborough', - 'roof-energy-eff': 'Very Poor', 'total-floor-area': '89.0', 'building-reference-number': '10001989430', - 'environment-impact-current': '55', 'co2-emissions-current': '4.4', - 'roof-description': 'Pitched, no insulation (assumed)', 'floor-energy-eff': 'N/A', - 'number-habitable-rooms': '5', 'address2': '', 'hot-water-env-eff': 'Good', 'posttown': 'GAINSBOROUGH', - 'mainheatc-energy-eff': 'Good', 'main-fuel': 'mains gas (not community)', 'lighting-env-eff': 'Very Poor', - 'windows-energy-eff': 'Average', 'floor-env-eff': 'N/A', 'sheating-env-eff': 'N/A', - 'lighting-description': 'No low energy lighting', 'roof-env-eff': 'Very Poor', - 'walls-energy-eff': 'Very Poor', 'photo-supply': '50.0', 'lighting-cost-potential': '72', - 'mainheat-env-eff': 'Good', 'multi-glaze-proportion': '100', 'main-heating-controls': '', - 'lodgement-datetime': '2021-12-21 17:33:09', 'flat-top-storey': '', 'current-energy-rating': 'D', - 'secondheat-description': 'Room heaters, mains gas', 'walls-env-eff': 'Very Poor', - 'transaction-type': 'ECO assessment', 'uprn': '100030949912', 'current-energy-efficiency': '65', - 'energy-consumption-current': '277', 'mainheat-description': 'Boiler and radiators, mains gas', - 'lighting-cost-current': '144', 'lodgement-date': '2021-12-21', 'extension-count': '2', - 'mainheatc-env-eff': 'Good', 'lmk-key': 'b0b19583c59afbc69db12f4d6c98cd8837e80da3214d577c426eb3e672d424fc', - 'wind-turbine-count': '0', 'tenure': 'Owner-occupied', 'floor-level': '', - 'potential-energy-efficiency': '88', 'hot-water-energy-eff': 'Good', 'low-energy-lighting': '0', - 'walls-description': 'Solid brick, as built, no insulation (assumed)', - 'hotwater-description': 'From main system' - } - - cleaning_data = read_dataframe_from_s3_parquet( - bucket_name="retrofit-data-dev", file_key="sap_change_model/cleaning_dataset.parquet", - ) - - cleaned = read_from_s3( - s3_file_name="cleaned_epc_data/cleaned.bson", - bucket_name="retrofit-data-dev" - ) - cleaned = msgpack.unpackb(cleaned, raw=False) - - photo_supply_lookup, floor_area_decile_thresholds = SolarPhotoSupply.load(bucket="retrofit-data-dev") - - epc = EPCRecord( - epc_records={ - 'original_epc': starting_epc, - 'full_sap_epc': {}, - 'old_data': [] - }, - run_mode="newdata", - cleaning_data=cleaning_data - ) - - home = Property( - id=0, - address="", - postcode="", - epc_record=epc, - already_installed={}, - non_invasive_recommendations={}, - ) - home.in_conservation_area = False - home.is_listed = False - home.is_heritage = False - home.restricted_measures = True - home.get_components( - cleaned=cleaned, - photo_supply_lookup=photo_supply_lookup, - floor_area_decile_thresholds=floor_area_decile_thresholds - ) - - recommender = SolarPvRecommendations(property_instance=home) - recommender.recommend(phase=0) - - coverage_50_percent = [x for x in recommender.recommendation if x["photo_supply"] == 50] - assert len(coverage_50_percent) == 2 - - property_recommendations = Recommendations.insert_temp_recommendation_id([coverage_50_percent]) - - home.create_base_difference_epc_record(cleaned_lookup=cleaned) - home.adjust_difference_record_with_recommendations( - property_recommendations, [] - ) - - scoring_data = pd.DataFrame(home.recommendations_scoring_data).drop( - columns=["rdsap_change", "heat_demand_change", "carbon_change", "sap_ending", "heat_demand_ending", - "carbon_ending"] - ) - - model_api = ModelApi(portfolio_id="ashp-test", timestamp=datetime.now().isoformat()) - model_api.MODEL_PREFIXES = ["sap_change_predictions"] - - predictions_dict = model_api.predict_all( - df=scoring_data, - bucket="retrofit-data-dev", - prediction_buckets={ - "sap_change_predictions": "retrofit-sap-predictions-dev", - } - ) - - assert predictions_dict["sap_change_predictions"]["predictions"].tolist() == [65.9, 65.9] - assert ending_epc["current-energy-efficiency"] == '65' - - def test_model2(self): - data[["uprn", "sap_ending"]] - # - - searcher = SearchEpc( - address1="", - postcode="", - auth_token="a2Nvbm5rb3dsZXNzYXJAZ21haWwuY29tOjY5MGJiMWM0NmIyOGI5ZDUxYzAxMzQzYzNiZGNlZGJjZDNmODQwMzA=", - os_api_key="", - full_address="", - uprn=100030952942, - ) - searcher.find_property(False) - - ending_epc = { - 'low-energy-fixed-light-count': '', 'address': '6 Kenmare Crescent', - 'uprn-source': 'Energy Assessor', 'floor-height': '2.49', 'heating-cost-potential': '464', - 'unheated-corridor-length': '', 'hot-water-cost-potential': '46', - 'construction-age-band': 'England and Wales: 1967-1975', 'potential-energy-rating': 'B', - 'mainheat-energy-eff': 'Good', 'windows-env-eff': 'Average', 'lighting-energy-eff': 'Very Good', - 'environment-impact-potential': '91', 'glazed-type': 'not defined', 'heating-cost-current': '535', - 'address3': '', 'mainheatcont-description': 'Programmer, room thermostat and TRVs', - 'sheating-energy-eff': 'N/A', 'property-type': 'Bungalow', - 'local-authority-label': 'West Lindsey', 'fixed-lighting-outlets-count': '9', - 'energy-tariff': 'Single', 'mechanical-ventilation': 'natural', 'hot-water-cost-current': '69', - 'county': 'Lincolnshire', 'postcode': 'DN21 1PR', 'solar-water-heating-flag': 'N', - 'constituency': 'E14000707', 'co2-emissions-potential': '0.7', 'number-heated-rooms': '3', - 'floor-description': 'Suspended, no insulation (assumed)', 'energy-consumption-potential': '56', - 'local-authority': 'E07000142', 'built-form': 'Semi-Detached', 'number-open-fireplaces': '0', - 'windows-description': 'Fully double glazed', 'glazed-area': 'Much More Than Typical', - 'inspection-date': '2022-08-24', 'mains-gas-flag': 'Y', 'co2-emiss-curr-per-floor-area': '18', - 'address1': '6 Kenmare Crescent', 'heat-loss-corridor': '', 'flat-storey-count': '', - 'constituency-label': 'Gainsborough', 'roof-energy-eff': 'Very Good', 'total-floor-area': '66.0', - 'building-reference-number': '10002845316', 'environment-impact-current': '85', - 'co2-emissions-current': '1.2', 'roof-description': 'Pitched, 300 mm loft insulation', - 'floor-energy-eff': 'N/A', 'number-habitable-rooms': '3', 'address2': '', - 'hot-water-env-eff': 'Good', 'posttown': 'GAINSBOROUGH', 'mainheatc-energy-eff': 'Good', - 'main-fuel': 'mains gas (not community)', 'lighting-env-eff': 'Very Good', - 'windows-energy-eff': 'Average', 'floor-env-eff': 'N/A', 'sheating-env-eff': 'N/A', - 'lighting-description': 'Low energy lighting in all fixed outlets', 'roof-env-eff': 'Very Good', - 'walls-energy-eff': 'Average', 'photo-supply': '40.0', 'lighting-cost-potential': '65', - 'mainheat-env-eff': 'Good', 'multi-glaze-proportion': '100', 'main-heating-controls': '', - 'lodgement-datetime': '2022-08-24 15:39:42', 'flat-top-storey': '', 'current-energy-rating': 'B', - 'secondheat-description': 'Room heaters, electric', 'walls-env-eff': 'Average', - 'transaction-type': 'ECO assessment', 'uprn': '100030952942', 'current-energy-efficiency': '87', - 'energy-consumption-current': '100', 'mainheat-description': 'Boiler and radiators, mains gas', - 'lighting-cost-current': '65', 'lodgement-date': '2022-08-24', 'extension-count': '0', - 'mainheatc-env-eff': 'Good', - 'lmk-key': 'e20be883431b1fed15db7fa1f52634fb7655d2b80c2fdad37df779f93ec4dafd', - 'wind-turbine-count': '0', 'tenure': 'Owner-occupied', 'floor-level': '', - 'potential-energy-efficiency': '91', 'hot-water-energy-eff': 'Good', 'low-energy-lighting': '100', - 'walls-description': 'Cavity wall, filled cavity', 'hotwater-description': 'From main system' - } - starting_epc = { - 'low-energy-fixed-light-count': '', 'address': '6 Kenmare Crescent', 'uprn-source': 'Energy Assessor', - 'floor-height': '2.49', 'heating-cost-potential': '464', 'unheated-corridor-length': '', - 'hot-water-cost-potential': '46', 'construction-age-band': 'England and Wales: 1967-1975', - 'potential-energy-rating': 'B', 'mainheat-energy-eff': 'Good', 'windows-env-eff': 'Average', - 'lighting-energy-eff': 'Very Good', 'environment-impact-potential': '85', 'glazed-type': 'not defined', - 'heating-cost-current': '535', 'address3': '', - 'mainheatcont-description': 'Programmer, room thermostat and TRVs', 'sheating-energy-eff': 'N/A', - 'property-type': 'Bungalow', 'local-authority-label': 'West Lindsey', 'fixed-lighting-outlets-count': '9', - 'energy-tariff': 'Single', 'mechanical-ventilation': 'natural', 'hot-water-cost-current': '69', - 'county': 'Lincolnshire', 'postcode': 'DN21 1PR', 'solar-water-heating-flag': 'N', - 'constituency': 'E14000707', 'co2-emissions-potential': '1.2', 'number-heated-rooms': '3', - 'floor-description': 'Suspended, no insulation (assumed)', 'energy-consumption-potential': '102', - 'local-authority': 'E07000142', 'built-form': 'Semi-Detached', 'number-open-fireplaces': '0', - 'windows-description': 'Fully double glazed', 'glazed-area': 'Much More Than Typical', - 'inspection-date': '2022-05-31', 'mains-gas-flag': 'Y', 'co2-emiss-curr-per-floor-area': '40', - 'address1': '6 Kenmare Crescent', 'heat-loss-corridor': '', 'flat-storey-count': '', - 'constituency-label': 'Gainsborough', 'roof-energy-eff': 'Very Good', 'total-floor-area': '66.0', - 'building-reference-number': '10002845316', 'environment-impact-current': '68', - 'co2-emissions-current': '2.6', 'roof-description': 'Pitched, 300 mm loft insulation', - 'floor-energy-eff': 'N/A', 'number-habitable-rooms': '3', 'address2': '', 'hot-water-env-eff': 'Good', - 'posttown': 'GAINSBOROUGH', 'mainheatc-energy-eff': 'Good', 'main-fuel': 'mains gas (not community)', - 'lighting-env-eff': 'Very Good', 'windows-energy-eff': 'Average', 'floor-env-eff': 'N/A', - 'sheating-env-eff': 'N/A', 'lighting-description': 'Low energy lighting in all fixed outlets', - 'roof-env-eff': 'Very Good', 'walls-energy-eff': 'Average', 'photo-supply': '0.0', - 'lighting-cost-potential': '65', 'mainheat-env-eff': 'Good', 'multi-glaze-proportion': '100', - 'main-heating-controls': '', 'lodgement-datetime': '2022-06-15 08:38:02', 'flat-top-storey': '', - 'current-energy-rating': 'D', 'secondheat-description': 'Room heaters, electric', - 'walls-env-eff': 'Average', 'transaction-type': 'ECO assessment', 'uprn': '100030952942', - 'current-energy-efficiency': '68', 'energy-consumption-current': '227', - 'mainheat-description': 'Boiler and radiators, mains gas', 'lighting-cost-current': '65', - 'lodgement-date': '2022-06-15', 'extension-count': '0', 'mainheatc-env-eff': 'Good', - 'lmk-key': 'ce181970b7077cb9b4626242bfb010b30a0e48541b5f22427e81f1adbeeec4f2', 'wind-turbine-count': '0', - 'tenure': 'Owner-occupied', 'floor-level': '', 'potential-energy-efficiency': '85', - 'hot-water-energy-eff': 'Good', 'low-energy-lighting': '100', - 'walls-description': 'Cavity wall, filled cavity', 'hotwater-description': 'From main system' - } - - cleaning_data = read_dataframe_from_s3_parquet( - bucket_name="retrofit-data-dev", file_key="sap_change_model/cleaning_dataset.parquet", - ) - - cleaned = read_from_s3( - s3_file_name="cleaned_epc_data/cleaned.bson", - bucket_name="retrofit-data-dev" - ) - cleaned = msgpack.unpackb(cleaned, raw=False) - - photo_supply_lookup, floor_area_decile_thresholds = SolarPhotoSupply.load(bucket="retrofit-data-dev") - - epc = EPCRecord( - epc_records={ - 'original_epc': starting_epc, - 'full_sap_epc': {}, - 'old_data': [] - }, - run_mode="newdata", - cleaning_data=cleaning_data - ) - - home = Property( - id=0, - address="", - postcode="", - epc_record=epc, - already_installed={}, - non_invasive_recommendations={}, - ) - home.in_conservation_area = False - home.is_listed = False - home.is_heritage = False - home.restricted_measures = True - home.get_components( - cleaned=cleaned, - photo_supply_lookup=photo_supply_lookup, - floor_area_decile_thresholds=floor_area_decile_thresholds - ) - - recommender = SolarPvRecommendations(property_instance=home) - recommender.recommend(phase=0) - - coverage_40_percent = [x for x in recommender.recommendation if x["photo_supply"] == 40] - assert len(coverage_40_percent) == 2 - - property_recommendations = Recommendations.insert_temp_recommendation_id([coverage_40_percent]) - - home.create_base_difference_epc_record(cleaned_lookup=cleaned) - home.adjust_difference_record_with_recommendations( - property_recommendations, [] - ) - - scoring_data = pd.DataFrame(home.recommendations_scoring_data).drop( - columns=["rdsap_change", "heat_demand_change", "carbon_change", "sap_ending", "heat_demand_ending", - "carbon_ending"] - ) - - model_api = ModelApi(portfolio_id="ashp-test", timestamp=datetime.now().isoformat()) - model_api.MODEL_PREFIXES = ["sap_change_predictions"] - - predictions_dict = model_api.predict_all( - df=scoring_data, - bucket="retrofit-data-dev", - prediction_buckets={ - "sap_change_predictions": "retrofit-sap-predictions-dev", - } - ) - - assert predictions_dict["sap_change_predictions"]["predictions"].tolist() == [87.1, 87.1] - assert ending_epc["current-energy-efficiency"] == '87' - assert starting_epc["current-energy-efficiency"] == '68' diff --git a/recommendations/tests/test_ventilation_recommendations.py b/recommendations/tests/test_ventilation_recommendations.py index aa992253..441f9a22 100644 --- a/recommendations/tests/test_ventilation_recommendations.py +++ b/recommendations/tests/test_ventilation_recommendations.py @@ -22,7 +22,7 @@ class TestVentilationRecommendations: assert len(recommender.recommendation) == 1 - assert recommender.recommendation[0]["total"] == 1000 + assert recommender.recommendation[0]["total"] == 1071.0 assert recommender.recommendation[0]["type"] == "mechanical_ventilation" assert len(recommender.recommendation[0]["parts"]) == 1 assert recommender.recommendation[0]["parts"][0]["description"] == 'Mechanical Extract Ventilation' @@ -44,7 +44,7 @@ class TestVentilationRecommendations: assert len(recommender2.recommendation) == 1 - assert recommender2.recommendation[0]["total"] == 1000 + assert recommender2.recommendation[0]["total"] == 1071.0 assert recommender2.recommendation[0]["type"] == "mechanical_ventilation" assert len(recommender2.recommendation[0]["parts"]) == 1 assert recommender2.recommendation[0]["parts"][0]["description"] == 'Mechanical Extract Ventilation' @@ -66,7 +66,7 @@ class TestVentilationRecommendations: assert len(recommender3.recommendation) == 1 - assert recommender3.recommendation[0]["total"] == 1000 + assert recommender3.recommendation[0]["total"] == 1071.0 assert recommender3.recommendation[0]["type"] == "mechanical_ventilation" assert len(recommender3.recommendation[0]["parts"]) == 1 assert recommender3.recommendation[0]["parts"][0]["description"] == 'Mechanical Extract Ventilation' diff --git a/recommendations/tests/test_wall_recommendations.py b/recommendations/tests/test_wall_recommendations.py index 580ebb91..a4093e58 100644 --- a/recommendations/tests/test_wall_recommendations.py +++ b/recommendations/tests/test_wall_recommendations.py @@ -10,8 +10,10 @@ from recommendations.tests.test_data.materials import materials from etl.epc.Record import EPCRecord +# import inspect +# file_path = inspect.getfile(lambda: None) # with open( -# os.path.abspath(os.path.dirname(__file__)) + "/recommendations/tests/test_data/input_properties.pkl", "rb" +# os.path.abspath(os.path.dirname(file_path)) + "/recommendations/tests/test_data/input_properties.pkl", "rb" # ) as f: # input_properties = pickle.load(f) @@ -86,17 +88,21 @@ class TestWallRecommendations: input_properties[1].walls["is_sandstone_or_limestone"] = False input_properties[1].age_band = "A" input_properties[1].restricted_measures = False + input_properties[1].already_installed = [] + input_properties[1].walls["is_park_home"] = False + input_properties[1].construction_age_band = "England and Wales: 1930-1949" + input_properties[1].non_invasive_recommendations = [] recommender = WallRecommendations( property_instance=input_properties[1], materials=materials ) assert recommender.property.walls["original_description"] == "Solid brick, as built, no insulation (assumed)" - assert not recommender.ewi_valid + assert not recommender.ewi_valid() assert recommender.property.in_conservation_area == "not_in_conservation_area" assert recommender.property.data["property-type"] == "Flat" - recommender.recommend() + recommender.recommend(phase=0) # This should result in some recommendations, all of which should be internal insulation assert recommender.recommendations @@ -131,7 +137,7 @@ class TestWallRecommendations: ) assert recommender.property.walls["original_description"] == "Solid brick, as built, insulated (assumed)" - assert not recommender.ewi_valid + assert not recommender.ewi_valid() assert recommender.property.in_conservation_area == "not_in_conservation_area" assert recommender.property.data["property-type"] == "Flat" assert recommender.estimated_u_value is None @@ -204,6 +210,11 @@ class TestWallRecommendationsBase: property_mock.restricted_measures = False property_mock.insulation_wall_area = 100 property_mock.data = {"county": "Derbyshire"} + property_mock.walls = { + "is_cob": False, + "is_sandstone_or_limestone": False, + "is_cavity_wall": False + } return property_mock @pytest.fixture @@ -216,24 +227,24 @@ class TestWallRecommendationsBase: def test_ewi_valid_in_conservation_area(self, wall_recommendations_instance): wall_recommendations_instance.property.in_conservation_area = "in_conversation_area" wall_recommendations_instance.property.restricted_measures = True - assert wall_recommendations_instance.ewi_valid is False + assert wall_recommendations_instance.ewi_valid() is False def test_ewi_valid_is_flat(self, wall_recommendations_instance): wall_recommendations_instance.property.data = {"property-type": "flat"} - assert wall_recommendations_instance.ewi_valid is False + assert wall_recommendations_instance.ewi_valid() is False def test_ewi_valid_not_in_conservation_area_and_not_flat(self, wall_recommendations_instance): wall_recommendations_instance.property.in_conservation_area = "not_in_conversation_area" wall_recommendations_instance.property.restricted_measures = False wall_recommendations_instance.property.data = {"property-type": "house"} - assert wall_recommendations_instance.ewi_valid is True + assert wall_recommendations_instance.ewi_valid() is True class TestCavityWallRecommensations: def test_fill_empty_cavity(self): epc_record = EPCRecord() - epc_record.prepared_epc = {"county": "Derbyshire"} + epc_record.prepared_epc = {"county": "Derbyshire", "walls-energy-eff": "Very Poor"} input_property = Property(id=1, postcode="F4k3", address="123 fake street", epc_record=epc_record) input_property.walls = { 'original_description': 'Cavity wall, as built, no insulation (assumed)', @@ -248,6 +259,7 @@ class TestCavityWallRecommensations: } input_property.age_band = "C" input_property.insulation_wall_area = 50 + input_property.construction_age_band = "England and Wales: 1930-1949" recommender = WallRecommendations( property_instance=input_property, @@ -261,14 +273,11 @@ class TestCavityWallRecommensations: assert recommender.recommendations assert recommender.estimated_u_value == 1.5 assert np.isclose(recommender.recommendations[0]["new_u_value"], 0.35) - assert np.isclose(recommender.recommendations[0]["total"], 1668.6600000000003) - - assert np.isclose(recommender.recommendations[1]["new_u_value"], 0.35) - assert np.isclose(recommender.recommendations[1]["total"], 2004.6600000000003) + assert np.isclose(recommender.recommendations[0]["total"], 710.5) def test_fill_partial_filled_cavity(self): epc_record = EPCRecord() - epc_record.prepared_epc = {"county": "County Durham"} + epc_record.prepared_epc = {"county": "County Durham", "walls-energy-eff": "Poor"} input_property = Property(id=1, postcode="F4k3", address="123 fake street", epc_record=epc_record) input_property.walls = { 'original_description': 'Cavity wall, as built, partial insulation (assumed)', @@ -283,6 +292,7 @@ class TestCavityWallRecommensations: } input_property.age_band = "C" input_property.insulation_wall_area = 50 + input_property.construction_age_band = "England and Wales: 1930-1949" recommender = WallRecommendations( property_instance=input_property, @@ -296,14 +306,13 @@ class TestCavityWallRecommensations: assert recommender.recommendations assert recommender.estimated_u_value == 1.3 assert np.isclose(recommender.recommendations[0]["new_u_value"], 0.41) - assert np.isclose(recommender.recommendations[0]["total"], 1663.9350000000002) - - assert np.isclose(recommender.recommendations[1]["new_u_value"], 0.41) - assert np.isclose(recommender.recommendations[1]["total"], 1999.9350000000002) + assert np.isclose(recommender.recommendations[0]["total"], 710.5) def test_system_built_wall(self): epc_record = EPCRecord() - epc_record.prepared_epc = {"property-type": "House", "county": "Derbyshire", "built-form": "Detached"} + epc_record.prepared_epc = { + "property-type": "House", "county": "Derbyshire", "built-form": "Detached", "walls-energy-eff": "Very Poor" + } input_property2 = Property(id=1, postcode="F4k3 2", address="223 fake street", epc_record=epc_record) input_property2.walls = { 'original_description': 'System built, as built, no insulation (assumed)', @@ -319,6 +328,7 @@ class TestCavityWallRecommensations: input_property2.age_band = "F" input_property2.insulation_wall_area = 120 input_property2.restricted_measures = False + input_property2.construction_age_band = "England and Wales: 1976-1982" assert input_property2.walls["is_system_built"] @@ -332,26 +342,24 @@ class TestCavityWallRecommensations: recommender2.recommend() assert recommender2.recommendations - assert len(recommender2.recommendations) == 9 + assert len(recommender2.recommendations) == 2 assert recommender2.estimated_u_value == 1 - assert np.isclose(recommender2.recommendations[0]["new_u_value"], 0.19) - assert np.isclose(recommender2.recommendations[0]["total"], 16429.960320000002) + assert np.isclose(recommender2.recommendations[0]["new_u_value"], 0.21) + assert np.isclose(recommender2.recommendations[0]["total"], 35802.0) assert recommender2.recommendations[0]["parts"][0]["type"] == "external_wall_insulation" - assert recommender2.recommendations[0]["parts"][0]["depth"] == 100 + assert recommender2.recommendations[0]["parts"][0]["depth"] == 150 - assert np.isclose(recommender2.recommendations[8]["new_u_value"], 0.23) - assert np.isclose(recommender2.recommendations[8]["total"], 11292.768) - assert recommender2.recommendations[8]["parts"][0]["type"] == "internal_wall_insulation" - assert recommender2.recommendations[8]["parts"][0]["depth"] == 72.5 - - assert np.isclose(recommender2.recommendations[6]["new_u_value"], 0.29) - assert np.isclose(recommender2.recommendations[6]["total"], 10988.208) - assert recommender2.recommendations[6]["parts"][0]["type"] == "internal_wall_insulation" - assert recommender2.recommendations[6]["parts"][0]["depth"] == 52.5 + assert np.isclose(recommender2.recommendations[1]["new_u_value"], 0.26) + assert np.isclose(recommender2.recommendations[1]["total"], 29376) + assert recommender2.recommendations[1]["parts"][0]["type"] == "internal_wall_insulation" + assert recommender2.recommendations[1]["parts"][0]["depth"] == 95 def test_timber_frame_wall(self): epc_record = EPCRecord() - epc_record.prepared_epc = {"property-type": "House", "county": "Derbyshire", "built-form": "Semi-Detached"} + epc_record.prepared_epc = { + "property-type": "House", "county": "Derbyshire", "built-form": "Semi-Detached", + "walls-energy-eff": "Very Poor" + } input_property3 = Property(id=1, postcode="F4k3 2", address="223 fake street", epc_record=epc_record) input_property3.walls = { 'original_description': 'Timber frame, as built, no insulation (assumed)', @@ -367,6 +375,7 @@ class TestCavityWallRecommensations: input_property3.age_band = "B" input_property3.insulation_wall_area = 99 input_property3.restricted_measures = False + input_property3.construction_age_band = "England and Wales: 1950-1966" assert input_property3.walls["is_timber_frame"] @@ -380,21 +389,24 @@ class TestCavityWallRecommensations: recommender3.recommend() assert recommender3.recommendations - assert len(recommender3.recommendations) == 6 + assert len(recommender3.recommendations) == 2 assert recommender3.estimated_u_value == 1.9 - assert np.isclose(recommender3.recommendations[0]["new_u_value"], 0.2) - assert np.isclose(recommender3.recommendations[0]["total"], 13554.717263999999) + assert np.isclose(recommender3.recommendations[0]["new_u_value"], 0.23) + assert np.isclose(recommender3.recommendations[0]["total"], 29536.65) assert recommender3.recommendations[0]["parts"][0]["type"] == "external_wall_insulation" - assert recommender3.recommendations[0]["parts"][0]["depth"] == 100.0 + assert recommender3.recommendations[0]["parts"][0]["depth"] == 150.0 - assert np.isclose(recommender3.recommendations[1]["new_u_value"], 0.23) - assert np.isclose(recommender3.recommendations[1]["total"], 35206.19308800001) - assert recommender3.recommendations[1]["parts"][0]["type"] == "external_wall_insulation" - assert recommender3.recommendations[1]["parts"][0]["depth"] == 150.0 + assert np.isclose(recommender3.recommendations[1]["new_u_value"], 0.29) + assert np.isclose(recommender3.recommendations[1]["total"], 24235.2) + assert recommender3.recommendations[1]["parts"][0]["type"] == "internal_wall_insulation" + assert recommender3.recommendations[1]["parts"][0]["depth"] == 95.0 def test_granite_or_whinstone_wall(self): epc_record = EPCRecord() - epc_record.prepared_epc = {"property-type": "Bungalow", "county": "Derbyshire", "built-form": "Detached"} + epc_record.prepared_epc = { + "property-type": "Bungalow", "county": "Derbyshire", "built-form": "Detached", + "walls-energy-eff": "Very Poor" + } input_property4 = Property(id=1, postcode="F4k3 2", address="223 fake street", epc_record=epc_record) input_property4.walls = { 'original_description': 'Granite or whinstone, as built, no insulation (assumed)', @@ -410,6 +422,7 @@ class TestCavityWallRecommensations: input_property4.age_band = "A" input_property4.insulation_wall_area = 223 input_property4.restricted_measures = False + input_property4.construction_age_band = "England and Wales: before 1900" assert input_property4.walls["is_granite_or_whinstone"] @@ -423,21 +436,24 @@ class TestCavityWallRecommensations: recommender4.recommend() assert recommender4.recommendations - assert len(recommender4.recommendations) == 6 + assert len(recommender4.recommendations) == 2 assert recommender4.estimated_u_value == 2.3 - assert np.isclose(recommender4.recommendations[0]["new_u_value"], 0.21) - assert np.isclose(recommender4.recommendations[0]["total"], 29547.42864) + assert np.isclose(recommender4.recommendations[0]["new_u_value"], 0.23) + assert np.isclose(recommender4.recommendations[0]["total"], 66532.05) assert recommender4.recommendations[0]["parts"][0]["type"] == "external_wall_insulation" - assert recommender4.recommendations[0]["parts"][0]["depth"] == 100 + assert recommender4.recommendations[0]["parts"][0]["depth"] == 150 - assert np.isclose(recommender4.recommendations[1]["new_u_value"], 0.23) - assert np.isclose(recommender4.recommendations[1]["total"], 76744.68288000001) - assert recommender4.recommendations[1]["parts"][0]["type"] == "external_wall_insulation" - assert recommender4.recommendations[1]["parts"][0]["depth"] == 150 + assert np.isclose(recommender4.recommendations[1]["new_u_value"], 0.3) + assert np.isclose(recommender4.recommendations[1]["total"], 54590.4) + assert recommender4.recommendations[1]["parts"][0]["type"] == "internal_wall_insulation" + assert recommender4.recommendations[1]["parts"][0]["depth"] == 95 def test_cob_wall(self): epc_record = EPCRecord() - epc_record.prepared_epc = {"property-type": "Bungalow", "county": "Derbyshire", "built-form": "Detached"} + epc_record.prepared_epc = { + "property-type": "Bungalow", "county": "Derbyshire", "built-form": "Detached", + "walls-energy-eff": "Very Poor" + } input_property5 = Property(id=1, postcode="F4k3 2", address="223 fake street", epc_record=epc_record) input_property5.walls = { 'original_description': 'Cob, as built', @@ -453,6 +469,7 @@ class TestCavityWallRecommensations: input_property5.age_band = "E" input_property5.insulation_wall_area = 77 input_property5.restricted_measures = False + input_property5.construction_age_band = "England and Wales: 1967-1975" assert input_property5.walls["is_cob"] @@ -465,22 +482,15 @@ class TestCavityWallRecommensations: recommender5.recommend() - assert recommender5.recommendations - assert len(recommender5.recommendations) == 5 - assert recommender5.estimated_u_value == 0.8 - assert np.isclose(recommender5.recommendations[0]["new_u_value"], 0.29) - assert np.isclose(recommender5.recommendations[0]["total"], 8963.834880000002) - assert recommender5.recommendations[0]["parts"][0]["type"] == "external_wall_insulation" - assert recommender5.recommendations[0]["parts"][0]["depth"] == 50 - - assert np.isclose(recommender5.recommendations[3]["new_u_value"], 0.26) - assert np.isclose(recommender5.recommendations[3]["total"], 20771.11344) - assert recommender5.recommendations[3]["parts"][0]["type"] == "internal_wall_insulation" - assert recommender5.recommendations[3]["parts"][0]["depth"] == 100 + # No insulation recommendations for cob walls + assert not recommender5.recommendations def test_sandstone_or_limestone_wall(self): epc_record = EPCRecord() - epc_record.prepared_epc = {"property-type": "House", "county": "Derbyshire", "built-form": "Mid-Terrace"} + epc_record.prepared_epc = { + "property-type": "House", "county": "Derbyshire", "built-form": "Mid-Terrace", + "walls-energy-eff": "Very Poor" + } input_property6 = Property(id=1, postcode="F4k3 6", address="623 fake street", epc_record=epc_record) input_property6.walls = { 'original_description': 'Sandstone or limestone, as built, no insulation (assumed)', @@ -496,6 +506,7 @@ class TestCavityWallRecommensations: input_property6.age_band = "F" input_property6.insulation_wall_area = 350 input_property6.restricted_measures = False + input_property6.construction_age_band = "England and Wales: 1976-1982" assert input_property6.walls["is_sandstone_or_limestone"] @@ -508,20 +519,11 @@ class TestCavityWallRecommensations: recommender6.recommend() + # For sandstone walls, we only recommend internal wall insulation assert recommender6.recommendations - assert len(recommender6.recommendations) == 9 + assert len(recommender6.recommendations) == 1 assert recommender6.estimated_u_value == 1 - assert np.isclose(recommender6.recommendations[0]["new_u_value"], 0.19) - assert np.isclose(recommender6.recommendations[0]["total"], 46374.888000000006) - assert recommender6.recommendations[0]["parts"][0]["type"] == "external_wall_insulation" - assert recommender6.recommendations[0]["parts"][0]["depth"] == 100 - - assert np.isclose(recommender6.recommendations[2]["new_u_value"], 0.21) - assert np.isclose(recommender6.recommendations[2]["total"], 120451.29600000002) - assert recommender6.recommendations[2]["parts"][0]["type"] == "external_wall_insulation" - assert recommender6.recommendations[2]["parts"][0]["depth"] == 150 - - assert np.isclose(recommender6.recommendations[4]["new_u_value"], 0.28) - assert np.isclose(recommender6.recommendations[4]["total"], 94414.15199999999) - assert recommender6.recommendations[4]["parts"][0]["type"] == "internal_wall_insulation" - assert recommender6.recommendations[4]["parts"][0]["depth"] == 100 + assert np.isclose(recommender6.recommendations[0]["new_u_value"], 0.26) + assert np.isclose(recommender6.recommendations[0]["total"], 85680.0) + assert recommender6.recommendations[0]["parts"][0]["type"] == "internal_wall_insulation" + assert recommender6.recommendations[0]["parts"][0]["depth"] == 95 diff --git a/recommendations/tests/test_window_recommendations.py b/recommendations/tests/test_window_recommendations.py index 36e70834..0e36d105 100644 --- a/recommendations/tests/test_window_recommendations.py +++ b/recommendations/tests/test_window_recommendations.py @@ -2,6 +2,8 @@ from recommendations.WindowsRecommendations import WindowsRecommendations from backend.Property import Property from recommendations.tests.test_data.materials import materials from etl.epc.Record import EPCRecord +import msgpack +from utils.s3 import read_dataframe_from_s3_parquet, read_from_s3 class TestWindowRecommendations: @@ -15,7 +17,8 @@ class TestWindowRecommendations: epc_record.prepared_epc = { "county": "Wychavon", "multi-glaze-proportion": 0, - "uprn": 0 + "uprn": 0, + "windows-energy-eff": "Very Poor" } property_1 = Property( id=1, @@ -36,12 +39,26 @@ class TestWindowRecommendations: recommender.recommend() + # The home is going from single glazing (v poor energy eff) -> double glazing (average energy eff) + assert recommender.recommendation == [ - {'parts': [], 'type': 'windows_glazing', 'description': 'Install double glazing to all windows', - 'starting_u_value': None, 'new_u_value': None, 'sap_points': None, 'total': 5721.943248, - 'subtotal': 4768.28604, 'vat': 953.6572080000001, 'contingency': 340.59186, 'preliminaries': 340.59186, - 'material': 1275.75, 'profit': 681.18372, 'labour_hours': 45.5, 'labour_cost': 994.8624, - 'labour_days': 2.84375, 'is_secondary_glazing': False}] + { + 'phase': 0, 'parts': [], 'type': 'windows_glazing', + 'description': 'Install double glazing to all windows', + 'starting_u_value': None, 'new_u_value': None, 'sap_points': None, 'already_installed': False, + 'total': 7980.0, 'labour_hours': 0.0, 'labour_days': 0.0, 'is_secondary_glazing': False, + 'description_simulation': { + 'multi-glaze-proportion': 100, 'windows-energy-eff': 'Average', + 'windows-description': 'Fully double glazed', + 'glazed-type': 'double glazing installed during or after 2002' + }, + 'simulation_config': { + 'has_glazing_ending': True, 'glazing_type_ending': 'double', + 'multi_glaze_proportion_ending': 100, 'windows_energy_eff_ending': 'Average', + 'glazed_type_ending': 'double glazing installed during or after 2002' + } + } + ] def test_partial_double_glazed(self): """ @@ -53,7 +70,8 @@ class TestWindowRecommendations: epc_record.prepared_epc = { "county": "Wychavon", "multi-glaze-proportion": 33, - "uprn": 0 + "uprn": 0, + "windows-energy-eff": "Good" # This has been observed in the EPC data } property_2 = Property( id=1, @@ -73,11 +91,24 @@ class TestWindowRecommendations: recommender2.recommend() assert recommender2.recommendation == [ - {'parts': [], 'type': 'windows_glazing', 'description': 'Install double glazing to the remaining windows', - 'starting_u_value': None, 'new_u_value': None, 'sap_points': None, 'total': 4087.10232, - 'subtotal': 3405.9186, 'vat': 681.18372, 'contingency': 243.2799, 'preliminaries': 243.2799, - 'material': 911.25, 'profit': 486.5598, 'labour_hours': 32.5, 'labour_cost': 710.6160000000001, - 'labour_days': 2.03125, 'is_secondary_glazing': False}] + { + 'phase': 0, 'parts': [], 'type': 'windows_glazing', + 'description': 'Install double glazing to the remaining windows', 'starting_u_value': None, + 'new_u_value': None, 'sap_points': None, 'already_installed': False, 'total': 5700.0, + 'labour_hours': 0.0, + 'labour_days': 0.0, 'is_secondary_glazing': False, + 'description_simulation': { + 'multi-glaze-proportion': 100, 'windows-energy-eff': 'Good', + 'windows-description': 'Fully double glazed', + 'glazed-type': 'double glazing installed during or after 2002' + }, + 'simulation_config': { + 'glazing_coverage_ending': 'full', 'multi_glaze_proportion_ending': 100, + 'windows_energy_eff_ending': 'Good', 'glazing_type_ending': 'double', + 'glazed_type_ending': 'double glazing installed during or after 2002' + } + } + ] def test_fully_double_glazed(self): """ @@ -140,7 +171,8 @@ class TestWindowRecommendations: epc_record.prepared_epc = { "county": "Wychavon", "multi-glaze-proportion": 50, - "uprn": 0 + "uprn": 0, + "windows-energy-eff": "Poor" # This has been observed in the EPC data } property_5 = Property( id=1, @@ -160,19 +192,31 @@ class TestWindowRecommendations: recommender5.recommend() assert recommender5.recommendation == [ - {'parts': [], 'type': 'windows_glazing', - 'description': 'Install secondary glazing to the remaining windows', - 'starting_u_value': None, 'new_u_value': None, 'sap_points': None, 'total': 1089.893952, - 'subtotal': 908.24496, 'vat': 181.64899200000002, 'contingency': 64.87464, 'preliminaries': 64.87464, - 'material': 729.0, 'profit': 129.74928, 'labour_hours': 13.0, 'labour_cost': 568.4928, - 'labour_days': 0.8125, 'is_secondary_glazing': True}] + { + 'phase': 0, 'parts': [], 'type': 'windows_glazing', + 'description': 'Install secondary glazing to the remaining windows', 'starting_u_value': None, + 'new_u_value': None, 'sap_points': None, 'already_installed': False, 'total': 4560.0, + 'labour_hours': 0.0, 'labour_days': 0.0, 'is_secondary_glazing': True, + 'description_simulation': { + 'multi-glaze-proportion': 100, 'windows-energy-eff': 'Good', + 'windows-description': 'Full secondary glazing', + 'glazed-type': 'secondary glazing' + }, + 'simulation_config': { + 'glazing_coverage_ending': 'full', 'multi_glaze_proportion_ending': 100, + 'windows_energy_eff_ending': 'Good', 'glazing_type_ending': 'secondary', + 'glazed_type_ending': 'secondary glazing' + } + } + ] def test_single_glazed_restricted_measures(self): epc_record = EPCRecord() epc_record.prepared_epc = { "county": "Wychavon", "multi-glaze-proportion": 0, - "uprn": 0 + "uprn": 0, + "windows-energy-eff": "Very Poor" } property_6 = Property( @@ -195,14 +239,23 @@ class TestWindowRecommendations: recommender6.recommend() assert recommender6.recommendation == [ - {'parts': [], 'type': 'windows_glazing', - 'description': 'Install secondary glazing to all windows. Secondary ' - 'glazing recommended due to herigate building status', - 'starting_u_value': None, 'new_u_value': None, 'sap_points': None, - 'total': 1907.314416, 'subtotal': 1589.42868, 'vat': 317.885736, - 'contingency': 113.53062, 'preliminaries': 113.53062, - 'material': 1275.75, 'profit': 227.06124, 'labour_hours': 22.75, - 'labour_cost': 994.8624, 'labour_days': 1.421875, 'is_secondary_glazing': True} + { + 'phase': 0, 'parts': [], 'type': 'windows_glazing', + 'description': 'Install secondary glazing to all windows. Secondary glazing recommended due to ' + 'herigate building status', + 'starting_u_value': None, 'new_u_value': None, 'sap_points': None, 'already_installed': False, + 'total': 7980.0, 'labour_hours': 0.0, 'labour_days': 0.0, 'is_secondary_glazing': True, + 'description_simulation': { + 'multi-glaze-proportion': 100, 'windows-energy-eff': 'Good', + 'windows-description': 'Full secondary glazing', + 'glazed-type': 'secondary glazing' + }, + 'simulation_config': { + 'has_glazing_ending': True, 'glazing_coverage_ending': 'full', + 'glazing_type_ending': 'secondary', 'multi_glaze_proportion_ending': 100, + 'windows_energy_eff_ending': 'Good', 'glazed_type_ending': 'secondary glazing' + } + } ] def test_full_triple_glazed(self): @@ -233,7 +286,7 @@ class TestWindowRecommendations: def test_partial_triple_glazed(self): """ - We should just recommend double glazing to the remaining windows, since it's a cheaper option + We don't recommend anything here """ epc_record = EPCRecord() epc_record.prepared_epc = { @@ -258,9 +311,362 @@ class TestWindowRecommendations: recommender8.recommend() - assert recommender8.recommendation == [ - {'parts': [], 'type': 'windows_glazing', 'description': 'Install double glazing to the remaining windows', - 'starting_u_value': None, 'new_u_value': None, 'sap_points': None, 'total': 1634.840928, - 'subtotal': 1362.36744, 'vat': 272.47348800000003, 'contingency': 97.31196, 'preliminaries': 97.31196, - 'material': 364.5, 'profit': 194.62392, 'labour_hours': 13.0, 'labour_cost': 284.2464, - 'labour_days': 0.8125, 'is_secondary_glazing': False}] + assert not recommender8.recommendation + + def test_simulating_outcome_single_glazed(self): + # Could move these to fixtures + cleaning_data = read_dataframe_from_s3_parquet( + bucket_name="retrofit-data-dev", file_key="sap_change_model/cleaning_dataset.parquet", + ) + cleaned = read_from_s3(s3_file_name="cleaned_epc_data/cleaned.bson", bucket_name="retrofit-data-dev") + cleaned = msgpack.unpackb(cleaned, raw=False) + + epc = { + 'lmk-key': 'f4cf43c90ab3140112a9d1c8cfb21ec1bf73f5a2ca3c75118f289d3447dddf15', 'address1': '3 The Green', + 'address2': 'Old Dalby', 'address3': None, 'postcode': 'LE14 3LL', + 'building-reference-number': 10006291833, 'current-energy-rating': 'E', 'potential-energy-rating': 'B', + 'current-energy-efficiency': 47, 'potential-energy-efficiency': 82, 'property-type': 'House', + 'built-form': 'Semi-Detached', 'inspection-date': '2024-07-19', 'local-authority': 'E07000133', + 'constituency': 'E14000909', 'county': 'Leicestershire', 'lodgement-date': '2024-07-21', + 'transaction-type': 'rental', 'environment-impact-current': 41, 'environment-impact-potential': 79, + 'energy-consumption-current': 478, 'energy-consumption-potential': 155.0, 'co2-emissions-current': 5.1, + 'co2-emiss-curr-per-floor-area': 85, 'co2-emissions-potential': 1.7, 'lighting-cost-current': 91.0, + 'lighting-cost-potential': 91.0, 'heating-cost-current': 1677.0, 'heating-cost-potential': 874.0, + 'hot-water-cost-current': 161.0, 'hot-water-cost-potential': 109.0, 'total-floor-area': 61.0, + 'energy-tariff': 'dual', 'mains-gas-flag': 'Y', 'floor-level': None, 'flat-top-storey': None, + 'flat-storey-count': None, 'main-heating-controls': None, 'multi-glaze-proportion': 0.0, + 'glazed-type': 'not defined', 'glazed-area': 'Normal', 'extension-count': 3.0, + 'number-habitable-rooms': 4.0, 'number-heated-rooms': 4.0, 'low-energy-lighting': 100.0, + 'number-open-fireplaces': 0.0, 'hotwater-description': 'From main system', + 'hot-water-energy-eff': 'Good', 'hot-water-env-eff': 'Good', + 'floor-description': 'Solid, no insulation (assumed)', 'floor-energy-eff': None, 'floor-env-eff': None, + 'windows-description': 'Single glazed', 'windows-energy-eff': 'Very Poor', + 'windows-env-eff': 'Very Poor', 'walls-description': 'Solid brick, as built, no insulation (assumed)', + 'walls-energy-eff': 'Very Poor', 'walls-env-eff': 'Very Poor', 'secondheat-description': 'None', + 'sheating-energy-eff': None, 'sheating-env-eff': None, + 'roof-description': 'Pitched, no insulation (assumed)', 'roof-energy-eff': 'Very Poor', + 'roof-env-eff': 'Very Poor', 'mainheat-description': 'Boiler and radiators, mains gas', + 'mainheat-energy-eff': 'Good', 'mainheat-env-eff': 'Good', + 'mainheatcont-description': 'Programmer and room thermostat', 'mainheatc-energy-eff': 'Average', + 'mainheatc-env-eff': 'Average', 'lighting-description': 'Low energy lighting in all fixed outlets', + 'lighting-energy-eff': 'Very Good', 'lighting-env-eff': 'Very Good', + 'main-fuel': 'mains gas (not community)', 'wind-turbine-count': 0.0, 'heat-loss-corridor': None, + 'unheated-corridor-length': None, 'floor-height': 2.37, 'photo-supply': 0.0, + 'solar-water-heating-flag': 'N', 'mechanical-ventilation': 'natural', + 'address': '3 The Green, Old Dalby', 'local-authority-label': 'Melton', + 'constituency-label': 'Rutland and Melton', 'posttown': 'MELTON MOWBRAY', + 'construction-age-band': 'England and Wales: before 1900', 'lodgement-datetime': '2024-07-21 19:29:04', + 'tenure': 'Rented (private)', 'fixed-lighting-outlets-count': 7.0, 'low-energy-fixed-light-count': None, + 'uprn': 200001041444.0, 'uprn-source': 'Energy Assessor' + } + + epc_records = { + "original_epc": epc, + "full_sap_epc": {}, + "old_data": [] + } + + epc_record = EPCRecord( + epc_records=epc_records, + run_mode="newdata", + cleaning_data=cleaning_data + ) + + property_9 = Property( + id=1, + postcode='1', + address='1', + epc_record=epc_record + ) + property_9.windows = { + 'original_description': 'Single glazed', 'has_glazing': False, 'glazing_coverage': None, + 'glazing_type': 'single', + 'no_data': False + } + + property_9.number_of_windows = 7 + property_9.restricted_measures = False + property_9.is_heritage = False + + recommender9 = WindowsRecommendations(property_instance=property_9, materials=materials) + + assert not recommender9.recommendation + + recommender9.recommend() + + assert recommender9.recommendation == [ + { + 'phase': 0, 'parts': [], 'type': 'windows_glazing', + 'description': 'Install double glazing to all windows', 'starting_u_value': None, 'new_u_value': None, + 'sap_points': None, 'already_installed': False, 'total': 7980.0, 'labour_hours': 0.0, + 'labour_days': 0.0, 'is_secondary_glazing': False, + 'description_simulation': { + 'multi-glaze-proportion': 100, 'windows-energy-eff': 'Average', + 'windows-description': 'Fully double glazed', + 'glazed-type': 'double glazing installed during or after 2002' + }, + 'simulation_config': { + 'has_glazing_ending': True, 'glazing_coverage_ending': 'full', + 'glazing_type_ending': 'double', 'multi_glaze_proportion_ending': 100, + 'windows_energy_eff_ending': 'Average', + 'glazed_type_ending': 'double glazing installed during or after 2002' + } + } + ] + + # We now simulate the outcome + windows_rec = recommender9.recommendation.copy() + windows_rec[0]["recommendation_id"] = 1 + property_recommendations = [windows_rec] + + property_9.create_base_difference_epc_record(cleaned_lookup=cleaned) + + starting_record = property_9.base_difference_record.df.to_dict("records")[0] + + expected_base_difference_record = { + 'uprn': 200001041444, 'rdsap_change': 0, 'heat_demand_change': 0, 'carbon_change': 0.0, + 'potential_energy_efficiency': 82.0, 'environment_impact_potential': 79.0, + 'energy_consumption_potential': 155.0, 'co2_emissions_potential': 1.7, 'property_type': 'House', + 'built_form': 'Semi-Detached', 'constituency': 'E14000909', 'number_habitable_rooms': 4.0, + 'number_heated_rooms': 4.0, 'construction_age_band': 'England and Wales: before 1900', + 'fixed_lighting_outlets_count': 7.0, 'walls_thermal_transmittance': 1.7, + 'walls_thermal_transmittance_unit': 'Unknown', 'is_cavity_wall': False, 'is_filled_cavity': False, + 'is_solid_brick': True, 'is_system_built': False, 'is_timber_frame': False, + 'is_granite_or_whinstone': False, 'is_as_built': True, 'is_cob': False, 'walls_is_assumed': True, + 'is_sandstone_or_limestone': False, 'is_park_home': False, 'walls_insulation_thickness': 'none', + 'external_insulation': False, 'internal_insulation': False, 'floor_thermal_transmittance': 0.96, + 'is_to_unheated_space': False, 'is_to_external_air': False, 'is_suspended': False, 'is_solid': True, + 'another_property_below': False, 'floor_insulation_thickness': 'none', 'roof_thermal_transmittance': 2.3, + 'is_pitched': True, 'is_roof_room': False, 'is_loft': False, 'is_flat': False, 'is_thatched': False, + 'is_at_rafters': False, 'has_dwelling_above': False, 'roof_insulation_thickness': 'none', + 'heater_type': 'Unknown', 'system_type': 'from main system', 'thermostat_characteristics': 'Unknown', + 'heating_scope': 'Unknown', 'energy_recovery': 'Unknown', 'hotwater_tariff_type': 'Unknown', + 'extra_features': 'Unknown', 'chp_systems': 'Unknown', 'distribution_system': 'Unknown', + 'no_system_present': 'Unknown', 'appliance': 'Unknown', 'has_radiators': True, 'has_fan_coil_units': False, + 'has_pipes_in_screed_above_insulation': False, 'has_pipes_in_insulated_timber_floor': False, + 'has_pipes_in_concrete_slab': False, 'has_boiler': True, 'has_air_source_heat_pump': False, + 'has_room_heaters': False, 'has_electric_storage_heaters': False, 'has_warm_air': False, + 'has_electric_underfloor_heating': False, 'has_electric_ceiling_heating': False, + 'has_community_scheme': False, 'has_ground_source_heat_pump': False, 'has_no_system_present': False, + 'has_portable_electric_heaters': False, 'has_water_source_heat_pump': False, + 'has_electric_heat_pump': False, 'has_micro-cogeneration': False, 'has_solar_assisted_heat_pump': False, + 'has_exhaust_source_heat_pump': False, 'has_community_heat_pump': False, 'has_electric': False, + 'has_mains_gas': True, 'has_wood_logs': False, 'has_coal': False, 'has_oil': False, + 'has_wood_pellets': False, 'has_anthracite': False, 'has_dual_fuel_mineral_and_wood': False, + 'has_smokeless_fuel': False, 'has_lpg': False, 'has_b30k': False, 'has_electricaire': False, + 'has_assumed_for_most_rooms': False, 'has_underfloor_heating': False, + 'thermostatic_control': 'room thermostat', 'charging_system': 'Unknown', 'switch_system': 'programmer', + 'no_control': 'Unknown', 'dhw_control': 'Unknown', 'community_heating': 'Unknown', + 'multiple_room_thermostats': False, 'auxiliary_systems': 'Unknown', 'trvs': 'Unknown', + 'rate_control': 'Unknown', 'glazing_type': 'single', 'fuel_type': 'mains gas', + 'main-fuel_tariff_type': 'Unknown', 'is_community': False, + 'no_individual_heating_or_community_network': False, 'complex_fuel_type': 'Unknown', + 'walls_thermal_transmittance_ending': 1.7, 'walls_thermal_transmittance_unit_ending': 'Unknown', + 'is_filled_cavity_ending': False, 'is_as_built_ending': True, 'walls_is_assumed_ending': True, + 'is_park_home_ending': False, 'walls_insulation_thickness_ending': 'none', + 'external_insulation_ending': False, 'internal_insulation_ending': False, + 'floor_thermal_transmittance_ending': 0.96, 'floor_insulation_thickness_ending': 'none', + 'roof_thermal_transmittance_ending': 2.3, 'is_at_rafters_ending': False, + 'roof_insulation_thickness_ending': 'none', 'heater_type_ending': 'Unknown', + 'system_type_ending': 'from main system', 'thermostat_characteristics_ending': 'Unknown', + 'heating_scope_ending': 'Unknown', 'energy_recovery_ending': 'Unknown', + 'hotwater_tariff_type_ending': 'Unknown', 'extra_features_ending': 'Unknown', + 'chp_systems_ending': 'Unknown', 'distribution_system_ending': 'Unknown', + 'no_system_present_ending': 'Unknown', 'appliance_ending': 'Unknown', 'has_radiators_ending': True, + 'has_fan_coil_units_ending': False, 'has_pipes_in_screed_above_insulation_ending': False, + 'has_pipes_in_insulated_timber_floor_ending': False, 'has_pipes_in_concrete_slab_ending': False, + 'has_boiler_ending': True, 'has_air_source_heat_pump_ending': False, 'has_room_heaters_ending': False, + 'has_electric_storage_heaters_ending': False, 'has_warm_air_ending': False, + 'has_electric_underfloor_heating_ending': False, 'has_electric_ceiling_heating_ending': False, + 'has_community_scheme_ending': False, 'has_ground_source_heat_pump_ending': False, + 'has_no_system_present_ending': False, 'has_portable_electric_heaters_ending': False, + 'has_water_source_heat_pump_ending': False, 'has_electric_heat_pump_ending': False, + 'has_micro-cogeneration_ending': False, 'has_solar_assisted_heat_pump_ending': False, + 'has_exhaust_source_heat_pump_ending': False, 'has_community_heat_pump_ending': False, + 'has_electric_ending': False, 'has_mains_gas_ending': True, 'has_wood_logs_ending': False, + 'has_coal_ending': False, 'has_oil_ending': False, 'has_wood_pellets_ending': False, + 'has_anthracite_ending': False, 'has_dual_fuel_mineral_and_wood_ending': False, + 'has_smokeless_fuel_ending': False, 'has_lpg_ending': False, 'has_b30k_ending': False, + 'has_electricaire_ending': False, 'has_assumed_for_most_rooms_ending': False, + 'has_underfloor_heating_ending': False, 'thermostatic_control_ending': 'room thermostat', + 'charging_system_ending': 'Unknown', 'switch_system_ending': 'programmer', 'no_control_ending': 'Unknown', + 'dhw_control_ending': 'Unknown', 'community_heating_ending': 'Unknown', + 'multiple_room_thermostats_ending': False, 'auxiliary_systems_ending': 'Unknown', 'trvs_ending': 'Unknown', + 'rate_control_ending': 'Unknown', 'glazing_type_ending': 'single', 'fuel_type_ending': 'mains gas', + 'main-fuel_tariff_type_ending': 'Unknown', 'is_community_ending': False, + 'no_individual_heating_or_community_network_ending': False, 'complex_fuel_type_ending': 'Unknown', + 'sap_starting': 47, 'sap_ending': 47, 'heat_demand_starting': 478, 'heat_demand_ending': 478, + 'carbon_starting': 5.1, 'carbon_ending': 5.1, 'lighting_cost_starting': 91.0, 'lighting_cost_ending': 91.0, + 'heating_cost_starting': 1677.0, 'heating_cost_ending': 1677.0, 'hot_water_cost_starting': 161.0, + 'hot_water_cost_ending': 161.0, 'mechanical_ventilation_starting': 'natural', + 'mechanical_ventilation_ending': 'natural', 'secondheat_description_starting': 'None', + 'secondheat_description_ending': 'None', 'glazed_type_starting': 'not defined', + 'glazed_type_ending': 'not defined', 'multi_glaze_proportion_starting': 0.0, + 'multi_glaze_proportion_ending': 0.0, 'low_energy_lighting_starting': 100.0, + 'low_energy_lighting_ending': 100.0, 'number_open_fireplaces_starting': 0.0, + 'number_open_fireplaces_ending': 0.0, 'solar_water_heating_flag_starting': 'N', + 'solar_water_heating_flag_ending': 'N', 'photo_supply_starting': 0.0, 'photo_supply_ending': 0.0, + 'transaction_type_starting': 'rental', 'transaction_type_ending': 'rental', + 'energy_tariff_starting': 'dual', 'energy_tariff_ending': 'dual', 'extension_count_starting': 3.0, + 'extension_count_ending': 3.0, 'total_floor_area_starting': 61.0, 'total_floor_area_ending': 61.0, + 'floor_height_starting': 2.37, 'floor_height_ending': 2.37, 'hot_water_energy_eff_starting': 'Good', + 'hot_water_energy_eff_ending': 'Good', 'floor_energy_eff_starting': 'NO_RATING', + 'floor_energy_eff_ending': 'NO_RATING', 'windows_energy_eff_starting': 'Very Poor', + 'windows_energy_eff_ending': 'Very Poor', 'walls_energy_eff_starting': 'Very Poor', + 'walls_energy_eff_ending': 'Very Poor', 'sheating_energy_eff_starting': 'NO_RATING', + 'sheating_energy_eff_ending': 'NO_RATING', 'roof_energy_eff_starting': 'Very Poor', + 'roof_energy_eff_ending': 'Very Poor', 'mainheat_energy_eff_starting': 'Good', + 'mainheat_energy_eff_ending': 'Good', 'mainheatc_energy_eff_starting': 'Average', + 'mainheatc_energy_eff_ending': 'Average', 'lighting_energy_eff_starting': 'Very Good', + 'lighting_energy_eff_ending': 'Very Good', 'number_habitable_rooms_starting': 4.0, + 'number_habitable_rooms_ending': 4.0, 'number_heated_rooms_starting': 4.0, + 'number_heated_rooms_ending': 4.0, 'days_to_starting': 3642, 'days_to_ending': 3642, + 'estimated_perimeter_starting': 23.430749027719962, 'estimated_perimeter_ending': 23.430749027719962 + } + + assert starting_record == expected_base_difference_record + + # Simulate outcome + property_9.adjust_difference_record_with_recommendations( + property_recommendations, windows_rec + ) + + simulated_data = property_9.recommendations_scoring_data.copy() + + assert len(simulated_data) == 1 + + expected_simulated_outcome = { + 'uprn': 200001041444, 'rdsap_change': 0, 'heat_demand_change': 0, 'carbon_change': 0.0, + 'potential_energy_efficiency': 82.0, 'environment_impact_potential': 79.0, + 'energy_consumption_potential': 155.0, 'co2_emissions_potential': 1.7, 'property_type': 'House', + 'built_form': 'Semi-Detached', 'constituency': 'E14000909', 'number_habitable_rooms': 4.0, + 'number_heated_rooms': 4.0, 'construction_age_band': 'England and Wales: before 1900', + 'fixed_lighting_outlets_count': 7.0, 'walls_thermal_transmittance': 1.7, + 'walls_thermal_transmittance_unit': 'Unknown', 'is_cavity_wall': False, 'is_filled_cavity': False, + 'is_solid_brick': True, 'is_system_built': False, 'is_timber_frame': False, + 'is_granite_or_whinstone': False, 'is_as_built': True, 'is_cob': False, 'walls_is_assumed': True, + 'is_sandstone_or_limestone': False, 'is_park_home': False, 'walls_insulation_thickness': 'none', + 'external_insulation': False, 'internal_insulation': False, 'floor_thermal_transmittance': 0.96, + 'is_to_unheated_space': False, 'is_to_external_air': False, 'is_suspended': False, 'is_solid': True, + 'another_property_below': False, 'floor_insulation_thickness': 'none', 'roof_thermal_transmittance': 2.3, + 'is_pitched': True, 'is_roof_room': False, 'is_loft': False, 'is_flat': False, 'is_thatched': False, + 'is_at_rafters': False, 'has_dwelling_above': False, 'roof_insulation_thickness': 'none', + 'heater_type': 'Unknown', 'system_type': 'from main system', 'thermostat_characteristics': 'Unknown', + 'heating_scope': 'Unknown', 'energy_recovery': 'Unknown', 'hotwater_tariff_type': 'Unknown', + 'extra_features': 'Unknown', 'chp_systems': 'Unknown', 'distribution_system': 'Unknown', + 'no_system_present': 'Unknown', 'appliance': 'Unknown', 'has_radiators': True, 'has_fan_coil_units': False, + 'has_pipes_in_screed_above_insulation': False, 'has_pipes_in_insulated_timber_floor': False, + 'has_pipes_in_concrete_slab': False, 'has_boiler': True, 'has_air_source_heat_pump': False, + 'has_room_heaters': False, 'has_electric_storage_heaters': False, 'has_warm_air': False, + 'has_electric_underfloor_heating': False, 'has_electric_ceiling_heating': False, + 'has_community_scheme': False, 'has_ground_source_heat_pump': False, 'has_no_system_present': False, + 'has_portable_electric_heaters': False, 'has_water_source_heat_pump': False, + 'has_electric_heat_pump': False, 'has_micro-cogeneration': False, 'has_solar_assisted_heat_pump': False, + 'has_exhaust_source_heat_pump': False, 'has_community_heat_pump': False, 'has_electric': False, + 'has_mains_gas': True, 'has_wood_logs': False, 'has_coal': False, 'has_oil': False, + 'has_wood_pellets': False, 'has_anthracite': False, 'has_dual_fuel_mineral_and_wood': False, + 'has_smokeless_fuel': False, 'has_lpg': False, 'has_b30k': False, 'has_electricaire': False, + 'has_assumed_for_most_rooms': False, 'has_underfloor_heating': False, + 'thermostatic_control': 'room thermostat', 'charging_system': 'Unknown', 'switch_system': 'programmer', + 'no_control': 'Unknown', 'dhw_control': 'Unknown', 'community_heating': 'Unknown', + 'multiple_room_thermostats': False, 'auxiliary_systems': 'Unknown', 'trvs': 'Unknown', + 'rate_control': 'Unknown', 'glazing_type': 'single', 'fuel_type': 'mains gas', + 'main-fuel_tariff_type': 'Unknown', 'is_community': False, + 'no_individual_heating_or_community_network': False, 'complex_fuel_type': 'Unknown', + 'walls_thermal_transmittance_ending': 1.7, 'walls_thermal_transmittance_unit_ending': 'Unknown', + 'is_filled_cavity_ending': False, 'is_as_built_ending': True, 'walls_is_assumed_ending': True, + 'is_park_home_ending': False, 'walls_insulation_thickness_ending': 'none', + 'external_insulation_ending': False, 'internal_insulation_ending': False, + 'floor_thermal_transmittance_ending': 0.96, 'floor_insulation_thickness_ending': 'none', + 'roof_thermal_transmittance_ending': 2.3, 'is_at_rafters_ending': False, + 'roof_insulation_thickness_ending': 'none', 'heater_type_ending': 'Unknown', + 'system_type_ending': 'from main system', 'thermostat_characteristics_ending': 'Unknown', + 'heating_scope_ending': 'Unknown', 'energy_recovery_ending': 'Unknown', + 'hotwater_tariff_type_ending': 'Unknown', 'extra_features_ending': 'Unknown', + 'chp_systems_ending': 'Unknown', 'distribution_system_ending': 'Unknown', + 'no_system_present_ending': 'Unknown', 'appliance_ending': 'Unknown', 'has_radiators_ending': True, + 'has_fan_coil_units_ending': False, 'has_pipes_in_screed_above_insulation_ending': False, + 'has_pipes_in_insulated_timber_floor_ending': False, 'has_pipes_in_concrete_slab_ending': False, + 'has_boiler_ending': True, 'has_air_source_heat_pump_ending': False, 'has_room_heaters_ending': False, + 'has_electric_storage_heaters_ending': False, 'has_warm_air_ending': False, + 'has_electric_underfloor_heating_ending': False, 'has_electric_ceiling_heating_ending': False, + 'has_community_scheme_ending': False, 'has_ground_source_heat_pump_ending': False, + 'has_no_system_present_ending': False, 'has_portable_electric_heaters_ending': False, + 'has_water_source_heat_pump_ending': False, 'has_electric_heat_pump_ending': False, + 'has_micro-cogeneration_ending': False, 'has_solar_assisted_heat_pump_ending': False, + 'has_exhaust_source_heat_pump_ending': False, 'has_community_heat_pump_ending': False, + 'has_electric_ending': False, 'has_mains_gas_ending': True, 'has_wood_logs_ending': False, + 'has_coal_ending': False, 'has_oil_ending': False, 'has_wood_pellets_ending': False, + 'has_anthracite_ending': False, 'has_dual_fuel_mineral_and_wood_ending': False, + 'has_smokeless_fuel_ending': False, 'has_lpg_ending': False, 'has_b30k_ending': False, + 'has_electricaire_ending': False, 'has_assumed_for_most_rooms_ending': False, + 'has_underfloor_heating_ending': False, 'thermostatic_control_ending': 'room thermostat', + 'charging_system_ending': 'Unknown', 'switch_system_ending': 'programmer', 'no_control_ending': 'Unknown', + 'dhw_control_ending': 'Unknown', 'community_heating_ending': 'Unknown', + 'multiple_room_thermostats_ending': False, 'auxiliary_systems_ending': 'Unknown', 'trvs_ending': 'Unknown', + 'rate_control_ending': 'Unknown', 'glazing_type_ending': 'double', 'fuel_type_ending': 'mains gas', + 'main-fuel_tariff_type_ending': 'Unknown', 'is_community_ending': False, + 'no_individual_heating_or_community_network_ending': False, 'complex_fuel_type_ending': 'Unknown', + 'sap_starting': 47, 'sap_ending': 47, 'heat_demand_starting': 478, 'heat_demand_ending': 478, + 'carbon_starting': 5.1, 'carbon_ending': 5.1, 'lighting_cost_starting': 91.0, 'lighting_cost_ending': 91.0, + 'heating_cost_starting': 1677.0, 'heating_cost_ending': 1677.0, 'hot_water_cost_starting': 161.0, + 'hot_water_cost_ending': 161.0, 'mechanical_ventilation_starting': 'natural', + 'mechanical_ventilation_ending': 'natural', 'secondheat_description_starting': 'None', + 'secondheat_description_ending': 'None', 'glazed_type_starting': 'not defined', + 'glazed_type_ending': 'double glazing installed during or after 2002', + 'multi_glaze_proportion_starting': 0.0, 'multi_glaze_proportion_ending': 100, + 'low_energy_lighting_starting': 100.0, 'low_energy_lighting_ending': 100.0, + 'number_open_fireplaces_starting': 0.0, 'number_open_fireplaces_ending': 0.0, + 'solar_water_heating_flag_starting': 'N', 'solar_water_heating_flag_ending': 'N', + 'photo_supply_starting': 0.0, 'photo_supply_ending': 0.0, 'transaction_type_starting': 'rental', + 'transaction_type_ending': 'rental', 'energy_tariff_starting': 'dual', 'energy_tariff_ending': 'dual', + 'extension_count_starting': 3.0, 'extension_count_ending': 3.0, 'total_floor_area_starting': 61.0, + 'total_floor_area_ending': 61.0, 'floor_height_starting': 2.37, 'floor_height_ending': 2.37, + 'hot_water_energy_eff_starting': 'Good', 'hot_water_energy_eff_ending': 'Good', + 'floor_energy_eff_starting': 'NO_RATING', 'floor_energy_eff_ending': 'NO_RATING', + 'windows_energy_eff_starting': 'Very Poor', 'windows_energy_eff_ending': 'Average', + 'walls_energy_eff_starting': 'Very Poor', 'walls_energy_eff_ending': 'Very Poor', + 'sheating_energy_eff_starting': 'NO_RATING', 'sheating_energy_eff_ending': 'NO_RATING', + 'roof_energy_eff_starting': 'Very Poor', 'roof_energy_eff_ending': 'Very Poor', + 'mainheat_energy_eff_starting': 'Good', 'mainheat_energy_eff_ending': 'Good', + 'mainheatc_energy_eff_starting': 'Average', 'mainheatc_energy_eff_ending': 'Average', + 'lighting_energy_eff_starting': 'Very Good', 'lighting_energy_eff_ending': 'Very Good', + 'number_habitable_rooms_starting': 4.0, 'number_habitable_rooms_ending': 4.0, + 'number_heated_rooms_starting': 4.0, 'number_heated_rooms_ending': 4.0, 'days_to_starting': 3642, + 'days_to_ending': 3713, 'estimated_perimeter_starting': 23.430749027719962, + 'estimated_perimeter_ending': 23.430749027719962, 'has_glazing_ending': True, + 'glazing_coverage_ending': 'full', 'id': '1+1' + } + + assert simulated_data[0] == expected_simulated_outcome + + # has_glazing_ending and glazing_coverage_ending are not in the starting record - test for this in case it + # changes + assert "has_glazing_ending" not in starting_record + assert "glazing_coverage_ending" not in starting_record + + # Check which keys are different + different = [] + for k in simulated_data[0].keys(): + if k in ["id", 'has_glazing_ending', 'glazing_coverage_ending']: + continue + if simulated_data[0][k] != starting_record[k]: + different.append( + { + "variable": k, + "starting": starting_record[k], + "simulated": simulated_data[0][k], + + } + ) + + expected_different = [ + {'variable': 'glazing_type_ending', 'starting': 'single', 'simulated': 'double'}, + {'variable': 'glazed_type_ending', 'starting': 'not defined', + 'simulated': 'double glazing installed during or after 2002'}, + {'variable': 'multi_glaze_proportion_ending', 'starting': 0.0, 'simulated': 100}, + {'variable': 'windows_energy_eff_ending', 'starting': 'Very Poor', 'simulated': 'Average'}, + {'variable': 'days_to_ending', 'starting': 3642, 'simulated': 3713} + ] + + assert different == expected_different