diff --git a/.idea/Model.iml b/.idea/Model.iml index b0f9c00d..4413bb06 100644 --- a/.idea/Model.iml +++ b/.idea/Model.iml @@ -7,7 +7,7 @@ - + diff --git a/.idea/misc.xml b/.idea/misc.xml index 1122b380..6f308057 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,7 +3,7 @@ - + diff --git a/etl/costs/app.py b/etl/costs/app.py index 4d53ce28..30eff735 100644 --- a/etl/costs/app.py +++ b/etl/costs/app.py @@ -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( diff --git a/etl/epc_clean/epc_attributes/WindowAttributes.py b/etl/epc_clean/epc_attributes/WindowAttributes.py index e962cd31..cd767359 100644 --- a/etl/epc_clean/epc_attributes/WindowAttributes.py +++ b/etl/epc_clean/epc_attributes/WindowAttributes.py @@ -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, @@ -83,4 +83,8 @@ class WindowAttributes(Definitions): if not result["glazing_coverage"]: result["glazing_coverage"] = "full" + # We reset some values if the glazing is single + if result["glazing_type"] == "single": + result["has_glazing"] = False + return result diff --git a/etl/epc_clean/tests/test_data/test_window_attributes_cases.py b/etl/epc_clean/tests/test_data/test_window_attributes_cases.py index 1eeeee21..f01ccba9 100644 --- a/etl/epc_clean/tests/test_data/test_window_attributes_cases.py +++ b/etl/epc_clean/tests/test_data/test_window_attributes_cases.py @@ -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}, diff --git a/recommendations/Costs.py b/recommendations/Costs.py index 0d9031b2..84a7468c 100644 --- a/recommendations/Costs.py +++ b/recommendations/Costs.py @@ -64,6 +64,12 @@ 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 + def __init__(self, property_instance): """ Initializes the Costs class with a property instance. @@ -719,3 +725,65 @@ class Costs: "labour_days": labour_days, "labour_cost": labour_costs } + + def window_glazing(self, number_of_windows, material): + """ + 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 + + :param number_of_windows: + :return: + """ + + number_of_windows = 7 + windows_cost = 345 + + material_cost = windows_cost * number_of_windows * self.SASH_WINDOW_INFLATION_FACTOR + + subtotal = material_cost + + contingency_cost = subtotal * 0.2 + preliminaries_cost = subtotal * 0.2 + profit_cost = subtotal * 0.2 + + subtotal_before_vat = subtotal + contingency_cost + preliminaries_cost + profit_cost + + vat_cost = subtotal_before_vat * 0.2 + + total_cost = subtotal_before_vat + vat_cost diff --git a/recommendations/WindowsRecommendations.py b/recommendations/WindowsRecommendations.py index e5a11f8a..d3424d1b 100644 --- a/recommendations/WindowsRecommendations.py +++ b/recommendations/WindowsRecommendations.py @@ -9,7 +9,7 @@ class WindowsRecommendations: self.property = property_instance self.costs = Costs(self.property) - self.recommendations = [] + self.recommendation = [] self.glazing_materials = [ material for material in materials if material["type"] == "window_glazing" @@ -29,5 +29,28 @@ class WindowsRecommendations: :return: """ - if not self.property.number_of_windows: + number_of_windows = self.property.number_of_windows + + 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 then price the job based on the number of windows that there are + + cost_result = {} + + description = None + + self.recommendation = [ + { + "parts": [], + "type": "window_glazing", + "description": description, + "starting_u_value": None, + "new_u_value": None, + "sap_points": None, + **cost_result + } + ] diff --git a/recommendations/tests/test_window_recommendations.py b/recommendations/tests/test_window_recommendations.py new file mode 100644 index 00000000..da50d25c --- /dev/null +++ b/recommendations/tests/test_window_recommendations.py @@ -0,0 +1,30 @@ +from recommendations.WindowsRecommendations import WindowsRecommendations +from backend.Property import Property +from unittest.mock import Mock + + +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" + } + ) + property_1.windows = { + 'original_description': 'Single glazed', 'has_glazing': False, 'glazing_coverage': 'full', + 'glazing_type': 'single', + 'no_data': False + } + property_1.number_of_windows = 5 + + recommender = WindowsRecommendations(property_instance=property_1, materials=[])