define db methods

This commit is contained in:
Daniel Roth 2026-02-12 09:23:47 +00:00
parent e0857ab7a2
commit 598a612b40
2 changed files with 175 additions and 102 deletions

View file

@ -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

View file

@ -1,5 +0,0 @@
from backend.app.db.connection import db_session
class CategorisationPostgres:
pass