from recommendations.Costs import Costs from unittest.mock import Mock 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, 5458.727999999999), (10, 6013.139999999999), (12, 6386.447999999999), (15, 7594.451999999999), ]) 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)