mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
updating costing methodology for new installer costs
This commit is contained in:
parent
503a19291d
commit
767d0d3132
3 changed files with 32 additions and 78 deletions
|
|
@ -42,6 +42,9 @@ class GoogleSolarApi:
|
|||
# your area
|
||||
installation_life_span = 20
|
||||
|
||||
MIN_UNIT_PANELS = 4 # Minimum number of panels we allow for a domestic building
|
||||
MIN_BUILDING_PANELS = 10 # Minimum number of panels we allow for a block of flats
|
||||
|
||||
def __init__(self, api_key, max_retries=5):
|
||||
"""
|
||||
Initialize the GoogleSolarApi class with the provided API key and maximum retries.
|
||||
|
|
@ -250,6 +253,9 @@ class GoogleSolarApi:
|
|||
Optimise the solar panel configuration for the building.
|
||||
:return:
|
||||
"""
|
||||
# If we look at the building level, we don't include any projects fewer than 10 panels, otherwise the
|
||||
# minimum is 4
|
||||
min_panels = self.MIN_BUILDING_PANELS if is_building else self.MIN_UNIT_PANELS
|
||||
|
||||
cost_instance = Costs(property_instance=property_instance) if property_instance is not None else None
|
||||
|
||||
|
|
@ -264,6 +270,10 @@ class GoogleSolarApi:
|
|||
|
||||
roi_summary = []
|
||||
for segment in roof_segment_summaries:
|
||||
|
||||
if segment["panelsCount"] < min_panels:
|
||||
continue
|
||||
|
||||
wattage = segment["panelsCount"] * self.insights_data["solarPotential"]["panelCapacityWatts"]
|
||||
generated_dc_energy = segment["yearlyEnergyDcKwh"]
|
||||
ratio = generated_dc_energy / wattage
|
||||
|
|
@ -272,7 +282,9 @@ class GoogleSolarApi:
|
|||
cost = MCS_SOLAR_PV_COST_DATA["average_cost_per_kwh"] * (wattage / 1000)
|
||||
else:
|
||||
cost = cost_instance.solar_pv(
|
||||
wattage=wattage, has_battery=False
|
||||
n_panels=segment["panelsCount"],
|
||||
has_battery=False,
|
||||
n_floors=property_instance.number_of_floors,
|
||||
)["total"]
|
||||
|
||||
roi_summary.append(
|
||||
|
|
@ -330,10 +342,6 @@ class GoogleSolarApi:
|
|||
# We can have duplicate configurations
|
||||
|
||||
panel_performance = panel_performance.drop_duplicates()
|
||||
# If we look at the building level, we don't include any projects fewer than 10 panels, otherwise the
|
||||
# minimum is 4
|
||||
min_panels = 10 if is_building else 4
|
||||
panel_performance = panel_performance[panel_performance["n_panels"] >= min_panels]
|
||||
|
||||
if panel_performance.empty:
|
||||
self.panel_performance = pd.DataFrame(
|
||||
|
|
|
|||
|
|
@ -54,6 +54,8 @@ INSTALLER_SOLAR_COSTS = [
|
|||
{'n_panels': 17, 'array_kwp': 7.0, 'cost': 5500.00, 'installer': 'CEG'},
|
||||
{'n_panels': 18, 'array_kwp': 7.4, 'cost': 6021.00, 'installer': 'CEG'}
|
||||
]
|
||||
# This is the maximum number of panels that we have a cost from the installers for
|
||||
INSTALLER_MAX_PANELS = 18
|
||||
|
||||
# CEG uses use Solshare as an inverter to provide solar PV to multiple flats. This costs £7500 for the inverter alone
|
||||
# https://midsummerwholesale.co.uk/buy/solshare
|
||||
|
|
@ -362,7 +364,7 @@ class Costs:
|
|||
"labour_days": labour_days
|
||||
}
|
||||
|
||||
def internal_wall_insulation(self, wall_area, material, non_insulation_materials):
|
||||
def internal_wall_insulation(self, wall_area, material):
|
||||
"""
|
||||
Broadly speaking, the high level steps to an internal wall insulation job are the following:
|
||||
|
||||
|
|
@ -401,74 +403,25 @@ class Costs:
|
|||
"labour_days": labour_days,
|
||||
}
|
||||
|
||||
# Extract and check the different types of data we'll need
|
||||
demolition_data = [x for x in non_insulation_materials if x["type"] == "iwi_wall_demolition"]
|
||||
vapour_barrier_data = [x for x in non_insulation_materials if x["type"] == "iwi_vapour_barrier"]
|
||||
redecoration_data = [x for x in non_insulation_materials if x["type"] == "iwi_redecoration"]
|
||||
if not demolition_data:
|
||||
raise ValueError("No data found for iwi_wall_demolition")
|
||||
|
||||
if (len(vapour_barrier_data) != 1) or (len(redecoration_data) != 3):
|
||||
raise ValueError("Incorrect number of data entries for non-insulation materials")
|
||||
|
||||
# Break out the individual material costs
|
||||
# Since we don't know the exact wall construction, we take an average for demolition costs, since
|
||||
# the cost will depend on the type of wall construction
|
||||
demolition_material_costs = np.mean([x["material_cost"] * wall_area for x in demolition_data])
|
||||
insulation_material_costs = material["material_cost"] * wall_area
|
||||
vapour_barrier_material_costs = vapour_barrier_data[0]["material_cost"] * wall_area
|
||||
redecoration_material_costs = sum([x["material_cost"] * wall_area for x in redecoration_data])
|
||||
|
||||
demolition_plant_costs = np.mean([x["plant_cost"] * wall_area for x in demolition_data])
|
||||
|
||||
# Again for demolition, we average since we aren't sure which demolition process will be used
|
||||
demolition_labour_costs = np.mean([x["labour_cost"] * wall_area for x in demolition_data])
|
||||
insulation_labour_costs = material["labour_cost"] * wall_area
|
||||
vapour_barrier_labour_costs = vapour_barrier_data[0]["labour_cost"] * wall_area
|
||||
redecoration_labour_costs = sum([x["labour_cost"] * wall_area for x in redecoration_data])
|
||||
|
||||
labour_costs = (demolition_labour_costs + insulation_labour_costs + vapour_barrier_labour_costs +
|
||||
redecoration_labour_costs)
|
||||
|
||||
labour_costs = labour_costs * self.labour_adjustment_factor
|
||||
|
||||
materials_costs = (demolition_material_costs + insulation_material_costs + vapour_barrier_material_costs +
|
||||
redecoration_material_costs)
|
||||
|
||||
subtotal_before_profit = labour_costs + materials_costs + demolition_plant_costs
|
||||
|
||||
contingency_cost = subtotal_before_profit * self.IWI_CONTINGENCY
|
||||
preliminaries_cost = subtotal_before_profit * self.PRELIMINARIES
|
||||
profit_cost = subtotal_before_profit * self.PROFIT_MARGIN
|
||||
|
||||
subtotal_before_vat = subtotal_before_profit + contingency_cost + preliminaries_cost + profit_cost
|
||||
|
||||
vat_cost = subtotal_before_vat * self.VAT_RATE
|
||||
|
||||
total_cost = subtotal_before_vat + vat_cost
|
||||
|
||||
demolition_labour_hours = np.mean([x["labour_hours_per_unit"] * wall_area for x in demolition_data])
|
||||
insulation_labour_hours = material["labour_hours_per_unit"] * wall_area
|
||||
vapour_barrier_labour_hours = vapour_barrier_data[0]["labour_hours_per_unit"] * wall_area
|
||||
redecoration_labour_hours = sum([x["labour_hours_per_unit"] * wall_area for x in redecoration_data])
|
||||
|
||||
labour_hours = (demolition_labour_hours + insulation_labour_hours + vapour_barrier_labour_hours +
|
||||
redecoration_labour_hours)
|
||||
total_including_vat = material["total_cost"] * wall_area
|
||||
total_excluding_vat = total_including_vat / (1 + self.VAT_RATE)
|
||||
vat_cost = total_including_vat - total_excluding_vat
|
||||
|
||||
# We estimate 1 weeks worth of work
|
||||
labour_hours = 160
|
||||
# To install internal wall insulation, a small to medium size project might be conducted by a team of 3-5 people
|
||||
labour_days = (labour_hours / 8) / 4
|
||||
|
||||
return {
|
||||
"total": total_cost,
|
||||
"subtotal": subtotal_before_vat,
|
||||
"total": total_including_vat,
|
||||
"subtotal": total_excluding_vat,
|
||||
"vat": vat_cost,
|
||||
"contingency": contingency_cost,
|
||||
"preliminaries": preliminaries_cost,
|
||||
"material": materials_costs,
|
||||
"profit": profit_cost,
|
||||
"labour_hours": labour_hours,
|
||||
"labour_days": labour_days,
|
||||
"labour_cost": labour_costs
|
||||
}
|
||||
|
||||
def suspended_floor_insulation(self, insulation_floor_area, material, non_insulation_materials):
|
||||
|
|
@ -1088,7 +1041,15 @@ class Costs:
|
|||
units
|
||||
"""
|
||||
|
||||
system_cost = [c for c in INSTALLER_SOLAR_COSTS if c["n_panels"] == n_panels][0]["cost"]
|
||||
if n_panels > INSTALLER_MAX_PANELS:
|
||||
base_cost = [c for c in INSTALLER_SOLAR_COSTS if c["n_panels"] == INSTALLER_MAX_PANELS][0]["cost"]
|
||||
cost_per_panel = [
|
||||
c for c in INSTALLER_SOLAR_COSTS if c["n_panels"] == (INSTALLER_MAX_PANELS - 1)
|
||||
][0]["cost"]
|
||||
cost_per_panel = base_cost - cost_per_panel
|
||||
system_cost = base_cost + (n_panels - INSTALLER_MAX_PANELS) * cost_per_panel
|
||||
else:
|
||||
system_cost = [c for c in INSTALLER_SOLAR_COSTS if c["n_panels"] == n_panels][0]["cost"]
|
||||
|
||||
total_cost = array_cost if array_cost is not None else system_cost
|
||||
|
||||
|
|
|
|||
|
|
@ -106,23 +106,10 @@ class WallRecommendations(Definitions):
|
|||
part for part in materials if part["type"] == "internal_wall_insulation"
|
||||
]
|
||||
|
||||
self.internal_wall_non_insulation_materials = [
|
||||
part
|
||||
for part in materials
|
||||
if part["type"]
|
||||
in ["iwi_wall_demolition", "iwi_vapour_barrier", "iwi_redecoration"]
|
||||
]
|
||||
|
||||
self.external_wall_insulation_materials = [
|
||||
part for part in materials if part["type"] == "external_wall_insulation"
|
||||
]
|
||||
|
||||
self.external_wall_non_insulation_materials = [
|
||||
part
|
||||
for part in materials
|
||||
if part["type"] in ["ewi_wall_demolition", "ewi_wall_preparation", "ewi_wall_redecoration"]
|
||||
]
|
||||
|
||||
def ewi_valid(self):
|
||||
"""
|
||||
This method check available data, to determine if a property is suitable for external wall insulation
|
||||
|
|
@ -508,7 +495,6 @@ class WallRecommendations(Definitions):
|
|||
cost_result = self.costs.internal_wall_insulation(
|
||||
wall_area=self.property.insulation_wall_area,
|
||||
material=material.to_dict(),
|
||||
non_insulation_materials=non_insulation_materials,
|
||||
)
|
||||
already_installed = (
|
||||
"internal_wall_insulation"
|
||||
|
|
@ -617,7 +603,6 @@ class WallRecommendations(Definitions):
|
|||
iwi_recommendations = self._find_insulation(
|
||||
u_value=u_value,
|
||||
insulation_materials=pd.DataFrame(self.internal_wall_insulation_materials),
|
||||
non_insulation_materials=self.internal_wall_non_insulation_materials,
|
||||
phase=phase,
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue