From d4a45a5ccebb0fead68364bdf9cfad2a6a566fae Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 8 Dec 2025 18:45:46 +0000 Subject: [PATCH] added additional fields to db and fixed flat roof recommendations bug --- backend/Property.py | 24 ++++++- .../db/functions/recommendations_functions.py | 6 ++ backend/app/db/models/portfolio.py | 4 ++ backend/app/db/models/recommendations.py | 3 + recommendations/recommendation_utils.py | 66 +++++++++++-------- 5 files changed, 74 insertions(+), 29 deletions(-) diff --git a/backend/Property.py b/backend/Property.py index 6328ac8c..a9a1ac1b 100644 --- a/backend/Property.py +++ b/backend/Property.py @@ -833,9 +833,29 @@ class Property: if self.current_energy_bill is None: raise ValueError("Current energy bill has not been set") + # IF we have a SAP05 overwrite, we pull out the relevant information + sap_05_overwritten = self.data.get("sap_05_overwritten", False) + + sap_05_score, sap_05_epc_rating = None, None + if sap_05_overwritten: + if not self.old_data: + # Trying to fetch SAP05 EPC but no data + raise ValueError("Trying to fetch SAP05 EPC but no old data available") + # We get the last rating from the old data + newest_old_epc = max(self.old_data, key=lambda d: pd.to_datetime(d["lodgement-date"])) + # Get the rating and score + sap_05_score = int(newest_old_epc["current-energy-efficiency"]) + sap_05_epc_rating = newest_old_epc["current-energy-rating"] + + lodgement_date = self.data["lodgement-date"] + # We check if the lodgement date is more than 10 years old + is_expired = (datetime.now() - pd.to_datetime(lodgement_date)) > timedelta(days=3650) + property_details_epc = { "property_id": self.id, "portfolio_id": portfolio_id, + "lodgement_date": datetime.fromisoformat(lodgement_date), + "is_expired": is_expired, "full_address": self.data["address"], "total_floor_area": float(self.data["total-floor-area"]), "walls": self.walls["clean_description"], @@ -891,7 +911,9 @@ class Property: "current_energy_demand_heating_hotwater": self.current_energy_consumption_heating_hotwater, "estimated": self.data.get("estimated", False), # We indicate if we've overwritten a SAP 05 EPC - "sap_05_overwritten": self.data.get("sap_05_overwritten", False), + "sap_05_overwritten": sap_05_overwritten, + "sap_05_score": sap_05_score, + "sap_05_epc_rating": sap_05_epc_rating, **self.current_energy_bill } diff --git a/backend/app/db/functions/recommendations_functions.py b/backend/app/db/functions/recommendations_functions.py index dfdd153e..f7b5f5eb 100644 --- a/backend/app/db/functions/recommendations_functions.py +++ b/backend/app/db/functions/recommendations_functions.py @@ -44,6 +44,10 @@ def prepare_plan_data( valuation_increase = valuations["average_increase"] valuation_post_retrofit = valuations["average_increased_value"] + # plan costing data + cost_of_works = sum([r["total"] for r in default_recommendations]) + contingency_cost = sum([r["contingency"] for r in default_recommendations]) + return { "portfolio_id": body.portfolio_id, "property_id": p.id, @@ -69,6 +73,8 @@ def prepare_plan_data( "energy_consumption_savings": float(energy_consumption_savings), "valuation_post_retrofit": valuation_post_retrofit, "valuation_increase": valuation_increase, + "cost_of_works": cost_of_works, + "contingency_cost": contingency_cost, "plan_type": eco_packages.get(p.id, (None, None, None))[2] } diff --git a/backend/app/db/models/portfolio.py b/backend/app/db/models/portfolio.py index 7fec8c14..ea9f9976 100644 --- a/backend/app/db/models/portfolio.py +++ b/backend/app/db/models/portfolio.py @@ -137,6 +137,8 @@ class PropertyDetailsEpcModel(Base): property_id = Column(Integer, ForeignKey('property.id'), nullable=False) portfolio_id = Column(Integer, ForeignKey('portfolio.id'), nullable=False) full_address = Column(Text) + lodgement_date = Column(DateTime) + is_expired = Column(Boolean) total_floor_area = Column(Float) walls = Column(Text) walls_rating = Column(Integer, CheckConstraint('walls_rating>=1 AND walls_rating<=5')) @@ -176,6 +178,8 @@ class PropertyDetailsEpcModel(Base): current_energy_demand_heating_hotwater = Column(Float) estimated = Column(Boolean, default=False) sap_05_overwritten = Column(Boolean, default=False) + sap_05_score = Column(Integer) + sap_05_epc_rating = Column(Enum(Epc)) # Include estimates for energy bills, across the different types of energy heating_cost_current = Column(Float) hot_water_cost_current = Column(Float) diff --git a/backend/app/db/models/recommendations.py b/backend/app/db/models/recommendations.py index 4c02268d..800596ec 100644 --- a/backend/app/db/models/recommendations.py +++ b/backend/app/db/models/recommendations.py @@ -89,6 +89,9 @@ class Plan(Base): energy_consumption_savings = Column(Float) valuation_post_retrofit = Column(Float) valuation_increase = Column(Float) + # Financial metrics, excluding funding + cost_of_works = Column(Float) + contingency_cost = Column(Float) class PlanRecommendations(Base): diff --git a/recommendations/recommendation_utils.py b/recommendations/recommendation_utils.py index 6acc04f9..adbeecf5 100644 --- a/recommendations/recommendation_utils.py +++ b/recommendations/recommendation_utils.py @@ -239,42 +239,52 @@ def get_wall_u_value( return float(mapped_value) +def _try_convert_to_int(value): + try: + return int(value) + except (TypeError, ValueError): + return None + + def extract_thickness(thickness, is_roof_room, is_at_rafters, is_loft, is_flat): + thickness_map = { + "below average": "50", + "average": "100", + "above average": "150", + "none": "0", + } + + # Normalise none value early + if thickness is None: + thickness = "none" + if is_roof_room or is_at_rafters: - # TODO: We get None instead of a string none, this should be fixed - if thickness is None: - thickness = "none" + + int_thickness = _try_convert_to_int(thickness) + if int_thickness is not None: + return int_thickness # We re-map the thickness - thickness_map = { - "below average": "50", - "average": "100", - "above average": "150", - "none": "0", - } - thickness = thickness_map[thickness] + + thickness = thickness_map.get(thickness) + if thickness is None: + return None + + return int(thickness) if is_flat: - try: - thickness = int(thickness) - return thickness - except (TypeError, ValueError): - # If thickness is not a valid number (could be a string or None), return None - return None + return _try_convert_to_int(thickness) - if thickness in ["below average", "average", "above average", "none", None] or ( - not is_loft and not is_roof_room and not is_at_rafters + # Thicknes will never be none + if thickness in thickness_map or ( + not (is_loft or is_roof_room or is_at_rafters) ): return None - elif thickness.endswith("+"): - thickness = int(thickness[:-1]) - return thickness - else: - try: - thickness = int(thickness) - return thickness - except ValueError: - # If thickness is not a valid number (could be a string or None), return None - return None + + if isinstance(thickness, str) and str(thickness).endswith("+"): + return _try_convert_to_int(thickness[:-1]) + + # final attempt + return _try_convert_to_int(thickness) def get_u_value_from_s9(