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, nullable=False) postcode = Column(Text, nullable=False) 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) )