diff --git a/recommendations/Costs.py b/recommendations/Costs.py index 09fe1a2b..0550183b 100644 --- a/recommendations/Costs.py +++ b/recommendations/Costs.py @@ -57,8 +57,6 @@ INSTALLER_SOLAR_COSTS = [ {'n_panels': 17, 'array_kwp': 17 * PANEL_SIZE, 'cost': 6637.95, 'installer': 'CEG'}, {'n_panels': 18, 'array_kwp': 18 * PANEL_SIZE, 'cost': 6792.57, 'installer': 'CEG'} ] -# This is the maximum number of panels that we have a cost from the installers for -INSTALLER_MAX_PANELS = 18 # CEG uses use Solshare as an inverter to provide solar PV to multiple flats. This costs £7500 for the inverter alone # https://midsummerwholesale.co.uk/buy/solshare @@ -185,6 +183,20 @@ class Costs: # When there is less uncertainty, a lower contingency rate is used LOW_RISK_CONTINGENCY = 0.05 + # Measure level contingency + CONTINGENCIES = { + "cavity_wall_insulation": 0.1, + "internal_wall_insulation": 0.26, + "external_wall_insulation": 0.26, + "solar_pv": 0.15, + "air_source_heat_pump": 0.2, + "flat_roof_insulation": 0.26, + "suspended_floor_insulation": 0.2, + "solid_floor_insulation": 0.26, + "low_energy_lighting": 0.26, + "high_heat_retention_storage_heaters": 0.1, + } + # Preliminaries are a percentage of the total cost of the work and covers the cost of site-specific costs # such as site preparation, safety measures and project management. This rate can vary but we'll assume a 10% # rate, on the total cost before VAT, as recommended by SPONs @@ -258,33 +270,14 @@ class Costs: 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, - } - - total_including_vat = material["total_cost"] * wall_area - - if is_extraction_and_refill: - total_including_vat = CAVITY_EXTRACTION_COST * wall_area - # Additional 2 days work - labour_hours += + (2 * 8) - labour_days += + 2 - - total_excluding_vat = total_including_vat / (1 + self.VAT_RATE) - vat_cost = total_including_vat - total_excluding_vat + total_cost = material["total_cost"] * wall_area return { - "total": total_including_vat, - "subtotal": total_excluding_vat, - "vat": vat_cost, + "total": total_cost, + "contingency": self.CONTINGENCIES["cavity_wall_insulation"] * total_cost, + "contingency_rate": self.CONTINGENCIES["cavity_wall_insulation"], "labour_hours": labour_hours, - "labour_days": labour_days + "labour_days": labour_days, } def loft_and_flat_insulation(self, floor_area, material): @@ -295,25 +288,20 @@ class Costs: :return: A dictionary containing detailed cost breakdown. """ - if material["is_installer_quote"]: - total_cost = material["total_cost"] * floor_area - - return { - "total": total_cost, - "labour_hours": 8, - "labour_days": 1, - } - - total_including_vat = material["total_cost"] * floor_area - total_excluding_vat = total_including_vat / (1 + self.VAT_RATE) - vat_cost = total_including_vat - total_excluding_vat + total_cost = material["total_cost"] * floor_area + if material["type"] == "loft_insulation": + contingency_rate = self.CONTINGENCIES["loft_insulation"] + contingency = contingency_rate * total_cost + else: + contingency_rate = self.CONTINGENCIES["flat_roof_insulation"] + contingency = contingency_rate * total_cost return { - "total": total_including_vat, - "subtotal": total_excluding_vat, - "vat": vat_cost, + "total": total_cost, + "contingency": contingency, + "contingency_rate": contingency_rate, "labour_hours": 8, - "labour_days": 1 + "labour_days": 1, } def solid_wall_insulation(self, wall_area, material): @@ -323,469 +311,155 @@ class Costs: """ # 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 + total_cost = material["total_cost"] * wall_area - labour_hours = material["labour_hours_per_unit"] * wall_area + if material["type"] == "internal_wall_insulation": + contingency_rate = self.CONTINGENCIES["internal_wall_insulation"] + else: + contingency_rate = self.CONTINGENCIES["external_wall_insulation"] - # 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 + labour_hours = material["labour_hours_per_unit"] * wall_area - return { - "total": total_cost, - "labour_hours": labour_hours, - "labour_days": labour_days, - } - - # Break out the individual material costs - # Since we don't know the exact wall construction, we take an average for demolition costs, since - # the cost will depend on the type of wall construction - - total_including_vat = material["total_cost"] * wall_area - total_excluding_vat = total_including_vat / (1 + self.VAT_RATE) - vat_cost = total_including_vat - total_excluding_vat - - # We estimate 1 weeks worth of work - labour_hours = 160 - # To install internal wall insulation, a small to medium size project might be conducted by a team of 3-5 people + # 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_including_vat, - "subtotal": total_excluding_vat, - "vat": vat_cost, + "total": total_cost, + "contingency": contingency_rate * total_cost, + "contingency_rate": contingency_rate, "labour_hours": labour_hours, "labour_days": labour_days, } - def suspended_floor_insulation(self, insulation_floor_area, material, non_insulation_materials): + def suspended_floor_insulation(self, insulation_floor_area, material): """ - We characterise the steps for suspended floor insulation as the following tasks: - - 1) Removal of Carpet and Underfelt: Where necessary, remove existing floor coverings to access the floorboards. - 2) Removal of Floor Boarding: Carefully remove floorboards to access the space beneath for insulation. - 3) Installation of Vapour Barrier: Install a vapour barrier to prevent moisture from affecting - the insulation and floor structure. - 4) Installation of Insulation: Fit the chosen insulation material between the joists in the floor void. - 5) Refixing Floorboards: Replace and secure the floorboards after insulation installation. - 6) Re-carpeting: Lay down the carpet or other floor coverings once the insulation and floorboards are in place. - :return: + Given an installer cost for the works, produces an estimate for the cost of works. + Includes contingency """ # if the material is based on an installer cost, we return the flat price - if material["is_installer_quote"]: - total_cost = material["total_cost"] * insulation_floor_area + total_cost = material["total_cost"] * insulation_floor_area - labour_hours = material["labour_hours_per_unit"] * insulation_floor_area - # To install suspended floor insulation, a small to medium size project might be conducted by a team of 3 - # people - labour_days = (labour_hours / 8) / 3 - - return { - "total": total_cost, - "labour_hours": labour_hours, - "labour_days": labour_days, - } - - demolition_data = [x for x in non_insulation_materials if x["type"] == "suspended_floor_demolition"] - vapour_barrier_data = [x for x in non_insulation_materials if x["type"] == "suspended_floor_vapour_barrier"] - redecoration_data = [x for x in non_insulation_materials if x["type"] == "suspended_floor_redecoration"] - - if (len(demolition_data) != 2) or (len(vapour_barrier_data) != 1) or (len(redecoration_data) != 2): - raise ValueError("Incorrect number of data entries for non-insulation materials") - - # Break out the individual material costs - demolition_material_costs = sum([x["material_cost"] * insulation_floor_area for x in demolition_data]) - insulation_material_costs = material["material_cost"] * insulation_floor_area - vapour_barrier_material_costs = vapour_barrier_data[0]["material_cost"] * insulation_floor_area - redecoration_material_costs = sum([x["material_cost"] * insulation_floor_area for x in redecoration_data]) - - demolition_labour_costs = sum([x["labour_cost"] * insulation_floor_area for x in demolition_data]) - insulation_labour_costs = material["labour_cost"] * insulation_floor_area - vapour_barrier_labour_costs = vapour_barrier_data[0]["labour_cost"] * insulation_floor_area - redecoration_labour_costs = sum([x["labour_cost"] * insulation_floor_area for x in redecoration_data]) - - labour_costs = (demolition_labour_costs + insulation_labour_costs + vapour_barrier_labour_costs + - redecoration_labour_costs) - - labour_costs = labour_costs * self.labour_adjustment_factor - - materials_costs = (demolition_material_costs + insulation_material_costs + vapour_barrier_material_costs + - redecoration_material_costs) - - subtotal_before_profit = labour_costs + materials_costs - - # Because of the possiblity of damage to the existing floor, or difficulties associated to moving fittings, - # we use a higher contingency rate - contingency_cost = subtotal_before_profit * self.HIGH_RISK_CONTINGENCY - preliminaries_cost = subtotal_before_profit * self.PRELIMINARIES - profit_cost = subtotal_before_profit * self.PROFIT_MARGIN - - subtotal_before_vat = subtotal_before_profit + contingency_cost + preliminaries_cost + profit_cost - - vat_cost = subtotal_before_vat * self.VAT_RATE - - total_cost = subtotal_before_vat + vat_cost - - demolition_labour_hours = sum([x["labour_hours_per_unit"] * insulation_floor_area for x in demolition_data]) - insulation_labour_hours = material["labour_hours_per_unit"] * insulation_floor_area - vapour_barrier_labour_hours = vapour_barrier_data[0]["labour_hours_per_unit"] * insulation_floor_area - redecoration_labour_hours = sum([x["labour_hours_per_unit"] * insulation_floor_area for x in redecoration_data]) - - labour_hours = (demolition_labour_hours + insulation_labour_hours + vapour_barrier_labour_hours + - redecoration_labour_hours) - - # Assume a team of 3 people for a small to medium size project + labour_hours = material["labour_hours_per_unit"] * insulation_floor_area + # To install suspended floor insulation, a small to medium size project might be conducted by a team of 3 + # people labour_days = (labour_hours / 8) / 3 return { "total": total_cost, - "subtotal": subtotal_before_vat, - "vat": vat_cost, - "contingency": contingency_cost, - "preliminaries": preliminaries_cost, - "material": materials_costs, - "profit": profit_cost, + "contengency": self.CONTINGENCIES["suspended_floor_insulation"] * total_cost, + "contingency_rate": self.CONTINGENCIES["suspended_floor_insulation"], "labour_hours": labour_hours, "labour_days": labour_days, - "labour_cost": labour_costs } - def solid_floor_insulation(self, insulation_floor_area, material, non_insulation_materials): + def solid_floor_insulation(self, insulation_floor_area, material): """ - We characterise the steps for solid floor insulation as the following tasks: - - 1) Removal of Carpet and Underfelt: This is the initial stage where any existing floor coverings, like carpets, - tiles, or linoleum, are carefully removed. This exposes the solid floor beneath, which is typically concrete. - - 2) Preparation of Flooring: This step is critical. It involves: - - Cleaning the existing floor surface thoroughly to remove debris and ensure a flat surface. - - Assessing and repairing any damage to the concrete floor. This might include filling cracks or leveling - uneven areas. - - 3) Installation of a Damp Proof Membrane (DPM): Before installing insulation, a DPM is often laid down to - prevent moisture from rising into the insulation and the interior space. This step is crucial in areas prone to - dampness. - - 4) Install Insulation: The insulation, often in the form of rigid foam boards, is laid over the DPM. - The choice of insulation material will depend on the desired thermal properties and the available floor height. - Care is taken to minimize thermal bridges and ensure a snug fit between insulation boards. - - 5) Laying a New Subfloor: Over the insulation, a new subfloor is often installed. This could be a layer of - screed (a type of concrete) or wooden boarding, depending on the specific requirements and preferences. - - 6) Re-decoration and Finishing Touches: Once the subfloor is in place and has set or dried (if necessary), - the final floor finish can be applied. This might involve: - - Laying new tiles, wooden flooring, or other chosen materials. - - If you're planning to re-carpet, this would be the stage to do it. - - Skirting boards may need to be refitted or replaced. - - 7) Considerations for Doors and Fixtures: It's important to note that raising the floor level can affect door - thresholds and other fixtures. Doors may need to be trimmed, and fixtures might need adjustments. + based on costing data from installers, produces an estimate for the cost of works. Returns contingency :param insulation_floor_area: Area of the floor to be insulated :param material: Selected insulation material - :param non_insulation_materials: Non-insulation materials required for the job :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"] * insulation_floor_area + total_cost = material["total_cost"] * insulation_floor_area - labour_hours = material["labour_hours_per_unit"] * insulation_floor_area - # To install suspended floor insulation, a small to medium size project might be conducted by a team of 3 - # people - labour_days = (labour_hours / 8) / 3 - - return { - "total": total_cost, - "labour_hours": labour_hours, - "labour_days": labour_days, - } - - demolition_data = [x for x in non_insulation_materials if x["type"] == "solid_floor_demolition"] - preparation_data = [x for x in non_insulation_materials if x["type"] == "solid_floor_preparation"] - vapour_barrier_data = [x for x in non_insulation_materials if x["type"] == "solid_floor_vapour_barrier"] - redecoration_data = [x for x in non_insulation_materials if x["type"] == "solid_floor_redecoration"] - - if ((len(demolition_data) != 1) or (len(preparation_data) != 2) or (len(vapour_barrier_data) != 1) or - (len(redecoration_data) != 3)): - raise ValueError("Incorrect number of data entries for non-insulation materials") - - # Break out the individual material costs - preparation_material_costs = sum([x["material_cost"] * insulation_floor_area for x in preparation_data]) - insulation_material_costs = material["material_cost"] * insulation_floor_area - vapour_barrier_material_costs = vapour_barrier_data[0]["material_cost"] * insulation_floor_area - redecoration_material_costs = sum([x["material_cost"] * insulation_floor_area for x in redecoration_data]) - - demolition_labour_costs = sum([x["labour_cost"] * insulation_floor_area for x in demolition_data]) - preparation_labour_costs = sum([x["labour_cost"] * insulation_floor_area for x in preparation_data]) - insulation_labour_costs = material["labour_cost"] * insulation_floor_area - vapour_barrier_labour_costs = vapour_barrier_data[0]["labour_cost"] * insulation_floor_area - redecoration_labour_costs = sum([x["labour_cost"] * insulation_floor_area for x in redecoration_data]) - - preparation_plant_costs = sum([x["plant_cost"] * insulation_floor_area for x in preparation_data]) - - labour_costs = (demolition_labour_costs + insulation_labour_costs + vapour_barrier_labour_costs + - redecoration_labour_costs + preparation_labour_costs) - - labour_costs = labour_costs * self.labour_adjustment_factor - - materials_cost = (preparation_material_costs + insulation_material_costs + vapour_barrier_material_costs + - redecoration_material_costs) - - subtotal_before_profit = labour_costs + materials_cost + preparation_plant_costs - - # We use HIGH_RISH_CONTINGENCY because of the potential for issues with moving fittings and trimming doors, - # as well as scope for damage to the existing floor during preparation. - contingency_cost = subtotal_before_profit * self.HIGH_RISK_CONTINGENCY - preliminaries_cost = subtotal_before_profit * self.PRELIMINARIES - profit_cost = subtotal_before_profit * self.PROFIT_MARGIN - - subtotal_before_vat = subtotal_before_profit + contingency_cost + preliminaries_cost + profit_cost - vat_cost = subtotal_before_vat * self.VAT_RATE - total_cost = subtotal_before_vat + vat_cost - - demolition_labour_hours = sum([x["labour_hours_per_unit"] * insulation_floor_area for x in demolition_data]) - preparation_labour_hours = sum([x["labour_hours_per_unit"] * insulation_floor_area for x in preparation_data]) - insulation_labour_hours = material["labour_hours_per_unit"] * insulation_floor_area - vapour_barrier_labour_hours = vapour_barrier_data[0]["labour_hours_per_unit"] * insulation_floor_area - redecoration_labour_hours = sum([x["labour_hours_per_unit"] * insulation_floor_area for x in redecoration_data]) - - labour_hours = (demolition_labour_hours + insulation_labour_hours + vapour_barrier_labour_hours + - redecoration_labour_hours + preparation_labour_hours) - - # Assume a team of 3 people for a small to medium size project + labour_hours = material["labour_hours_per_unit"] * insulation_floor_area + # To install suspended floor insulation, a small to medium size project might be conducted by a team of 3 + # people labour_days = (labour_hours / 8) / 3 return { "total": total_cost, - "subtotal": subtotal_before_vat, - "vat": vat_cost, - "contingency": contingency_cost, - "preliminaries": preliminaries_cost, - "material": materials_cost, - "profit": profit_cost, + "contingency": self.CONTINGENCIES["solid_floor_insulation"] * total_cost, + "contingency_rate": self.CONTINGENCIES["solid_floor_insulation"], "labour_hours": labour_hours, "labour_days": labour_days, - "labour_cost": labour_costs } - def low_energy_lighting(self, number_of_lights, number_current_lel_lights, material): + def low_energy_lighting(self, number_of_lights, material): """ Calculates the total cost for low energy lighting based on material and labor costs, including contingency, preliminaries, profit, and VAT. :param number_of_lights: Int, number of light - :param number_current_lel_lights: Int, number of low energy lights currently installed in the home :material: Dict, material data containing costs of fittings """ # If there are no lights fitted in the property, we increase the contingency in case there are potential wiring # blockers - if number_current_lel_lights == 0: - contingency = self.HIGH_RISK_CONTINGENCY - else: - contingency = self.CONTINGENCY + total_cost = material["total_cost"] * number_of_lights - if material["is_installer_quote"]: - total_cost = material["total_cost"] * number_of_lights * (1 + contingency) - - labour_hours = 1 - labour_days = (labour_hours / 8) - - return { - "total": total_cost, - "labour_hours": labour_hours, - "labour_days": labour_days, - } - - material_cost = material["material_cost"] * number_of_lights - labour_cost = material["labour_cost"] * number_of_lights * self.labour_adjustment_factor - - subtotal_before_profit = material_cost + labour_cost - - contingency_cost = subtotal_before_profit * contingency - - subtotal_before_vat = subtotal_before_profit + contingency_cost - vat_cost = subtotal_before_vat * self.VAT_RATE - total_cost = subtotal_before_vat + vat_cost - - labour_hours = material["labour_hours_per_unit"] * number_of_lights - # Assume a single electrician installing + labour_hours = 1 labour_days = (labour_hours / 8) return { "total": total_cost, - "subtotal": subtotal_before_vat, - "vat": vat_cost, - "contingency": contingency_cost, - "material": material_cost, + "contingency": self.CONTINGENCIES["low_energy_lighting"] * total_cost, + "contingency_rate": self.CONTINGENCIES["low_energy_lighting"], "labour_hours": labour_hours, "labour_days": labour_days, - "labour_cost": labour_cost } def window_glazing(self, number_of_windows, material, is_secondary_glazing=False): """ - We characterise the jobs to be done for window glazing as the following: - 1) Initial Assessment and Measurements: Before removing the existing window, it's essential to assess the - condition of the window frame and opening. Precise measurements are taken to ensure the new double glazed - windows fit perfectly. - - 2) Remove the Existing Window: This involves carefully dismantling and removing the old single glazed window. It - requires skill to avoid damaging the surrounding wall and the window frame (if it's to be reused). - - 3) Dispose of the Existing Window: The old window, especially if it's a single glazed unit, needs to be - disposed of responsibly. Glass and other materials should be recycled where possible. - - 4) Surface Preparation: The window opening might need some preparation, especially if there's damage or if - adjustments are needed to accommodate the new window. This can include repairing or replacing parts of the - window frame, sealing gaps, and ensuring the opening is level and square. - - 5) Install the Window Frame (if new frames are used): In many cases, double glazed windows come with their - frames. These need to be installed securely into the window opening. This process involves aligning, leveling, - and fixing the frame in place. - - 6) Install the Window Sill: If a new window sill is required, it is installed at this stage. It needs to be - correctly aligned with the frame and securely attached. - - 7) Install the Double Glazed Glass Units: The glass units are carefully inserted into the frame. This step - requires precision to ensure a snug fit without causing stress on the glass, which could lead to cracking or - breaking. - - 8) Sealing and Weatherproofing: After the glass units are in place, it's crucial to seal around the frame and - between the glass and frame to ensure there are no drafts and that the installation is weather-tight. This - typically involves applying silicone sealant or other appropriate sealing materials. - - 9) Finishing Touches: This includes any cosmetic work, such as trimming, painting, or staining the frame and - sill to match the rest of the property. It might also involve cleaning up any mess created during the - installation. - - 10) Inspection and Testing: Finally, the new windows should be inspected to ensure they open, close, and lock - correctly. This is also a good time to check for any gaps or issues with the sealing. - - For this cost estimation process, we factor in initial assement into the preliminaries + Given an isntaller quote, produces an estimate for the cost of works. """ - if material["is_installer_quote"]: - total_cost = material["total_cost"] * number_of_windows - - labour_hours = material["labour_hours_per_unit"] * number_of_windows - # To install windows, a small to medium size project might be conducted by a team of 2-3 people - labour_days = (labour_hours / 8) / 2 - - return { - "total": total_cost, - "labour_hours": labour_hours, - "labour_days": labour_days, - } - - material_cost = material["material_cost"] * number_of_windows - - labour_cost = ( - material["labour_cost"] * number_of_windows * self.labour_adjustment_factor - ) - multiplier = self.SECONDARY_GLAZING_SCALING_FACTOR if is_secondary_glazing else ( - self.SASH_WINDOW_INFLATION_FACTOR) - - subtotal = (material_cost + labour_cost) * multiplier - - contingency_cost = subtotal * self.CONTINGENCY - preliminaries_cost = subtotal * self.PRELIMINARIES - profit_cost = subtotal * self.PROFIT_MARGIN - - subtotal_before_vat = subtotal + contingency_cost + preliminaries_cost + profit_cost - - vat_cost = subtotal_before_vat * self.VAT_RATE - - total_cost = subtotal_before_vat + vat_cost + total_cost = material["total_cost"] * number_of_windows labour_hours = material["labour_hours_per_unit"] * number_of_windows - labour_hours = labour_hours * self.SECONDARY_GLAZING_SCALING_FACTOR if is_secondary_glazing else labour_hours - - # Assume a team of 2 + # To install windows, a small to medium size project might be conducted by a team of 2-3 people labour_days = (labour_hours / 8) / 2 return { "total": total_cost, - "subtotal": subtotal_before_vat, - "vat": vat_cost, - "contingency": contingency_cost, - "preliminaries": preliminaries_cost, - "material": material_cost, - "profit": profit_cost, + "contingency": self.CONTINGENCIES["windows_glazing"] * total_cost, + "contingency_rate": self.CONTINGENCIES["windows_glazing"], "labour_hours": labour_hours, - "labour_cost": labour_cost, - "labour_days": labour_days + "labour_days": labour_days, } @classmethod def solar_pv( cls, - n_panels: int | float, - has_battery: bool = False, - array_cost=None, - n_floors: int = 1, - battery_kwh: int = 5, - needs_inverter=False + solar_product, + scaffolding_options, + n_floors ): """ - Calculates the total cost for solar PV based data provided by the MCS dashboard, which contains - costing data for installations of renewable and clean energy measures. - The data in the dashboard is filtered on domestic building installations and then the data across the - various regions is manually collected. There is currently no automated way to get the data from the MCS - dashboard - - Price can also be benchmarked against this checkatrade article: - https://www.checkatrade.com/blog/cost-guides/cost-of-solar-panel-installation/ - :param n_panels: Number of solar panels - :param has_battery: Bool, whether the system includes a battery - :param array_cost: float, containing the cost of the solar PV array - :param n_floors: int, number of floors in the property, used to estimate the cost of scaffolding - :param battery_kwh: int, capacity of the battery in kWh. Defaulted to 5 - :param needs_inverter: Bool, whether the system needs an inverter, where the solar panels are feeding multiple - units """ - if n_panels > INSTALLER_MAX_PANELS: - base_cost = [c for c in INSTALLER_SOLAR_COSTS if c["n_panels"] == INSTALLER_MAX_PANELS][0]["cost"] - cost_per_panel = [ - c for c in INSTALLER_SOLAR_COSTS if c["n_panels"] == (INSTALLER_MAX_PANELS - 1) - ][0]["cost"] - cost_per_panel = base_cost - cost_per_panel - system_cost = base_cost + (n_panels - INSTALLER_MAX_PANELS) * cost_per_panel - else: - system_cost = [c for c in INSTALLER_SOLAR_COSTS if c["n_panels"] == n_panels][0]["cost"] + system_cost = solar_product["total_cost"] - subtotal = array_cost if array_cost is not None else system_cost + if not solar_product["includes_scaffolding"]: + # We base this on the number of floors + scaffolding = [x["total_cost"] for x in scaffolding_options if x["size"] == n_floors] + if not scaffolding: + # If we have no options, handle this + if n_floors <= 3: + raise ValueError("No scaffolding options available for 3 or fewer floors") + # We take the largest scaffolding option available + scaffolding_cost = max([x["total_cost"] for x in scaffolding_options]) + else: + scaffolding_cost = min(scaffolding) - if has_battery: - battery_cost = [c for c in INSTALLER_SOLAR_BATTERY_COSTS if c["capacity_kwh"] == battery_kwh][0]["cost"] - subtotal += battery_cost - - if needs_inverter: - subtotal += INSTALLER_SOLAR_PV_INVERTER_COST - # We also add an additional labour cost - subtotal += INSTALLER_SOLAR_PV_INVERTER_LABOUR_COST - - # Solar doesn't have VAT but we add a high risk contingency - # to account for design variation that we see in practice - total_cost = subtotal * (1 + cls.HIGH_RISK_CONTINGENCY) + system_cost += scaffolding_cost # Labour hours are based on estimates from online research but an average team seems to consist of 3 people # and most jobs take around 2 days. Assuming an 8 hour day for 3 people across 2 days, gives us 48 hours of # labour return { - "total": total_cost, - "subtotal": subtotal, + "total": system_cost, + "subtotal": system_cost, + "contingency": system_cost * cls.CONTINGENCIES["solar_pv"], + "contingency_rate": cls.CONTINGENCIES["solar_pv"], "vat": 0, "labour_hours": 48, "labour_days": 2, @@ -811,6 +485,8 @@ class Costs: # We estimate the cost of an appliance thermostat at £400, which is the upper end of the range return { "total": total_cost, + "contengency": total_cost * self.CONTINGENCY, + "contingency_rate": self.CONTINGENCY, "subtotal": subtotal_before_vat, "vat": vat, "labour_hours": labour_hours, @@ -837,6 +513,8 @@ class Costs: return { "total": total_cost, + "contingency": total_cost * self.CONTINGENCY, + "contingency_rate": self.CONTINGENCY, "subtotal": subtotal_before_vat, "vat": vat, "labour_hours": labour_hours, @@ -875,6 +553,8 @@ class Costs: return { "total": total_cost, + "contingency": total_cost * self.CONTINGENCIES["high_heat_retention_storage_heaters"], + "contingency_rate": self.CONTINGENCIES["high_heat_retention_storage_heaters"], "subtotal": subtotal_before_vat, "vat": vat, "labour_hours": labour_hours, @@ -894,6 +574,8 @@ class Costs: # We estimate the labour hours to be 4 return { "total": total_cost, + "contingency": total_cost * self.CONTINGENCY, + "contingency_rate": self.CONTINGENCY, "subtotal": subtotal_before_vat, "vat": vat, "labour_hours": 4, @@ -913,6 +595,8 @@ class Costs: # We estimate the labour hours to be 2 return { "total": total_cost, + "contingency": total_cost * self.CONTINGENCY, + "contingency_rate": self.CONTINGENCY, "subtotal": subtotal_before_vat, "vat": vat, "labour_hours": 2, @@ -931,6 +615,8 @@ class Costs: return { "total": total_cost, + "contingency": total_cost * self.CONTINGENCY, + "contingency_rate": self.CONTINGENCY, "subtotal": subtotal_before_vat, "vat": vat, "labour_hours": 0, @@ -965,6 +651,8 @@ class Costs: return { "total": total_cost, + "contingency": total_cost * self.CONTINGENCY, + "contingency_rate": self.CONTINGENCY, "subtotal": subtotal_before_vat, "vat": vat, "labour_hours": labour_hours, @@ -997,6 +685,8 @@ class Costs: return { "total": total_cost, + "contingency": total_cost * self.CONTINGENCY, + "contingency_rate": self.CONTINGENCY, "subtotal": subtotal_before_vat, "vat": vat, "labour_hours": labour_hours, @@ -1025,6 +715,8 @@ class Costs: return { "total": total_cost, + "contingency": total_cost * self.CONTINGENCY, + "contingency_rate": self.CONTINGENCY, "subtotal": subtotal_before_vat, "vat": vat, "labour_hours": labour_hours, @@ -1047,6 +739,8 @@ class Costs: return { "total": total_cost, + "contingency": total_cost * self.CONTINGENCY, + "contingency_rate": self.CONTINGENCY, "subtotal": subtotal_before_vat, "vat": vat, "labour_hours": removal_labour_hours, @@ -1110,7 +804,6 @@ class Costs: # To be pessimistic, assume 2 days work labour_cost = labour_rate * self.labour_adjustment_factor * labour_days # Add contingency and preliminaries - labour_cost = labour_cost * (1 + self.CONTINGENCY + self.PRELIMINARIES) vat = labour_cost * self.VAT_RATE @@ -1150,6 +843,8 @@ class Costs: return { "total": total_cost, + "contingency": total_cost * self.CONTINGENCY, + "contingency_rate": self.CONTINGENCY, "subtotal": subtotal_before_vat, "vat": vat, "labour_hours": labour_hours, @@ -1169,19 +864,18 @@ class Costs: else: cost = [x for x in INSTALLER_ASHP_COSTS if x][0]["cost"] - # We add some contingency since there are additional costs such as resizing radiators, that could be required - subtotal = cost * (1 + self.ASHP_CONTINGENCY) # The costs from installers exclude VAT - vat = subtotal * self.VAT_RATE - total_cost = subtotal + vat + vat = cost * self.VAT_RATE + cost = cost + vat # We assume 5 days installation labour_days = 5 labour_hours = labour_days * 8 return { - "total": total_cost, - "subtotal": subtotal, + "total": cost, + "contingency": cost * self.CONTINGENCIES["air_source_heat_pump"], + "contingency_rate": self.CONTINGENCIES["air_source_heat_pump"], "vat": vat, "labour_hours": labour_hours, "labour_days": labour_days, diff --git a/recommendations/FloorRecommendations.py b/recommendations/FloorRecommendations.py index ca74ac4b..2610c842 100644 --- a/recommendations/FloorRecommendations.py +++ b/recommendations/FloorRecommendations.py @@ -200,7 +200,6 @@ class FloorRecommendations(Definitions): cost_result = self.costs.suspended_floor_insulation( insulation_floor_area=self.property.insulation_floor_area, material=material.to_dict(), - non_insulation_materials=non_insulation_materials ) already_installed = "suspended_floor_insulation" in self.property.already_installed @@ -213,7 +212,6 @@ class FloorRecommendations(Definitions): cost_result = self.costs.solid_floor_insulation( insulation_floor_area=self.property.insulation_floor_area, material=material.to_dict(), - non_insulation_materials=non_insulation_materials ) already_installed = "solid_floor_insulation" in self.property.already_installed diff --git a/recommendations/HotwaterRecommendations.py b/recommendations/HotwaterRecommendations.py index d8404cc1..d735b002 100644 --- a/recommendations/HotwaterRecommendations.py +++ b/recommendations/HotwaterRecommendations.py @@ -110,7 +110,8 @@ class HotwaterRecommendations: "description_simulation": { "hot-water-energy-eff": "Poor" }, - "survey": survey + "survey": survey, + "innovation_rate": 0 } if _return: return to_append @@ -160,7 +161,8 @@ class HotwaterRecommendations: "hot-water-energy-eff": self.property.data["hot-water-energy-eff"], "hotwater-description": new_epc_description, }, - "survey": survey + "survey": survey, + "innovation_rate": 0 } if _return: return to_append @@ -222,7 +224,8 @@ class HotwaterRecommendations: "hot-water-energy-eff": simulation_config["hot_water_energy_eff_ending"], "hotwater-description": new_epc_description, }, - "survey": False + "survey": False, + "innovation_rate": 0 } self.recommendations.append(to_append) diff --git a/recommendations/LightingRecommendations.py b/recommendations/LightingRecommendations.py index 9e7b3af2..b96cefb2 100644 --- a/recommendations/LightingRecommendations.py +++ b/recommendations/LightingRecommendations.py @@ -126,7 +126,6 @@ class LightingRecommendations: cost_result = self.costs.low_energy_lighting( number_of_lights=number_non_lel_outlets, - number_current_lel_lights=number_lighting_outlets - number_non_lel_outlets, material=self.material ) diff --git a/recommendations/SecondaryHeating.py b/recommendations/SecondaryHeating.py index e63951d9..ee7eae1c 100644 --- a/recommendations/SecondaryHeating.py +++ b/recommendations/SecondaryHeating.py @@ -50,6 +50,7 @@ class SecondaryHeating: }, "description_simulation": { "secondheat-description": "None" - } + }, + "innovation_rate": 0.0, # No innovation rate for this measure } ) diff --git a/recommendations/SolarPvRecommendations.py b/recommendations/SolarPvRecommendations.py index ee07ff28..548ae54d 100644 --- a/recommendations/SolarPvRecommendations.py +++ b/recommendations/SolarPvRecommendations.py @@ -34,7 +34,9 @@ class SolarPvRecommendations: ] ) - def __init__(self, property_instance): + PANEL_SIZES = [400, 435, 440] + + def __init__(self, property_instance, materials: list): """ :param property_instance: Instance of the Property class, for the home associated to property_id """ @@ -44,6 +46,14 @@ class SolarPvRecommendations: self.recommendation = [] + self.panels_products = [ + material for material in materials if material["type"] == "solar_pv" + ] + + self.scaffolding_options = [ + material for material in materials if material["type"] == "scaffolding" + ] + @staticmethod def trim_solar_wattage_options(scenarios_with_wattage): # Initialize the list with the first element, assuming the list is not empty @@ -143,6 +153,8 @@ class SolarPvRecommendations: if not self.property.is_solar_pv_valid(): return + raise ValueError("DOO BUILDING") + # If we have a buiilding level analysis, we implement separate logic if self.property.building_id is not None: self.recommend_building_analysis(phase) @@ -201,11 +213,22 @@ class SolarPvRecommendations: # of this. This minimum is used in Recommendations.calculate_recommendation_impact minimum_sap_points = (roof_coverage_percent / 5) * self.SAP_POINTS_PER_5_PERCENT_ROOF_COVERAGE - for has_battery in [False, True]: + n_panels = recommendation_config["n_panels"] + + # Different panel sizes: 400, 435, 440 + available_products = [] + for panel_size in self.PANEL_SIZES: + system_size = (n_panels * panel_size) / 1000 + available_products.extend([ + x for x in self.panels_products if abs(x["size"] - system_size) < 0.01 + ]) + + # Given the available products in the database, we product the possible array of recommendations + for solar_pv_product in available_products: + cost_result = self.costs.solar_pv( - has_battery=has_battery, - array_cost=non_invasive_recommendation.get("cost", None), - n_panels=recommendation_config["n_panels"], + product=solar_pv_product, + scaffolding_options=self.scaffolding_options, n_floors=self.property.number_of_floors ) kw = np.floor(recommendation_config["array_wattage"] / 100) / 10