Flat roof costs - will need review

This commit is contained in:
Khalim Conn-Kowlessar 2023-12-04 21:30:32 +00:00
parent 2174f9d283
commit fb71042c8f
10 changed files with 432 additions and 101 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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'}
]

View file

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

View file

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