mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Flat roof costs - will need review
This commit is contained in:
parent
2174f9d283
commit
fb71042c8f
10 changed files with 432 additions and 101 deletions
|
|
@ -33,6 +33,9 @@ class MaterialType(enum.Enum):
|
|||
ewi_wall_preparation = "ewi_wall_preparation"
|
||||
ewi_wall_redecoration = "ewi_wall_redecoration"
|
||||
low_energy_lighting_installation = "low_energy_lighting_installation"
|
||||
flat_roof_preparation = "flat_roof_preparation"
|
||||
flat_roof_vapour_barrier = "flat_roof_vapour_barrier"
|
||||
flat_roof_waterpoofing = "flat_roof_waterpoofing"
|
||||
|
||||
|
||||
class DepthUnit(enum.Enum):
|
||||
|
|
@ -43,6 +46,7 @@ class CostUnit(enum.Enum):
|
|||
gbp_sq_meter = "gbp_sq_meter"
|
||||
gbp_per_unit = "gbp_per_unit"
|
||||
gbp_per_m2 = "gbp_per_m2"
|
||||
gbp_per_m = "gbp_per_m"
|
||||
|
||||
|
||||
class RValueUnit(enum.Enum):
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ def app():
|
|||
solid_floor_costs = pd.read_excel(DATA_DIRECTORY, sheet_name="solid_floor_insulation", header=0)
|
||||
ewi_costs = pd.read_excel(DATA_DIRECTORY, sheet_name="external_wall_insulation", header=0)
|
||||
lel_costs = pd.read_excel(DATA_DIRECTORY, sheet_name="low_energy_lighting", header=0)
|
||||
flat_roof_costs = pd.read_excel(DATA_DIRECTORY, sheet_name="flat_roof_insulation", header=0)
|
||||
|
||||
# Form a single table to be uploaded
|
||||
costs = pd.concat(
|
||||
|
|
@ -84,7 +85,8 @@ def app():
|
|||
suspended_floor_costs,
|
||||
solid_floor_costs,
|
||||
ewi_costs,
|
||||
lel_costs
|
||||
lel_costs,
|
||||
flat_roof_costs
|
||||
]
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,10 @@ class Costs:
|
|||
# We assume a conservative 10% contingency for all works which is a rate defined by SPONs
|
||||
CONTINGENCY = 0.1
|
||||
|
||||
# For flat roof, we assume it's a high risk project as it's very weather dependent and also is heavily
|
||||
# dependent on the quality of the existing roof
|
||||
FLAT_ROOF_CONTINGENCY = 0.15
|
||||
|
||||
# We use a higher contingency rate for internal wall insulation because of the potential for issues with moving
|
||||
# fittings and trimming doors, as well as scope for damage to the existing wall during preparation.
|
||||
IWI_CONTINGENCY = 0.15
|
||||
|
|
@ -627,3 +631,89 @@ class Costs:
|
|||
"labour_days": labour_days,
|
||||
"labour_cost": labour_cost
|
||||
}
|
||||
|
||||
def flat_roof_insulation(self, floor_area, material, non_insulation_materials):
|
||||
"""
|
||||
A model of a warm, flat roof construction can be seen in this video:
|
||||
https://www.youtube.com/watch?v=WZ6Ng6YI9OA
|
||||
Warm, flat roof insulation will normally be 100-125mm in depth
|
||||
|
||||
We break this measure down into the following jobs to be done
|
||||
1) Preparation of the room. This involves cleaning the existing roof surface, removing any debris and repairing
|
||||
any damage. Additionally, an edge barrier will likely need to be installed, to protect the sides of the
|
||||
roof from water ingress.
|
||||
2) Primer Application. A layer of primer is applied to the clean roof surface to enhance the adhestia of
|
||||
subsequent layers, and seal the existing roof surface.
|
||||
3) Vapour Proof Layer Installation. Lay a vapour control layer to prevent moisture ingress from inside the
|
||||
building, which is essential in warm roof construction.
|
||||
4) Insulation Layer Application. Place and securely fix insulation boards over the roof. These could be rigid
|
||||
boards like PIR (Polyisocyanurate).
|
||||
5) Waterproofing Membrane Installation: Cover the insulation (and timber layer, if used) with a
|
||||
waterproofing membrane, like EPDM, PVC, or bituminous felt. Carefully seal all joints, edges, and around any
|
||||
roof penetrations to ensure water tightness
|
||||
|
||||
:param floor_area: Area of the flat roof to be insulated, based on the area of the floor
|
||||
:param material: Selected insulation material
|
||||
:param non_insulation_materials: Non-insulation materials required for the job
|
||||
:return:
|
||||
"""
|
||||
|
||||
preparation_data_m2 = [
|
||||
x for x in non_insulation_materials if
|
||||
(x["type"] == "flat_roof_preparation") and (x["cost_unit"] == "gbp_per_m2")
|
||||
]
|
||||
vapour_barrier_data = [x for x in non_insulation_materials if x["type"] == "flat_roof_vapour_barrier"]
|
||||
waterproofing_data = [x for x in non_insulation_materials if x["type"] == "flat_roof_waterproofing"]
|
||||
|
||||
if (len(preparation_data_m2) != 2) or (len(vapour_barrier_data) != 1) or (
|
||||
len(waterproofing_data) != 1):
|
||||
raise ValueError("Incorrect number of data entries for non-insulation materials")
|
||||
|
||||
# Break out the individual material costs
|
||||
preparation_m2_material_costs = sum([x["material_cost"] * floor_area for x in preparation_data_m2])
|
||||
vapour_barrier_material_costs = vapour_barrier_data[0]["material_cost"] * floor_area
|
||||
insulation_material_costs = material["material_cost"] * floor_area
|
||||
|
||||
preparation_m2_labour_costs = sum([x["labour_cost"] * floor_area for x in preparation_data_m2])
|
||||
vapour_barrier_labour_costs = vapour_barrier_data[0]["labour_cost"] * floor_area
|
||||
|
||||
# For waterproofing and upstand, we only have a total cost
|
||||
waterproofing_total_costs = waterproofing_data[0]["total_cost"] * floor_area
|
||||
|
||||
labour_costs = preparation_m2_labour_costs + vapour_barrier_labour_costs
|
||||
labour_costs = labour_costs * self.labour_adjustment_factor
|
||||
|
||||
materials_costs = preparation_m2_material_costs + vapour_barrier_material_costs + insulation_material_costs
|
||||
|
||||
subtotal_before_profit = labour_costs + materials_costs + waterproofing_total_costs
|
||||
|
||||
contingency_cost = subtotal_before_profit * self.FLAT_ROOF_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
|
||||
|
||||
preparation_m2_labour_hours = sum([x["labour_hours_per_unit"] * floor_area for x in preparation_data_m2])
|
||||
vapour_barrier_labour_hours = vapour_barrier_data[0]["labour_hours_per_unit"] * floor_area
|
||||
waterproofing_labour_hours = waterproofing_data[0]["labour_hours_per_unit"] * floor_area
|
||||
|
||||
labour_hours = preparation_m2_labour_hours + vapour_barrier_labour_hours + waterproofing_labour_hours
|
||||
|
||||
# To install flat roof insulation, assume a small/medium project might be conducted by a team of 2-4.
|
||||
# We'll assume a team of 2 since a lot of the roofs will be on the smaller side and will review this later
|
||||
labour_days = (labour_hours / 8) / 2
|
||||
|
||||
return {
|
||||
"total": total_cost,
|
||||
"subtotal": subtotal_before_vat,
|
||||
"vat": vat_cost,
|
||||
"contingency": contingency_cost,
|
||||
"preliminaries": preliminaries_cost,
|
||||
"material": materials_costs,
|
||||
"profit": profit_cost,
|
||||
"labour_hours": labour_hours,
|
||||
"labour_days": labour_days,
|
||||
"labour_cost": labour_costs
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ class RoofRecommendations:
|
|||
|
||||
# It is recommended that lofts should have at least 270mm of insulation
|
||||
MINIMUM_LOFT_ISULATION_MM = 270
|
||||
# Flat roof should have at least 100mm of insulation
|
||||
MINIMUM_FLAT_ROOF_ISULATION_MM = 100
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -41,6 +43,16 @@ class RoofRecommendations:
|
|||
]
|
||||
self.loft_non_insulation_materials = []
|
||||
|
||||
self.flat_roof_insulation_materials = [
|
||||
part for part in materials if part["type"] == "flat_roof_insulation"
|
||||
]
|
||||
|
||||
self.flat_roof_non_insulation_materials = [
|
||||
part for part in materials if part["type"] in [
|
||||
"flat_roof_preparation", "flat_roof_vapour_barrier", "flat_roof_waterproofing"
|
||||
]
|
||||
]
|
||||
|
||||
def recommend(self):
|
||||
|
||||
if self.property.roof["has_dwelling_above"]:
|
||||
|
|
@ -50,17 +62,24 @@ class RoofRecommendations:
|
|||
|
||||
insulation_thickness = convert_thickness_to_numeric(
|
||||
self.property.roof["insulation_thickness"],
|
||||
self.property.roof["is_pitched"]
|
||||
self.property.roof["is_pitched"],
|
||||
self.property.roof["is_flat"]
|
||||
)
|
||||
|
||||
# We check if the roof is already insulated and if so, we exit
|
||||
|
||||
# Building regulations part L recommend installing at least 270mm of insulation, however generally we
|
||||
# experience diminishing returns in terms of SAP once we go beyond around 150mm of insulation
|
||||
# This only holds true for pitched roofs
|
||||
# This only holds true for pitched roofs.
|
||||
if (insulation_thickness >= self.MINIMUM_LOFT_ISULATION_MM) and self.property.roof["is_pitched"]:
|
||||
return
|
||||
|
||||
if (insulation_thickness >= self.MINIMUM_FLAT_ROOF_ISULATION_MM) and self.property.roof["is_flat"]:
|
||||
return
|
||||
|
||||
if self.property.roof["is_roof_room"]:
|
||||
raise ValueError("Update convert_thickness_to_numeric for room roof and implement")
|
||||
|
||||
# If we have a u-value already, need to implement this
|
||||
if u_value:
|
||||
if u_value <= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE:
|
||||
|
|
@ -139,7 +158,8 @@ class RoofRecommendations:
|
|||
insulation_materials = self.loft_insulation_materials
|
||||
non_insulation_materials = self.loft_non_insulation_materials
|
||||
elif roof["is_flat"]:
|
||||
raise ValueError("UPDATE ME")
|
||||
insulation_materials = self.flat_roof_insulation_materials
|
||||
non_insulation_materials = self.flat_roof_non_insulation_materials
|
||||
else:
|
||||
raise ValueError("Roof is not pitched or flat")
|
||||
|
||||
|
|
@ -186,7 +206,11 @@ class RoofRecommendations:
|
|||
material=material
|
||||
)
|
||||
elif material["type"] == "flat_roof_insulation":
|
||||
raise ValueError("COMPLETE ME")
|
||||
cost_result = self.costs.flat_roof_insulation(
|
||||
floor_area=self.property.insulation_floor_area,
|
||||
material=material,
|
||||
non_insulation_materials=non_insulation_materials
|
||||
)
|
||||
else:
|
||||
raise ValueError("Invalid material type")
|
||||
|
||||
|
|
|
|||
|
|
@ -157,6 +157,7 @@ county_to_region_map = {
|
|||
'Sheffield': 'Yorkshire and the Humber', 'South Yorkshire': 'Yorkshire and the Humber',
|
||||
'Wakefield': 'Yorkshire and the Humber', 'West Yorkshire': 'Yorkshire and the Humber',
|
||||
'York': 'Yorkshire and the Humber',
|
||||
'Westmorland': 'North West England',
|
||||
|
||||
# Additional mappings requried, based on what we find in the EPC database
|
||||
'Greater London Authority': 'Inner London',
|
||||
|
|
|
|||
|
|
@ -573,7 +573,7 @@ def calculate_r_value_per_mm(thickness_mm, thermal_conductivity_w_mK):
|
|||
return r_value_per_mm
|
||||
|
||||
|
||||
def convert_thickness_to_numeric(string_thickness, is_pitched):
|
||||
def convert_thickness_to_numeric(string_thickness, is_pitched, is_flat):
|
||||
"""
|
||||
Roof insulation thickness could be a string like "None", "300mm+" or a numeric string.
|
||||
This function will convert these strings to a number for easy usage
|
||||
|
|
@ -597,6 +597,14 @@ def convert_thickness_to_numeric(string_thickness, is_pitched):
|
|||
"average": 100,
|
||||
"above average": 270
|
||||
}
|
||||
elif is_flat:
|
||||
# For a flat roof, if it's below average, we assume it's 0 and requires a re-roof
|
||||
lookup = {
|
||||
"none": 0,
|
||||
"below average": 0,
|
||||
"average": 100,
|
||||
"above average": 150
|
||||
}
|
||||
else:
|
||||
lookup = {
|
||||
"none": 0,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from recommendations.Costs import Costs
|
||||
from unittest.mock import Mock
|
||||
import datetime
|
||||
|
||||
|
||||
class TestCosts:
|
||||
|
|
@ -418,3 +419,83 @@ class TestCosts:
|
|||
'profit': 1617.9654432399325, 'labour_hours': 187.02533486285358, 'labour_days': 5.8445417144641745,
|
||||
'labour_cost': 3921.5600094613983
|
||||
}
|
||||
|
||||
def test_flat_roof_insulation(self):
|
||||
mock_property = Mock()
|
||||
mock_property.data = {
|
||||
"county": "Northamptonshire"
|
||||
}
|
||||
|
||||
costs = Costs(mock_property)
|
||||
flat_roof_material = {'id': 1225, 'type': 'flat_roof_insulation',
|
||||
'description': 'Kingspan Thermaroof TR21 zero OPD '
|
||||
'urethene insulation board',
|
||||
'depth': 100.0, 'depth_unit': 'mm', 'cost': None,
|
||||
'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.04,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt',
|
||||
'thermal_conductivity': 0.025,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin',
|
||||
'link': 'SPONs',
|
||||
'created_at': "now", 'is_active': True,
|
||||
'prime_material_cost': None, 'material_cost': 50.95,
|
||||
'labour_cost': 10.66, 'labour_hours_per_unit': 0.48,
|
||||
'plant_cost': 0.0, 'total_cost': 61.61,
|
||||
'notes': "SPONs didn't have a labour hours so we use "
|
||||
"0.48 which is similar to other materials"}
|
||||
|
||||
flat_roof_non_insulation_materials = [
|
||||
{'id': 17, 'type': 'mechanical_ventilation', 'description': 'Mechanical Extract Ventilation', 'depth': None,
|
||||
'depth_unit': None, 'cost': 500, 'cost_unit': 'gbp_per_unit', 'r_value_per_mm': None, 'r_value_unit': None,
|
||||
'thermal_conductivity': None, 'thermal_conductivity_unit': None, 'link': None,
|
||||
'created_at': datetime.datetime(2023, 10, 18, 16, 39, 9, 827188), 'is_active': True,
|
||||
'prime_material_cost': None,
|
||||
'material_cost': None, 'labour_cost': None, 'labour_hours_per_unit': None, 'plant_cost': None,
|
||||
'total_cost': None,
|
||||
'notes': None},
|
||||
{'id': 1221, 'type': 'flat_roof_preparation',
|
||||
'description': 'clean surface to receive new damp-proof membrane',
|
||||
'depth': 0.0, 'depth_unit': None, 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': None,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None,
|
||||
'thermal_conductivity_unit': None,
|
||||
'link': 'SPONs', 'created_at': datetime.datetime(2023, 12, 4, 20, 1, 49, 298076), 'is_active': True,
|
||||
'prime_material_cost': None, 'material_cost': 0.0, 'labour_cost': 4.36, 'labour_hours_per_unit': 0.14,
|
||||
'plant_cost': 0.0, 'total_cost': 4.36,
|
||||
'notes': 'This data is based on concrete however forms a decent baseline for a Bituminous Felt flat roof'},
|
||||
{'id': 1223, 'type': 'flat_roof_preparation',
|
||||
'description': 'One coat primer; on wood surfaces before fixing; General surfaces; over 300 mm girth',
|
||||
'depth': 0.0, 'depth_unit': None, 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': None,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None,
|
||||
'thermal_conductivity_unit': None,
|
||||
'link': 'SPONs', 'created_at': datetime.datetime(2023, 12, 4, 20, 1, 49, 298076), 'is_active': True,
|
||||
'prime_material_cost': None, 'material_cost': 2.49, 'labour_cost': 1.5, 'labour_hours_per_unit': 0.08,
|
||||
'plant_cost': 0.0, 'total_cost': 3.99, 'notes': 'SPONs data gives us a baseline for a wood surface'},
|
||||
{'id': 1224, 'type': 'flat_roof_vapour_barrier', 'description': 'Visqueen High Performance Vapour Barrier',
|
||||
'depth': 0.0, 'depth_unit': None, 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': None,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None,
|
||||
'thermal_conductivity_unit': None,
|
||||
'link': 'SPONs', 'created_at': datetime.datetime(2023, 12, 4, 20, 1, 49, 298076), 'is_active': True,
|
||||
'prime_material_cost': 0.58, 'material_cost': 1.21, 'labour_cost': 0.48, 'labour_hours_per_unit': 0.02,
|
||||
'plant_cost': 0.0, 'total_cost': 1.69, 'notes': None},
|
||||
{'id': 1234, 'type': 'flat_roof_waterproofing',
|
||||
'description': '20 mm thick two coat coverings; felt isolating membrane; to concrete (or '
|
||||
'timber) base; flat or to falls or slopes not exceeding 10° from horizontal',
|
||||
'depth': 0.0, 'depth_unit': None, 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': None,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None,
|
||||
'thermal_conductivity_unit': None, 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 12, 4, 20, 1, 49, 298076), 'is_active': True,
|
||||
'prime_material_cost': None, 'material_cost': 0.0, 'labour_cost': 0.0,
|
||||
'labour_hours_per_unit': 0.5, 'plant_cost': 0.0, 'total_cost': 31.13, 'notes': None}
|
||||
]
|
||||
|
||||
flat_roof_floor_results = costs.flat_roof_insulation(
|
||||
floor_area=33.5,
|
||||
material=flat_roof_material,
|
||||
non_insulation_materials=flat_roof_non_insulation_materials
|
||||
)
|
||||
|
||||
assert flat_roof_floor_results == {'total': 5325.327767999999, 'subtotal': 4437.773139999999,
|
||||
'vat': 887.5546279999999, 'contingency': 459.07998,
|
||||
'preliminaries': 306.05332, 'material': 1830.775, 'profit': 612.10664,
|
||||
'labour_hours': 24.79, 'labour_days': 1.549375, 'labour_cost': 186.9032}
|
||||
|
||||
assert costs.labour_adjustment_factor == 0.88
|
||||
|
|
|
|||
|
|
@ -7,6 +7,119 @@ materials = [
|
|||
'created_at': datetime.datetime(2023, 10, 18, 16, 39, 9, 827188), 'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': None, 'labour_cost': None, 'labour_hours_per_unit': None, 'plant_cost': None, 'total_cost': None,
|
||||
'notes': None},
|
||||
{'id': 1221, 'type': 'flat_roof_preparation', 'description': 'clean surface to receive new damp-proof membrane',
|
||||
'depth': 0.0, 'depth_unit': None, 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': None,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, 'thermal_conductivity_unit': None,
|
||||
'link': 'SPONs', 'created_at': datetime.datetime(2023, 12, 4, 20, 1, 49, 298076), 'is_active': True,
|
||||
'prime_material_cost': None, 'material_cost': 0.0, 'labour_cost': 4.36, 'labour_hours_per_unit': 0.14,
|
||||
'plant_cost': 0.0, 'total_cost': 4.36,
|
||||
'notes': 'This data is based on concrete however forms a decent baseline for a Bituminous Felt flat roof'},
|
||||
{'id': 1223, 'type': 'flat_roof_preparation',
|
||||
'description': 'One coat primer; on wood surfaces before fixing; General surfaces; over 300 mm girth',
|
||||
'depth': 0.0, 'depth_unit': None, 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': None,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, 'thermal_conductivity_unit': None,
|
||||
'link': 'SPONs', 'created_at': datetime.datetime(2023, 12, 4, 20, 1, 49, 298076), 'is_active': True,
|
||||
'prime_material_cost': None, 'material_cost': 2.49, 'labour_cost': 1.5, 'labour_hours_per_unit': 0.08,
|
||||
'plant_cost': 0.0, 'total_cost': 3.99, 'notes': 'SPONs data gives us a baseline for a wood surface'},
|
||||
{'id': 1224, 'type': 'flat_roof_vapour_barrier', 'description': 'Visqueen High Performance Vapour Barrier',
|
||||
'depth': 0.0, 'depth_unit': None, 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': None,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, 'thermal_conductivity_unit': None,
|
||||
'link': 'SPONs', 'created_at': datetime.datetime(2023, 12, 4, 20, 1, 49, 298076), 'is_active': True,
|
||||
'prime_material_cost': 0.58, 'material_cost': 1.21, 'labour_cost': 0.48, 'labour_hours_per_unit': 0.02,
|
||||
'plant_cost': 0.0, 'total_cost': 1.69, 'notes': None}, {'id': 1225, 'type': 'flat_roof_insulation',
|
||||
'description': 'Kingspan Thermaroof TR21 zero OPD '
|
||||
'urethene insulation board',
|
||||
'depth': 100.0, 'depth_unit': 'mm', 'cost': None,
|
||||
'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.04,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt',
|
||||
'thermal_conductivity': 0.025,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin',
|
||||
'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 12, 4, 20, 1, 49,
|
||||
298076), 'is_active': True,
|
||||
'prime_material_cost': None, 'material_cost': 50.95,
|
||||
'labour_cost': 10.66, 'labour_hours_per_unit': 0.48,
|
||||
'plant_cost': 0.0, 'total_cost': 61.61,
|
||||
'notes': "SPONs didn't have a labour hours so we use "
|
||||
"0.48 which is similar to other materials"},
|
||||
{'id': 1226, 'type': 'flat_roof_insulation', 'description': 'Ravatherm XPS × 500 SL', 'depth': 100.0,
|
||||
'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.03125,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.032,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 12, 4, 20, 1, 49, 298076), 'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': 22.14, 'labour_cost': 10.66, 'labour_hours_per_unit': 0.48, 'plant_cost': 0.0, 'total_cost': 32.8,
|
||||
'notes': None},
|
||||
{'id': 1227, 'type': 'flat_roof_insulation', 'description': 'Ravatherm XPS × 500 SL', 'depth': 120.0,
|
||||
'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.03125,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.032,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin',
|
||||
'link': 'https://www.panelsystems.co.uk/product/floormate-ravatherm-sb?attribute_pa_group=floormate-500a'
|
||||
'&attribute_pa_product-name=ravatherm-xps-x-500-sl&attribute_pa_length=1250&attribute_pa_width=600'
|
||||
'&attribute_pa_thickness=120&attribute_pa_unit-of-sale=pack-3-brds&attribute_pa_min-order-qty=10&gclid'
|
||||
'=CjwKCAiAjrarBhAWEiwA2qWdCKJK2iqlzUZ-mBFOfCLy2f5TldAbOj7G3LrvYw5JLaigplJAajzYpRoCtB8QAvD_BwE',
|
||||
'created_at': datetime.datetime(2023, 12, 4, 20, 1, 49, 298076), 'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': 26.187656, 'labour_cost': 10.66, 'labour_hours_per_unit': 0.48, 'plant_cost': 0.0,
|
||||
'total_cost': 36.847656,
|
||||
'notes': "SPONs didn't have this thickness, so the material price is based on the fact that on the link, "
|
||||
"the 120mm thickness is 18% more expensive per board than the 100mm thickness"},
|
||||
{'id': 1228, 'type': 'flat_roof_insulation', 'description': 'Ravatherm XPS × 500 SL', 'depth': 140.0,
|
||||
'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.03125,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.032,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin',
|
||||
'link': 'https://www.panelsystems.co.uk/product/floormate-ravatherm-sb?attribute_pa_group=floormate-500a'
|
||||
'&attribute_pa_product-name=ravatherm-xps-x-500-sl&attribute_pa_length=1250&attribute_pa_width=600'
|
||||
'&attribute_pa_thickness=120&attribute_pa_unit-of-sale=pack-3-brds&attribute_pa_min-order-qty=10&gclid'
|
||||
'=CjwKCAiAjrarBhAWEiwA2qWdCKJK2iqlzUZ-mBFOfCLy2f5TldAbOj7G3LrvYw5JLaigplJAajzYpRoCtB8QAvD_BwE',
|
||||
'created_at': datetime.datetime(2023, 12, 4, 20, 1, 49, 298076), 'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': 31.114737, 'labour_cost': 10.66, 'labour_hours_per_unit': 0.48, 'plant_cost': 0.0,
|
||||
'total_cost': 41.77474,
|
||||
'notes': "SPONs didn't have this thickness, so the material price is based on the fact that on the link, "
|
||||
"the 140mm thickness is 40% more expensive per board than the 100mm thickness"},
|
||||
{'id': 1229, 'type': 'flat_roof_insulation', 'description': 'Foamglas T3+ Flat Roof Insulation', 'depth': 100.0,
|
||||
'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.027777778,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.036,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 12, 4, 20, 1, 49, 298076), 'is_active': True, 'prime_material_cost': 95.83,
|
||||
'material_cost': 109.09, 'labour_cost': 30.7, 'labour_hours_per_unit': 1.3, 'plant_cost': 0.0,
|
||||
'total_cost': 139.79, 'notes': None},
|
||||
{'id': 1230, 'type': 'flat_roof_insulation', 'description': 'Foamglas T4+ Flat Roof Insulation', 'depth': 100.0,
|
||||
'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.024390243,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.041,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 12, 4, 20, 1, 49, 298076), 'is_active': True, 'prime_material_cost': 63.89,
|
||||
'material_cost': 76.19, 'labour_cost': 28.34, 'labour_hours_per_unit': 1.2, 'plant_cost': 0.0,
|
||||
'total_cost': 104.53, 'notes': None},
|
||||
{'id': 1231, 'type': 'flat_roof_insulation', 'description': 'Ecotherm Eco-Versal General Purpose Insulation Board',
|
||||
'depth': 100.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 12, 4, 20, 1, 49, 298076), 'is_active': True, 'prime_material_cost': 15.12,
|
||||
'material_cost': 25.96, 'labour_cost': 30.7, 'labour_hours_per_unit': 1.3, 'plant_cost': 0.0, 'total_cost': 56.66,
|
||||
'notes': None},
|
||||
{'id': 1232, 'type': 'flat_roof_insulation', 'description': 'Ecotherm Eco-Versal General Purpose Insulation Board',
|
||||
'depth': 120.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 12, 4, 20, 1, 49, 298076), 'is_active': True, 'prime_material_cost': 20.16,
|
||||
'material_cost': 34.613335, 'labour_cost': 30.7, 'labour_hours_per_unit': 1.3, 'plant_cost': 0.0,
|
||||
'total_cost': 65.31333,
|
||||
'notes': "SPONs didn't have this thickness, so the material price is based on the fact that on the link, "
|
||||
"the 120mm thickness is 33% more expensive than the 100mm thickness"},
|
||||
{'id': 1233, 'type': 'flat_roof_insulation', 'description': 'Ecotherm Eco-Versal General Purpose Insulation Board',
|
||||
'depth': 150.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.045454547,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.022,
|
||||
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 12, 4, 20, 1, 49, 298076), 'is_active': True, 'prime_material_cost': 23.53,
|
||||
'material_cost': 34.62, 'labour_cost': 33.06, 'labour_hours_per_unit': 1.4, 'plant_cost': 0.0, 'total_cost': 67.68,
|
||||
'notes': None}, {'id': 1234, 'type': 'flat_roof_waterproofing',
|
||||
'description': '20 mm thick two coat coverings; felt isolating membrane; to concrete (or '
|
||||
'timber) base; flat or to falls or slopes not exceeding 10° from horizontal',
|
||||
'depth': 0.0, 'depth_unit': None, 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': None,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None,
|
||||
'thermal_conductivity_unit': None, 'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 12, 4, 20, 1, 49, 298076), 'is_active': True,
|
||||
'prime_material_cost': None, 'material_cost': 0.0, 'labour_cost': 0.0,
|
||||
'labour_hours_per_unit': 0.5, 'plant_cost': 0.0, 'total_cost': 31.13, 'notes': None},
|
||||
{'id': 1109, 'type': 'cavity_wall_insulation', 'description': 'Expanded Polystyrene Beads cavity wall insulation',
|
||||
'depth': 75.0, 'depth_unit': 'mm', 'cost': None, 'cost_unit': 'gbp_per_m2', 'r_value_per_mm': 0.030303031,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': 0.033,
|
||||
|
|
@ -832,4 +945,5 @@ materials = [
|
|||
'material_cost': 20.0, 'labour_cost': 46.0, 'labour_hours_per_unit': 0.8, 'plant_cost': 0.0, 'total_cost': 66.0,
|
||||
'notes': 'We estimate the unit economics from the checkatrade article. We assume that the average job consists '
|
||||
'of installing 6 lights based on the hamuch article. We use the median value of 400 for a job of 6 '
|
||||
'lights'}]
|
||||
'lights'}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -282,19 +282,24 @@ class TestRecommendationUtils:
|
|||
|
||||
def test_convert_thickness_to_numeric(self):
|
||||
|
||||
assert recommendation_utils.convert_thickness_to_numeric("none", True) == 0
|
||||
assert recommendation_utils.convert_thickness_to_numeric("below average", True) == 50
|
||||
assert recommendation_utils.convert_thickness_to_numeric("average", True) == 100
|
||||
assert recommendation_utils.convert_thickness_to_numeric("above average", True) == 270
|
||||
assert recommendation_utils.convert_thickness_to_numeric("none", True, False) == 0
|
||||
assert recommendation_utils.convert_thickness_to_numeric("below average", True, False) == 50
|
||||
assert recommendation_utils.convert_thickness_to_numeric("average", True, False) == 100
|
||||
assert recommendation_utils.convert_thickness_to_numeric("above average", True, False) == 270
|
||||
|
||||
assert recommendation_utils.convert_thickness_to_numeric("300+", True) == 300
|
||||
assert recommendation_utils.convert_thickness_to_numeric("400+", True) == 400
|
||||
assert recommendation_utils.convert_thickness_to_numeric("270", True) == 270
|
||||
assert recommendation_utils.convert_thickness_to_numeric("300+", True, False) == 300
|
||||
assert recommendation_utils.convert_thickness_to_numeric("400+", True, False) == 400
|
||||
assert recommendation_utils.convert_thickness_to_numeric("270", True, False) == 270
|
||||
|
||||
assert recommendation_utils.convert_thickness_to_numeric("none", False) == 0
|
||||
assert recommendation_utils.convert_thickness_to_numeric("below average", False) == 100
|
||||
assert recommendation_utils.convert_thickness_to_numeric("average", False) == 270
|
||||
assert recommendation_utils.convert_thickness_to_numeric("above average", False) == 270
|
||||
assert recommendation_utils.convert_thickness_to_numeric("none", False, False) == 0
|
||||
assert recommendation_utils.convert_thickness_to_numeric("below average", False, False) == 100
|
||||
assert recommendation_utils.convert_thickness_to_numeric("average", False, False) == 270
|
||||
assert recommendation_utils.convert_thickness_to_numeric("above average", False, False) == 270
|
||||
|
||||
assert recommendation_utils.convert_thickness_to_numeric("none", False, True) == 0
|
||||
assert recommendation_utils.convert_thickness_to_numeric("below average", False, True) == 0
|
||||
assert recommendation_utils.convert_thickness_to_numeric("average", False, True) == 100
|
||||
assert recommendation_utils.convert_thickness_to_numeric("above average", False, True) == 150
|
||||
|
||||
|
||||
def test_estimate_perimeter_regular_inputs():
|
||||
|
|
|
|||
|
|
@ -276,89 +276,91 @@ class TestRoofRecommendations:
|
|||
# "Insulate your room roof with 220mm of Example room roof insulation"
|
||||
# assert roof_recommender10.recommendations[1]["description"] == \
|
||||
# "Insulate your room roof with 270mm of Example room roof insulation"
|
||||
#
|
||||
# def test_flat_no_insulation(self):
|
||||
# property_instance11 = Property(id=11, address1="fake", postcode="fake", epc_client=Mock())
|
||||
# property_instance11.age_band = "D"
|
||||
# property_instance11.insulation_floor_area = 150
|
||||
# property_instance11.roof = {
|
||||
# 'original_description': 'Flat, no insulation (assumed)',
|
||||
# 'clean_description': 'Flat, no insulation',
|
||||
# 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': False,
|
||||
# 'is_roof_room': False, 'is_loft': False, 'is_flat': True, 'is_thatched': False, 'is_at_rafters': False,
|
||||
# 'is_assumed': True, 'has_dwelling_above': False, 'is_valid': True, 'insulation_thickness': 'none'
|
||||
# }
|
||||
# property_instance11.data = {"county": "Swindon"}
|
||||
#
|
||||
# roof_recommender11 = RoofRecommendations(property_instance=property_instance11, materials=materials)
|
||||
#
|
||||
# assert not roof_recommender11.recommendations
|
||||
#
|
||||
# roof_recommender11.recommend()
|
||||
#
|
||||
# assert len(roof_recommender11.recommendations) == 1
|
||||
#
|
||||
# assert roof_recommender11.recommendations[0]["parts"][0]["depths"] == [270]
|
||||
#
|
||||
# assert roof_recommender11.recommendations[0]["new_u_value"] == 0.11
|
||||
#
|
||||
# assert roof_recommender11.recommendations[0]["starting_u_value"] == 2.3
|
||||
#
|
||||
# assert roof_recommender11.recommendations[0]["description"] == \
|
||||
# "Insulate the home's flat roof with 270mm of Example flat roof insulation"
|
||||
#
|
||||
# def test_flat_insulated(self):
|
||||
# property_instance12 = Property(id=12, address1="fake", postcode="fake", epc_client=Mock())
|
||||
# property_instance12.age_band = "D"
|
||||
# property_instance12.insulation_floor_area = 150
|
||||
# property_instance12.roof = {
|
||||
# 'original_description': 'Flat, insulated (assumed)',
|
||||
# 'clean_description': 'Flat, insulated',
|
||||
# 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': False,
|
||||
# 'is_roof_room': False,
|
||||
# 'is_loft': False, 'is_flat': True, 'is_thatched': False, 'is_at_rafters': False, 'is_assumed': True,
|
||||
# 'has_dwelling_above': False, 'is_valid': True, 'insulation_thickness': 'average'
|
||||
# }
|
||||
# property_instance12.data = {"county": "Thurrock"}
|
||||
#
|
||||
# roof_recommender12 = RoofRecommendations(property_instance=property_instance12, materials=materials)
|
||||
#
|
||||
# assert not roof_recommender12.recommendations
|
||||
#
|
||||
# roof_recommender12.recommend()
|
||||
#
|
||||
# assert not roof_recommender12.recommendations
|
||||
#
|
||||
# def test_flat_limited_insulation(self):
|
||||
# property_instance13 = Property(id=12, address1="fake", postcode="fake", epc_client=Mock())
|
||||
# property_instance13.age_band = "D"
|
||||
# property_instance13.insulation_floor_area = 150
|
||||
# property_instance13.roof = {
|
||||
# 'original_description': 'Flat, limited insulation (assumed)',
|
||||
# 'clean_description': 'Flat, limited insulation',
|
||||
# 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': False,
|
||||
# 'is_roof_room': False,
|
||||
# 'is_loft': False, 'is_flat': True, 'is_thatched': False, 'is_at_rafters': False, 'is_assumed': True,
|
||||
# 'has_dwelling_above': False, 'is_valid': True, 'insulation_thickness': 'below average'
|
||||
# }
|
||||
# property_instance13.data = {"county": "Tyne and Wear"}
|
||||
#
|
||||
# roof_recommender13 = RoofRecommendations(property_instance=property_instance13, materials=materials)
|
||||
#
|
||||
# assert not roof_recommender13.recommendations
|
||||
#
|
||||
# roof_recommender13.recommend()
|
||||
#
|
||||
# assert len(roof_recommender13.recommendations) == 1
|
||||
#
|
||||
# assert roof_recommender13.recommendations[0]["parts"][0]["depths"] == [220]
|
||||
#
|
||||
# assert roof_recommender13.recommendations[0]["new_u_value"] == 0.14
|
||||
#
|
||||
# assert roof_recommender13.recommendations[0]["starting_u_value"] == 2.3
|
||||
#
|
||||
# assert roof_recommender13.recommendations[0]["description"] == \
|
||||
# "Insulate the home's flat roof with 220mm of Example flat roof insulation"
|
||||
|
||||
def test_flat_no_insulation(self):
|
||||
property_instance11 = Property(id=11, address1="fake", postcode="fake", epc_client=Mock())
|
||||
property_instance11.age_band = "D"
|
||||
property_instance11.insulation_floor_area = 33.5
|
||||
property_instance11.perimeter = 24
|
||||
property_instance11.roof = {
|
||||
'original_description': 'Flat, no insulation (assumed)',
|
||||
'clean_description': 'Flat, no insulation',
|
||||
'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': False,
|
||||
'is_roof_room': False, 'is_loft': False, 'is_flat': True, 'is_thatched': False, 'is_at_rafters': False,
|
||||
'is_assumed': True, 'has_dwelling_above': False, 'is_valid': True, 'insulation_thickness': 'none'
|
||||
}
|
||||
property_instance11.data = {"county": "Swindon"}
|
||||
|
||||
roof_recommender11 = RoofRecommendations(property_instance=property_instance11, materials=materials)
|
||||
|
||||
assert not roof_recommender11.recommendations
|
||||
|
||||
roof_recommender11.recommend()
|
||||
|
||||
assert len(roof_recommender11.recommendations) == 1
|
||||
|
||||
assert roof_recommender11.recommendations[0]["parts"][0]["depth"] == 150
|
||||
assert roof_recommender11.recommendations[0]["total"] == 4380.84324
|
||||
assert roof_recommender11.recommendations[0]["new_u_value"] == 0.14
|
||||
assert roof_recommender11.recommendations[0]["starting_u_value"] == 2.3
|
||||
assert roof_recommender11.recommendations[0]["description"] == \
|
||||
"Insulate the home's flat roof with 150mm of Ecotherm Eco-Versal General Purpose Insulation Board"
|
||||
|
||||
def test_flat_insulated(self):
|
||||
property_instance12 = Property(id=12, address1="fake", postcode="fake", epc_client=Mock())
|
||||
property_instance12.age_band = "D"
|
||||
property_instance12.insulation_floor_area = 40
|
||||
property_instance12.perimeter = 30
|
||||
|
||||
property_instance12.roof = {
|
||||
'original_description': 'Flat, insulated (assumed)',
|
||||
'clean_description': 'Flat, insulated',
|
||||
'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': False,
|
||||
'is_roof_room': False,
|
||||
'is_loft': False, 'is_flat': True, 'is_thatched': False, 'is_at_rafters': False, 'is_assumed': True,
|
||||
'has_dwelling_above': False, 'is_valid': True, 'insulation_thickness': 'average'
|
||||
}
|
||||
property_instance12.data = {"county": "Thurrock"}
|
||||
|
||||
roof_recommender12 = RoofRecommendations(property_instance=property_instance12, materials=materials)
|
||||
|
||||
assert not roof_recommender12.recommendations
|
||||
|
||||
roof_recommender12.recommend()
|
||||
|
||||
assert not roof_recommender12.recommendations
|
||||
|
||||
def test_flat_limited_insulation(self):
|
||||
property_instance13 = Property(id=12, address1="fake", postcode="fake", epc_client=Mock())
|
||||
property_instance13.age_band = "D"
|
||||
property_instance13.insulation_floor_area = 40
|
||||
property_instance13.perimeter = 40
|
||||
property_instance13.roof = {
|
||||
'original_description': 'Flat, limited insulation (assumed)',
|
||||
'clean_description': 'Flat, limited insulation',
|
||||
'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': False,
|
||||
'is_roof_room': False,
|
||||
'is_loft': False, 'is_flat': True, 'is_thatched': False, 'is_at_rafters': False, 'is_assumed': True,
|
||||
'has_dwelling_above': False, 'is_valid': True, 'insulation_thickness': 'below average'
|
||||
}
|
||||
property_instance13.data = {"county": "Tyne and Wear"}
|
||||
|
||||
roof_recommender13 = RoofRecommendations(property_instance=property_instance13, materials=materials)
|
||||
|
||||
assert not roof_recommender13.recommendations
|
||||
|
||||
roof_recommender13.recommend()
|
||||
|
||||
assert len(roof_recommender13.recommendations) == 1
|
||||
|
||||
assert roof_recommender13.recommendations[0]["parts"][0]["depth"] == 150
|
||||
|
||||
assert roof_recommender13.recommendations[0]["total"] == 5199.969120000002
|
||||
assert roof_recommender13.recommendations[0]["new_u_value"] == 0.14
|
||||
assert roof_recommender13.recommendations[0]["starting_u_value"] == 2.3
|
||||
|
||||
assert roof_recommender13.recommendations[0]["description"] == \
|
||||
"Insulate the home's flat roof with 150mm of Ecotherm Eco-Versal General Purpose Insulation Board"
|
||||
|
||||
def test_property_above(self):
|
||||
property_instance14 = Property(id=0, address1="fake", postcode="fake", epc_client=Mock())
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue