diff --git a/backend/Property.py b/backend/Property.py index 2d1dbd5d..2e6cbbb6 100644 --- a/backend/Property.py +++ b/backend/Property.py @@ -61,7 +61,7 @@ class Property: n_bedrooms = None def __init__( - self, id, postcode, address, epc_record, already_installed=None, property_non_invasive_recommendations=None, + self, id, postcode, address, epc_record, already_installed=None, non_invasive_recommendations=None, **kwargs ): @@ -82,8 +82,8 @@ class Property: self.already_installed = ast.literal_eval(already_installed['already_installed']) if already_installed else [] self.non_invasive_recommendations = ( - ast.literal_eval(property_non_invasive_recommendations['recommendations']) if - property_non_invasive_recommendations else [] + ast.literal_eval(non_invasive_recommendations['recommendations']) if + non_invasive_recommendations else [] ) self.uprn = epc_record.get("uprn") @@ -284,6 +284,7 @@ class Property: recommendation_record=recommendation_record, recommendations=previous_phase_representatives + [rec], primary_recommendation_id=rec["recommendation_id"], + non_invasive_recommendations=self.non_invasive_recommendations, ) self.recommendations_scoring_data.append(scoring_dict) @@ -293,6 +294,7 @@ class Property: recommendation_record, recommendations: list, primary_recommendation_id: int, + non_invasive_recommendations: list = None, ): """ This function will iterate through a list of recommendations and apply a simulation for each recommendation @@ -301,10 +303,12 @@ class Property: :param recommendation_record: The record of the property, which will be updated :param recommendations: The list of recommendations to apply :param primary_recommendation_id: The id of the primary recommendation, which is used to identify the record + :param non_invasive_recommendations: The list of non-invasive recommendations :return: The updated recommendation record """ output = recommendation_record.copy() + non_invasive_recommendations = [] if non_invasive_recommendations is None else non_invasive_recommendations for col in [ "walls_insulation_thickness", @@ -323,6 +327,13 @@ class Property: "external_wall_insulation", "cavity_wall_insulation", ]: + + # # If we have a non-incasive recommendation that the cavity wall is partially filled, we skip the + # # cavity wall insulation recommendation (since on the EPC, the property will look like how it did + # # before any works) + # if "cavity_surveyed_as_filled_is_partial" in non_invasive_recommendations: + # continue + # The upgrade made here is to the u-value of the walls and the description of the # insulation thickness output["walls_thermal_transmittance_ending"] = recommendation[ diff --git a/backend/app/plan/router.py b/backend/app/plan/router.py index 7200d2ef..9854abe8 100644 --- a/backend/app/plan/router.py +++ b/backend/app/plan/router.py @@ -171,11 +171,13 @@ def extract_portfolio_aggregation_data( def format_money(amount): return f"£{amount:,.0f}" - valuation_improvment_per_unit = format_money( - total_valuation_increase / n_units) + (f" ({format_money(valuation_improvement_lower_bound_per_unit)} - " - f"{format_money(valuation_improvement_upper_bound_per_unit)})") + valuation_improvment_per_unit = str( + format_money( + total_valuation_increase / n_units) + (f" ({format_money(valuation_improvement_lower_bound_per_unit)} - " + f"{format_money(valuation_improvement_upper_bound_per_unit)})") + ) - valuation_return_on_investment = ( + valuation_return_on_investment = str( str(round(total_valuation_increase / agg_data["cost"].sum(), 2)) + f" (" f"{agg_data['lower_bound_valuation_uplift'].sum() / agg_data['cost'].sum():,.2f} - " @@ -189,8 +191,8 @@ def extract_portfolio_aggregation_data( "epc_breakdown_post_retrofit": json.dumps( reformat_epc_data(agg_data["post_retrofit_epc"].value_counts().to_dict()) ), - "number_of_properties": n_units, - "n_units_to_retrofit": n_units_to_retrofit, + "number_of_properties": int(n_units), + "n_units_to_retrofit": int(n_units_to_retrofit), "co2_per_unit_pre_retrofit": str(round(agg_data["pre_retrofit_co2"].mean(), 2)) + "t", "co2_per_unit_post_retrofit": str(round(agg_data["post_retrofit_co2"].mean(), 2)) + "t", "energy_bill_per_unit_pre_retrofit": format_money(agg_data["pre_retrofit_energy_bill"].mean()), diff --git a/recommendations/Costs.py b/recommendations/Costs.py index 0e67b352..852bb11f 100644 --- a/recommendations/Costs.py +++ b/recommendations/Costs.py @@ -91,6 +91,10 @@ DOUBLE_RADIATOR_COST = 300 FLUE_COST = 600 PIPEWORK_COST = 750 # Min cost is £500 +# This is the cost per meter squared for cavity extraction +# https://www.checkatrade.com/blog/cost-guides/cavity-wall-insulation-removal-cost/ +CAVITY_EXTRACTION_COST = 21.5 + class Costs: """ @@ -173,7 +177,7 @@ class Costs: if not self.labour_adjustment_factor: raise ValueError("Labour adjustment factor not found") - def cavity_wall_insulation(self, wall_area, material): + def cavity_wall_insulation(self, wall_area, material, is_extraction_and_refill=False): """ Calculates the total cost for cavity wall insulation based on material and labor costs, including contingency, preliminaries, profit, and VAT. @@ -208,6 +212,13 @@ class Costs: # Assume a team of 2 labour_days = (labour_hours / 8) / 2 + if is_extraction_and_refill: + # bump up the cost of the work + total_cost = total_cost + CAVITY_EXTRACTION_COST * wall_area + # Additional 2 days work + labour_hours = labour_hours + (2 * 8) + labour_days = labour_days + 2 + return { "total": total_cost, "subtotal": subtotal_before_vat, diff --git a/recommendations/Recommendations.py b/recommendations/Recommendations.py index e626ecfa..5960d7be 100644 --- a/recommendations/Recommendations.py +++ b/recommendations/Recommendations.py @@ -149,12 +149,14 @@ class Recommendations: property_recommendations = self.insert_temp_recommendation_id(property_recommendations) # We also need to create the representative recommendations for each recommendation type - property_representative_recommendations = self.create_representative_recommendations(property_recommendations) + property_representative_recommendations = self.create_representative_recommendations( + property_recommendations, non_invasive_recommendations=self.property_instance.non_invasive_recommendations + ) return property_recommendations, property_representative_recommendations @staticmethod - def create_representative_recommendations(property_recommendations): + def create_representative_recommendations(property_recommendations, non_invasive_recommendations): """ This method will create a representative recommendation for each recommendation type In order to create a representative recommendation, we choose the recommendation that has: @@ -169,6 +171,13 @@ class Recommendations: for recommendations_by_type in property_recommendations: + # If the property was initially surveyed as filled, but the cavity was only partially filled, we don't + # want to include the cavity wall insulation recommendation in the defaults + # if (recommendations_by_type[0].get("type") == "cavity_wall_insulation") and ( + # "cavity_surveyed_as_filled_is_partial" in non_invasive_recommendations + # ): + # continue + if recommendations_by_type[0].get("type") == "mechanical_ventilation": continue @@ -238,13 +247,13 @@ class Recommendations: property_sap_predictions = all_predictions["sap_change_predictions"][ all_predictions["sap_change_predictions"]["property_id"] == str(property_instance.id) - ] + ].copy() property_heat_predictions = all_predictions["heat_demand_predictions"][ all_predictions["heat_demand_predictions"]["property_id"] == str(property_instance.id) - ] + ].copy() property_carbon_predictions = all_predictions["carbon_change_predictions"][ all_predictions["carbon_change_predictions"]["property_id"] == str(property_instance.id) - ] + ].copy() property_recommendations = recommendations[property_instance.id].copy() diff --git a/recommendations/WallRecommendations.py b/recommendations/WallRecommendations.py index feb2620b..20fc453c 100644 --- a/recommendations/WallRecommendations.py +++ b/recommendations/WallRecommendations.py @@ -113,7 +113,9 @@ class WallRecommendations(Definitions): insulation_thickness = self.property.walls["insulation_thickness"] # We check if the wall is already insulated and if so, we exit - if (insulation_thickness in ["average", "above average"]) or self.property.walls["is_filled_cavity"]: + if ((insulation_thickness in ["average", "above average"]) or self.property.walls["is_filled_cavity"]) and ( + "cavity_extract_and_refill" not in self.property.non_invasive_recommendations + ): return if u_value: @@ -216,15 +218,26 @@ class WallRecommendations(Definitions): if new_u_value <= self.BUILDING_REGULATIONS_PART_L_CAVITY_WALL_MAX_U_VALUE: lowest_selected_u_value = update_lowest_selected_u_value(lowest_selected_u_value, new_u_value) + is_extraction_and_refill = "cavity_extract_and_refill" in self.property.non_invasive_recommendations + cost_result = self.costs.cavity_wall_insulation( wall_area=self.property.insulation_wall_area, material=material.to_dict(), + is_extraction_and_refill=is_extraction_and_refill ) already_installed = "cavity_wall_insulation" in self.property.already_installed if already_installed: cost_result = override_costs(cost_result) + if is_extraction_and_refill: + description = f"Extract and refill cavity wall insulation with {material['description']}" + else: + description = self._make_description(material) + + # updated the new u-value with the best possible our installers have + new_u_value = max(0.31, new_u_value) + recommendations.append( { "phase": phase, @@ -237,7 +250,7 @@ class WallRecommendations(Definitions): ) ], "type": "cavity_wall_insulation", - "description": self._make_description(material), + "description": description, "starting_u_value": u_value, "new_u_value": new_u_value, "sap_points": None,