mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Merge pull request #264 from Hestia-Homes/windows-recommendations
Windows recommendations
This commit is contained in:
commit
75bb414471
21 changed files with 777 additions and 30 deletions
|
|
@ -14,7 +14,7 @@ from epc_api.client import EpcClient
|
|||
from BaseUtility import Definitions
|
||||
from recommendations.rdsap_tables import england_wales_age_band_lookup, FLOOR_LEVEL_MAP
|
||||
from recommendations.recommendation_utils import (
|
||||
estimate_perimeter, get_wall_type, estimate_external_wall_area, esimtate_pitched_roof_area
|
||||
estimate_perimeter, get_wall_type, estimate_external_wall_area, esimtate_pitched_roof_area, estimate_windows
|
||||
)
|
||||
|
||||
ENVIRONMENT = os.environ.get('ENVIRONMENT', 'dev')
|
||||
|
|
@ -87,6 +87,7 @@ class Property(Definitions):
|
|||
self.insulation_floor_area = None
|
||||
self.number_lighting_outlets = None
|
||||
self.floor_level = None
|
||||
self.number_of_windows = None
|
||||
|
||||
self.current_adjusted_energy = None
|
||||
self.expected_adjusted_energy = None
|
||||
|
|
@ -332,6 +333,7 @@ class Property(Definitions):
|
|||
self.set_wall_type()
|
||||
self.set_floor_type()
|
||||
self.set_floor_level()
|
||||
self.set_windows_count()
|
||||
|
||||
def set_age_band(self):
|
||||
"""
|
||||
|
|
@ -850,3 +852,18 @@ class Property(Definitions):
|
|||
"""
|
||||
self.current_adjusted_energy = current_adjusted_energy
|
||||
self.expected_adjusted_energy = expected_adjusted_energy
|
||||
|
||||
def set_windows_count(self):
|
||||
"""
|
||||
Using the estimate_windows function, this method will set the number of windows in the property
|
||||
:return:
|
||||
"""
|
||||
|
||||
self.number_of_windows = estimate_windows(
|
||||
property_type=self.data["property-type"],
|
||||
built_form=self.data["built-form"],
|
||||
construction_age_band=self.construction_age_band,
|
||||
floor_area=self.floor_area,
|
||||
number_habitable_rooms=self.number_of_rooms,
|
||||
extension_count=float(self.data["extension-count"]),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ class MaterialType(enum.Enum):
|
|||
exposed_floor_insulation = "exposed_floor_insulation"
|
||||
flat_roof_insulation = "flat_roof_insulation"
|
||||
room_roof_insulation = "room_roof_insulation"
|
||||
windows_glazing = "windows_glazing"
|
||||
|
||||
iwi_wall_demolition = "iwi_wall_demolition"
|
||||
iwi_vapour_barrier = "iwi_vapour_barrier"
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ async def trigger_plan(body: PlanTriggerRequest):
|
|||
logger.info("Getting the inputs")
|
||||
epc_client = EpcClient(auth_token=get_settings().EPC_AUTH_TOKEN)
|
||||
plan_input = read_csv_from_s3(bucket_name=get_settings().PLAN_TRIGGER_BUCKET, filepath=body.trigger_file_path)
|
||||
|
||||
uprn_filenames = read_dataframe_from_s3_parquet(
|
||||
bucket_name=get_settings().DATA_BUCKET, file_key="spatial/filename_meta.parquet"
|
||||
)
|
||||
|
|
@ -117,9 +118,6 @@ async def trigger_plan(body: PlanTriggerRequest):
|
|||
|
||||
logger.info("Getting components and epc recommendations")
|
||||
|
||||
# TODO: Move this to a class. We probably want a Recommender class which takes the injects the optimisers
|
||||
# in as a dependency and then the optimisers can take the input measures in as part of the setup() method
|
||||
|
||||
recommendations = {}
|
||||
recommendations_scoring_data = []
|
||||
property_scoring_data = {}
|
||||
|
|
@ -546,9 +544,7 @@ async def trigger_plan(body: PlanTriggerRequest):
|
|||
|
||||
valuations = PropertyValuation.estimate(property_instance=p, target_epc=new_epc)
|
||||
|
||||
property_valuation_increases.append(
|
||||
valuations["average_increased_value"] - valuations["current_value"]
|
||||
)
|
||||
property_valuation_increases.append(valuations["average_increase"])
|
||||
|
||||
# Commit the session after each batch
|
||||
session.commit()
|
||||
|
|
|
|||
|
|
@ -175,11 +175,31 @@ def create_recommendation_scoring_data(
|
|||
scoring_dict["LOW_ENERGY_LIGHTING_ENDING"] = 100
|
||||
scoring_dict["LIGHTING_ENERGY_EFF_STARTING"] = "Very Good"
|
||||
|
||||
if recommendation["type"] == "windows_glazing":
|
||||
scoring_dict["MULTI_GLAZE_PROPORTION_ENDING"] = 100
|
||||
scoring_dict["WINDOWS_ENERGY_EFF_ENDING"] = "Average"
|
||||
|
||||
is_secondary_glazing = recommendation["is_secondary_glazing"]
|
||||
|
||||
if scoring_dict["glazing_type_ENDING"] == "multiple":
|
||||
pass
|
||||
elif scoring_dict["glazing_type_ENDING"] == "single":
|
||||
scoring_dict["glazing_type_ENDING"] = "secondary" if is_secondary_glazing else "double"
|
||||
elif scoring_dict["glazing_type_ENDING"] == "double":
|
||||
scoring_dict["glazing_type_ENDING"] = "multiple" if is_secondary_glazing else "double"
|
||||
elif scoring_dict["glazing_type_ENDING"] == "secondary":
|
||||
scoring_dict["glazing_type_ENDING"] = "secondary" if is_secondary_glazing else "multiple"
|
||||
elif scoring_dict["glazing_type_ENDING"] in ["triple", "high performance"]:
|
||||
scoring_dict["glazing_type_ENDING"] = "multiple"
|
||||
else:
|
||||
raise ValueError("Invalid glazing type - implement me")
|
||||
|
||||
if recommendation["type"] not in [
|
||||
"mechanical_ventilation", "sealing_open_fireplace", "low_energy_lighting",
|
||||
"internal_wall_insulation", "external_wall_insulation", "cavity_wall_insulation",
|
||||
"loft_insulation", "room_roof_insulation", "flat_roof_insulation",
|
||||
"solid_floor_insulation", "suspended_floor_insulation", "exposed_floor_insulation"
|
||||
"solid_floor_insulation", "suspended_floor_insulation", "exposed_floor_insulation",
|
||||
"windows_glazing"
|
||||
]:
|
||||
raise NotImplementedError("Implement me")
|
||||
|
||||
|
|
|
|||
|
|
@ -93,7 +93,13 @@ class PropertyValuation:
|
|||
value = cls.UPRN_VALUE_LOOKUP.get(property_instance.uprn)
|
||||
|
||||
if not value:
|
||||
raise ValueError("Have not implemented valuation for this property")
|
||||
return {
|
||||
"current_value": None,
|
||||
"lower_bound_increased_value": None,
|
||||
"upper_bound_increased_value": None,
|
||||
"average_increased_value": None,
|
||||
"average_increase": None
|
||||
}
|
||||
|
||||
current_epc = property_instance.data["current-energy-rating"]
|
||||
# We get the spectrum of ratings between the current and target EPC
|
||||
|
|
@ -119,4 +125,5 @@ class PropertyValuation:
|
|||
"lower_bound_increased_value": value * (1 + min_increase),
|
||||
"upper_bound_increased_value": value * (1 + max_increase),
|
||||
"average_increased_value": value * (1 + avg_increase),
|
||||
"average_increase": value * (1 + avg_increase) - value
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,8 @@ mock_epc_response = {
|
|||
"floor-height": 2.5,
|
||||
"total-floor-area": 100,
|
||||
"construction-age-band": "England and Wales: 1967-1975",
|
||||
"floor-description": "Floor Description"
|
||||
"floor-description": "Floor Description",
|
||||
"floor-level": "Ground"
|
||||
},
|
||||
{
|
||||
"lmk-key": 2,
|
||||
|
|
@ -68,7 +69,8 @@ mock_epc_response = {
|
|||
"floor-height": 2.5,
|
||||
"total-floor-area": 100,
|
||||
"construction-age-band": "England and Wales: 1967-1975",
|
||||
"floor-description": "Floor Description"
|
||||
"floor-description": "Floor Description",
|
||||
"floor-level": "Ground"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -100,7 +102,8 @@ mock_epc_response_dupe = {
|
|||
"floor-height": 2.5,
|
||||
"total-floor-area": 100,
|
||||
"construction-age-band": "England and Wales: 1967-1975",
|
||||
"floor-description": "Floor Description"
|
||||
"floor-description": "Floor Description",
|
||||
"floor-level": "Ground"
|
||||
},
|
||||
{
|
||||
"lmk-key": 2,
|
||||
|
|
@ -128,7 +131,8 @@ mock_epc_response_dupe = {
|
|||
"floor-height": 2.5,
|
||||
"total-floor-area": 100,
|
||||
"construction-age-band": "England and Wales: 1967-1975",
|
||||
"floor-description": "Floor Description"
|
||||
"floor-description": "Floor Description",
|
||||
"floor-level": "Ground"
|
||||
},
|
||||
{
|
||||
"lmk-key": 3,
|
||||
|
|
@ -156,7 +160,8 @@ mock_epc_response_dupe = {
|
|||
"floor-height": 2.5,
|
||||
"total-floor-area": 100,
|
||||
"construction-age-band": "England and Wales: 1967-1975",
|
||||
"floor-description": "Floor Description"
|
||||
"floor-description": "Floor Description",
|
||||
"floor-level": "Ground"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ def app():
|
|||
ewi_costs = pd.read_excel(DATA_DIRECTORY, sheet_name="external_wall_insulation", header=0)
|
||||
lel_costs = pd.read_excel(DATA_DIRECTORY, sheet_name="low_energy_lighting", header=0)
|
||||
flat_roof_costs = pd.read_excel(DATA_DIRECTORY, sheet_name="flat_roof_insulation", header=0)
|
||||
window_costs = pd.read_excel(DATA_DIRECTORY, sheet_name="window_glazing", header=0)
|
||||
|
||||
# Form a single table to be uploaded
|
||||
costs = pd.concat(
|
||||
|
|
|
|||
|
|
@ -631,6 +631,19 @@ def app():
|
|||
file_key="sap_change_model/all_equal_rows.parquet",
|
||||
)
|
||||
|
||||
from utils.s3 import read_dataframe_from_s3_parquet
|
||||
dataset = read_dataframe_from_s3_parquet(
|
||||
bucket_name="retrofit-data-dev",
|
||||
file_key="sap_change_model/dataset_test.parquet",
|
||||
)
|
||||
|
||||
z = dataset[dataset["CONSTITUENCY"].isin(["E14000707", "E14000909"])]
|
||||
z["CONSTITUENCY"].value_counts()
|
||||
|
||||
z[z["CONSTITUENCY"] == "E14000909"]["UPRN"].sample(1)
|
||||
|
||||
self.data[self.data["UPRN"] == "100030549358"]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ class WindowAttributes(Definitions):
|
|||
raise ValueError('Invalid description')
|
||||
|
||||
def process(self) -> Dict[str, Union[str, bool]]:
|
||||
result: Dict[str, Union[str, bool]] = {
|
||||
result: Dict[str, Union[str, bool, None]] = {
|
||||
"has_glazing": False,
|
||||
"glazing_coverage": None,
|
||||
"glazing_type": None,
|
||||
|
|
@ -80,7 +80,11 @@ class WindowAttributes(Definitions):
|
|||
break
|
||||
|
||||
# If we didn't find any coverage or type, we assume full coverage
|
||||
if not result["glazing_coverage"]:
|
||||
if (not result["glazing_coverage"]) & (result["glazing_type"] != "single"):
|
||||
result["glazing_coverage"] = "full"
|
||||
|
||||
# We reset some values if the glazing is single
|
||||
if result["glazing_type"] == "single":
|
||||
result["has_glazing"] = False
|
||||
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -30,7 +30,8 @@ windows_cases = [
|
|||
'glazing_type': 'triple', 'no_data': False},
|
||||
{'original_description': 'Gwydrau triphlyg rhannol', 'has_glazing': True, 'glazing_coverage': 'partial',
|
||||
'glazing_type': 'triple', 'no_data': False},
|
||||
{'original_description': 'Single glazed', 'has_glazing': True, 'glazing_coverage': 'full', 'glazing_type': 'single',
|
||||
{'original_description': 'Single glazed', 'has_glazing': False, 'glazing_coverage': None,
|
||||
'glazing_type': 'single',
|
||||
'no_data': False},
|
||||
{'original_description': 'Some double glazing', 'has_glazing': True, 'glazing_coverage': 'partial',
|
||||
'glazing_type': 'double', 'no_data': False},
|
||||
|
|
@ -46,7 +47,8 @@ windows_cases = [
|
|||
'glazing_type': 'double', 'no_data': False},
|
||||
{'original_description': 'Gwydrau dwbl gan mwyaf', 'has_glazing': True, 'glazing_coverage': 'most',
|
||||
'glazing_type': 'double', 'no_data': False},
|
||||
{'original_description': 'Gwydrau sengl', 'has_glazing': True, 'glazing_coverage': 'full', 'glazing_type': 'single',
|
||||
{'original_description': 'Gwydrau sengl', 'has_glazing': False, 'glazing_coverage': None,
|
||||
'glazing_type': 'single',
|
||||
'no_data': False},
|
||||
{'original_description': 'Ffenestri perfformiad uchel', 'has_glazing': True, 'glazing_coverage': 'full',
|
||||
'glazing_type': 'high performance', 'no_data': False},
|
||||
|
|
|
|||
|
|
@ -3,12 +3,13 @@ from pathlib import Path
|
|||
from etl.epc_clean.tests.test_data.test_roof_attributes_cases import clean_roof_test_cases
|
||||
from etl.epc_clean.epc_attributes.RoofAttributes import RoofAttributes
|
||||
|
||||
|
||||
# For local testing
|
||||
if __file__ == "<input>":
|
||||
input_data_path = Path("./model_data/tests/test_data/EpcClean_inputs.obj")
|
||||
else:
|
||||
current_file_path = Path(__file__)
|
||||
input_data_path = current_file_path.parent / 'test_data' / 'EpcClean_inputs.obj'
|
||||
# if __file__ == "<input>":
|
||||
# input_data_path = Path("./model_data/tests/test_data/EpcClean_inputs.obj")
|
||||
# else:
|
||||
# current_file_path = Path(__file__)
|
||||
# input_data_path = current_file_path.parent / 'test_data' / 'EpcClean_inputs.obj'
|
||||
|
||||
|
||||
class TestRoofAttributes:
|
||||
|
|
@ -88,7 +89,12 @@ class TestRoofAttributes:
|
|||
|
||||
def test_clean_roof_no_description(self):
|
||||
roof = RoofAttributes('').process()
|
||||
assert roof == {}
|
||||
assert roof == {
|
||||
'thermal_transmittance': False, 'thermal_transmittance_unit': False, 'is_pitched': False,
|
||||
'is_roof_room': False, 'is_loft': False, 'is_flat': False, 'is_thatched': False,
|
||||
'is_at_rafters': False, 'is_assumed': False, 'has_dwelling_above': False, 'is_valid': False,
|
||||
'insulation_thickness': False
|
||||
}
|
||||
|
||||
def test_clean_roof_edge_cases(self):
|
||||
# Insulation thickness edge case
|
||||
|
|
|
|||
43
etl/testing_data/windows_portfolio.py
Normal file
43
etl/testing_data/windows_portfolio.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
"""
|
||||
This script will create an input csv for the recommendation engine and upload it to S3, which can be used for
|
||||
testing
|
||||
"""
|
||||
import pandas as pd
|
||||
from utils.s3 import save_csv_to_s3
|
||||
|
||||
USER_ID = 8
|
||||
PORTFOLIO_ID = 56
|
||||
|
||||
|
||||
def app():
|
||||
"""
|
||||
This portfolio is for testing windows recommendations
|
||||
:return:
|
||||
"""
|
||||
|
||||
test_file = pd.DataFrame(
|
||||
[
|
||||
{"address": "3 Church Terrace", "postcode": "LE13 0PW", "Notes": None},
|
||||
{"address": "3, Main Street, Redmile", "postcode": "NG13 0GA", "Notes": None},
|
||||
{"address": "Manor House, Kennel Lane, Reepham", "postcode": "LN3 4DZ", "Notes": None},
|
||||
{"address": "13 Main Street", "postcode": "LE14 2JU", "Notes": None},
|
||||
{"address": "8 The Crescent, Coston Road, Buckminster", "postcode": "NG33 5SF", "Notes": None},
|
||||
]
|
||||
)
|
||||
|
||||
# Store the data in s3
|
||||
filename = f"{USER_ID}/{PORTFOLIO_ID}/windows_portfolio_inputs.csv"
|
||||
save_csv_to_s3(
|
||||
dataframe=test_file,
|
||||
bucket_name="retrofit-plan-inputs-dev",
|
||||
file_name=filename
|
||||
)
|
||||
|
||||
body = {
|
||||
"portfolio_id": str(PORTFOLIO_ID),
|
||||
"housing_type": "Social",
|
||||
"goal": "Increase EPC",
|
||||
"goal_value": "A",
|
||||
"trigger_file_path": filename
|
||||
}
|
||||
print(body)
|
||||
|
|
@ -64,6 +64,16 @@ class Costs:
|
|||
VAT_RATE = 0.2
|
||||
PROFIT_MARGIN = 0.2
|
||||
|
||||
# Based on this greenmatch article, on average, a Sash window is around 50% more expensive than a casement window.
|
||||
# Therefore, for a conservative cost estimate, and allowance for a more premium window type, we inflate the material
|
||||
# cost of the windows to allow for a sash window type
|
||||
# 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.
|
||||
|
|
@ -719,3 +729,85 @@ class Costs:
|
|||
"labour_days": labour_days,
|
||||
"labour_cost": labour_costs
|
||||
}
|
||||
|
||||
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
|
||||
condition of the window frame and opening. Precise measurements are taken to ensure the new double glazed
|
||||
windows fit perfectly.
|
||||
|
||||
2) Remove the Existing Window: This involves carefully dismantling and removing the old single glazed window. It
|
||||
requires skill to avoid damaging the surrounding wall and the window frame (if it's to be reused).
|
||||
|
||||
3) Dispose of the Existing Window: The old window, especially if it's a single glazed unit, needs to be
|
||||
disposed of responsibly. Glass and other materials should be recycled where possible.
|
||||
|
||||
4) Surface Preparation: The window opening might need some preparation, especially if there's damage or if
|
||||
adjustments are needed to accommodate the new window. This can include repairing or replacing parts of the
|
||||
window frame, sealing gaps, and ensuring the opening is level and square.
|
||||
|
||||
5) Install the Window Frame (if new frames are used): In many cases, double glazed windows come with their
|
||||
frames. These need to be installed securely into the window opening. This process involves aligning, leveling,
|
||||
and fixing the frame in place.
|
||||
|
||||
6) Install the Window Sill: If a new window sill is required, it is installed at this stage. It needs to be
|
||||
correctly aligned with the frame and securely attached.
|
||||
|
||||
7) Install the Double Glazed Glass Units: The glass units are carefully inserted into the frame. This step
|
||||
requires precision to ensure a snug fit without causing stress on the glass, which could lead to cracking or
|
||||
breaking.
|
||||
|
||||
8) Sealing and Weatherproofing: After the glass units are in place, it's crucial to seal around the frame and
|
||||
between the glass and frame to ensure there are no drafts and that the installation is weather-tight. This
|
||||
typically involves applying silicone sealant or other appropriate sealing materials.
|
||||
|
||||
9) Finishing Touches: This includes any cosmetic work, such as trimming, painting, or staining the frame and
|
||||
sill to match the rest of the property. It might also involve cleaning up any mess created during the
|
||||
installation.
|
||||
|
||||
10) Inspection and Testing: Finally, the new windows should be inspected to ensure they open, close, and lock
|
||||
correctly. This is also a good time to check for any gaps or issues with the sealing.
|
||||
|
||||
For this cost estimation process, we factor in initial assement into the preliminaries
|
||||
|
||||
"""
|
||||
|
||||
material_cost = material["material_cost"] * number_of_windows
|
||||
|
||||
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
|
||||
profit_cost = subtotal * self.PROFIT_MARGIN
|
||||
|
||||
subtotal_before_vat = subtotal + contingency_cost + preliminaries_cost + profit_cost
|
||||
|
||||
vat_cost = subtotal_before_vat * self.VAT_RATE
|
||||
|
||||
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
|
||||
|
||||
return {
|
||||
"total": total_cost,
|
||||
"subtotal": subtotal_before_vat,
|
||||
"vat": vat_cost,
|
||||
"contingency": contingency_cost,
|
||||
"preliminaries": preliminaries_cost,
|
||||
"material": material_cost,
|
||||
"profit": profit_cost,
|
||||
"labour_hours": labour_hours,
|
||||
"labour_cost": labour_cost,
|
||||
"labour_days": labour_days
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from recommendations.RoofRecommendations import RoofRecommendations
|
|||
from recommendations.VentilationRecommendations import VentilationRecommendations
|
||||
from recommendations.FireplaceRecommendations import FireplaceRecommendations
|
||||
from recommendations.LightingRecommendations import LightingRecommendations
|
||||
from recommendations.WindowsRecommendations import WindowsRecommendations
|
||||
from backend.ml_models.AnnualBillSavings import AnnualBillSavings
|
||||
|
||||
|
||||
|
|
@ -35,6 +36,7 @@ class Recommendations:
|
|||
)
|
||||
self.fireplace_recommender = FireplaceRecommendations(property_instance=property_instance)
|
||||
self.lighting_recommender = LightingRecommendations(property_instance=property_instance, materials=materials)
|
||||
self.windows_recommender = WindowsRecommendations(property_instance=property_instance, materials=materials)
|
||||
|
||||
def recommend(self):
|
||||
|
||||
|
|
@ -77,6 +79,11 @@ class Recommendations:
|
|||
if self.lighting_recommender.recommendation:
|
||||
property_recommendations.append(self.lighting_recommender.recommendation)
|
||||
|
||||
# Windows recommendations
|
||||
self.windows_recommender.recommend()
|
||||
if self.windows_recommender.recommendation:
|
||||
property_recommendations.append(self.windows_recommender.recommendation)
|
||||
|
||||
# We insert temporary ids into the recommendations which is important for the optimiser later
|
||||
property_recommendations = self.insert_temp_recommendation_id(property_recommendations)
|
||||
|
||||
|
|
|
|||
97
recommendations/WindowsRecommendations.py
Normal file
97
recommendations/WindowsRecommendations.py
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
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
|
||||
self.costs = Costs(self.property)
|
||||
|
||||
self.recommendation = []
|
||||
|
||||
self.glazing_material = [
|
||||
material for material in materials if material["type"] == "windows_glazing"
|
||||
]
|
||||
|
||||
if len(self.glazing_material) != 1:
|
||||
raise ValueError("There should only be one window glazing material")
|
||||
self.glazing_material = self.glazing_material[0]
|
||||
|
||||
def recommend(self):
|
||||
"""
|
||||
This method will recommend the best possible glazing options for a property.
|
||||
|
||||
In order to do this, we need to estimate the number of windows that the home has. This information will be
|
||||
stored in the property object, under property.number_of_windows
|
||||
: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")
|
||||
|
||||
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 - (int(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
|
||||
)
|
||||
|
||||
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 herigate building status"
|
||||
elif self.property.in_conservation_area:
|
||||
description += ". Secondary glazing recommended due to conservation area status"
|
||||
|
||||
self.recommendation = [
|
||||
{
|
||||
"parts": [],
|
||||
"type": "windows_glazing",
|
||||
"description": description,
|
||||
"starting_u_value": None,
|
||||
"new_u_value": None,
|
||||
"sap_points": None,
|
||||
**cost_result,
|
||||
"is_secondary_glazing": is_secondary_glazing
|
||||
}
|
||||
]
|
||||
|
|
@ -652,3 +652,64 @@ def esimtate_pitched_roof_area(floor_area: float, floor_height: float) -> float:
|
|||
area = 2 * (slope * wall_width)
|
||||
|
||||
return area
|
||||
|
||||
|
||||
def estimate_windows(
|
||||
property_type, built_form, construction_age_band, floor_area, number_habitable_rooms, extension_count
|
||||
):
|
||||
# Base window count based on habitable rooms
|
||||
window_count = number_habitable_rooms
|
||||
|
||||
# Additional windows for non-habitable rooms (e.g., kitchen, bathroom)
|
||||
# Assuming most houses will have at least one kitchen and one bathroom
|
||||
# Scale non-habitable windows with the number of habitable rooms
|
||||
non_habitable_base = 2 # Base for kitchen and bathroom
|
||||
extra_non_habitable = max(0, (number_habitable_rooms - 3) // 2) # Extra for large houses
|
||||
window_count += non_habitable_base + extra_non_habitable
|
||||
|
||||
# Adjustments based on built form and property type
|
||||
if property_type in ["House", "Bungalow"] and built_form in ["Semi-Detached", "Detached"]:
|
||||
built_form_lookup = {
|
||||
"Semi-Detached": 3,
|
||||
"Detached": 4,
|
||||
}
|
||||
else:
|
||||
# For Flats and Maisonettes, adjustments might be less
|
||||
built_form_lookup = {
|
||||
"Mid-Terrace": 0,
|
||||
"End-Terrace": 1,
|
||||
"Semi-Detached": 1,
|
||||
"Detached": 2,
|
||||
}
|
||||
window_count += built_form_lookup.get(built_form, 0)
|
||||
|
||||
# Adjust for floor area (larger floor area might indicate more rooms/windows)
|
||||
if floor_area < 85: # Small to medium properties
|
||||
# Standard window count likely sufficient
|
||||
pass
|
||||
elif 85 <= floor_area <= 120: # Medium to large properties
|
||||
# More rooms or larger rooms likely, potentially more windows
|
||||
window_count += 1
|
||||
elif floor_area > 120: # Very large properties
|
||||
# Likely to have significantly more or larger rooms
|
||||
window_count += 2
|
||||
|
||||
# Adjust for construction age band
|
||||
if construction_age_band in ["England and Wales: before 1900", "England and Wales: 1900-1929"]:
|
||||
# Older houses with smaller, more numerous windows
|
||||
window_count += 1
|
||||
|
||||
# Adjust for extensions (each extension might add windows)
|
||||
window_count += extension_count
|
||||
|
||||
# Adjustments for specific property types
|
||||
if property_type in ["Flat", "Maisontte"]:
|
||||
# Flats might have fewer windows due to shared walls
|
||||
# Maisonettes might follow a similar pattern to flats or small houses
|
||||
window_count -= 1
|
||||
|
||||
# Ensure window count is not negative
|
||||
if window_count < 0:
|
||||
raise ValueError("Window count cannot be negative.")
|
||||
|
||||
return window_count
|
||||
|
|
|
|||
|
|
@ -942,8 +942,24 @@ materials = [
|
|||
'https://www.hamuch.com/cost/led-spot-light#:~:text=It%20costs%20an%20average%20of,'
|
||||
'will%20drive%20up%20the%20cost.',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907), 'is_active': True, 'prime_material_cost': None,
|
||||
'material_cost': 20.0, 'labour_cost': 46.0, 'labour_hours_per_unit': 0.8, 'plant_cost': 0.0, 'total_cost': 66.0,
|
||||
'material_cost': 20.0, 'labour_cost': 15.0, 'labour_hours_per_unit': 0.8, 'plant_cost': 0.0, 'total_cost': 66.0,
|
||||
'notes': 'We estimate the unit economics from the checkatrade article. We assume that the average job consists '
|
||||
'of installing 6 lights based on the hamuch article. We use the median value of 400 for a job of 6 '
|
||||
'lights'}
|
||||
'lights'},
|
||||
{'id': 1235, 'type': 'windows_glazing',
|
||||
'description': 'uPVC windows; Profile 22 or other equal and approved; reinforced where appropriate with '
|
||||
'aluminium alloy; in refurbishment work, including standard ironmongery; sills and factory glazed '
|
||||
'with low-e 24 mm double glazing; removing existing windows and fixing new in position; including '
|
||||
'lugs plugged and screwed to brickwork or blockwork; Casement/fixed light; including vents; '
|
||||
'e.p.d.m. glazing gaskets and weather seals; 1770 mm × 1200 mm; ref P312WW',
|
||||
'depth': 0.0, 'depth_unit': None, 'cost': None, 'cost_unit': 'gbp_per_unit', 'r_value_per_mm': None,
|
||||
'r_value_unit': 'square_meter_kelvin_per_watt', 'thermal_conductivity': None, 'thermal_conductivity_unit': None,
|
||||
'link': 'SPONs',
|
||||
'created_at': datetime.datetime(2023, 11, 28, 22, 49, 12, 244907),
|
||||
'is_active': True, 'prime_material_cost': 176.55,
|
||||
'material_cost': 182.25, 'labour_cost': 163.36, 'labour_hours_per_unit': 6.5, 'plant_cost': 0.0,
|
||||
'total_cost': 345.61,
|
||||
'notes': 'This is the cost of removal of existing windows and installation of new windows. This is a casement '
|
||||
'style window, which is the most common but also the cheapest style. In the cost estimation framework, '
|
||||
'we can inflate prices for different finishes, to be conservative on price.'}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ class TestFloorRecommendations:
|
|||
input_properties[2].wall_type = "solid brick"
|
||||
input_properties[2].floor_type = "suspended"
|
||||
input_properties[2].number_of_floors = 1
|
||||
input_properties[2].floor_level = 0
|
||||
|
||||
recommender = FloorRecommendations(property_instance=input_properties[2], materials=materials)
|
||||
assert recommender.estimated_u_value is None
|
||||
|
|
@ -93,6 +94,8 @@ class TestFloorRecommendations:
|
|||
input_properties[3].insulation_floor_area = 100
|
||||
input_properties[3].insulation_wall_area = 100
|
||||
input_properties[3].number_of_floors = 1
|
||||
input_properties[3].floor_level = 0
|
||||
|
||||
recommender = FloorRecommendations(property_instance=input_properties[3], materials=materials)
|
||||
assert recommender.estimated_u_value is None
|
||||
recommender.recommend()
|
||||
|
|
@ -114,6 +117,7 @@ class TestFloorRecommendations:
|
|||
input_properties[4].wall_type = "solid brick"
|
||||
input_properties[4].floor_type = "solid"
|
||||
input_properties[4].number_of_floors = 1
|
||||
input_properties[4].floor_level = 0
|
||||
|
||||
# In this case, we have no county, so in this case, it should yse the local-authority-label if possible
|
||||
input_properties[4].data["county"] = ""
|
||||
|
|
|
|||
|
|
@ -40,8 +40,7 @@ class TestLightingRecommendations:
|
|||
|
||||
assert lr.recommendation == [
|
||||
{'parts': [], 'type': 'low_energy_lighting', 'description': 'Install low energy lighting in 4 outlets',
|
||||
'starting_u_value': None, 'new_u_value': None, 'sap_points': 0.4, 'total': 458.976, 'subtotal': 382.48,
|
||||
'vat': 76.49600000000001, 'contingency': 27.320000000000007, 'preliminaries': 27.320000000000007,
|
||||
'material': 80.0, 'profit': 54.640000000000015, 'labour_hours': 3.2, 'labour_days': 0.4,
|
||||
'labour_cost': 193.20000000000002}
|
||||
'starting_u_value': None, 'new_u_value': None, 'sap_points': 0.4, 'total': 240.24,
|
||||
'subtotal': 200.20000000000002, 'vat': 40.040000000000006, 'contingency': 14.3, 'preliminaries': 14.3,
|
||||
'material': 80.0, 'profit': 28.6, 'labour_hours': 3.2, 'labour_days': 0.4, 'labour_cost': 63.0}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -427,3 +427,106 @@ def test_external_wall_area():
|
|||
for num_floors, floor_height, perimeter, built_form, expected in test_cases:
|
||||
result = recommendation_utils.estimate_external_wall_area(num_floors, floor_height, perimeter, built_form)
|
||||
assert result == expected, f"Test failed for {built_form}: Expected {expected}, got {result}"
|
||||
|
||||
|
||||
def test_estimate_windows():
|
||||
# Based on data from an EPR that has 4 windows
|
||||
windows_case_1 = recommendation_utils.estimate_windows(
|
||||
property_type="Flat",
|
||||
built_form="Semi-Detached",
|
||||
construction_age_band="England and Wales: 1976-1982",
|
||||
floor_area=37,
|
||||
number_habitable_rooms=2,
|
||||
extension_count=0,
|
||||
)
|
||||
|
||||
assert windows_case_1 == 4, f"Expected 4 windows, got {windows_case_1}"
|
||||
|
||||
# Based on data from an EPR that has 7 winows, however two of the windows were very small, having areas of
|
||||
# 0.21m^2 and 0.3m^2 respectively. We see 6 as a reasonable estimate for the number of windows
|
||||
windows_case_2 = recommendation_utils.estimate_windows(
|
||||
property_type="House",
|
||||
built_form="Mid-Terrace",
|
||||
construction_age_band="England and Wales: 1950-1966",
|
||||
floor_area=69,
|
||||
number_habitable_rooms=4,
|
||||
extension_count=0,
|
||||
)
|
||||
|
||||
assert windows_case_2 == 6, f"Expected 6 windows, got {windows_case_2}"
|
||||
|
||||
# Based on data from an EPR on a bungalow, that has 6 windows. Two of the windows are small, both have a 0.4m^2 area
|
||||
# and so 5 windows is an acceptable estimate
|
||||
windows_case_3 = recommendation_utils.estimate_windows(
|
||||
property_type="Bungalow",
|
||||
built_form="Mid-Terrace",
|
||||
construction_age_band="England and Wales: 1967-1975",
|
||||
floor_area=56,
|
||||
number_habitable_rooms=3,
|
||||
extension_count=0,
|
||||
)
|
||||
|
||||
assert windows_case_3 == 5, f"Expected 5 windows, got {windows_case_3}"
|
||||
|
||||
# Based on data from an EPR on a end terrace house that has 8 windows. One of the windows is very small, with an
|
||||
# area of 0.25 m^2 and so 7 windows is an acceptable estimate
|
||||
windows_case_4 = recommendation_utils.estimate_windows(
|
||||
property_type="House",
|
||||
built_form="End-Terrace",
|
||||
construction_age_band="England and Wales: 1967-1975",
|
||||
floor_area=77.28,
|
||||
number_habitable_rooms=4,
|
||||
extension_count=0,
|
||||
)
|
||||
|
||||
assert windows_case_4 == 7, f"Expected 7 windows, got {windows_case_4}"
|
||||
|
||||
# Based on data from an EPR on a Semi-detatched house that has 11 windows based on the associated condition report
|
||||
# Right now, we estimate 12 windows for this property
|
||||
windows_case_5 = recommendation_utils.estimate_windows(
|
||||
property_type="House",
|
||||
built_form="Semi-Detached",
|
||||
construction_age_band="England and Wales: 1950-1966",
|
||||
floor_area=88.4,
|
||||
number_habitable_rooms=5,
|
||||
extension_count=0,
|
||||
)
|
||||
|
||||
assert windows_case_5 == 12, f"Expected 12 windows, got {windows_case_5}"
|
||||
|
||||
# Based on Khalim's flat which has 3 windows. There is no construction age band on the EPC. The windows are large
|
||||
# so an estimate of 5 windows is a reasonable estimate
|
||||
windows_case_6 = recommendation_utils.estimate_windows(
|
||||
property_type="Flat",
|
||||
built_form="",
|
||||
construction_age_band="",
|
||||
floor_area=100,
|
||||
number_habitable_rooms=3,
|
||||
extension_count=0,
|
||||
)
|
||||
|
||||
assert windows_case_6 == 5, f"Expected 5 windows, got {windows_case_6}"
|
||||
|
||||
# Based on an EPR semi detatched house though we don't have the exact number of windows. We estimate 10
|
||||
windows_case_7 = recommendation_utils.estimate_windows(
|
||||
property_type="House",
|
||||
built_form="Semi-Detached",
|
||||
construction_age_band="England and Wales: 1967-1975",
|
||||
floor_area=85,
|
||||
number_habitable_rooms=4,
|
||||
extension_count=0,
|
||||
)
|
||||
|
||||
assert windows_case_7 == 10, f"Expected 10 windows, got {windows_case_7}"
|
||||
|
||||
# Base on Khalim's parents flat
|
||||
windows_case_8 = recommendation_utils.estimate_windows(
|
||||
property_type="Flat",
|
||||
built_form="End-Terrace",
|
||||
construction_age_band="",
|
||||
floor_area=50,
|
||||
number_habitable_rooms=3,
|
||||
extension_count=0,
|
||||
)
|
||||
|
||||
assert windows_case_8 == 5, f"Expected 5 windows, got {windows_case_8}"
|
||||
|
|
|
|||
253
recommendations/tests/test_window_recommendations.py
Normal file
253
recommendations/tests/test_window_recommendations.py
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
from recommendations.WindowsRecommendations import WindowsRecommendations
|
||||
from backend.Property import Property
|
||||
from unittest.mock import Mock
|
||||
from recommendations.tests.test_data.materials import materials
|
||||
|
||||
|
||||
class TestWindowRecommendations:
|
||||
|
||||
def test_fully_single_glazed(self):
|
||||
"""
|
||||
For this property, we expect all windows to be single glazed and should recommend full double glazing
|
||||
:return:
|
||||
"""
|
||||
|
||||
property_1 = Property(
|
||||
id=1,
|
||||
postcode='1',
|
||||
address1='1',
|
||||
epc_client=Mock(),
|
||||
data={
|
||||
"county": "Wychavon",
|
||||
"multi-glaze-proportion": 0
|
||||
}
|
||||
)
|
||||
property_1.windows = {
|
||||
'original_description': 'Single glazed', 'has_glazing': False, 'glazing_coverage': 'full',
|
||||
'glazing_type': 'single',
|
||||
'no_data': False
|
||||
}
|
||||
property_1.number_of_windows = 7
|
||||
|
||||
recommender = WindowsRecommendations(property_instance=property_1, materials=materials)
|
||||
|
||||
assert not recommender.recommendation
|
||||
|
||||
recommender.recommend()
|
||||
|
||||
assert recommender.recommendation == [
|
||||
{'parts': [], 'type': 'windows_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': 'windows_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):
|
||||
property_5 = Property(
|
||||
id=1,
|
||||
postcode='1',
|
||||
address1='1',
|
||||
epc_client=Mock(),
|
||||
data={
|
||||
"county": "Wychavon",
|
||||
"multi-glaze-proportion": 50
|
||||
}
|
||||
)
|
||||
property_5.windows = {'original_description': 'Partial secondary glazing', 'has_glazing': True,
|
||||
'glazing_coverage': 'partial',
|
||||
'glazing_type': 'secondary', 'no_data': False}
|
||||
property_5.number_of_windows = 7
|
||||
|
||||
recommender5 = WindowsRecommendations(property_instance=property_5, materials=materials)
|
||||
|
||||
assert not recommender5.recommendation
|
||||
|
||||
recommender5.recommend()
|
||||
|
||||
assert recommender5.recommendation == [
|
||||
{'parts': [], 'type': 'windows_glazing',
|
||||
'description': 'Install secondary glazing to the remaining windows',
|
||||
'starting_u_value': None, 'new_u_value': None, 'sap_points': None, 'total': 1089.893952,
|
||||
'subtotal': 908.24496, 'vat': 181.64899200000002, 'contingency': 64.87464, 'preliminaries': 64.87464,
|
||||
'material': 729.0, 'profit': 129.74928, 'labour_hours': 13.0, 'labour_cost': 568.4928,
|
||||
'labour_days': 0.8125}]
|
||||
|
||||
def test_single_glazed_restricted_measures(self):
|
||||
property_6 = Property(
|
||||
id=1,
|
||||
postcode='1',
|
||||
address1='1',
|
||||
epc_client=Mock(),
|
||||
data={
|
||||
"county": "Wychavon",
|
||||
"multi-glaze-proportion": 0
|
||||
}
|
||||
)
|
||||
property_6.windows = {'original_description': 'Single glazed', 'has_glazing': False, 'glazing_coverage': None,
|
||||
'glazing_type': 'single',
|
||||
'no_data': False}
|
||||
property_6.number_of_windows = 7
|
||||
property_6.restricted_measures = True
|
||||
property_6.is_heritage = True
|
||||
|
||||
recommender6 = WindowsRecommendations(property_instance=property_6, materials=materials)
|
||||
|
||||
assert not recommender6.recommendation
|
||||
|
||||
recommender6.recommend()
|
||||
|
||||
assert recommender6.recommendation == [
|
||||
{'parts': [], 'type': 'windows_glazing',
|
||||
'description': 'Install secondary glazing to all windows. Secondary '
|
||||
'glazing recommended due to herigate building status',
|
||||
'starting_u_value': None, 'new_u_value': None, 'sap_points': None,
|
||||
'total': 1907.314416, 'subtotal': 1589.42868, 'vat': 317.885736,
|
||||
'contingency': 113.53062, 'preliminaries': 113.53062,
|
||||
'material': 1275.75, 'profit': 227.06124, 'labour_hours': 22.75,
|
||||
'labour_cost': 994.8624, 'labour_days': 1.421875}
|
||||
]
|
||||
|
||||
def test_full_triple_glazed(self):
|
||||
property_7 = Property(
|
||||
id=1,
|
||||
postcode='1',
|
||||
address1='1',
|
||||
epc_client=Mock(),
|
||||
data={
|
||||
"county": "Wychavon",
|
||||
"multi-glaze-proportion": 100
|
||||
}
|
||||
)
|
||||
property_7.windows = {'original_description': 'Fully triple glazed', 'has_glazing': True,
|
||||
'glazing_coverage': 'full',
|
||||
'glazing_type': 'triple', 'no_data': False}
|
||||
property_7.number_of_windows = 7
|
||||
|
||||
recommender7 = WindowsRecommendations(property_instance=property_7, materials=materials)
|
||||
|
||||
assert not recommender7.recommendation
|
||||
|
||||
recommender7.recommend()
|
||||
|
||||
assert not recommender7.recommendation
|
||||
|
||||
def test_partial_triple_glazed(self):
|
||||
"""
|
||||
We should just recommend double glazing to the remaining windows, since it's a cheaper option
|
||||
"""
|
||||
|
||||
property_8 = Property(
|
||||
id=1,
|
||||
postcode='1',
|
||||
address1='1',
|
||||
epc_client=Mock(),
|
||||
data={
|
||||
"county": "Wychavon",
|
||||
"multi-glaze-proportion": 80
|
||||
}
|
||||
)
|
||||
property_8.windows = {'original_description': 'Mostly triple glazing', 'has_glazing': True,
|
||||
'glazing_coverage': 'most',
|
||||
'glazing_type': 'triple', 'no_data': False}
|
||||
property_8.number_of_windows = 7
|
||||
|
||||
recommender8 = WindowsRecommendations(property_instance=property_8, materials=materials)
|
||||
|
||||
assert not recommender8.recommendation
|
||||
|
||||
recommender8.recommend()
|
||||
|
||||
assert recommender8.recommendation == [
|
||||
{'parts': [], 'type': 'windows_glazing', 'description': 'Install double glazing to the remaining windows',
|
||||
'starting_u_value': None, 'new_u_value': None, 'sap_points': None, 'total': 1634.840928,
|
||||
'subtotal': 1362.36744, 'vat': 272.47348800000003, 'contingency': 97.31196, 'preliminaries': 97.31196,
|
||||
'material': 364.5, 'profit': 194.62392, 'labour_hours': 13.0, 'labour_cost': 284.2464,
|
||||
'labour_days': 0.8125}]
|
||||
Loading…
Add table
Reference in a new issue