updated the costings with new products and measurse specific contingencies

This commit is contained in:
Khalim Conn-Kowlessar 2025-08-18 16:29:52 +01:00
parent ab150c799d
commit 1579d010ee
6 changed files with 152 additions and 434 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -50,6 +50,7 @@ class SecondaryHeating:
},
"description_simulation": {
"secondheat-description": "None"
}
},
"innovation_rate": 0.0, # No innovation rate for this measure
}
)

View file

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