mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
Implementing recommendation logic and tests
This commit is contained in:
parent
0b52dc8097
commit
ebad3d7dd6
3 changed files with 157 additions and 11 deletions
|
|
@ -70,6 +70,10 @@ class Costs:
|
||||||
# https://www.greenmatch.co.uk/windows/double-glazing/cost
|
# https://www.greenmatch.co.uk/windows/double-glazing/cost
|
||||||
SASH_WINDOW_INFLATION_FACTOR = 1.5
|
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):
|
def __init__(self, property_instance):
|
||||||
"""
|
"""
|
||||||
Initializes the Costs class with a property instance.
|
Initializes the Costs class with a property instance.
|
||||||
|
|
@ -726,7 +730,7 @@ class Costs:
|
||||||
"labour_cost": labour_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:
|
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
|
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
|
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
|
material_cost = material["material_cost"] * number_of_windows
|
||||||
labour_cost = (
|
|
||||||
material["labour_cost"] * number_of_windows * self.SASH_WINDOW_INFLATION_FACTOR *
|
|
||||||
self.labour_adjustment_factor
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
contingency_cost = subtotal * self.CONTINGENCY
|
||||||
preliminaries_cost = subtotal * self.PRELIMINARIES
|
preliminaries_cost = subtotal * self.PRELIMINARIES
|
||||||
|
|
@ -790,6 +794,7 @@ class Costs:
|
||||||
total_cost = subtotal_before_vat + vat_cost
|
total_cost = subtotal_before_vat + vat_cost
|
||||||
|
|
||||||
labour_hours = material["labour_hours_per_unit"] * number_of_windows
|
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
|
# Assume a team of 2
|
||||||
labour_days = (labour_hours / 8) / 2
|
labour_days = (labour_hours / 8) / 2
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,20 @@
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
from backend.Property import Property
|
from backend.Property import Property
|
||||||
from recommendations.Costs import Costs
|
from recommendations.Costs import Costs
|
||||||
|
|
||||||
|
|
||||||
class WindowsRecommendations:
|
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):
|
def __init__(self, property_instance: Property, materials: List):
|
||||||
self.property = property_instance
|
self.property = property_instance
|
||||||
|
|
@ -28,7 +39,14 @@ class WindowsRecommendations:
|
||||||
:return:
|
: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
|
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:
|
if not number_of_windows:
|
||||||
raise ValueError("Number of windows not specified")
|
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"):
|
if self.property.windows["has_glazing"] & (self.property.windows["glazing_coverage"] == "full"):
|
||||||
return
|
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
|
# We then price the job based on the number of windows that there are
|
||||||
cost_result = self.costs.window_glazing(
|
cost_result = self.costs.window_glazing(
|
||||||
number_of_windows=number_of_windows,
|
number_of_windows=number_of_windows,
|
||||||
material=self.glazing_material,
|
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 = [
|
self.recommendation = [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,8 @@ class TestWindowRecommendations:
|
||||||
address1='1',
|
address1='1',
|
||||||
epc_client=Mock(),
|
epc_client=Mock(),
|
||||||
data={
|
data={
|
||||||
"county": "Wychavon"
|
"county": "Wychavon",
|
||||||
|
"multi-glaze-proportion": 0
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
property_1.windows = {
|
property_1.windows = {
|
||||||
|
|
@ -33,3 +34,104 @@ class TestWindowRecommendations:
|
||||||
assert not recommender.recommendation
|
assert not recommender.recommendation
|
||||||
|
|
||||||
recommender.recommend()
|
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
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue