diff --git a/backend/app/db/functions/recommendations_functions.py b/backend/app/db/functions/recommendations_functions.py index 51562f55..c16adea2 100644 --- a/backend/app/db/functions/recommendations_functions.py +++ b/backend/app/db/functions/recommendations_functions.py @@ -3,15 +3,29 @@ from sqlalchemy import insert, delete from sqlalchemy.orm import Session from sqlalchemy.exc import SQLAlchemyError from backend.app.db.models.recommendations import ( - Plan, Recommendation, RecommendationMaterials, PlanRecommendations, Scenario + Plan, + Recommendation, + RecommendationMaterials, + PlanRecommendations, + Scenario, ) from backend.app.db.models.portfolio import PropertyModel from backend.app.db.connection import db_session, db_read_session def prepare_plan_data( - p, body, scenario_id, eco_packages, valuations, new_sap_points, new_epc, default_recommendations, - rebaselining_carbon=0, rebaselining_heat_demand=0, rebaselining_kwh=0, rebaselining_bills=0, + p, + body, + scenario_id, + eco_packages, + valuations, + new_sap_points, + new_epc, + default_recommendations, + rebaselining_carbon=0, + rebaselining_heat_demand=0, + rebaselining_kwh=0, + rebaselining_bills=0, ): """ Utility function to prepare the data that goes into the production of a plan. Is a fairly rough and unstructured @@ -32,21 +46,37 @@ def prepare_plan_data( """ # Plan carbon savings co2_savings = sum( - [r["co2_equivalent_savings"] for r in default_recommendations if not r.get("already_installed", False)] + [ + r["co2_equivalent_savings"] + for r in default_recommendations + if not r.get("already_installed", False) + ] ) post_co2_emissions = p.energy["co2_emissions"] - rebaselining_carbon - co2_savings # Plan bill savings energy_bill_savings = sum( - [r["energy_cost_savings"] for r in default_recommendations if not r.get("already_installed", False)] + [ + r["energy_cost_savings"] + for r in default_recommendations + if not r.get("already_installed", False) + ] + ) + post_energy_bill = ( + sum(p.current_energy_bill.values()) - rebaselining_bills - energy_bill_savings ) - post_energy_bill = sum(p.current_energy_bill.values()) - rebaselining_bills - energy_bill_savings # energy consumption energy_consumption_savings = sum( - [r["kwh_savings"] for r in default_recommendations if not r.get("already_installed", False)] + [ + r["kwh_savings"] + for r in default_recommendations + if not r.get("already_installed", False) + ] + ) + post_energy_consumption = ( + p.current_energy_consumption - rebaselining_kwh - energy_consumption_savings ) - post_energy_consumption = p.current_energy_consumption - rebaselining_kwh - energy_consumption_savings valuation_post_retrofit, valuation_increase = None, None if valuations["current_value"]: @@ -54,9 +84,19 @@ def prepare_plan_data( valuation_post_retrofit = valuations["average_increased_value"] # plan costing data - cost_of_works = sum([r["total"] for r in default_recommendations if not r.get("already_installed", False)]) + cost_of_works = sum( + [ + r["total"] + for r in default_recommendations + if not r.get("already_installed", False) + ] + ) contingency_cost = sum( - [r.get("contingency", 0) for r in default_recommendations if not r.get("already_installed", False)] + [ + r.get("contingency", 0) + for r in default_recommendations + if not r.get("already_installed", False) + ] ) return { @@ -86,7 +126,7 @@ def prepare_plan_data( "valuation_increase": valuation_increase, "cost_of_works": float(cost_of_works), "contingency_cost": float(contingency_cost), - "plan_type": eco_packages.get(p.id, (None, None, None))[2] + "plan_type": eco_packages.get(p.id, (None, None, None))[2], } @@ -119,11 +159,7 @@ def bulk_create_plans(session: Session, plans_to_create: list[dict]) -> dict[int for p in plans_to_create ] - stmt = ( - insert(Plan) - .values(payload) - .returning(Plan.id, Plan.property_id) - ) + stmt = insert(Plan).values(payload).returning(Plan.id, Plan.property_id) result = session.execute(stmt).all() @@ -133,9 +169,7 @@ def bulk_create_plans(session: Session, plans_to_create: list[dict]) -> dict[int def create_scenario(session: Session, scenario: dict) -> int: existing_scenario = ( - session.query(Scenario) - .filter_by(portfolio_id=scenario["portfolio_id"]) - .first() + session.query(Scenario).filter_by(portfolio_id=scenario["portfolio_id"]).first() ) scenario["is_default"] = not bool(existing_scenario) @@ -167,7 +201,9 @@ def create_recommendation(session: Session, recommendation): raise e -def create_recommendation_material(session: Session, recommendation_id, material_id, depth): +def create_recommendation_material( + session: Session, recommendation_id, material_id, depth +): """ This function will create a record for the recommendation_material in the database if it does not exist. :param session: The databse session @@ -177,9 +213,7 @@ def create_recommendation_material(session: Session, recommendation_id, material """ new_recommendation_material = RecommendationMaterials( - recommendation_id=recommendation_id, - material_id=material_id, - depth=depth + recommendation_id=recommendation_id, material_id=material_id, depth=depth ) session.add(new_recommendation_material) session.flush() @@ -196,13 +230,17 @@ def create_plan_recommendations(session: Session, plan_id, recommendation_ids): """ # Prepare a list of dictionaries for bulk insert - data = [{"plan_id": plan_id, "recommendation_id": rid} for rid in recommendation_ids] + data = [ + {"plan_id": plan_id, "recommendation_id": rid} for rid in recommendation_ids + ] # Bulk insert using SQLAlchemy's core API session.execute(insert(PlanRecommendations).values(data)) -def upload_recommendations(session: Session, recommendations_to_upload, property_id, new_plan_id): +def upload_recommendations( + session: Session, recommendations_to_upload, property_id, new_plan_id +): try: # Prepare data for bulk insert for Recommendation recommendations_data = [ @@ -213,8 +251,14 @@ def upload_recommendations(session: Session, recommendations_to_upload, property "description": rec["description"], "estimated_cost": float(rec["total"]), "default": rec["default"], - "starting_u_value": float(rec.get("starting_u_value")) if rec.get("starting_u_value") else None, - "new_u_value": float(rec.get("new_u_value")) if rec.get("new_u_value") else None, + "starting_u_value": ( + float(rec.get("starting_u_value")) + if rec.get("starting_u_value") + else None + ), + "new_u_value": ( + float(rec.get("new_u_value")) if rec.get("new_u_value") else None + ), "sap_points": float(rec["sap_points"]), "energy_savings": float(rec["heat_demand"]), "kwh_savings": float(rec["kwh_savings"]), @@ -223,13 +267,17 @@ def upload_recommendations(session: Session, recommendations_to_upload, property "energy_cost_savings": float(rec["energy_cost_savings"]), "labour_days": float(rec["labour_days"]), "already_installed": rec["already_installed"], - "heat_demand": float(rec["heat_demand"]) + "heat_demand": float(rec["heat_demand"]), } for rec in recommendations_to_upload ] # Insert the recommendations, get back the IDs - stmt = insert(Recommendation).returning(Recommendation.id).values(recommendations_data) + stmt = ( + insert(Recommendation) + .returning(Recommendation.id) + .values(recommendations_data) + ) result = session.execute(stmt) uploaded_recommendation_ids = [row[0] for row in result] @@ -243,11 +291,15 @@ def upload_recommendations(session: Session, recommendations_to_upload, property "quantity_unit": part.get("quantity_unit", None), "estimated_cost": float(part.get("total", part.get("total_cost"))), } - for rec, recommendation_id in zip(recommendations_to_upload, uploaded_recommendation_ids) + for rec, recommendation_id in zip( + recommendations_to_upload, uploaded_recommendation_ids + ) for part in rec["parts"] ] - session.bulk_insert_mappings(RecommendationMaterials, recommendation_materials_data) + session.bulk_insert_mappings( + RecommendationMaterials, recommendation_materials_data + ) # flush the changes to get the newly created IDs session.flush() @@ -283,25 +335,27 @@ def bulk_upload_recommendations_and_materials( plan_ids_by_index = [] for rec in recommendation_payload: - recommendation_rows.append({ - "property_id": rec["property_id"], - "type": rec["type"], - "measure_type": rec["measure_type"], - "description": rec["description"], - "estimated_cost": rec["estimated_cost"], - "default": rec["default"], - "starting_u_value": rec["starting_u_value"], - "new_u_value": rec["new_u_value"], - "sap_points": rec["sap_points"], - "heat_demand": rec["heat_demand"], - "kwh_savings": rec["kwh_savings"], - "co2_equivalent_savings": rec["co2_equivalent_savings"], - "energy_savings": rec["energy_savings"], - "energy_cost_savings": rec["energy_cost_savings"], - "total_work_hours": rec["total_work_hours"], - "labour_days": rec["labour_days"], - "already_installed": rec["already_installed"], - }) + recommendation_rows.append( + { + "property_id": rec["property_id"], + "type": rec["type"], + "measure_type": rec["measure_type"], + "description": rec["description"], + "estimated_cost": rec["estimated_cost"], + "default": rec["default"], + "starting_u_value": rec["starting_u_value"], + "new_u_value": rec["new_u_value"], + "sap_points": rec["sap_points"], + "heat_demand": rec["heat_demand"], + "kwh_savings": rec["kwh_savings"], + "co2_equivalent_savings": rec["co2_equivalent_savings"], + "energy_savings": rec["energy_savings"], + "energy_cost_savings": rec["energy_cost_savings"], + "total_work_hours": rec["total_work_hours"], + "labour_days": rec["labour_days"], + "already_installed": rec["already_installed"], + } + ) parts_by_index.append(rec["parts"]) plan_ids_by_index.append(rec["plan_id"]) @@ -310,9 +364,7 @@ def bulk_upload_recommendations_and_materials( # 2. Insert recommendations and get IDs # --------------------------------------------------------- result = session.execute( - insert(Recommendation) - .values(recommendation_rows) - .returning(Recommendation.id) + insert(Recommendation).values(recommendation_rows).returning(Recommendation.id) ) recommendation_ids = [row[0] for row in result] @@ -324,19 +376,19 @@ def bulk_upload_recommendations_and_materials( for recommendation_id, parts in zip(recommendation_ids, parts_by_index): for part in parts: - materials_rows.append({ - "recommendation_id": recommendation_id, - "material_id": part["material_id"], - "depth": part["depth"], - "quantity": part["quantity"], - "quantity_unit": part["quantity_unit"], - "estimated_cost": part["estimated_cost"], - }) + materials_rows.append( + { + "recommendation_id": recommendation_id, + "material_id": part["material_id"], + "depth": part["depth"], + "quantity": part["quantity"], + "quantity_unit": part["quantity_unit"], + "estimated_cost": part["estimated_cost"], + } + ) if materials_rows: - session.execute( - insert(RecommendationMaterials).values(materials_rows) - ) + session.execute(insert(RecommendationMaterials).values(materials_rows)) # --------------------------------------------------------- # 4. Insert plan ↔ recommendation links @@ -346,26 +398,22 @@ def bulk_upload_recommendations_and_materials( "plan_id": plan_id, "recommendation_id": recommendation_id, } - for plan_id, recommendation_id in zip( - plan_ids_by_index, recommendation_ids - ) + for plan_id, recommendation_id in zip(plan_ids_by_index, recommendation_ids) ] - session.execute( - insert(PlanRecommendations).values(plan_recommendation_rows) - ) + session.execute(insert(PlanRecommendations).values(plan_recommendation_rows)) def chunked(iterable, size=100): for i in range(0, len(iterable), size): - yield iterable[i:i + size] + yield iterable[i : i + size] def get_property_ids(portfolio_id: int) -> list[int]: with db_read_session() as session: return [ - pid for (pid,) in - session.query(PropertyModel.id) + pid + for (pid,) in session.query(PropertyModel.id) .filter(PropertyModel.portfolio_id == portfolio_id) .all() ] @@ -381,12 +429,14 @@ def delete_property_batch(session: Session, property_ids: list[int]): # recommendation_materials (via recommendation) # -------------------------------------------------- session.execute( - text(""" + text( + """ DELETE FROM recommendation_materials rm USING recommendation r WHERE rm.recommendation_id = r.id AND r.property_id = ANY(:property_ids) - """), + """ + ), params, ) @@ -394,12 +444,14 @@ def delete_property_batch(session: Session, property_ids: list[int]): # plan_recommendations (via plan) # -------------------------------------------------- session.execute( - text(""" + text( + """ DELETE FROM plan_recommendations pr USING plan p WHERE pr.plan_id = p.id AND p.property_id = ANY(:property_ids) - """), + """ + ), params, ) @@ -407,13 +459,15 @@ def delete_property_batch(session: Session, property_ids: list[int]): # funding_package_measures # -------------------------------------------------- session.execute( - text(""" + text( + """ DELETE FROM funding_package_measures fpm USING funding_package fp, plan p WHERE fpm.funding_package_id = fp.id AND fp.plan_id = p.id AND p.property_id = ANY(:property_ids) - """), + """ + ), params, ) @@ -421,10 +475,12 @@ def delete_property_batch(session: Session, property_ids: list[int]): # inspections (direct) # -------------------------------------------------- session.execute( - text(""" + text( + """ DELETE FROM inspections WHERE property_id = ANY(:property_ids) - """), + """ + ), params, ) @@ -432,12 +488,14 @@ def delete_property_batch(session: Session, property_ids: list[int]): # funding_package # -------------------------------------------------- session.execute( - text(""" + text( + """ DELETE FROM funding_package fp USING plan p WHERE fp.plan_id = p.id AND p.property_id = ANY(:property_ids) - """), + """ + ), params, ) @@ -445,10 +503,12 @@ def delete_property_batch(session: Session, property_ids: list[int]): # recommendation (direct — CRITICAL FIX) # -------------------------------------------------- session.execute( - text(""" + text( + """ DELETE FROM recommendation WHERE property_id = ANY(:property_ids) - """), + """ + ), params, ) @@ -456,10 +516,12 @@ def delete_property_batch(session: Session, property_ids: list[int]): # plan (direct) # -------------------------------------------------- session.execute( - text(""" + text( + """ DELETE FROM plan WHERE property_id = ANY(:property_ids) - """), + """ + ), params, ) @@ -467,18 +529,22 @@ def delete_property_batch(session: Session, property_ids: list[int]): # property-scoped tables # -------------------------------------------------- session.execute( - text(""" + text( + """ DELETE FROM property_details_epc WHERE property_id = ANY(:property_ids) - """), + """ + ), params, ) session.execute( - text(""" + text( + """ DELETE FROM property_targets WHERE property_id = ANY(:property_ids) - """), + """ + ), params, ) @@ -486,10 +552,12 @@ def delete_property_batch(session: Session, property_ids: list[int]): # properties LAST # -------------------------------------------------- session.execute( - text(""" + text( + """ DELETE FROM property WHERE id = ANY(:property_ids) - """), + """ + ), params, ) @@ -509,10 +577,7 @@ def delete_portfolio_scenarios_if_empty(portfolio_id: int): return with db_session() as session: - session.execute( - delete(Scenario) - .where(Scenario.portfolio_id == portfolio_id) - ) + session.execute(delete(Scenario).where(Scenario.portfolio_id == portfolio_id)) print("Deleted scenarios for empty portfolio") @@ -530,6 +595,7 @@ def clear_portfolio_in_batches( total = (len(property_ids) + property_batch_size - 1) // property_batch_size import time + for i, batch in enumerate(chunked(property_ids, property_batch_size), start=1): print(f"Deleting batch {i}/{total} ({len(batch)} properties)") start_time = time.time() @@ -542,3 +608,15 @@ def clear_portfolio_in_batches( delete_portfolio_scenarios_if_empty(portfolio_id) print("Portfolio cleared in batches.") + + +def get_plans_by_portfolio_id(portfolio_id: int) -> list[Plan]: + raise NotImplementedError + + +def get_scenario(scenario_id: int) -> list[Scenario]: + raise NotImplementedError + + +def set_plan_default(plan_id: int, is_default: bool) -> bool: + raise NotImplementedError diff --git a/backend/categorisation/categorisation_postgres.py b/backend/categorisation/categorisation_postgres.py deleted file mode 100644 index f2a44e5b..00000000 --- a/backend/categorisation/categorisation_postgres.py +++ /dev/null @@ -1,5 +0,0 @@ -from backend.app.db.connection import db_session - - -class CategorisationPostgres: - pass