mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +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
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue