diff --git a/backend/app/db/models/materials.py b/backend/app/db/models/materials.py index 97085d7a..f0af3343 100644 --- a/backend/app/db/models/materials.py +++ b/backend/app/db/models/materials.py @@ -88,3 +88,4 @@ class Material(Base): plant_cost = Column(Float) total_cost = Column(Float) notes = Column(String) + is_installer_quote = Column(Boolean, nullable=False, default=False) diff --git a/backend/app/plan/router.py b/backend/app/plan/router.py index 5e10080e..80392c88 100644 --- a/backend/app/plan/router.py +++ b/backend/app/plan/router.py @@ -284,16 +284,16 @@ async def trigger_plan(body: PlanTriggerRequest): property_id, is_new = create_property( session, body.portfolio_id, epc_searcher.address_clean, epc_searcher.postcode_clean, epc_searcher.uprn ) - if not is_new: - continue - - create_property_targets( - session, - property_id=property_id, - portfolio_id=body.portfolio_id, - epc_target=body.goal_value, - heat_demand_target=None - ) + # if not is_new: + # continue + # + # create_property_targets( + # session, + # property_id=property_id, + # portfolio_id=body.portfolio_id, + # epc_target=body.goal_value, + # heat_demand_target=None + # ) epc_records = { 'original_epc': epc_searcher.newest_epc.copy(), diff --git a/etl/costs/app.py b/etl/costs/app.py index 30eff735..59852cc5 100644 --- a/etl/costs/app.py +++ b/etl/costs/app.py @@ -7,10 +7,13 @@ from sqlalchemy.orm import Session from sqlalchemy import create_engine from backend.app.db.models.materials import Material from recommendations.recommendation_utils import calculate_r_value_per_mm +import inspect -DATA_DIRECTORY = Path(__file__).parent / "local_data" / "Hestia Materials.xlsx" +src_file_path = inspect.getfile(lambda: None) + +DATA_DIRECTORY = Path(src_file_path).parent / "local_data" / "20240626 Hestia Materials.xlsx" # Environment file is at the same level as this file -ENV_FILE = Path(__file__).parent / "etl" / "costs" / ".env" +ENV_FILE = Path(src_file_path).parent / "etl" / "costs" / ".env" dotenv.load_dotenv(ENV_FILE) DB_USERNAME = os.getenv('DB_USERNAME') @@ -87,7 +90,8 @@ def app(): solid_floor_costs, ewi_costs, lel_costs, - flat_roof_costs + flat_roof_costs, + window_costs ] ) diff --git a/etl/customers/vander_elliot/non_intrusives.py b/etl/customers/vander_elliot/non_intrusives.py index 7d092b5d..bbc46754 100644 --- a/etl/customers/vander_elliot/non_intrusives.py +++ b/etl/customers/vander_elliot/non_intrusives.py @@ -6,6 +6,14 @@ from etl.non_intrusive_surveys.upload.UploadNonIntrusives import UploadNonIntrus PORTFOLIO_ID = 82 USER_ID = 8 +already_installed = [ + { + 'address': 'Flat 3 2 Linacre Lane', + 'postcode': 'L20 5AH', + "already_installed": ["windows_glazing"] + } +] + def app(): """ @@ -91,6 +99,14 @@ def app(): asset_list = pd.DataFrame(asset_list) + # Store overrides in s3 + already_installed_filename = f"{USER_ID}/{PORTFOLIO_ID}/already_installed.json" + save_csv_to_s3( + dataframe=pd.DataFrame(already_installed), + bucket_name="retrofit-plan-inputs-dev", + file_name=already_installed_filename + ) + # Store the asset list in s3 filename = f"{USER_ID}/{PORTFOLIO_ID}/non_intrusives.csv" save_csv_to_s3( @@ -105,7 +121,7 @@ def app(): "goal": "Increase EPC", "goal_value": "A", "trigger_file_path": filename, - "already_installed_file_path": "", + "already_installed_file_path": already_installed_filename, "patches_file_path": "", "non_invasive_recommendations_file_path": "", "budget": None, diff --git a/recommendations/Costs.py b/recommendations/Costs.py index 5f752730..b056274e 100644 --- a/recommendations/Costs.py +++ b/recommendations/Costs.py @@ -104,9 +104,9 @@ DOUBLE_RADIATOR_COST = 300 FLUE_COST = 600 PIPEWORK_COST = 750 # Min cost is £500 -# This is the cost per meter squared for cavity extraction -# https://www.checkatrade.com/blog/cost-guides/cavity-wall-insulation-removal-cost/ -CAVITY_EXTRACTION_COST = 21.5 +# Based on SCIS figures +# TODO: Add this to databse +CAVITY_EXTRACTION_COST = 25 class Costs: @@ -203,6 +203,20 @@ class Costs: :return: A dictionary containing detailed cost breakdown. """ + # CWI usually takes 1 day + labour_hours = 8 + labour_days = 1 + + # if the material is based on an installer cost, we return the flat price + if material["is_installer_quote"]: + total_cost = material["total_cost"] * wall_area + + return { + "total": total_cost, + "labour_hours": labour_hours, + "labour_days": labour_days, + } + material_cost_per_m2 = material["material_cost"] base_material_cost = material_cost_per_m2 * wall_area @@ -220,11 +234,6 @@ class Costs: total_cost = subtotal_before_vat + vat_cost - labour_hours = material["labour_hours_per_unit"] * wall_area - - # Assume a team of 2 - labour_days = (labour_hours / 8) / 2 - if is_extraction_and_refill: # bump up the cost of the work total_cost = total_cost + CAVITY_EXTRACTION_COST * wall_area @@ -314,6 +323,22 @@ class Costs: :return: """ + # if the material is based on an installer cost, we return the flat price + if material["is_installer_quote"]: + total_cost = material["total_cost"] * wall_area + + labour_hours = material["labour_hours_per_unit"] * wall_area + + # To install internal wall insulation, a small to medium size project might be conducted by a team of 3-5 + # people + labour_days = (labour_hours / 8) / 4 + + return { + "total": total_cost, + "labour_hours": labour_hours, + "labour_days": labour_days, + } + # Extract and check the different types of data we'll need demolition_data = [x for x in non_insulation_materials if x["type"] == "iwi_wall_demolition"] vapour_barrier_data = [x for x in non_insulation_materials if x["type"] == "iwi_vapour_barrier"] @@ -619,6 +644,24 @@ class Costs: :return: """ + if material["is_installer_quote"]: + total_cost = material["total_cost"] * wall_area + # Add on a buffer for scaffolding + if self.property.data["property-type"] == "House": + total_cost += self.EWI_SCAFFOLDING_PRELIMINARIES * total_cost + + labour_hours = material["labour_hours_per_unit"] * wall_area + + # To install internal wall insulation, a small to medium size project might be conducted by a team of 3-5 + # people + labour_days = (labour_hours / 8) / 4 + + return { + "total": total_cost, + "labour_hours": labour_hours, + "labour_days": labour_days, + } + # For semi detatched and detatched houses, as well as maisonettes, we price for scaffolding if self.property.data["property-type"] == "House":