diff --git a/backend/Property.py b/backend/Property.py index 47e885aa..4a55e504 100644 --- a/backend/Property.py +++ b/backend/Property.py @@ -470,7 +470,7 @@ class Property: to_update[k] = None return to_update - def get_full_property_data(self): + def get_full_property_data(self, current_valuation=None): """ This method extracts the data which is pushed to the database, containing core information, from the EPC about a property @@ -492,6 +492,7 @@ class Property: "tenure": self.data["tenure"], "current_epc_rating": self.data["current-energy-rating"], "current_sap_points": self.data["current-energy-efficiency"], + "current_valuation": current_valuation } property_data = self._clean_upload_data(property_data) diff --git a/backend/app/db/models/portfolio.py b/backend/app/db/models/portfolio.py index f7c0370b..830866e6 100644 --- a/backend/app/db/models/portfolio.py +++ b/backend/app/db/models/portfolio.py @@ -86,6 +86,7 @@ class PropertyModel(Base): tenure = Column(Text) current_epc_rating = Column(Enum(Epc)) current_sap_points = Column(Float) + current_valuation = Column(Float) class FeatureRating(enum.Enum): diff --git a/backend/app/db/models/recommendations.py b/backend/app/db/models/recommendations.py index 42ecbddf..a492f2f2 100644 --- a/backend/app/db/models/recommendations.py +++ b/backend/app/db/models/recommendations.py @@ -53,6 +53,9 @@ class Plan(Base): property_id = Column(BigInteger, ForeignKey(PropertyModel.id), nullable=False) created_at = Column(TIMESTAMP, nullable=False, server_default=func.now()) is_default = Column(Boolean, nullable=False) + valuation_increase_lower_bound = Column(Float) + valuation_increase_upper_bound = Column(Float) + valuation_increase_average = Column(Float) class PlanRecommendations(Base): diff --git a/backend/app/plan/router.py b/backend/app/plan/router.py index a79af32b..56d586ae 100644 --- a/backend/app/plan/router.py +++ b/backend/app/plan/router.py @@ -291,27 +291,43 @@ async def trigger_plan(body: PlanTriggerRequest): batch_properties = input_properties[i:i + BATCH_SIZE] for p in batch_properties: + recommendations_to_upload = recommendations.get(p.id, []) + default_recommendations = [r for r in recommendations_to_upload if r["default"]] + total_sap_points = sum([r["sap_points"] for r in default_recommendations]) + new_sap_points = float(p.data["current-energy-efficiency"]) + total_sap_points + new_epc = sap_to_epc(new_sap_points) + + valuations = PropertyValuation.estimate(property_instance=p, target_epc=new_epc) + # Your existing operations property_details_epc = p.get_property_details_epc( - portfolio_id=body.portfolio_id, rating_lookup=rating_lookup + portfolio_id=body.portfolio_id, rating_lookup=rating_lookup, ) create_property_details_epc(session, property_details_epc) update_or_create_property_spatial_details(session, p.uprn, p.spatial) - property_data = p.get_full_property_data() + property_data = p.get_full_property_data(current_valuation=valuations["current_value"]) update_property_data( session, property_id=p.id, portfolio_id=body.portfolio_id, property_data=property_data ) - recommendations_to_upload = recommendations.get(p.id, []) if not recommendations_to_upload: continue new_plan_id = create_plan(session, { "portfolio_id": body.portfolio_id, "property_id": p.id, - "is_default": True + "is_default": True, + "valuation_increase_lower_bound": ( + valuations["lower_bound_increased_value"] - valuations["current_value"] + ), + "valuation_increase_upper_bound": ( + valuations["upper_bound_increased_value"] - valuations["current_value"] + ), + "valuation_increase_average": ( + valuations["average_increased_value"] - valuations["current_value"] + ), }) uploaded_recommendation_ids = upload_recommendations(session, recommendations_to_upload, p.id) @@ -320,14 +336,6 @@ async def trigger_plan(body: PlanTriggerRequest): session, plan_id=new_plan_id, recommendation_ids=uploaded_recommendation_ids ) - # Get defaults - default_recommendations = [r for r in recommendations_to_upload if r["default"]] - total_sap_points = sum([r["sap_points"] for r in default_recommendations]) - new_sap_points = float(p.data["current-energy-efficiency"]) + total_sap_points - new_epc = sap_to_epc(new_sap_points) - - valuations = PropertyValuation.estimate(property_instance=p, target_epc=new_epc) - property_valuation_increases.append( valuations["average_increased_value"] - valuations["current_value"] ) diff --git a/etl/customers/slide_utils.py b/etl/customers/slide_utils.py index 1a2a894e..a0f6a68c 100644 --- a/etl/customers/slide_utils.py +++ b/etl/customers/slide_utils.py @@ -1,4 +1,5 @@ import os +from pptx.enum.text import PP_ALIGN from pptx import Presentation from pptx.util import Inches, Pt import matplotlib.pyplot as plt @@ -148,26 +149,64 @@ def save_figure_as_image(figure, filename='temp_plot.png'): plt.close(figure) # Close the figure to prevent it from displaying in notebooks or Python environments +def add_commentary_with_bullets(slide, commentary, top_inches, left_inches=Inches(1), width_inches=Inches(8), + height_inches=Inches(2)): + """ + Adds commentary with bullet points to a slide. + + :param slide: The slide object to add the commentary to. + :param commentary: The commentary text, with sections separated by newlines for bullet points. + :param top_inches: The top position of the commentary text box. + :param left_inches: The left position of the commentary text box. + :param width_inches: The width of the commentary text box. + :param height_inches: The height of the commentary text box. + """ + txBox = slide.shapes.add_textbox(left_inches, top_inches, width_inches, height_inches) + tf = txBox.text_frame + + # Configure text frame + tf.word_wrap = True + tf.auto_size = True + tf.paragraphs[0].alignment = PP_ALIGN.LEFT + + # Split the commentary into sections for bullet points + sections = commentary.split("\n") + + for i, section in enumerate(sections): + if i > 0: + p = tf.add_paragraph() # Add a new paragraph for each section after the first + else: + p = tf.paragraphs[0] # Use the first paragraph for the first section + p.text = section + p.space_after = Pt(14) # Adjust space after each bullet point as needed + p.font.size = Pt(14) # Adjust font size as needed + p.level = 0 # Bullet level, can be adjusted for nested bullets + p.space_before = Pt(0) + + def add_slide_with_image(prs, title, img_path=None, commentary=None): """ - Adds a slide with an image and optional commentary. + Adds a slide with an image (if provided) and optional commentary. If no image is provided, + places the commentary text in the middle of the slide. """ slide_layout = prs.slide_layouts[5] # Title and Content layout slide = prs.slides.add_slide(slide_layout) title_placeholder = slide.shapes.title title_placeholder.text = title - # Add the image + # Determine the position of the commentary text box based on whether an image is included if img_path: + # Add the image slide.shapes.add_picture(img_path, Inches(1), Inches(1.5), Inches(8), Inches(4.5)) + # Position for commentary when image is present + commentary_top = Inches(6) + else: + # Position for commentary when image is not present (centered vertically) + commentary_top = Inches(3) # Add commentary if provided if commentary: - txBox = slide.shapes.add_textbox(Inches(1), Inches(6), Inches(8), Inches(1)) - tf = txBox.text_frame - p = tf.add_paragraph() - p.text = commentary - p.font.size = Pt(14) # Adjust font size as needed + add_commentary_with_bullets(slide, commentary, commentary_top) def create_powerpoint(data, save_location): diff --git a/etl/customers/urban_splash/slides.py b/etl/customers/urban_splash/slides.py index 616939b2..4c2593ba 100644 --- a/etl/customers/urban_splash/slides.py +++ b/etl/customers/urban_splash/slides.py @@ -104,16 +104,18 @@ def app(): ) # Valuation: upper and lower bounds - TODO! - min_valuation, max_valuation, average_valuation = 0, 0, 0 + min_valuation, max_valuation, average_valuation = (0, 0, 0) + + recommendations_df.keys() slide_1_commentary = ( f"Floor areas range from {min_area} to {max_area} square meters, with an average of {average_area} square " - f"meters. " + f"meters. \n" f"Annual energy consumption ranges from {min_energy_consumption} to {max_energy_consumption} kWh, with an " - f"average of {average_consumption} kWh. " - f"CO2 emissions range from {min_co2} to {max_co2} tonnes, with an average of {average_co2} tonnes. " + f"average of {average_consumption} kWh. \n" + f"CO2 emissions range from {min_co2} to {max_co2} tonnes, with an average of {average_co2} tonnes. \n" f"Valuations range from £{min_valuation} to £{max_valuation} £, with an average of £" - f"{average_valuation}." + f"{average_valuation}.\n" ) ############ @@ -154,13 +156,13 @@ def app(): ) slide_2_commentary = ( - f"{n_units_to_target} expected to achieve EPC {EPC_TARGET} " - f"Measures include: {measures}" + f"{n_units_to_target} units expected to achieve EPC {EPC_TARGET} \n" + f"Measures include: {measures}\n" f"Valuation increase per property: £{min_valuation_impact}-{max_valuation_impact}, average: £" - f"{average_valuation_impact}" - f"Bill savings per property: £{min_bill_savings}-{max_bill_savings}, average: £{average_bill_savings}" - f"Total CO2 reduction: {min_co2_reduction}-{max_co2_reduction} tonnes, average: {average_co2_reduction}" - f"tonnes, total for the {n_units_to_target} properties: {total_co2_reduction} tonnes" + f"{average_valuation_impact}\n" + f"Bill savings per property: £{min_bill_savings}-{max_bill_savings}, average: £{average_bill_savings}\n" + f"Total CO2 reduction: {min_co2_reduction}-{max_co2_reduction} tonnes, average: {average_co2_reduction}\n" + f"tonnes, total for the {n_units_to_target} properties: {total_co2_reduction} tonnes\n" ) ############