diff --git a/backend/app/db/functions/recommendations_functions.py b/backend/app/db/functions/recommendations_functions.py index b544b43b..e4f22fd2 100644 --- a/backend/app/db/functions/recommendations_functions.py +++ b/backend/app/db/functions/recommendations_functions.py @@ -1,3 +1,4 @@ +from sqlalchemy import text from sqlalchemy.orm import sessionmaker from backend.app.db.connection import db_engine from backend.app.db.models.recommendations import Plan, Recommendation, RecommendationMaterials @@ -60,12 +61,13 @@ def create_plan_recommendations(plan_id, recommendation_ids): :param plan_id: ID of the plan :param recommendation_ids: list of recommendation IDs """ - Session = sessionmaker(bind=db_engine) with Session() as session: for recommendation_id in recommendation_ids: session.execute( - 'INSERT INTO plan_recommendations (plan_id, recommendation_id) VALUES (:plan_id, :recommendation_id)', + text( + 'INSERT INTO plan_recommendations (plan_id, recommendation_id) VALUES (:plan_id, ' + ':recommendation_id)'), {'plan_id': plan_id, 'recommendation_id': recommendation_id} ) session.commit() diff --git a/backend/app/db/models/materials.py b/backend/app/db/models/materials.py index 4c4a8a09..00430b1c 100644 --- a/backend/app/db/models/materials.py +++ b/backend/app/db/models/materials.py @@ -1,6 +1,6 @@ import enum -from sqlalchemy import Column, Integer, String, Float, Enum, TIMESTAMP +from sqlalchemy import Column, Integer, String, Float, Enum, TIMESTAMP, Boolean from sqlalchemy.orm import declarative_base from sqlalchemy.sql import func @@ -38,7 +38,7 @@ class Material(Base): description = Column(String, nullable=False) depths = Column(String) # You may want to use a specific JSON type depending on the database depth_unit = Column(Enum(DepthUnit, values_callable=lambda x: [e.value for e in x]), nullable=False) - cost = Column(Float) + cost = Column(String) cost_unit = Column(Enum(CostUnit, values_callable=lambda x: [e.value for e in x]), nullable=False) r_value_per_mm = Column(Float) r_value_unit = Column(Enum(RValueUnit, values_callable=lambda x: [e.value for e in x]), nullable=False) @@ -49,3 +49,4 @@ class Material(Base): ) link = Column(String) created_at = Column(TIMESTAMP, nullable=False, server_default=func.now()) + is_active = Column(Boolean, nullable=False, default=True) diff --git a/backend/app/db/models/recommendations.py b/backend/app/db/models/recommendations.py index 66d1a063..9c11ab83 100644 --- a/backend/app/db/models/recommendations.py +++ b/backend/app/db/models/recommendations.py @@ -36,6 +36,7 @@ class RecommendationMaterials(Base): recommendation_id = Column(BigInteger, ForeignKey('recommendation.id'), nullable=False) material_id = Column(BigInteger, ForeignKey(Material.id), nullable=False) created_at = Column(TIMESTAMP, nullable=False, server_default=func.now()) + depth = Column(Float, nullable=False) class Plan(Base): diff --git a/backend/app/plan/router.py b/backend/app/plan/router.py index eaa9a2ce..96f591ba 100644 --- a/backend/app/plan/router.py +++ b/backend/app/plan/router.py @@ -200,8 +200,10 @@ async def trigger_plan(body: PlanTriggerRequest): # Floor recommendations floor_recommender = FloorRecommendations( - property_instance=p, uvalue_estimates=floors_u_value_estimate, - total_floor_area_group_decile=total_floor_area_group_decile + property_instance=p, + uvalue_estimates=floors_u_value_estimate, + total_floor_area_group_decile=total_floor_area_group_decile, + materials=materials_by_type["suspended_floor_insulation"] + materials_by_type["solid_floor_insulation"], ) floor_recommender.recommend() @@ -256,8 +258,9 @@ async def trigger_plan(body: PlanTriggerRequest): # TODO: We start off by optimising the recommendations recommendations_to_upload = recommendations[p.id] - if not recommendations: + if not recommendations_to_upload: continue + # Create a plan new_plan_id = create_plan( { @@ -269,14 +272,13 @@ async def trigger_plan(body: PlanTriggerRequest): # upload recommendations uploaded_recommendation_ids = [] for rec in recommendations_to_upload: - # TODO: implement costs (at least a placeholder) - estimated_cost = sum([x["cost"] if x["cost"] else 0 for x in rec["parts"]]) recommendation_id = create_recommendation( { + "property_id": p.id, "type": rec["type"], "description": rec["description"], - "estimated_cost": estimated_cost, + "estimated_cost": rec["cost"], "default": True, "starting_u_value": rec.get("starting_u_value"), "new_u_value": rec.get("new_u_value"), diff --git a/recommendations/FloorRecommendations.py b/recommendations/FloorRecommendations.py index b45f6953..681d267d 100644 --- a/recommendations/FloorRecommendations.py +++ b/recommendations/FloorRecommendations.py @@ -116,6 +116,13 @@ class FloorRecommendations(BaseUtility): else: self.materials = parts + self.suspended_floor_insulation_parts = [ + part for part in self.materials if part["type"] == "suspended_floor_insulation" + ] + self.solid_floor_insulation_parts = [ + part for part in self.materials if part["type"] == "solid_floor_insulation" + ] + @staticmethod def _estimate_perimeter(floor_area, num_rooms): # Compute average room size based on total floor area and number of rooms @@ -266,11 +273,11 @@ class FloorRecommendations(BaseUtility): if is_suspended: # Given the U-value, we recommend underfloor insulation - self.recommend_floor_insulation(u_value=u_value, parts=suspended_floor_insulation_parts) + self.recommend_floor_insulation(u_value=u_value, parts=self.suspended_floor_insulation_parts) if is_solid: # Given the U-value, we recommend solid floor insulation options which are usually solid foam - self.recommend_floor_insulation(u_value=u_value, parts=solid_floor_insulation_parts) + self.recommend_floor_insulation(u_value=u_value, parts=self.solid_floor_insulation_parts) @staticmethod def _make_floor_description(part, depth): @@ -284,7 +291,8 @@ class FloorRecommendations(BaseUtility): lowest_selected_u_value = None for part in parts: - for depth in part["depths"]: + for depth, cost_per_unit in zip(part["depths"], part["cost"]): + part_u_value = r_value_per_mm_to_u_value(depth, part["r_value_per_mm"]) _, new_u_value = calculate_u_value_uplift(u_value, part_u_value) new_u_value = math.ceil(new_u_value * 100.0) / 100.0 @@ -300,13 +308,14 @@ class FloorRecommendations(BaseUtility): self.recommendations.append( { "parts": [ - get_recommended_part(part, depth), + get_recommended_part(part, depth, cost_per_unit), ], "type": "floor_insulation", "description": self._make_floor_description(part, depth), "starting_u_value": u_value, "new_u_value": new_u_value, - "sap_points": estimate_sap_points() + "sap_points": estimate_sap_points(), + "cost": cost_per_unit * self.property.floor_area, } ) diff --git a/recommendations/WallRecommendations.py b/recommendations/WallRecommendations.py index a9c590dc..fc8f3c7b 100644 --- a/recommendations/WallRecommendations.py +++ b/recommendations/WallRecommendations.py @@ -310,7 +310,8 @@ class WallRecommendations(BaseUtility): recommendations = [] for part in parts: - for depth in part["depths"]: + for depth, cost_per_unit in zip(part["depths"], part["cost"]): + part_u_value = r_value_per_mm_to_u_value(depth, part["r_value_per_mm"]) _, new_u_value = calculate_u_value_uplift(u_value, part_u_value) @@ -333,12 +334,13 @@ class WallRecommendations(BaseUtility): recommendations.append( { - "parts": [get_recommended_part(part, depth)], + "parts": [get_recommended_part(part, depth, cost_per_unit)], "type": "wall_insulation", "description": "Install " + self._make_description(part, depth), "starting_u_value": u_value, "new_u_value": new_u_value, "sap_points": estimate_sap_points(), + "cost": cost_per_unit * self.property.insulation_wall_area, } ) @@ -371,7 +373,10 @@ class WallRecommendations(BaseUtility): # By looping through ewi first, if there is nothing there, that ensures not combinations are tested for ewi_part in ewi_parts: for iwi_part in iwi_parts: - for ewi_depth, iwi_depth in itertools.product(ewi_part["depths"], iwi_part["depths"]): + for (ewi_depth, ewi_cost_per_unit), (iwi_depth, iwi_cost_per_unit) in itertools.product( + zip(ewi_part["depths"], ewi_part["cost"]), + zip(iwi_part["depths"], iwi_part["cost"]) + ): ewi_part_u_value = r_value_per_mm_to_u_value(ewi_depth, ewi_part["r_value_per_mm"]) iwi_part_u_value = r_value_per_mm_to_u_value(iwi_depth, iwi_part["r_value_per_mm"]) @@ -391,8 +396,8 @@ class WallRecommendations(BaseUtility): # For now, I'm adding them as separate items in the list recommendation = { "parts": [ - get_recommended_part(ewi_part, ewi_depth), - get_recommended_part(iwi_part, iwi_depth) + get_recommended_part(ewi_part, ewi_depth, ewi_cost_per_unit), + get_recommended_part(iwi_part, iwi_depth, iwi_cost_per_unit) ], "type": "wall_insulation", "description": ( @@ -401,7 +406,11 @@ class WallRecommendations(BaseUtility): ), "starting_u_value": u_value, "new_u_value": combined_new_u_value, - "sap_points": estimate_sap_points() + "sap_points": estimate_sap_points(), + "cost": ( + ewi_cost_per_unit * self.property.insulation_wall_area + iwi_cost_per_unit * + self.property.insulation_wall_area + ), } self.recommendations.append(recommendation) diff --git a/recommendations/recommendation_utils.py b/recommendations/recommendation_utils.py index 4ab7f6b6..5bbcbe9f 100644 --- a/recommendations/recommendation_utils.py +++ b/recommendations/recommendation_utils.py @@ -110,15 +110,17 @@ def update_lowest_selected_u_value(lowest_selected_u_value, new_u_value): return lowest_selected_u_value -def get_recommended_part(part, selected_depth): +def get_recommended_part(part, selected_depth, selected_cost): """ Utility function to return a recommended part with the selected depth. - :param part: - :param selected_depth: + :param part: part to be recommended + :param selected_depth: depth of the selected part + :param selected_cost: cost of the selected depth :return: """ recommended_part = deepcopy(part) recommended_part["depths"] = [selected_depth] + recommended_part["cost"] = [selected_cost] return recommended_part