Implementing recommendation logic and tests

This commit is contained in:
Khalim Conn-Kowlessar 2023-12-20 15:55:34 +00:00
parent 0b52dc8097
commit ebad3d7dd6
3 changed files with 157 additions and 11 deletions

View file

@ -70,6 +70,10 @@ class Costs:
# https://www.greenmatch.co.uk/windows/double-glazing/cost
SASH_WINDOW_INFLATION_FACTOR = 1.5
# Typically, secondary glazing can be installed for 25% of the cost of double glazed windows - to be conservative,
# we scale the cost by half
SECONDARY_GLAZING_SCALING_FACTOR = 0.5
def __init__(self, property_instance):
"""
Initializes the Costs class with a property instance.
@ -726,7 +730,7 @@ class Costs:
"labour_cost": labour_costs
}
def window_glazing(self, number_of_windows, material):
def window_glazing(self, number_of_windows, material, is_secondary_glazing=False):
"""
We characterise the jobs to be done for window glazing as the following:
1) Initial Assessment and Measurements: Before removing the existing window, it's essential to assess the
@ -767,17 +771,17 @@ class Costs:
For this cost estimation process, we factor in initial assement into the preliminaries
:param number_of_windows:
:return:
"""
material_cost = material["material_cost"] * number_of_windows * self.SASH_WINDOW_INFLATION_FACTOR
labour_cost = (
material["labour_cost"] * number_of_windows * self.SASH_WINDOW_INFLATION_FACTOR *
self.labour_adjustment_factor
)
material_cost = material["material_cost"] * number_of_windows
subtotal = material_cost + labour_cost
labour_cost = (
material["labour_cost"] * number_of_windows * self.labour_adjustment_factor
)
multiplier = self.SECONDARY_GLAZING_SCALING_FACTOR if is_secondary_glazing else (
self.SASH_WINDOW_INFLATION_FACTOR)
subtotal = (material_cost + labour_cost) * multiplier
contingency_cost = subtotal * self.CONTINGENCY
preliminaries_cost = subtotal * self.PRELIMINARIES
@ -790,6 +794,7 @@ class Costs:
total_cost = subtotal_before_vat + vat_cost
labour_hours = material["labour_hours_per_unit"] * number_of_windows
labour_hours = labour_hours * self.SECONDARY_GLAZING_SCALING_FACTOR if is_secondary_glazing else labour_hours
# Assume a team of 2
labour_days = (labour_hours / 8) / 2

View file

@ -1,9 +1,20 @@
from typing import List
import numpy as np
from backend.Property import Property
from recommendations.Costs import Costs
class WindowsRecommendations:
# If the property has existing glazing, we scale down the number of windows that need to be glazed
COVERAGE_MAP = {
# If most of the windows have already been glazed, we assume that 2/3 are glazed and 1/2 are remaining to be
# glazed
"most": 0.33,
# If glazing is partial, we assume 50/50 split between glazed and unglazed
"partial": 0.5
}
def __init__(self, property_instance: Property, materials: List):
self.property = property_instance
@ -28,7 +39,14 @@ class WindowsRecommendations:
:return:
"""
# If the property is in a conservation area or is a listed building, it becomes more difficult to install
# double glazing. Therefore, we don't recommend it. It is still possible but is not practical as it
# requires planning permission and might require a more expensive window type, such as timber.
number_of_windows = self.property.number_of_windows
is_secondary_glazing = self.property.restricted_measures or (
self.property.windows["glazing_type"] == "secondary"
)
if not number_of_windows:
raise ValueError("Number of windows not specified")
@ -36,13 +54,34 @@ class WindowsRecommendations:
if self.property.windows["has_glazing"] & (self.property.windows["glazing_coverage"] == "full"):
return
# We scale the number of windows based on the proportion of existing glazing
if self.property.data["multi-glaze-proportion"] != "":
n_windows_scalar = 1 - (self.property.data["multi-glaze-proportion"] / 100)
else:
n_windows_scalar = self.COVERAGE_MAP.get(self.property.windows["glazing_coverage"], 1)
number_of_windows *= n_windows_scalar
number_of_windows = np.ceil(number_of_windows)
# We then price the job based on the number of windows that there are
cost_result = self.costs.window_glazing(
number_of_windows=number_of_windows,
material=self.glazing_material,
is_secondary_glazing=is_secondary_glazing
)
description = None
glazing_type = "secondary glazing" if is_secondary_glazing else "double glazing"
if self.property.windows["glazing_coverage"] in ["partial", "most"]:
description = f"Install {glazing_type} to the remaining windows"
else:
description = f"Install {glazing_type} to all windows"
if self.property.is_listed:
description += ". Secondary glazing recommended due to listed building status"
elif self.property.is_heritage:
description += ". Secondary glazing recommended due to conservation area status"
elif self.property.in_conservation_area:
description += ". Secondary glazing recommended due to conservation area status"
self.recommendation = [
{

View file

@ -18,7 +18,8 @@ class TestWindowRecommendations:
address1='1',
epc_client=Mock(),
data={
"county": "Wychavon"
"county": "Wychavon",
"multi-glaze-proportion": 0
}
)
property_1.windows = {
@ -33,3 +34,104 @@ class TestWindowRecommendations:
assert not recommender.recommendation
recommender.recommend()
assert recommender.recommendation == [
{'parts': [], 'type': 'window_glazing', 'description': 'Install double glazing to all windows',
'starting_u_value': None, 'new_u_value': None, 'sap_points': None, 'total': 5721.943248,
'subtotal': 4768.28604, 'vat': 953.6572080000001, 'contingency': 340.59186, 'preliminaries': 340.59186,
'material': 1275.75, 'profit': 681.18372, 'labour_hours': 45.5, 'labour_cost': 994.8624,
'labour_days': 2.84375}]
def test_partial_double_glazed(self):
"""
For this property, the double glazing is describes as partial, therefore we recommend completion of
double glazing
:return:
"""
property_2 = Property(
id=1,
postcode='1',
address1='1',
epc_client=Mock(),
data={
"county": "Wychavon",
"multi-glaze-proportion": 33
}
)
property_2.windows = {'original_description': 'Mostly double glazing', 'has_glazing': True,
'glazing_coverage': 'most',
'glazing_type': 'double', 'no_data': False}
property_2.number_of_windows = 7
recommender2 = WindowsRecommendations(property_instance=property_2, materials=materials)
assert not recommender2.recommendation
recommender2.recommend()
assert recommender2.recommendation == [
{'parts': [], 'type': 'window_glazing', 'description': 'Install double glazing to the remaining windows',
'starting_u_value': None, 'new_u_value': None, 'sap_points': None, 'total': 4087.10232,
'subtotal': 3405.9186, 'vat': 681.18372, 'contingency': 243.2799, 'preliminaries': 243.2799,
'material': 911.25, 'profit': 486.5598, 'labour_hours': 32.5, 'labour_cost': 710.6160000000001,
'labour_days': 2.03125}]
def test_fully_double_glazed(self):
"""
This property has full double glazing so we shouldn't recommend anything
:return:
"""
property_3 = Property(
id=1,
postcode='1',
address1='1',
epc_client=Mock(),
data={
"county": "Wychavon",
"multi-glaze-proportion": 80
}
)
property_3.windows = {'original_description': 'Fully double glazed', 'has_glazing': True,
'glazing_coverage': 'full',
'glazing_type': 'double', 'no_data': False}
property_3.number_of_windows = 7
recommender3 = WindowsRecommendations(property_instance=property_3, materials=materials)
assert not recommender3.recommendation
recommender3.recommend()
assert not recommender3.recommendation
def test_fully_secondary_glazed(self):
property_4 = Property(
id=1,
postcode='1',
address1='1',
epc_client=Mock(),
data={
"county": "Wychavon",
"multi-glaze-proportion": 100
}
)
property_4.windows = {'original_description': 'Full secondary glazing', 'has_glazing': True,
'glazing_coverage': 'full',
'glazing_type': 'secondary', 'no_data': False}
property_4.number_of_windows = 7
recommender4 = WindowsRecommendations(property_instance=property_4, materials=materials)
assert not recommender4.recommendation
recommender4.recommend()
assert not recommender4.recommendation
def test_partial_secondary_glazing(self):
pass
def test_single_glazed_restricted_measures(self):
pass