Model/recommendations/tests/test_costs.py
2024-09-30 14:55:13 +01:00

311 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from recommendations.Costs import Costs
from unittest.mock import Mock
import datetime
import pytest
class TestCosts:
def test_cavity_wall_insulation(self):
mock_property = Mock()
mock_property.data = {
"county": "Northamptonshire"
}
costs = Costs(mock_property)
cwi_material = {
"description": "cwi",
"depth": 75,
"thermal_conductivity": 0.037,
"total_cost": 14,
"labour_hours_per_unit": 0.065,
"is_installer_quote": True
}
cwi_results = costs.cavity_wall_insulation(
wall_area=95.9104281347967,
material=cwi_material,
)
assert cwi_results == {'total': 1342.7459938871539, 'labour_hours': 8, 'labour_days': 1}
def test_loft_insulation(self):
mock_property = Mock()
mock_property.data = {
"county": "Northamptonshire"
}
costs = Costs(mock_property)
loft_material = {
"description": "Crown Loft Roll 44 glass fibre roll",
"depth": 270,
"thermal_conductivity": 0.044,
"total_cost": 11,
"labour_hours_per_unit": 0.11,
"is_installer_quote": True,
}
loft_results = costs.loft_and_flat_insulation(
floor_area=33.5,
material=loft_material,
)
assert loft_results == {'total': 368.5, 'labour_hours': 8, 'labour_days': 1}
def test_internal_wall_insulation(self):
mock_property = Mock()
mock_property.data = {
"county": "Northamptonshire"
}
costs = Costs(mock_property)
iwi_material = {
"type": "internal_wall_insulation",
"description": "Ecotherm Eco-Versal PIR Insulation Board",
"depth": 150,
"depth_unit": "mm",
"cost_unit": "gbp_per_m2",
"thermal_conductivity": 0.022,
"thermal_conductivity_unit": "watt_per_meter_kelvin",
"labour_hours_per_unit": 0.18,
"total_cost": 200,
"link": "link",
"is_installer_quote": True
}
iwi_results = costs.solid_wall_insulation(
wall_area=95.9104281347967,
material=iwi_material,
)
assert iwi_results == {
'total': 19182.085626959342, 'labour_hours': 17.263877064263404, 'labour_days': 0.5394961582582314
}
def test_suspended_floor_insulation(self):
mock_property = Mock()
mock_property.data = {
"county": "Northamptonshire"
}
costs = Costs(mock_property)
sus_floor_material = {
'type': 'suspended_floor_insulation', 'description': 'Thermafleece CosyWool Roll',
'depth': 140.0,
'depth_unit': 'mm', 'cost_unit': 'gbp_per_m2', 'thermal_conductivity': 0.039,
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'prime_material_cost': 0,
'material_cost': 11.68, 'labour_cost': 1.78, 'labour_hours_per_unit': 0.1,
'plant_cost': 0,
'total_cost': 13.46, 'link': 'SPONs',
'Notes': 'Spons did not contain labour costs so we use values for similar insulations. '
'We use the '
'same values as in Crown loft roll 44, since it is also an insulation roll',
"is_installer_quote": False
}
sus_floor_non_insulation_materials = [
{'type': 'suspended_floor_demolition', 'description': 'Removal of carpet and underfelt', 'depth': 0,
'depth_unit': 0, 'cost_unit': 0, 'thermal_conductivity': 0, 'thermal_conductivity_unit': 0,
'prime_material_cost': 0, 'material_cost': 0, 'labour_cost': 3.32, 'labour_hours_per_unit': 0.11,
'plant_cost': 0, 'total_cost': 3.32, 'link': 'SPONs',
'Notes': 'We ignore the plant cost that is in SPONs because we assume the carpet is not scrapped and '
'therefore there is no need for a skip'},
{'type': 'suspended_floor_demolition',
'description': 'Remove boarding; withdraw nails; set aside for reuse; ground level', 'depth': 0,
'depth_unit': 0, 'cost_unit': 0, 'thermal_conductivity': 0, 'thermal_conductivity_unit': 0,
'prime_material_cost': 0, 'material_cost': 0, 'labour_cost': 9.34, 'labour_hours_per_unit': 0.3,
'plant_cost': 0, 'total_cost': 9.34, 'link': 'SPONs', 'Notes': 0},
{'type': 'suspended_floor_vapour_barrier', 'description': 'Visqueen High Performance Vapour Barrier',
'depth': 0, 'depth_unit': 0, 'cost_unit': 0, 'thermal_conductivity': 0,
'thermal_conductivity_unit': 0, 'prime_material_cost': 0.58, 'material_cost': 1.21, 'labour_cost': 0.48,
'labour_hours_per_unit': 0.02, 'plant_cost': 0, 'total_cost': 1.69, 'link': 'SPONs', 'Notes': 0},
{'type': 'suspended_floor_redecoration', 'description': 'refix floorboards previously set aside',
'depth': 0, 'depth_unit': 0, 'cost_unit': 0, 'thermal_conductivity': 0,
'thermal_conductivity_unit': 0, 'prime_material_cost': 0, 'material_cost': 1.54, 'labour_cost': 24.98,
'labour_hours_per_unit': 0.74, 'plant_cost': 0, 'total_cost': 26.52, 'link': 'SPONs', 'Notes': 0},
{'type': 'suspended_floor_redecoration', 'description': 'Fitting carpet', 'depth': 0, 'depth_unit': 0,
'cost_unit': 0, 'thermal_conductivity': 0, 'thermal_conductivity_unit': 0,
'prime_material_cost': 0, 'material_cost': 0, 'labour_cost': 6.59, 'labour_hours_per_unit': 0.37,
'plant_cost': 0, 'total_cost': 6.59, 'link': 'SPONs',
'Notes': 'SPONs does not have data on re-fitting the carpet so we use the data in Fitted carpeting; '
'Gradus woven polypropylene tufted loop\n\n as a baseline. We assume re-use of carpets, '
'therefore we need just labour rates'}]
sus_floor_results = costs.suspended_floor_insulation(
insulation_floor_area=33.5,
material=sus_floor_material,
non_insulation_materials=sus_floor_non_insulation_materials
)
assert sus_floor_results == {
'total': 3337.07436, 'subtotal': 2780.8953, 'vat': 556.17906, 'contingency': 370.78604,
'preliminaries': 185.39302, 'material': 483.405, 'profit': 370.78604, 'labour_hours': 54.940000000000005,
'labour_days': 2.289166666666667, 'labour_cost': 1370.5252
}
def test_solid_floor_insulation(self):
mock_property = Mock()
mock_property.data = {
"county": "Northamptonshire"
}
costs = Costs(mock_property)
sol_floor_material = {
'type': 'solid_floor_insulation', 'description': 'Kay-Cel Expanded Polystyrene Board',
'depth': 100.0, 'depth_unit': 'mm', 'cost_unit': 'gbp_per_m2', 'thermal_conductivity': 0.033,
'thermal_conductivity_unit': 'watt_per_meter_kelvin', 'prime_material_cost': 0,
'material_cost': 12.02, 'labour_cost': 4.4, 'labour_hours_per_unit': 0.19, 'plant_cost': 0,
'total_cost': 16.42, 'link': 'SPONs', 'Notes': 0, "is_installer_quote": False
}
sol_floor_non_insulation_materials = [
{'type': 'solid_floor_demolition', 'description': 'Removal of carpet and underfelt', 'depth': 0,
'depth_unit': 0, 'cost_unit': 0, 'thermal_conductivity': 0, 'thermal_conductivity_unit': 0,
'prime_material_cost': 0, 'material_cost': 0, 'labour_cost': 3.32, 'labour_hours_per_unit': 0.11,
'plant_cost': 0, 'total_cost': 3.32, 'link': 'SPONs',
'Notes': 'We ignore the plant cost that is in SPONs because we assume the carpet is not scrapped and '
'therefore there is no need for a skip'},
{'type': 'solid_floor_preparation',
'description': 'clean surface of concrete to receive new damp-proof membrane', 'depth': 0,
'depth_unit': 0, 'cost_unit': 0, 'thermal_conductivity': 0, 'thermal_conductivity_unit': 0,
'prime_material_cost': 0, 'material_cost': 0, 'labour_cost': 4.36, 'labour_hours_per_unit': 0.14,
'plant_cost': 0, 'total_cost': 4.36, 'link': 0, 'Notes': 0}, {
'type': 'solid_floor_preparation',
'description': 'Clean out crack to '
'form a 20mm×20mm '
'groove and fill with '
'cement: mortar mixed '
'with bonding agent',
'depth': 0, 'depth_unit': 0,
'cost_unit': 0,
'thermal_conductivity': 0,
'thermal_conductivity_unit': 0,
'prime_material_cost': 0,
'material_cost': 6.91,
'labour_cost': 18.99,
'labour_hours_per_unit': 0.61,
'plant_cost': 0.16,
'total_cost': 26.06, 'link': 0,
'Notes': 'This step is the '
'assessment and repair of '
'any damage to the concrete '
'floor such as filling '
'cracks or levelling uneven '
'areas'},
{'type': 'solid_floor_vapour_barrier', 'description': 'Visqueen High Performance Vapour Barrier',
'depth': 0, 'depth_unit': 0, 'cost_unit': 0, 'thermal_conductivity': 0,
'thermal_conductivity_unit': 0, 'prime_material_cost': 0.58, 'material_cost': 1.21, 'labour_cost': 0.48,
'labour_hours_per_unit': 0.02, 'plant_cost': 0, 'total_cost': 1.69, 'link': 'SPONs', 'Notes': 0},
{'type': 'solid_floor_redecoration',
'description': 'Screeded beds; protection to compressible formwork exceeding 600mm wide', 'depth': 0,
'depth_unit': 0, 'cost_unit': 0, 'thermal_conductivity': 0, 'thermal_conductivity_unit': 0,
'prime_material_cost': 9.6, 'material_cost': 9.89, 'labour_cost': 2.67, 'labour_hours_per_unit': 0.15,
'plant_cost': 0, 'total_cost': 12.56, 'link': 'SPONs',
'Notes': 'This is the screed layer, placed on top of the insulation'},
{'type': 'solid_floor_redecoration', 'description': 'Fitting carpet', 'depth': 0, 'depth_unit': 0,
'cost_unit': 0, 'thermal_conductivity': 0, 'thermal_conductivity_unit': 0,
'prime_material_cost': 0, 'material_cost': 0, 'labour_cost': 6.59, 'labour_hours_per_unit': 0.37,
'plant_cost': 0, 'total_cost': 6.59, 'link': 'SPONs',
'Notes': 'SPONs does not have data on re-fitting the carpet so we use the data in Fitted carpeting; '
'Gradus woven polypropylene tufted loop\n\n as a baseline. We assume re-use of carpets, '
'therefore we need just labour rates'},
{'type': 'solid_floor_redecoration',
'description': 'Fitting existing softwood skirting or architrave to new frames; 150mm high', 'depth': 0,
'depth_unit': 0, 'cost_unit': 0, 'thermal_conductivity': 0, 'thermal_conductivity_unit': 0,
'prime_material_cost': 0, 'material_cost': 0.01, 'labour_cost': 4.87, 'labour_hours_per_unit': 0.12,
'plant_cost': 0, 'total_cost': 4.88, 'link': 'SPONs', 'Notes': 0}
]
sol_floor_results = costs.solid_floor_insulation(
insulation_floor_area=33.5,
material=sol_floor_material,
non_insulation_materials=sol_floor_non_insulation_materials
)
assert sol_floor_results == {
'total': 4245.023520000001, 'subtotal': 3537.5196, 'vat': 707.5039200000001, 'contingency': 471.66928,
'preliminaries': 235.83464, 'material': 1006.3399999999999, 'profit': 471.66928, 'labour_hours': 57.285,
'labour_days': 2.386875, 'labour_cost': 1346.6464
}
def test_external_wall_insulation(self):
mock_property = Mock()
mock_property.data = {
"county": "Northamptonshire",
"property-type": "House",
"built-form": 'End-Terrace'
}
costs = Costs(mock_property)
ewi_material = {
'type': 'external_wall_insulation', 'description': 'Ecotherm Eco-Versal PIR Insulation Board',
'depth': 150.0, 'depth_unit': 'mm', 'cost_unit': 'gbp_per_m2', 'thermal_conductivity': 0.022,
'thermal_conductivity_unit': 'watt_per_meter_kelvin',
'labour_hours_per_unit': 1.4,
'total_cost': 300, 'link': 'SPONs', 'Notes': 0, "is_installer_quote": True
}
ewi_results = costs.solid_wall_insulation(
wall_area=95.9104281347967,
material=ewi_material,
)
assert ewi_results == {
'total': 28773.12844043901, 'labour_hours': 134.2745993887154, 'labour_days': 4.196081230897356
}
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",
"is_installer_quote": False
}
flat_roof_floor_results = costs.loft_and_flat_insulation(
floor_area=33.5,
material=flat_roof_material,
)
assert flat_roof_floor_results == {
'total': 2063.935, 'subtotal': 1719.9458333333334, 'vat': 343.9891666666665, 'labour_hours': 8,
'labour_days': 1
}
assert costs.labour_adjustment_factor == 0.88
# Test for different wattages
@pytest.mark.parametrize("n_panels, expected_cost", [
(7, 4055.0),
(10, 4540.0),
(12, 4863.0),
(15, 5707.0),
])
def test_solar_pv_different_wattages(self, n_panels, expected_cost):
mock_property = Mock()
mock_property.data = {"county": "Mansfield"}
costs = Costs(mock_property)
result = costs.solar_pv(n_panels)
assert result['total'] == pytest.approx(expected_cost, rel=0.01)