mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
added pps matrix to test data
This commit is contained in:
parent
30e1eca74b
commit
98d64b9430
6 changed files with 17089 additions and 82 deletions
|
|
@ -1,6 +1,6 @@
|
|||
from enum import IntEnum, Enum
|
||||
|
||||
CRM_PIPELINE_NAME = 'Operations - Housing Associations'
|
||||
CRM_PIPELINE_NAME = 'Operations - Social Housing'
|
||||
|
||||
|
||||
class HubspotProcessStatus(IntEnum):
|
||||
|
|
|
|||
|
|
@ -44,26 +44,27 @@ def app():
|
|||
"""
|
||||
|
||||
# inputs:
|
||||
reconcile_programme = True # If True, the hubspot upload will include all properties with a project code
|
||||
customer_domain = "https://southend.gov.uk"
|
||||
installer_name = "J & J CRUMP"
|
||||
reconcile_programme = False # If True, the hubspot upload will include all properties with a project code
|
||||
customer_domain = "https://shgroup.org.uk"
|
||||
installer_name = "SCIS"
|
||||
asset_list_filepath = (
|
||||
"/Users/khalimconn-kowlessar/Documents/hestia/Customers/Southend/July 2025 Programme/SOUTHEND - RYAN - "
|
||||
"Standardised 2.xlsx"
|
||||
"/Users/khalimconn-kowlessar/Downloads/20250701 Optivo Southern - Standardised.xlsx"
|
||||
)
|
||||
asset_list_sheet_name = "Standardised Asset List"
|
||||
asset_list_sheet_name = "Solar Route Revised (100)"
|
||||
asset_list_header = 0
|
||||
|
||||
contact_details_filepath = None
|
||||
contacts_sheet_name = "Sheet 1"
|
||||
contacts_landlord_property_id = "UPRN"
|
||||
contacts_phone_number_column = "phone_number"
|
||||
contacts_secondary_phone_number_column = "secondary_phone_number"
|
||||
contacts_secondary_contact_full_name = "secondary_contact_full_name"
|
||||
contacts_email_column = "email"
|
||||
contacts_fullname_column = "fullname"
|
||||
contacts_firstname_column = "First Name"
|
||||
contacts_lastname_column = "Last Name"
|
||||
contact_details_filepath = (
|
||||
"/Users/khalimconn-kowlessar/Downloads/southern_optivo_solar_pv.xlsx"
|
||||
)
|
||||
contacts_sheet_name = "Sheet1"
|
||||
contacts_landlord_property_id = "landlord_property_id"
|
||||
contacts_phone_number_column = "Primary phone number"
|
||||
contacts_secondary_phone_number_column = "Secondary phone number"
|
||||
contacts_secondary_contact_full_name = None
|
||||
contacts_email_column = "Email Address"
|
||||
contacts_fullname_column = None
|
||||
contacts_firstname_column = "Name"
|
||||
contacts_lastname_column = None
|
||||
|
||||
existing_programme_filepath = None
|
||||
|
||||
|
|
@ -89,6 +90,18 @@ def app():
|
|||
reconcile_programme=reconcile_programme
|
||||
)
|
||||
|
||||
for x in asset_list.hubspot_data["Phone <CONTACT phone>"].values:
|
||||
normalize_uk_phone(x)
|
||||
|
||||
asset_list.hubspot_data["Phone <CONTACT phone>"] = (
|
||||
asset_list.hubspot_data["Phone <CONTACT phone>"].astype("Int64").astype(str).apply(normalize_uk_phone)
|
||||
)
|
||||
asset_list.hubspot_data["Secondary Phone <CONTACT secondary_phone_number>"] = asset_list.hubspot_data[
|
||||
"Secondary Phone <CONTACT secondary_phone_number>"].astype(
|
||||
"Int64").astype(
|
||||
str).apply(
|
||||
normalize_uk_phone)
|
||||
|
||||
# Remove the existing programme
|
||||
# existing_programme = pd.read_csv(existing_programme_filepath, encoding="utf-8-sig")
|
||||
# asset_list.hubspot_data = asset_list.hubspot_data[
|
||||
|
|
|
|||
|
|
@ -3,13 +3,14 @@ import pandas as pd
|
|||
import numpy as np
|
||||
from typing import List
|
||||
|
||||
from backend.app.plan.schemas import HousingType
|
||||
from backend.app.plan.schemas import HousingType, WALL_INSULATION_MEASURES, ROOF_INSULATION_MEASURES
|
||||
|
||||
|
||||
class EligibilityCaveats(Enum):
|
||||
TENANT_ON_BENEFITS_OR_LOW_INCOME = "tenant_on_benefits_or_low_income"
|
||||
INNOVATION_REQUIRED = "innovation_required"
|
||||
SOLAR_NEEDS_HEATING = "solar_needs_heating"
|
||||
NEEDS_INSULATION_TO_MINIMUM_STANDARDS = "needs_insulation_to_minimum_standards"
|
||||
|
||||
|
||||
class Funding:
|
||||
|
|
@ -46,6 +47,12 @@ class Funding:
|
|||
self.gbis_eligible = False
|
||||
self.gbis_eligibility_caveats = []
|
||||
|
||||
# Funding calculation variables
|
||||
self.full_project_abs = None
|
||||
self.eco4_funding = None
|
||||
|
||||
self.partial_project_abs = None
|
||||
|
||||
# -----------------------
|
||||
# Utility Helpers
|
||||
# -----------------------
|
||||
|
|
@ -111,8 +118,10 @@ class Funding:
|
|||
# Private Rented Sector
|
||||
# -----------------------
|
||||
|
||||
def eco4_prs_eligibility(self, starting_sap: int, ending_sap: int, measure_types: List, mainheat_description: str,
|
||||
heating_control_description: str):
|
||||
def eco4_prs_eligibility(
|
||||
self, starting_sap: int, ending_sap: int, measure_types: List, has_solar: bool, solar_eligible: bool
|
||||
):
|
||||
|
||||
"""
|
||||
ECO4 PRS eligibility:
|
||||
- EPC E–G
|
||||
|
|
@ -122,27 +131,30 @@ class Funding:
|
|||
meets_epc = starting_sap <= 54 # EPC E–G
|
||||
meets_upgrade_target = self._meets_upgrade_target(starting_sap, ending_sap)
|
||||
|
||||
if not meets_epc or not meets_upgrade_target:
|
||||
self.eco4_eligible = False
|
||||
self.eco4_eligibility_caveats = []
|
||||
return
|
||||
|
||||
if has_solar and not solar_eligible:
|
||||
self.eco4_eligible = False
|
||||
self.eco4_eligibility_caveats.append(EligibilityCaveats.SOLAR_NEEDS_HEATING)
|
||||
return
|
||||
|
||||
has_swi = "internal_wall_insulation" in measure_types or "external_wall_insulation" in measure_types
|
||||
has_renewable = "air_source_heat_pump" in measure_types or "ground_source_heat_pump" in measure_types
|
||||
has_ftch = "first_time_central_heating" in measure_types
|
||||
has_dhc = "district_heating_connection" in measure_types
|
||||
|
||||
has_eligible_electric_heating = (
|
||||
any(x in mainheat_description for x in [
|
||||
"air source heat pump", "ground source heat pump", "boiler and radiators, electric"
|
||||
]) or (
|
||||
("electric storage heaters" in mainheat_description)
|
||||
and (heating_control_description.lower() == "controls for high heat retention storage heaters")
|
||||
)
|
||||
)
|
||||
|
||||
solar_counts_as_renewable = has_eligible_electric_heating and "solar_pv" in measure_types
|
||||
|
||||
if meets_upgrade_target and meets_epc and (
|
||||
has_swi or has_renewable or has_ftch or has_dhc or solar_counts_as_renewable
|
||||
has_swi or has_renewable or has_ftch or has_dhc or solar_eligible
|
||||
):
|
||||
self.eco4_eligible = True
|
||||
self.eco4_eligibility_caveats.append(EligibilityCaveats.TENANT_ON_BENEFITS_OR_LOW_INCOME)
|
||||
return
|
||||
|
||||
self.eco4_eligible = False
|
||||
self.eco4_eligibility_caveats = []
|
||||
|
||||
def gbis_prs_eligibility(self, starting_sap: int, council_tax_band: str, measure_types: List):
|
||||
"""
|
||||
|
|
@ -152,19 +164,26 @@ class Funding:
|
|||
"""
|
||||
gbis_measures = {
|
||||
"general": [
|
||||
# Cannot do CWI
|
||||
"internal_wall_insulation", "external_wall_insulation",
|
||||
"flat_roof_insulation", "suspended_floor_insulation",
|
||||
"room_roof_insulation", "solid_floor_insulation", "park_home_insulation"
|
||||
"room_roof_insulation", "solid_floor_insulation"
|
||||
],
|
||||
"low_income": [
|
||||
"internal_wall_insulation", "external_wall_insulation",
|
||||
"flat_roof_insulation", "suspended_floor_insulation",
|
||||
"room_roof_insulation", "solid_floor_insulation",
|
||||
"cavity_wall_insulation", "loft_insulation", "park_home_insulation"
|
||||
"cavity_wall_insulation", "loft_insulation"
|
||||
]
|
||||
}
|
||||
|
||||
meets_epc = starting_sap <= 69 # EPC D–G
|
||||
is_single_measure = len(measure_types) == 1
|
||||
|
||||
if not is_single_measure or not meets_epc:
|
||||
self.gbis_eligible = False
|
||||
self.gbis_eligibility_caveats = []
|
||||
return
|
||||
|
||||
# General route
|
||||
if meets_epc and council_tax_band in ["A", "B", "C", "D", "E"]:
|
||||
|
|
@ -180,14 +199,24 @@ class Funding:
|
|||
# Social Housing
|
||||
# -----------------------
|
||||
|
||||
def eco4_sh_eligibility(self, starting_sap: int, ending_sap: int, measure_types: List, has_innovation: bool,
|
||||
innovation_measures: List):
|
||||
def eco4_sh_eligibility(
|
||||
self,
|
||||
starting_sap: int,
|
||||
ending_sap: int,
|
||||
has_innovation: bool,
|
||||
has_solar: bool,
|
||||
solar_eligible: bool
|
||||
):
|
||||
"""
|
||||
ECO4 Social Housing eligibility.
|
||||
- EPC E–G: eligible, no income check.
|
||||
- EPC D: innovation measure required.
|
||||
If solar PV is the innovation measure, must also install ASHP or HHRSH.
|
||||
"""
|
||||
if has_solar and not solar_eligible:
|
||||
# The package contins solar PV but it doesn't meet the eligibility requirements
|
||||
return
|
||||
|
||||
meets_epc = starting_sap <= 69
|
||||
meets_upgrade_target = self._meets_upgrade_target(starting_sap, ending_sap)
|
||||
|
||||
|
|
@ -196,43 +225,53 @@ class Funding:
|
|||
|
||||
# EPC D innovation rule
|
||||
if 55 <= starting_sap <= 68: # EPC D
|
||||
|
||||
# If we don't meet the innovation requirements, we're not eligible
|
||||
if not has_innovation:
|
||||
self.eco4_eligible = False
|
||||
self.eco4_eligibility_caveats.append(EligibilityCaveats.INNOVATION_REQUIRED)
|
||||
return
|
||||
|
||||
if "solar_pv" in innovation_measures and not any(
|
||||
m in measure_types for m in ["air_source_heat_pump", "high_heat_retention_storage_heater"]
|
||||
):
|
||||
self.eco4_eligible = False
|
||||
self.eco4_eligibility_caveats.append(EligibilityCaveats.SOLAR_NEEDS_HEATING)
|
||||
return
|
||||
self.eco4_eligible = True
|
||||
self.eco4_eligibility_caveats = []
|
||||
|
||||
self.eco4_eligible = True
|
||||
self.eco4_eligibility_caveats = []
|
||||
|
||||
def gbis_sh_eligibility(self, starting_sap: int, measure_types: List, has_innovation: bool,
|
||||
innovation_measures: List):
|
||||
def gbis_sh_eligibility(self, starting_sap: int, measure_types: List, has_innovation: bool):
|
||||
"""
|
||||
GBIS Social Housing eligibility.
|
||||
- EPC E–G: insulation measures OK.
|
||||
- EPC D: innovation measure required (same solar PV + heating rule).
|
||||
- EPC E–G: single insulation measure
|
||||
- EPC D: single insulation, innovation measure
|
||||
"""
|
||||
meets_epc = starting_sap <= 69
|
||||
if not meets_epc:
|
||||
|
||||
meets_epc = starting_sap <= 69 # EPC D–G
|
||||
is_single_measure = len(measure_types) == 1
|
||||
# Check if has a valid measure
|
||||
insulation_measures = [
|
||||
'internal_wall_insulation', 'external_wall_insulation', 'cavity_wall_insulation',
|
||||
'loft_insulation', 'flat_roof_insulation', 'room_roof_insulation',
|
||||
'suspended_floor_insulation', 'solid_floor_insulation',
|
||||
]
|
||||
has_valid_measures = any(m in measure_types for m in insulation_measures)
|
||||
|
||||
if not is_single_measure or not meets_epc or not has_valid_measures:
|
||||
self.gbis_eligible = False
|
||||
self.gbis_eligibility_caveats = []
|
||||
return
|
||||
|
||||
if 55 <= starting_sap <= 68: # EPC D
|
||||
# Since it's single measure if has_innovation is true, the single insulation measure
|
||||
# must be the innovation measure
|
||||
if not has_innovation:
|
||||
self.gbis_eligible = False
|
||||
self.gbis_eligibility_caveats.append(EligibilityCaveats.INNOVATION_REQUIRED)
|
||||
return
|
||||
|
||||
if "solar_pv" in innovation_measures and not any(
|
||||
m in measure_types for m in ["air_source_heat_pump", "high_heat_retention_storage_heater"]
|
||||
):
|
||||
self.gbis_eligible = False
|
||||
self.gbis_eligibility_caveats.append(EligibilityCaveats.SOLAR_NEEDS_HEATING)
|
||||
return
|
||||
# If we don't have an innovation measure, we're not eligible
|
||||
self.gbis_eligible = False
|
||||
self.gbis_eligibility_caveats.append(EligibilityCaveats.SOLAR_NEEDS_HEATING)
|
||||
return
|
||||
|
||||
self.gbis_eligible = True
|
||||
|
||||
|
|
@ -253,27 +292,212 @@ class Funding:
|
|||
|
||||
return data["Cost Savings"].values[0]
|
||||
|
||||
@staticmethod
|
||||
def get_starting_ending_uvalues(current_uvalue: float) -> tuple[str, str]:
|
||||
"""
|
||||
Returns the closest starting U-value and appropriate ending U-value for solid wall insulation.
|
||||
|
||||
- If current_uvalue is closest to 0.45, assume the improvement target is 0.21.
|
||||
- Otherwise, assume the target is 0.30.
|
||||
"""
|
||||
possible_starting_u_values = [2.00, 1.70, 1.00, 0.60, 0.45]
|
||||
closest_starting = min(possible_starting_u_values, key=lambda x: abs(x - current_uvalue))
|
||||
ending_uvalue = 0.21 if closest_starting == 0.45 else 0.30
|
||||
|
||||
return f"{closest_starting:.2f}", f"{ending_uvalue:.2f}"
|
||||
|
||||
def calculate_partial_project_abs(
|
||||
self,
|
||||
measure_type: str,
|
||||
current_wall_uvalue: float = None,
|
||||
is_partial: bool = False,
|
||||
existing_li_thickness: float = None,
|
||||
# is_roof_insulated: bool = False
|
||||
):
|
||||
"""
|
||||
Calculate the partial project ABS score for a single measure.
|
||||
"""
|
||||
df = self.partial_project_scores_matrix[
|
||||
(self.partial_project_scores_matrix["Total Floor Area Band"] == self.floor_area_band) &
|
||||
(self.partial_project_scores_matrix["Starting Band"] == self.starting_sap_band)
|
||||
]
|
||||
|
||||
if measure_type == "internal_wall_insulation":
|
||||
if current_wall_uvalue is None:
|
||||
raise ValueError("current_wall_uvalue is required for IWI")
|
||||
|
||||
starting_str, ending_str = self.get_starting_ending_uvalues(current_wall_uvalue)
|
||||
measure_code = f"IWI_solid_{starting_str}_{ending_str}"
|
||||
pps = df[df["Measure_Type"] == measure_code]
|
||||
|
||||
if pps.shape[0] != 1:
|
||||
raise ValueError(f"Invalid IWI category: {measure_code}")
|
||||
return pps.squeeze()["Cost Savings"]
|
||||
|
||||
if measure_type == "external_wall_insulation":
|
||||
if current_wall_uvalue is None:
|
||||
raise ValueError("current_wall_uvalue is required for EWI")
|
||||
|
||||
starting_str, ending_str = self.get_starting_ending_uvalues(current_wall_uvalue)
|
||||
measure_code = f"EWI_solid_{starting_str}_{ending_str}"
|
||||
pps = df[df["Measure_Type"] == measure_code]
|
||||
|
||||
if pps.shape[0] != 1:
|
||||
raise ValueError(f"Invalid EWI category: {measure_code}")
|
||||
return pps.squeeze()["Cost Savings"]
|
||||
|
||||
if measure_type == "cavity_wall_insulation":
|
||||
measure_code = "CWI_partial_fill" if is_partial else "CWI_0.033"
|
||||
pps = df[df["Measure_Type"] == measure_code]
|
||||
|
||||
if pps.shape[0] != 1:
|
||||
raise ValueError(f"Invalid CWI category: {measure_code}")
|
||||
return pps.squeeze()["Cost Savings"]
|
||||
|
||||
if measure_type == "loft_insulation":
|
||||
if existing_li_thickness is None:
|
||||
raise ValueError("existing_li_thickness is required for LI")
|
||||
|
||||
measure_code = "LI_lessequal100" if existing_li_thickness <= 100 else "LI_greater100"
|
||||
pps = df[df["Measure_Type"] == measure_code]
|
||||
|
||||
if pps.shape[0] != 1:
|
||||
raise ValueError(f"Invalid LI category: {measure_code}")
|
||||
return pps.squeeze()["Cost Savings"]
|
||||
|
||||
if measure_type == "flat_roof_insulation":
|
||||
pps = df[df["Measure_Type"] == "FRI"]
|
||||
if pps.shape[0] != 1:
|
||||
raise ValueError("Invalid FRI category")
|
||||
return pps.squeeze()["Cost Savings"]
|
||||
|
||||
if measure_type == "room_roof_insulation":
|
||||
# Use the more conservative score (unin is usually lower)
|
||||
# code = "RIRI_res_unin" if not is_roof_insulated else "RIRI_res_in"
|
||||
code = "RIRI_res_unin"
|
||||
pps = df[df["Measure_Type"] == code]
|
||||
if pps.shape[0] != 1:
|
||||
raise ValueError(f"Invalid RIRI category: {code}")
|
||||
return pps.squeeze()["Cost Savings"]
|
||||
|
||||
if measure_type == "suspended_floor_insulation":
|
||||
pps = df[df["Measure_Type"] == "UFI"]
|
||||
if pps.shape[0] != 1:
|
||||
raise ValueError("Invalid UFI category")
|
||||
return pps.squeeze()["Cost Savings"]
|
||||
|
||||
if measure_type == "solid_floor_insulation":
|
||||
pps = df[df["Measure_Type"] == "SFI"]
|
||||
if pps.shape[0] != 1:
|
||||
raise ValueError("Invalid SFI category")
|
||||
return pps.squeeze()["Cost Savings"]
|
||||
|
||||
raise ValueError(f"Invalid measure type for partial project ABS calculation: {measure_type}")
|
||||
|
||||
# -----------------------
|
||||
# Main Entry Point
|
||||
# -----------------------
|
||||
|
||||
@staticmethod
|
||||
def check_solar_eligible_heating_system(mainheat_description, heating_control_description):
|
||||
"""
|
||||
Checks if the main heating system is eligible for solar PV funding.
|
||||
:param mainheat_description: Describes the primary heating system
|
||||
:param heating_control_description: Heating controls associated to the primary heating system
|
||||
:return:
|
||||
"""
|
||||
|
||||
return (
|
||||
any(x in mainheat_description.lower() for x in [
|
||||
"air source heat pump", "ground source heat pump", "boiler and radiators, electric"
|
||||
]) or (
|
||||
("electric storage heaters" in mainheat_description)
|
||||
and (heating_control_description.lower() == "controls for high heat retention storage heaters")
|
||||
)
|
||||
)
|
||||
|
||||
def check_solar_eligibility(
|
||||
self,
|
||||
measure_types,
|
||||
mainheat_description,
|
||||
heating_control_description,
|
||||
has_wall_insulation_recommendation: bool = False,
|
||||
has_roof_insulation_recommendation: bool = False,
|
||||
):
|
||||
"""
|
||||
Because of the various pre-requisites for solar, we have a self-contained function to check for
|
||||
eligibility
|
||||
|
||||
Returns a tuple of booleans (has_solar, solar_eligible)
|
||||
"""
|
||||
|
||||
if "solar_pv" not in measure_types:
|
||||
return False, False
|
||||
|
||||
# 1) We check if there is an eligible heating system in place
|
||||
has_eligibile_heating = self.check_solar_eligible_heating_system(
|
||||
mainheat_description, heating_control_description
|
||||
)
|
||||
|
||||
if not has_eligibile_heating:
|
||||
# We check if there is a recommendation for an ASHP or HHRSH
|
||||
if ("air_source_heat_pump" not in measure_types) and (
|
||||
"high_heat_retention_storage_heater" not in measure_types):
|
||||
return True, False
|
||||
|
||||
# 2) We check if there is a wall insulation measure for this property. If so, we make sure
|
||||
# we have a wall insulation recommendation in this package
|
||||
if has_wall_insulation_recommendation:
|
||||
# Make sure we have a wall insulation recommendation
|
||||
if not any(m in measure_types for m in WALL_INSULATION_MEASURES):
|
||||
return True, False
|
||||
|
||||
# 3) We check if there is a roof insulation measure for this property. If so, we make sure
|
||||
# we have a roof insulation recommendation in this package
|
||||
if has_roof_insulation_recommendation:
|
||||
# Make sure we have a roof insulation recommendation
|
||||
if not any(m in measure_types for m in ROOF_INSULATION_MEASURES):
|
||||
return True, False
|
||||
|
||||
return True, True
|
||||
|
||||
def check_funding(
|
||||
self, measures: List[dict],
|
||||
self,
|
||||
measures: List[dict],
|
||||
starting_sap: int,
|
||||
ending_sap: int,
|
||||
floor_area: float,
|
||||
mainheat_description: str,
|
||||
heating_control_description: str,
|
||||
is_cavity: bool,
|
||||
council_tax_band: str = None
|
||||
current_wall_uvalue: float,
|
||||
is_partial: False,
|
||||
existing_li_thickness: float,
|
||||
council_tax_band: str = None,
|
||||
has_wall_insulation_recommendation: bool = False,
|
||||
has_roof_insulation_recommendation: bool = False,
|
||||
):
|
||||
"""
|
||||
Given a list of measures, check ECO4/GBIS eligibility.
|
||||
|
||||
Because measures like solar PV are subject to the minimum insulation requirements and we can get
|
||||
exemptions on floor insulation recommendations, if has_wall_insulation_recommendation or
|
||||
has_roof_insulation_recommendation are true, we check that the measures package contain a wall or roof
|
||||
insulation measure otherwise solar PV isn't eligible
|
||||
"""
|
||||
|
||||
# Normalize measures
|
||||
measure_types, has_innovation, innovation_measures = self._split_measures(measures)
|
||||
|
||||
# Determine if we have a solar eligible heating system
|
||||
has_solar, solar_eligible = self.check_solar_eligibility(
|
||||
measure_types,
|
||||
mainheat_description,
|
||||
heating_control_description,
|
||||
has_wall_insulation_recommendation,
|
||||
has_roof_insulation_recommendation,
|
||||
)
|
||||
|
||||
# Track EPC bands and floor area
|
||||
self.starting_sap_band = self.get_sap_band(starting_sap)
|
||||
self.ending_sap_band = self.get_sap_band(ending_sap)
|
||||
|
|
@ -281,26 +505,36 @@ class Funding:
|
|||
|
||||
if self.tenure == "Private":
|
||||
# ECO4 PRS
|
||||
self.eco4_prs_eligibility(starting_sap, ending_sap, measure_types, mainheat_description,
|
||||
heating_control_description)
|
||||
self.eco4_prs_eligibility(starting_sap, ending_sap, measure_types, has_solar, solar_eligible)
|
||||
# GBIS PRS
|
||||
self.gbis_prs_eligibility(starting_sap, council_tax_band or "", measure_types)
|
||||
|
||||
if self.eco4_eligible:
|
||||
eco4_abs = self.calculate_full_project_abs()
|
||||
eco4_funding = eco4_abs * (self.private_cavity_abs_rate if is_cavity else self.private_solid_abs_rate)
|
||||
return {"eco4_funding": eco4_funding}
|
||||
self.full_project_abs = self.calculate_full_project_abs()
|
||||
self.eco4_funding = self.full_project_abs * (
|
||||
self.private_cavity_abs_rate if is_cavity else self.private_solid_abs_rate)
|
||||
|
||||
|
||||
elif self.tenure == "Social":
|
||||
# ECO4 Social
|
||||
self.eco4_sh_eligibility(starting_sap, ending_sap, measure_types, has_innovation, innovation_measures)
|
||||
self.eco4_sh_eligibility(starting_sap, ending_sap, has_innovation, has_solar, solar_eligible)
|
||||
|
||||
# GBIS Social
|
||||
self.gbis_sh_eligibility(starting_sap, measure_types, has_innovation, innovation_measures)
|
||||
self.gbis_sh_eligibility(starting_sap, measure_types, has_innovation)
|
||||
|
||||
if self.eco4_eligible:
|
||||
eco4_abs = self.calculate_full_project_abs()
|
||||
eco4_funding = eco4_abs * (self.social_cavity_abs_rate if is_cavity else self.social_solid_abs_rate)
|
||||
return {"eco4_funding": eco4_funding}
|
||||
# Calculate the full project ABS for ECO4
|
||||
self.full_project_abs = self.calculate_full_project_abs()
|
||||
self.eco4_funding = self.full_project_abs * (
|
||||
self.social_cavity_abs_rate if is_cavity else self.social_solid_abs_rate
|
||||
)
|
||||
|
||||
if self.gbis_eligible:
|
||||
# Calculate the partial project score - this is dependent on the measure
|
||||
self.partial_project_abs = self.calculate_partial_project_abs(
|
||||
measure_types[0], current_wall_uvalue, is_partial, existing_li_thickness,
|
||||
)
|
||||
|
||||
|
||||
else:
|
||||
raise NotImplementedError("Only 'Private' and 'Social' tenures are supported.")
|
||||
|
|
|
|||
|
|
@ -8,9 +8,10 @@ TYPICAL_MEASURE_TYPES = [
|
|||
"secondary_heating", "solar_pv"
|
||||
]
|
||||
|
||||
SPECIFIC_MEASURES = [
|
||||
"internal_wall_insulation", "external_wall_insulation", "cavity_wall_insulation",
|
||||
"loft_insulation", "flat_roof_insulation", "room_roof_insulation",
|
||||
WALL_INSULATION_MEASURES = ["internal_wall_insulation", "external_wall_insulation", "cavity_wall_insulation"]
|
||||
ROOF_INSULATION_MEASURES = ["loft_insulation", "flat_roof_insulation", "room_roof_insulation"]
|
||||
|
||||
SPECIFIC_MEASURES = WALL_INSULATION_MEASURES + ROOF_INSULATION_MEASURES + [
|
||||
"suspended_floor_insulation", "solid_floor_insulation",
|
||||
"boiler_upgrade", "high_heat_retention_storage_heater", "air_source_heat_pump",
|
||||
"secondary_heating", "solar_pv", "double_glazing", "secondary_glazing",
|
||||
|
|
|
|||
|
|
@ -30,7 +30,11 @@ def mock_project_scores_matrix():
|
|||
|
||||
@pytest.fixture
|
||||
def mock_partial_scores_matrix():
|
||||
return pd.DataFrame([{"dummy": "data"}]) # not used for eligibility tests yet
|
||||
df = pd.read_csv("recommendations/tests/test_data/ECO4_Partial_Project_Scores_Matrix_v6.csv")
|
||||
df.columns = ['Measure category', 'Measure_Type', 'Pre_Main_Heating_Source',
|
||||
'Post_Main_Heating_Source', 'Total Floor Area Band', 'Starting Band',
|
||||
'Average Treatable Factor', 'Cost Savings', 'SAP Savings']
|
||||
return df
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
@ -69,7 +73,10 @@ def test_eco4_prs_eligible_with_swi(mock_project_scores_matrix, mock_partial_sco
|
|||
mainheat_description="Boiler and radiators, mains gas",
|
||||
heating_control_description="Programmer, room thermostat and TRVs",
|
||||
is_cavity=True,
|
||||
council_tax_band="B"
|
||||
council_tax_band="B",
|
||||
is_partial=False,
|
||||
existing_li_thickness=0,
|
||||
current_wall_uvalue=2
|
||||
)
|
||||
|
||||
assert funding.eco4_eligible
|
||||
|
|
@ -98,7 +105,10 @@ def test_eco4_prs_not_eligible_high_epc(mock_project_scores_matrix, mock_partial
|
|||
mainheat_description="Boiler and radiators, mains gas",
|
||||
heating_control_description="Programmer, room thermostat and TRVs",
|
||||
is_cavity=True,
|
||||
council_tax_band="B"
|
||||
council_tax_band="B",
|
||||
is_partial=False,
|
||||
existing_li_thickness=0,
|
||||
current_wall_uvalue=2,
|
||||
)
|
||||
|
||||
assert not funding.eco4_eligible
|
||||
|
|
@ -126,7 +136,10 @@ def test_gbis_prs_general_eligibility(mock_project_scores_matrix, mock_partial_s
|
|||
mainheat_description="Boiler and radiators, mains gas",
|
||||
heating_control_description="Programmer, room thermostat and TRVs",
|
||||
is_cavity=True,
|
||||
council_tax_band="A"
|
||||
council_tax_band="A",
|
||||
is_partial=False,
|
||||
existing_li_thickness=0,
|
||||
current_wall_uvalue=2,
|
||||
)
|
||||
|
||||
assert funding.gbis_eligible
|
||||
|
|
@ -154,7 +167,10 @@ def test_gbis_prs_low_income_caveat(mock_project_scores_matrix, mock_partial_sco
|
|||
mainheat_description="Boiler and radiators, mains gas",
|
||||
heating_control_description="Programmer, room thermostat and TRVs",
|
||||
is_cavity=True,
|
||||
council_tax_band="B"
|
||||
council_tax_band="B",
|
||||
is_partial=False,
|
||||
existing_li_thickness=0,
|
||||
current_wall_uvalue=2,
|
||||
)
|
||||
|
||||
assert funding.gbis_eligible
|
||||
|
|
@ -186,7 +202,10 @@ def test_eco4_sh_epc_e_eligible(mock_project_scores_matrix, mock_partial_scores_
|
|||
floor_area=80,
|
||||
mainheat_description="Boiler and radiators, mains gas",
|
||||
heating_control_description="Programmer, room thermostat and TRVs",
|
||||
is_cavity=True
|
||||
is_cavity=True,
|
||||
current_wall_uvalue=2,
|
||||
is_partial=False,
|
||||
existing_li_thickness=0,
|
||||
)
|
||||
|
||||
assert funding.eco4_eligible
|
||||
|
|
@ -213,12 +232,169 @@ def test_eco4_sh_epc_d_requires_innovation(mock_project_scores_matrix, mock_part
|
|||
floor_area=80,
|
||||
mainheat_description="Boiler and radiators, mains gas",
|
||||
heating_control_description="Programmer, room thermostat and TRVs",
|
||||
is_cavity=True
|
||||
is_cavity=True,
|
||||
current_wall_uvalue=2,
|
||||
is_partial=False,
|
||||
existing_li_thickness=0,
|
||||
)
|
||||
|
||||
assert not funding.eco4_eligible
|
||||
assert EligibilityCaveats.INNOVATION_REQUIRED in funding.eco4_eligibility_caveats
|
||||
|
||||
# Test with an innovation measure
|
||||
funding2 = Funding(
|
||||
project_scores_matrix=mock_project_scores_matrix,
|
||||
partial_project_scores_matrix=mock_partial_scores_matrix,
|
||||
whlg_eligible_postcodes=mock_whlg_postcodes,
|
||||
social_cavity_abs_rate=13.5,
|
||||
social_solid_abs_rate=17,
|
||||
private_cavity_abs_rate=13.5,
|
||||
private_solid_abs_rate=17,
|
||||
tenure="Social",
|
||||
)
|
||||
measures2 = [{"type": "internal_wall_insulation", "is_innovation": True}]
|
||||
funding2.check_funding(
|
||||
measures=measures2,
|
||||
starting_sap=60, # EPC D
|
||||
ending_sap=69,
|
||||
floor_area=80,
|
||||
mainheat_description="Boiler and radiators, mains gas",
|
||||
heating_control_description="Programmer, room thermostat and TRVs",
|
||||
is_cavity=True,
|
||||
current_wall_uvalue=2,
|
||||
is_partial=False,
|
||||
existing_li_thickness=0,
|
||||
)
|
||||
|
||||
assert funding2.eco4_eligible
|
||||
assert not funding2.eco4_eligibility_caveats
|
||||
|
||||
# Test with innovation solar. If the measure is solar, we need to have an eligible heating system.
|
||||
# If we don't have an eligible heating system in place, we need to have one as part of the measure
|
||||
# package
|
||||
# THIS SHOULD NOT BE ELIGIBLE
|
||||
funding3 = Funding(
|
||||
project_scores_matrix=mock_project_scores_matrix,
|
||||
partial_project_scores_matrix=mock_partial_scores_matrix,
|
||||
whlg_eligible_postcodes=mock_whlg_postcodes,
|
||||
social_cavity_abs_rate=13.5,
|
||||
social_solid_abs_rate=17,
|
||||
private_cavity_abs_rate=13.5,
|
||||
private_solid_abs_rate=17,
|
||||
tenure="Social",
|
||||
)
|
||||
measures3 = [{"type": "solar_pv", "is_innovation": True}]
|
||||
funding3.check_funding(
|
||||
measures=measures3,
|
||||
starting_sap=60, # EPC D
|
||||
ending_sap=69,
|
||||
floor_area=80,
|
||||
mainheat_description="Boiler and radiators, mains gas",
|
||||
heating_control_description="Programmer, room thermostat and TRVs",
|
||||
is_cavity=True,
|
||||
current_wall_uvalue=2,
|
||||
is_partial=False,
|
||||
existing_li_thickness=0,
|
||||
)
|
||||
|
||||
assert not funding3.eco4_eligible
|
||||
assert EligibilityCaveats.SOLAR_NEEDS_HEATING in funding3.eco4_eligibility_caveats
|
||||
|
||||
# Test with innovation solar and ASHP. This should be eligible
|
||||
funding4 = Funding(
|
||||
project_scores_matrix=mock_project_scores_matrix,
|
||||
partial_project_scores_matrix=mock_partial_scores_matrix,
|
||||
whlg_eligible_postcodes=mock_whlg_postcodes,
|
||||
social_cavity_abs_rate=13.5,
|
||||
social_solid_abs_rate=17,
|
||||
private_cavity_abs_rate=13.5,
|
||||
private_solid_abs_rate=17,
|
||||
tenure="Social",
|
||||
)
|
||||
|
||||
measures4 = [{"type": "solar_pv", "is_innovation": True}]
|
||||
funding4.check_funding(
|
||||
measures=measures4,
|
||||
starting_sap=60, # EPC D
|
||||
ending_sap=69,
|
||||
floor_area=80,
|
||||
mainheat_description="Air source heat pump, radiators",
|
||||
heating_control_description="Programmer, room thermostat and TRVs",
|
||||
is_cavity=True,
|
||||
current_wall_uvalue=2,
|
||||
is_partial=False,
|
||||
existing_li_thickness=0,
|
||||
)
|
||||
|
||||
assert funding4.eco4_eligible
|
||||
assert not funding4.eco4_eligibility_caveats
|
||||
|
||||
# Test with innovation solar, a non-eligible heating system but a heating upgrade
|
||||
funding5 = Funding(
|
||||
project_scores_matrix=mock_project_scores_matrix,
|
||||
partial_project_scores_matrix=mock_partial_scores_matrix,
|
||||
whlg_eligible_postcodes=mock_whlg_postcodes,
|
||||
social_cavity_abs_rate=13.5,
|
||||
social_solid_abs_rate=17,
|
||||
private_cavity_abs_rate=13.5,
|
||||
private_solid_abs_rate=17,
|
||||
tenure="Social",
|
||||
)
|
||||
|
||||
measures5 = [
|
||||
{"type": "solar_pv", "is_innovation": True},
|
||||
{"type": "high_heat_retention_storage_heater", "is_innovation": False}
|
||||
]
|
||||
funding5.check_funding(
|
||||
measures=measures5,
|
||||
starting_sap=60, # EPC D
|
||||
ending_sap=69,
|
||||
floor_area=80,
|
||||
mainheat_description="Electric storage heaters",
|
||||
heating_control_description="Manual charge control",
|
||||
is_cavity=True,
|
||||
current_wall_uvalue=2,
|
||||
is_partial=False,
|
||||
existing_li_thickness=0,
|
||||
)
|
||||
|
||||
assert funding5.eco4_eligible
|
||||
assert not funding5.eco4_eligibility_caveats
|
||||
|
||||
# Test with innovation solar, an eligible heating system but a package that excludes the required
|
||||
# fabric upgrades
|
||||
funding6 = Funding(
|
||||
project_scores_matrix=mock_project_scores_matrix,
|
||||
partial_project_scores_matrix=mock_partial_scores_matrix,
|
||||
whlg_eligible_postcodes=mock_whlg_postcodes,
|
||||
social_cavity_abs_rate=13.5,
|
||||
social_solid_abs_rate=17,
|
||||
private_cavity_abs_rate=13.5,
|
||||
private_solid_abs_rate=17,
|
||||
tenure="Social",
|
||||
)
|
||||
|
||||
measures6 = [
|
||||
{"type": "solar_pv", "is_innovation": True},
|
||||
]
|
||||
funding6.check_funding(
|
||||
measures=measures6,
|
||||
starting_sap=60, # EPC D
|
||||
ending_sap=69,
|
||||
floor_area=80,
|
||||
mainheat_description="Electric storage heaters",
|
||||
heating_control_description="controls for high heat retention storage heaters",
|
||||
is_cavity=True,
|
||||
has_wall_insulation_recommendation=True,
|
||||
has_roof_insulation_recommendation=False,
|
||||
current_wall_uvalue=2,
|
||||
is_partial=False,
|
||||
existing_li_thickness=0,
|
||||
)
|
||||
|
||||
assert not funding6.eco4_eligible
|
||||
assert not funding6.eco4_eligibility_caveats
|
||||
|
||||
|
||||
def test_eco4_sh_solar_pv_requires_heating(mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes):
|
||||
"""Solar PV as innovation measure requires ASHP or HHRSH."""
|
||||
|
|
@ -241,7 +417,10 @@ def test_eco4_sh_solar_pv_requires_heating(mock_project_scores_matrix, mock_part
|
|||
floor_area=80,
|
||||
mainheat_description="Boiler and radiators, mains gas",
|
||||
heating_control_description="Programmer, room thermostat and TRVs",
|
||||
is_cavity=True
|
||||
is_cavity=True,
|
||||
current_wall_uvalue=2,
|
||||
is_partial=False,
|
||||
existing_li_thickness=0,
|
||||
)
|
||||
|
||||
assert not funding.eco4_eligible
|
||||
|
|
@ -273,7 +452,10 @@ def test_eco4_sh_solar_pv_with_heating_is_ok(mock_project_scores_matrix, mock_pa
|
|||
floor_area=80,
|
||||
mainheat_description="Boiler and radiators, mains gas",
|
||||
heating_control_description="Programmer, room thermostat and TRVs",
|
||||
is_cavity=True
|
||||
is_cavity=True,
|
||||
current_wall_uvalue=2,
|
||||
is_partial=False,
|
||||
existing_li_thickness=0,
|
||||
)
|
||||
|
||||
assert funding.eco4_eligible
|
||||
|
|
@ -305,7 +487,10 @@ def test_eco4_upgrade_requirement_e_to_c_pass(mock_project_scores_matrix, mock_p
|
|||
mainheat_description="Boiler and radiators, mains gas",
|
||||
heating_control_description="Programmer, room thermostat and TRVs",
|
||||
is_cavity=True,
|
||||
council_tax_band="B"
|
||||
council_tax_band="B",
|
||||
current_wall_uvalue=2,
|
||||
is_partial=False,
|
||||
existing_li_thickness=0,
|
||||
)
|
||||
|
||||
assert funding.eco4_eligible
|
||||
|
|
@ -336,7 +521,10 @@ def test_eco4_upgrade_requirement_e_to_d_fail(mock_project_scores_matrix, mock_p
|
|||
mainheat_description="Boiler and radiators, mains gas",
|
||||
heating_control_description="Programmer, room thermostat and TRVs",
|
||||
is_cavity=True,
|
||||
council_tax_band="B"
|
||||
council_tax_band="B",
|
||||
current_wall_uvalue=2,
|
||||
is_partial=False,
|
||||
existing_li_thickness=0,
|
||||
)
|
||||
|
||||
assert not funding.eco4_eligible
|
||||
|
|
@ -367,7 +555,10 @@ def test_eco4_upgrade_requirement_f_to_d_pass(mock_project_scores_matrix, mock_p
|
|||
mainheat_description="Boiler and radiators, mains gas",
|
||||
heating_control_description="Programmer, room thermostat and TRVs",
|
||||
is_cavity=True,
|
||||
council_tax_band="B"
|
||||
council_tax_band="B",
|
||||
current_wall_uvalue=2,
|
||||
is_partial=False,
|
||||
existing_li_thickness=0,
|
||||
)
|
||||
|
||||
assert funding.eco4_eligible
|
||||
|
|
@ -398,7 +589,10 @@ def test_eco4_upgrade_requirement_f_to_e_fail(mock_project_scores_matrix, mock_p
|
|||
mainheat_description="Boiler and radiators, mains gas",
|
||||
heating_control_description="Programmer, room thermostat and TRVs",
|
||||
is_cavity=True,
|
||||
council_tax_band="B"
|
||||
council_tax_band="B",
|
||||
current_wall_uvalue=2,
|
||||
is_partial=False,
|
||||
existing_li_thickness=0,
|
||||
)
|
||||
|
||||
assert not funding.eco4_eligible
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue