From 544949d102130e18305b346f3b7a06ee5cad7cfc Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 14 Jul 2025 16:10:31 +0100 Subject: [PATCH 1/2] debugging find epc pull from dorrington --- .idea/Model.iml | 2 +- .idea/misc.xml | 2 +- asset_list/app.py | 34 ++++++++++++++++++++++++++++ backend/Property.py | 2 +- etl/find_my_epc/RetrieveFindMyEpc.py | 3 +++ 5 files changed, 40 insertions(+), 3 deletions(-) diff --git a/.idea/Model.iml b/.idea/Model.iml index c6561970..09f2e496 100644 --- a/.idea/Model.iml +++ b/.idea/Model.iml @@ -7,7 +7,7 @@ - + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 50cad4ca..fb10c6b0 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,7 +3,7 @@ - + diff --git a/asset_list/app.py b/asset_list/app.py index e431f723..1f0fe570 100644 --- a/asset_list/app.py +++ b/asset_list/app.py @@ -59,6 +59,40 @@ def app(): Property UPRN """ + # Dorrington + data_folder = "/Users/khalimconn-kowlessar/Documents/hestia/Customers/Dorrington" + data_filename = "Copy of Eco Funding.xlsx" + sheet_name = "Sheet1" + postcode_column = 'Postcode' + fulladdress_column = "Property Address" + address1_column = None + address1_method = "house_number_extraction" + address_cols_to_concat = [] + missing_postcodes_method = None + landlord_year_built = None + landlord_os_uprn = None + landlord_property_type = None + landlord_built_form = None + landlord_wall_construction = None + landlord_heating_system = None + landlord_existing_pv = None + landlord_property_id = "Row ID" + outcomes_filename = [] + outcomes_sheetname = [] + outcomes_postcode = [] + outcomes_houseno = [] + outcomes_address = [] + outcomes_id = [] + master_filepaths = [] + master_to_asset_list_filepath = None + asset_list_header = 0 + landlord_block_reference = None + master_id_colnames = [] + landlord_roof_construction = None + phase = False + landlord_sap = None + ecosurv_landlords = None + # CDS data_folder = "/Users/khalimconn-kowlessar/Documents/hestia/Customers/CDS" data_filename = "Founder Estates - Asset List.xlsx" diff --git a/backend/Property.py b/backend/Property.py index 22eb2fc3..a8fd925b 100644 --- a/backend/Property.py +++ b/backend/Property.py @@ -1360,4 +1360,4 @@ class Property: 'mechanical, supply and extract' ] - return self.data["mechanical-ventilation"] in ventilation_descriptions + return self.data.get("mechanical-ventilation") in ventilation_descriptions diff --git a/etl/find_my_epc/RetrieveFindMyEpc.py b/etl/find_my_epc/RetrieveFindMyEpc.py index 142d0cea..216a14de 100644 --- a/etl/find_my_epc/RetrieveFindMyEpc.py +++ b/etl/find_my_epc/RetrieveFindMyEpc.py @@ -678,6 +678,9 @@ class RetrieveFindMyEpc: "Internal wall insulation": ["internal_wall_insulation"], "High heat retention storage heaters and dual immersion cylinder and dual rate meter": [ "high_heat_retention_storage_heater" + ], + "High heat retention storage heaters and dual rate meter": [ + "high_heat_retention_storage_heater" ] } From 0bc1299e69bd9da21f056231755115c34391bcf4 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 15 Jul 2025 18:49:35 +0100 Subject: [PATCH 2/2] adding utilising existing scenario to engine --- .idea/Model.iml | 2 +- .idea/misc.xml | 2 +- asset_list/AssetList.py | 15 ++++- asset_list/app.py | 55 ++++++++++++++---- asset_list/hubspot/config.py | 3 +- asset_list/hubspot/prepare_for_hubspot.py | 10 ++-- asset_list/mappings/built_form.py | 15 ++++- asset_list/mappings/heating_systems.py | 16 +++++- asset_list/mappings/property_type.py | 15 ++++- backend/app/plan/schemas.py | 4 +- backend/engine/engine.py | 68 +++++++++++++++-------- 11 files changed, 158 insertions(+), 47 deletions(-) diff --git a/.idea/Model.iml b/.idea/Model.iml index 09f2e496..c6561970 100644 --- a/.idea/Model.iml +++ b/.idea/Model.iml @@ -7,7 +7,7 @@ - + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index fb10c6b0..50cad4ca 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,7 +3,7 @@ - + diff --git a/asset_list/AssetList.py b/asset_list/AssetList.py index 21376708..611d0257 100644 --- a/asset_list/AssetList.py +++ b/asset_list/AssetList.py @@ -3155,10 +3155,19 @@ class AssetList: date_col = "Date letters sent" elif "Date Letter sent" in self.outcomes.columns: date_col = "Date Letter sent" + elif "WEEK COMMENCING" in self.outcomes.columns: + date_col = "WEEK COMMENCING" else: raise NotImplementedError("Invalid date in outcomes - implement me") - notes_col = "Notes" if "Notes" in self.outcomes.columns else "Notes / Outcomes" + if "Notes" in self.outcomes.columns: + notes_col = "Notes" + elif "Notes / Outcomes" in self.outcomes.columns: + notes_col = "Notes / Outcomes" + elif "NOTES" in self.outcomes.columns: + notes_col = "NOTES" + else: + raise NotImplementedError("Invalid notes in outcomes - implement me") lookup = lookup.merge( self.outcomes[["row_id", "Outcome", notes_col, date_col]], how="left", on="row_id" @@ -3342,6 +3351,10 @@ class AssetList: installer_notes_col = 'Installers Notes' elif 'NOTES ; REASONS FOR CANCELLATIONS OR WHERE INSTALL DATE WAS OBTAINED FROM' in master_data.columns: installer_notes_col = 'NOTES ; REASONS FOR CANCELLATIONS OR WHERE INSTALL DATE WAS OBTAINED FROM' + elif ('INSTALLERS NOTES / REASONS FOR CANCELLATIONS / WHERE INSTALL DATE WAS RECEIVED FROM' in + master_data.columns): + installer_notes_col = ('INSTALLERS NOTES / REASONS FOR CANCELLATIONS / WHERE INSTALL DATE WAS RECEIVED ' + 'FROM') else: raise ValueError("No installer notes column found in master data") diff --git a/asset_list/app.py b/asset_list/app.py index 1f0fe570..efc9cf44 100644 --- a/asset_list/app.py +++ b/asset_list/app.py @@ -59,12 +59,12 @@ def app(): Property UPRN """ - # Dorrington - data_folder = "/Users/khalimconn-kowlessar/Documents/hestia/Customers/Dorrington" - data_filename = "Copy of Eco Funding.xlsx" - sheet_name = "Sheet1" - postcode_column = 'Postcode' - fulladdress_column = "Property Address" + # TODO: Delete me + data_folder = "/Users/khalimconn-kowlessar/Documents/hestia/Customers/Bromford/Apr 2025 Programme Rebuild" + data_filename = "Bromford Asset List.xlsx" + sheet_name = "Asset List" + postcode_column = 'PostCode' + fulladdress_column = "FullAddress" address1_column = None address1_method = "house_number_extraction" address_cols_to_concat = [] @@ -76,23 +76,58 @@ def app(): landlord_wall_construction = None landlord_heating_system = None landlord_existing_pv = None - landlord_property_id = "Row ID" + landlord_property_id = "Asset" outcomes_filename = [] outcomes_sheetname = [] outcomes_postcode = [] outcomes_houseno = [] outcomes_address = [] - outcomes_id = [] - master_filepaths = [] + outcomes_id = [None] + master_filepaths = [os.path.join("/Users/khalimconn-kowlessar/Documents/hestia/Customers/Bromford/", + "Needs ID/SOLAR PV ONLY-Table 1.csv")] master_to_asset_list_filepath = None asset_list_header = 0 landlord_block_reference = None - master_id_colnames = [] + master_id_colnames = [None] landlord_roof_construction = None phase = False landlord_sap = None ecosurv_landlords = None + # For Housing + data_folder = "/Users/khalimconn-kowlessar/Documents/hestia/Customers/For Housing/New Programme July 2025" + data_filename = "FOR HOUSING Asset List (Combined).xlsx" + sheet_name = "Asset List" + postcode_column = 'Postcode' + fulladdress_column = "Address" + address1_column = None + address1_method = "house_number_extraction" + address_cols_to_concat = [] + missing_postcodes_method = None + landlord_year_built = None + landlord_os_uprn = None + landlord_property_type = "Type" + landlord_built_form = "Type" + landlord_wall_construction = None + landlord_heating_system = "Heating - full" + landlord_existing_pv = None + landlord_property_id = "UPRN" + outcomes_filename = [os.path.join(data_folder, "Khalim Combined - for analysis.xlsx")] + outcomes_sheetname = ["Sheet1"] + outcomes_postcode = ["POSTCODE"] + outcomes_houseno = ["NO"] + outcomes_address = ["ADDRESS"] + outcomes_id = [None] + master_filepaths = [os.path.join(data_folder, "submissions.csv")] + master_to_asset_list_filepath = None + asset_list_header = 0 + landlord_block_reference = None + master_id_colnames = [None] + landlord_roof_construction = None + phase = False + landlord_sap = "SAP" + ecosurv_landlords = "for housing" + # CDS data_folder = "/Users/khalimconn-kowlessar/Documents/hestia/Customers/CDS" data_filename = "Founder Estates - Asset List.xlsx" diff --git a/asset_list/hubspot/config.py b/asset_list/hubspot/config.py index 23ff900a..403bead4 100644 --- a/asset_list/hubspot/config.py +++ b/asset_list/hubspot/config.py @@ -15,7 +15,7 @@ class HubspotProcessStatus(IntEnum): # This is the first stage, where a survey is ready to go READY_TO_BE_SCHEDULED = 1, "READY TO BE SCHEDULED" # The property didn't get access and needs sign off - SURVEYED_NO_ACCESS_NEEDS_SIGN_OFF = 2, "SURVEYED - NO ACCESS - NEED SIGN OFF" + SURVEYED_NO_ACCESS_NEEDS_SIGN_OFF = 2, "NO ACCESS - NEED SIGN OFF" # The survey has been completed. We don't have any update as to whether the property has been installed SURVEYED_COMPLETED_SIGNED_OFF = 3, "SURVEYED - AUTOMATED SIGNED OFF" # The property turned out to be ineligibile @@ -34,6 +34,7 @@ class Installer(Enum): SCIS = "SCIS" JJ_CRUMP = "J & J CRUMP" SGEC = "SGEC" + WARMFRONT = "WARM FRONT" @classmethod def is_valid_value(cls, value): diff --git a/asset_list/hubspot/prepare_for_hubspot.py b/asset_list/hubspot/prepare_for_hubspot.py index b12f4c04..ba2a2d23 100644 --- a/asset_list/hubspot/prepare_for_hubspot.py +++ b/asset_list/hubspot/prepare_for_hubspot.py @@ -45,13 +45,13 @@ def app(): # inputs: reconcile_programme = True # If True, the hubspot upload will include all properties with a project code - customer_domain = "https://ealing.gov.uk" - installer_name = "SCIS" + customer_domain = "https://calico.org.uk" + installer_name = "WARM FRONT" asset_list_filepath = ( - "/Users/khalimconn-kowlessar/Documents/hestia/Customers/Ealing/Hubspot/20250707 Ealing Flats - Prepared " - "programme.xlsx" + "/Users/khalimconn-kowlessar/Documents/hestia/Customers/Calico/Hubspot/07.04 CALICO - Final List - " + "Standardised.xlsx" ) - asset_list_sheet_name = "Standardised Asset List" + asset_list_sheet_name = "Final Route March" asset_list_header = 0 contact_details_filepath = None diff --git a/asset_list/mappings/built_form.py b/asset_list/mappings/built_form.py index c17e0ed4..4ebe016f 100644 --- a/asset_list/mappings/built_form.py +++ b/asset_list/mappings/built_form.py @@ -372,5 +372,18 @@ BUILT_FORM_MAPPINGS = { 'MAISONETTE': 'unknown', 'HOUSE': 'unknown', 'FLAT': 'unknown', - 'BLOCK': 'unknown' + 'BLOCK': 'unknown', + + 'Semi Detached Bungalow': 'semi-detached', + 'End Terraced Bungalow': 'end-terrace', + 'Mid Terraced Town House': 'mid-terrace', + 'Semi-Detached House': 'detached', + 'Low Rise Flat': 'low rise', + 'Mid Terraced Bungalow': 'mid-terrace', + 'End Terraced Town House': 'end-terrace', + 'Cottage Flat': 'ground floor', + 'Maisonette Over Shop': 'mid-floor', + 'Medium Rise Flat': 'mid-floor', + 'Maisonette Medium Rise': 'unknown' + } diff --git a/asset_list/mappings/heating_systems.py b/asset_list/mappings/heating_systems.py index 010d49a5..89bc2933 100644 --- a/asset_list/mappings/heating_systems.py +++ b/asset_list/mappings/heating_systems.py @@ -364,5 +364,19 @@ HEATING_MAPPINGS = { 'Boiler, Electricity': 'electric boiler', 'Boiler, LPG': 'gas boiler, radiators', 'Boiler, Mains gas': 'gas boiler, radiators', - 'Storage heating, Electricity': 'electric storage heaters' + 'Storage heating, Electricity': 'electric storage heaters', + + 'No Heating Types None': 'no heating', + 'Boiler Smokeless': 'boiler - other fuel', + 'Boiler House coal': 'boiler - other fuel', + 'Warm air Mains gas': 'warm air heating', + 'Storage heaters None': 'electric storage heaters', + 'Boiler Anthracite': 'boiler - other fuel', + 'Mains gas': 'gas boiler, radiators', + 'Community heating Mains Gas': 'communal gas boiler', + 'Warm air Electricity': 'warm air heating', + 'None': 'no heating', + 'Boiler None': 'unknown', + 'Storage heaters Electricity': 'electric storage heaters' + } diff --git a/asset_list/mappings/property_type.py b/asset_list/mappings/property_type.py index caca0cf0..d45fd109 100644 --- a/asset_list/mappings/property_type.py +++ b/asset_list/mappings/property_type.py @@ -270,6 +270,19 @@ PROPERTY_MAPPING = { 'HFOP FLAT': 'flat', 'HFOP BEDSIT': 'bedsit', 'LINKED FLAT': 'flat', - 'LINKED BUNGALOW': 'bungalow' + 'LINKED BUNGALOW': 'bungalow', + + 'Semi Detached Bungalow': 'bungalow', + 'End Terraced Bungalow': 'bungalow', + 'Mid Terraced Town House': 'house', + 'Cottage Flat': 'flat', + 'Semi-Detached House': 'house', + 'Low Rise Flat': 'flat', + 'Mid Terraced Bungalow': 'bungalow', + 'Maisonette Over Shop': 'maisonette', + 'Flat Over Shop': 'flat', + 'Medium Rise Flat': 'flat', + 'End Terraced Town House': 'house', + 'Maisonette Medium Rise': 'maisonette' } diff --git a/backend/app/plan/schemas.py b/backend/app/plan/schemas.py index 6b8b192d..ef73f133 100644 --- a/backend/app/plan/schemas.py +++ b/backend/app/plan/schemas.py @@ -14,7 +14,8 @@ SPECIFIC_MEASURES = [ "suspended_floor_insulation", "solid_floor_insulation", "boiler_upgrade", "high_heat_retention_storage_heater", "air_source_heat_pump", "secondary_heating", "solar_pv", "double_glazing", "secondary_glazing", - "ventilation", "low_energy_lighting", "fireplace", "hot_water" + "ventilation", "low_energy_lighting", "fireplace", "hot_water_tank_insulation", + "cylinder_thermostat" ] NON_INVASIVE_SPECIFIC_MEASURES = [ @@ -87,6 +88,7 @@ class PlanTriggerRequest(BaseModel): required_measures: Optional[List[InclusionOrExclusionItem]] = Field(default=[], min_length=0) scenario_name: Optional[str] = "" + scenario_id: Optional[str | int] = None # Used to utilise and existing scenario for a engine run multi_plan: Optional[bool] = False optimise: Optional[bool] = True default_u_values: Optional[bool] = True diff --git a/backend/engine/engine.py b/backend/engine/engine.py index d631e349..98862107 100644 --- a/backend/engine/engine.py +++ b/backend/engine/engine.py @@ -186,13 +186,28 @@ def extract_portfolio_aggregation_data( f"{format_money(valuation_improvement_upper_bound_per_unit)})") ) + if agg_data["cost"].sum() == 0: + valuation_percentage_increase = 0 + valuation_increase_lower = 0 + valuation_increase_upper = 0 + else: + valuation_percentage_increase = round(total_valuation_increase / agg_data["cost"].sum(), 2) + valuation_increase_lower = agg_data['lower_bound_valuation_uplift'].sum() / agg_data['cost'].sum() + valuation_increase_upper = agg_data['upper_bound_valuation_uplift'].sum() / agg_data['cost'].sum() + valuation_return_on_investment = str( - str(round(total_valuation_increase / agg_data["cost"].sum(), 2)) + + str(valuation_percentage_increase) + f" (" - f"{agg_data['lower_bound_valuation_uplift'].sum() / agg_data['cost'].sum():,.2f} - " - f"{agg_data['upper_bound_valuation_uplift'].sum() / agg_data['cost'].sum():,.2f})" + f"{valuation_increase_lower:,.2f} - " + f"{valuation_increase_upper:,.2f})" ) + cost_per_co2_saved = agg_data["cost"].sum() / total_carbon_saved if total_carbon_saved > 0 else 0 + cost_per_co2_saved = format_money(cost_per_co2_saved) + + cost_per_sap_point = agg_data["cost"].sum() / total_sap_points if total_sap_points > 0 else 0 + cost_per_sap_point = format_money(cost_per_sap_point) + aggregation_data = { "epc_breakdown_pre_retrofit": json.dumps( reformat_epc_data(agg_data["pre_retrofit_epc"].value_counts().to_dict()) @@ -212,8 +227,8 @@ def extract_portfolio_aggregation_data( round(agg_data["post_retrofit_energy_consumption"].mean())) + "kWh", "valuation_improvement_per_unit": valuation_improvment_per_unit, "cost_per_unit": format_money(agg_data["cost"].mean()), - "cost_per_co2_saved": format_money(agg_data["cost"].sum() / total_carbon_saved), - "cost_per_sap_point": format_money(agg_data["cost"].sum() / total_sap_points), + "cost_per_co2_saved": cost_per_co2_saved, + "cost_per_sap_point": cost_per_sap_point, "valuation_return_on_investment": valuation_return_on_investment, # TODO: Could we add 10yr carbon credits value? } @@ -917,23 +932,28 @@ async def model_engine(body: PlanTriggerRequest): logger.info("Uploading recommendations to the database") # If we have any work to do, we create a new scenario - engine_scenario = create_scenario( - session=session, - scenario={ - "name": body.scenario_name, - "created_at": created_at, - "budget": body.budget, - "portfolio_id": body.portfolio_id, - "housing_type": body.housing_type, - "goal": body.goal, - "trigger_file_path": body.trigger_file_path, - "already_installed_file_path": body.already_installed_file_path, - "patches_file_path": body.patches_file_path, - "non_invasive_recommendations_file_path": body.non_invasive_recommendations_file_path, - "exclusions": body.exclusions, - "multi_plan": body.multi_plan - } - ) + if body.scenario_id: + # We don't need to create a new scenario, we just use the existing one + scenario_id = body.scenario_id + else: + engine_scenario = create_scenario( + session=session, + scenario={ + "name": body.scenario_name, + "created_at": created_at, + "budget": body.budget, + "portfolio_id": body.portfolio_id, + "housing_type": body.housing_type, + "goal": body.goal, + "trigger_file_path": body.trigger_file_path, + "already_installed_file_path": body.already_installed_file_path, + "patches_file_path": body.patches_file_path, + "non_invasive_recommendations_file_path": body.non_invasive_recommendations_file_path, + "exclusions": body.exclusions, + "multi_plan": body.multi_plan + } + ) + scenario_id = engine_scenario.id property_valuation_increases = [] session.commit() @@ -979,7 +999,7 @@ async def model_engine(body: PlanTriggerRequest): new_plan_id = create_plan(session, { "portfolio_id": body.portfolio_id, "property_id": p.id, - "scenario_id": engine_scenario.id, + "scenario_id": scenario_id, "is_default": True if p.is_new else False, "name": body.scenario_name, "valuation_increase_lower_bound": ( @@ -1033,7 +1053,7 @@ async def model_engine(body: PlanTriggerRequest): aggregate_portfolio_recommendations( session, portfolio_id=body.portfolio_id, - scenario_id=engine_scenario.id, + scenario_id=scenario_id, total_valuation_increase=total_valuation_increase, labour_days=labour_days, aggregated_data=aggregated_data