Model/backend/app/db/models/portfolio.py
2026-04-13 16:18:17 +00:00

331 lines
12 KiB
Python

import enum
import pytz
import datetime
from sqlalchemy import (
Column,
Integer,
BigInteger,
Text,
Boolean,
Float,
DateTime,
Enum,
ForeignKey,
CheckConstraint,
)
from backend.app.db.base import Base
from backend.app.db.models.users import UserModel # noqa
from backend.app.db.models.materials import MaterialType
from datatypes.epc.domain.epc import Epc
class PortfolioStatus(enum.Enum):
SCOPING = "scoping"
ASSESSMENT = "assessment"
SURVEY = "survey"
TENDERING = "tendering"
PROJECT_UNDERWAY = "project underway"
COMPLETION_ON_TRACK = "completion; status: on track"
COMPLETION_DELAYED = "completion; status: delayed"
COMPLETION_AT_RISK = "completion; status: at risk"
COMPLETED = "completion; status: completed"
NEEDS_REVIEW = "needs review"
class PortfolioGoal(enum.Enum): # TODO: Move to domain?
VALUATION_IMPROVEMENT = "Valuation Improvement"
INCREASING_EPC = "Increasing EPC"
REDUCING_CO2_EMISSIONS = "Reducing CO2 emissions"
ENERGY_SAVINGS = "Energy Savings"
NONE = "None"
class Portfolio(Base):
__tablename__ = "portfolio"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(Text, nullable=False)
budget = Column(Float)
status = Column(
Enum(PortfolioStatus, values_callable=lambda x: [e.value for e in x]),
nullable=False,
)
goal = Column(
Enum(PortfolioGoal, values_callable=lambda x: [e.value for e in x]),
nullable=False,
)
cost = Column(Float)
number_of_properties = Column(Integer)
co2_equivalent_savings = Column(
Float
) # Unit is always tonnes so we don't need to store the unit
energy_savings = Column(
Float
) # Unit is always kWh so we don't need to store the unit
energy_cost_savings = Column(
Float
) # Unit is always £ so we don't need to store the unit for the moment
property_valuation_increase = Column(
Float
) # Unit is always £ so we don't need to store the unit for the moment
rental_yield_increase = Column(
Float
) # Unit is always £ so we don't need to store the unit for the moment
total_work_hours = Column(Float)
labour_days = Column(Float)
created_at = Column(
DateTime, nullable=False, default=datetime.datetime.now(pytz.utc)
)
updated_at = Column(
DateTime, nullable=False, default=datetime.datetime.now(pytz.utc)
)
# Aggregations for summary
epc_breakdown_pre_retrofit = Column(Text)
epc_breakdown_post_retrofit = Column(Text)
n_units_to_retrofit = Column(Integer)
co2_per_unit_pre_retrofit = Column(Text)
co2_per_unit_post_retrofit = Column(Text)
energy_bill_per_unit_pre_retrofit = Column(Text)
energy_bill_per_unit_post_retrofit = Column(Text)
energy_consumption_per_unit_pre_retrofit = Column(Text)
energy_consumption_per_unit_post_retrofit = Column(Text)
valuation_improvement_per_unit = Column(Text)
cost_per_unit = Column(Text)
cost_per_co2_saved = Column(Text)
cost_per_sap_point = Column(Text)
valuation_return_on_investment = Column(Text)
class PropertyCreationStatus(enum.Enum):
LOADING = "LOADING"
READY = "READY"
ERROR = "ERROR"
class PropertyModel(Base):
__tablename__ = "property"
id = Column(Integer, primary_key=True, autoincrement=True)
portfolio_id = Column(Integer, ForeignKey("portfolio.id"), nullable=False)
creation_status = Column(Enum(PropertyCreationStatus), nullable=False)
uprn = Column(BigInteger)
landlord_property_id = Column(Text)
building_reference_number = Column(BigInteger)
status = Column(
Enum(PortfolioStatus, values_callable=lambda x: [e.value for e in x]),
nullable=False,
)
address = Column(Text)
postcode = Column(Text)
has_pre_condition_report = Column(Boolean)
has_recommendations = Column(Boolean)
created_at = Column(
DateTime, nullable=False, default=datetime.datetime.now(pytz.utc)
)
updated_at = Column(
DateTime, nullable=False, default=datetime.datetime.now(pytz.utc)
)
property_type = Column(Text)
built_form = Column(Text)
local_authority = Column(Text)
constituency = Column(Text)
number_of_rooms = Column(Integer)
year_built = Column(Text)
tenure = Column(Text)
current_epc_rating = Column(Enum(Epc))
current_sap_points = Column(Float)
current_valuation = Column(Float)
# Following fields are for recording already installed adjustments to a property's SAP
installed_measures_sap_point_adjustment = Column(Float)
is_sap_points_adjusted_for_installed_measures = Column(Boolean, default=False)
original_sap_points = Column(Float)
# New for re-scoring - we will need to delete some of the redundant fields but there is a ticket for this
lodged_sap_points = Column(Float)
lodged_epc_rating = Column(Enum(Epc))
class FeatureRating(enum.Enum):
VERY_GOOD = 5
GOOD = 4
AVERAGE = 3
POOR = 2
VERY_POOR = 1
NA = None
rating_lookup = {
"Very Good": FeatureRating.VERY_GOOD,
"Good": FeatureRating.GOOD,
"Average": FeatureRating.AVERAGE,
"Poor": FeatureRating.POOR,
"Very Poor": FeatureRating.VERY_POOR,
"N/A": FeatureRating.NA,
}
def get_feature_rating_from_string(rating_str: str):
return rating_lookup.get(rating_str, FeatureRating.NA)
class PropertyDetailsEpcModel(Base):
__tablename__ = "property_details_epc"
id = Column(Integer, primary_key=True, autoincrement=True)
property_id = Column(Integer, ForeignKey("property.id"), nullable=False)
portfolio_id = Column(Integer, ForeignKey("portfolio.id"), nullable=False)
full_address = Column(Text)
lodgement_date = Column(DateTime)
is_expired = Column(Boolean)
total_floor_area = Column(Float)
walls = Column(Text)
walls_rating = Column(
Integer, CheckConstraint("walls_rating>=1 AND walls_rating<=5")
)
roof = Column(Text)
roof_rating = Column(Integer, CheckConstraint("roof_rating>=1 AND roof_rating<=5"))
floor = Column(Text)
floor_rating = Column(
Integer, CheckConstraint("floor_rating>=1 AND floor_rating<=5")
)
windows = Column(Text)
windows_rating = Column(
Integer, CheckConstraint("windows_rating>=1 AND windows_rating<=5")
)
heating = Column(Text)
heating_rating = Column(
Integer, CheckConstraint("heating_rating>=1 AND heating_rating<=5")
)
heating_controls = Column(Text)
heating_controls_rating = Column(
Integer,
CheckConstraint("heating_controls_rating>=1 AND heating_controls_rating<=5"),
)
hot_water = Column(Text)
hot_water_rating = Column(
Integer, CheckConstraint("hot_water_rating>=1 AND hot_water_rating<=5")
)
lighting = Column(Text)
lighting_rating = Column(
Integer, CheckConstraint("lighting_rating>=1 AND lighting_rating<=5")
)
mainfuel = Column(Text)
ventilation = Column(Text)
solar_pv = Column(Text)
solar_hot_water = Column(Text)
wind_turbine = Column(Text)
floor_height = Column(Float)
number_heated_rooms = Column(Integer)
heat_loss_corridor = Column(Boolean)
unheated_corridor_length = Column(Float)
number_of_open_fireplaces = Column(Integer)
number_of_extensions = Column(Integer)
number_of_storeys = Column(Integer)
mains_gas = Column(Boolean)
energy_tariff = Column(Text)
primary_energy_consumption = Column(Float)
co2_emissions = Column(Float)
current_energy_demand = Column(Float)
current_energy_demand_heating_hotwater = Column(Float)
estimated = Column(Boolean, default=False)
sap_05_overwritten = Column(Boolean, default=False)
sap_05_score = Column(Integer)
sap_05_epc_rating = Column(Enum(Epc))
# Include estimates for energy bills, across the different types of energy
heating_cost_current = Column(Float)
hot_water_cost_current = Column(Float)
lighting_cost_current = Column(Float)
appliances_cost_current = Column(Float)
gas_standing_charge = Column(Float)
electricity_standing_charge = Column(Float)
# Columns for re-baselining if we have an already installed measure
original_co2_emissions = Column(Float)
original_primary_energy_consumption = Column(Float)
original_current_energy_demand = Column(Float)
original_current_energy_demand_heating_hotwater = Column(Float)
# Adjustments
installed_measures_co2_adjustment = Column(Float)
installed_measures_energy_demand_adjustment = Column(Float)
installed_measures_total_energy_bill_adjustment = Column(Float)
installed_measures_heat_demand_adjustment = Column(Float)
is_epc_adjusted_for_installed_measures = Column(Boolean, default=False)
# New columns - we'll need to delete some of the redundant fields, associated to "already installed" but
# we have a ticket for this piece of work
lodged_co2_emissions = Column(Float)
lodged_heat_demand = Column(Float)
has_been_remodelled = Column(Boolean, default=False)
environment_impact_current = Column(Float)
class PropertyDetailsSpatial(Base):
__tablename__ = "property_details_spatial"
id = Column(Integer, primary_key=True, autoincrement=True)
uprn = Column(Integer, nullable=False)
x_coordinate = Column(Float)
y_coordinate = Column(Float)
latitude = Column(Float)
longitude = Column(Float)
conservation_status = Column(Boolean)
is_listed_building = Column(Boolean)
is_heritage_building = Column(Boolean)
class PropertyDetailsMeter(Base):
__tablename__ = "property_details_meter"
id = Column(Integer, primary_key=True, autoincrement=True)
uprn = Column(Integer, nullable=False)
energy_supplier = Column(Text)
gas_supplier = Column(Text)
meter_reading_total = Column(Float)
meter_reading_electricity = Column(Float)
meter_reading_gas = Column(Float)
class PropertyTargetsModel(Base):
__tablename__ = "property_targets"
id = Column(Integer, primary_key=True, autoincrement=True)
property_id = Column(Integer, ForeignKey("property.id"), nullable=False)
portfolio_id = Column(Integer, ForeignKey("portfolio.id"), nullable=False)
created_at = Column(
DateTime, nullable=False, default=datetime.datetime.now(pytz.utc)
)
epc = Column(Enum(Epc))
heat_demand = Column(Text)
class PortfolioUsers(Base):
__tablename__ = "portfolioUsers"
id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer, ForeignKey("user.id"), nullable=False)
portfolioId = Column(Integer, ForeignKey("portfolio.id"), nullable=False)
role = Column(Text, nullable=False)
created_at = Column(
DateTime, nullable=False, default=datetime.datetime.now(pytz.utc)
)
updated_at = Column(
DateTime, nullable=False, default=datetime.datetime.now(pytz.utc)
)
class PropertyInstalledMeasures(Base):
"""
This model keeps a record of the installed measures for each property, at the UPRN level
"""
__tablename__ = "property_installed_measures"
id = Column(Integer, primary_key=True, autoincrement=True)
uprn = Column(Integer, nullable=False)
measure_type = Column(
Enum(
MaterialType,
values_callable=lambda x: [e.value for e in x],
create_constraint=False,
),
nullable=False,
)
created_at = Column(
DateTime, nullable=False, default=datetime.datetime.now(pytz.utc)
)
installed_at = Column(
DateTime, nullable=False, default=datetime.datetime.now(pytz.utc)
)