mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
Merge branch 'main' into feature/pashub-to-ara
This commit is contained in:
commit
b03892f7fc
34 changed files with 5724 additions and 47 deletions
|
|
@ -7,6 +7,7 @@ python-jose==3.3.0
|
|||
cryptography==43.0.3
|
||||
mangum==0.19.0
|
||||
playwright==1.58.0
|
||||
pymupdf
|
||||
# AWS
|
||||
boto3==1.35.44
|
||||
# Data
|
||||
|
|
|
|||
|
|
@ -18,5 +18,6 @@ utils/tests/*
|
|||
etl/epc/tests/*
|
||||
etl/epc_clean/tests/*
|
||||
etl/spatial/tests/*
|
||||
backend/documents_parser/tests/*
|
||||
|
||||
|
||||
|
|
|
|||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -280,7 +280,7 @@ cache/
|
|||
*.pptx
|
||||
*.csv
|
||||
*.xlsx
|
||||
*.pdf
|
||||
# *.pdf
|
||||
**/Chunks/
|
||||
*.ipynb
|
||||
|
||||
|
|
|
|||
658
backend/app/db/models/epc_property.py
Normal file
658
backend/app/db/models/epc_property.py
Normal file
|
|
@ -0,0 +1,658 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
from sqlmodel import SQLModel, Field
|
||||
|
||||
from datatypes.epc.domain.epc_property_data import (
|
||||
EpcPropertyData,
|
||||
EnergyElement,
|
||||
MainHeatingDetail,
|
||||
SapBuildingPart,
|
||||
SapFloorDimension,
|
||||
SapFlatDetails,
|
||||
SapWindow,
|
||||
)
|
||||
|
||||
|
||||
class EpcPropertyModel(SQLModel, table=True):
|
||||
__tablename__ = "epc_property"
|
||||
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
property_id: int = Field(foreign_key="property.id", nullable=False)
|
||||
portfolio_id: int = Field(foreign_key="portfolio.id", nullable=False)
|
||||
|
||||
# Identity / admin
|
||||
uprn: Optional[int] = Field(default=None)
|
||||
uprn_source: Optional[str] = Field(default=None)
|
||||
report_reference: Optional[str] = Field(default=None)
|
||||
report_type: Optional[str] = Field(default=None)
|
||||
assessment_type: Optional[str] = Field(default=None)
|
||||
sap_version: Optional[float] = Field(default=None)
|
||||
schema_type: Optional[str] = Field(default=None)
|
||||
schema_versions_original: Optional[str] = Field(default=None)
|
||||
status: Optional[str] = Field(default=None)
|
||||
calculation_software_version: Optional[str] = Field(default=None)
|
||||
|
||||
# Address
|
||||
address_line_1: Optional[str] = Field(default=None)
|
||||
address_line_2: Optional[str] = Field(default=None)
|
||||
post_town: Optional[str] = Field(default=None)
|
||||
postcode: Optional[str] = Field(default=None)
|
||||
region_code: Optional[str] = Field(default=None)
|
||||
country_code: Optional[str] = Field(default=None)
|
||||
language_code: Optional[str] = Field(default=None)
|
||||
|
||||
# Property description
|
||||
dwelling_type: str
|
||||
property_type: Optional[str] = Field(default=None)
|
||||
built_form: Optional[str] = Field(default=None)
|
||||
tenure: str
|
||||
transaction_type: str
|
||||
inspection_date: str # store as ISO string; cast on read if needed
|
||||
completion_date: Optional[str] = Field(default=None)
|
||||
registration_date: Optional[str] = Field(default=None)
|
||||
total_floor_area_m2: float
|
||||
measurement_type: Optional[int] = Field(default=None)
|
||||
|
||||
# Flags
|
||||
solar_water_heating: bool
|
||||
has_hot_water_cylinder: bool
|
||||
has_fixed_air_conditioning: bool
|
||||
has_conservatory: Optional[bool] = Field(default=None)
|
||||
has_heated_separate_conservatory: Optional[bool] = Field(default=None)
|
||||
conservatory_type: Optional[int] = Field(default=None)
|
||||
|
||||
# Counts
|
||||
door_count: int
|
||||
wet_rooms_count: int
|
||||
extensions_count: int
|
||||
heated_rooms_count: int
|
||||
open_chimneys_count: int
|
||||
habitable_rooms_count: int
|
||||
insulated_door_count: int
|
||||
cfl_fixed_lighting_bulbs_count: int
|
||||
led_fixed_lighting_bulbs_count: int
|
||||
incandescent_fixed_lighting_bulbs_count: int
|
||||
blocked_chimneys_count: Optional[int] = Field(default=None)
|
||||
draughtproofed_door_count: Optional[int] = Field(default=None)
|
||||
energy_rating_average: Optional[int] = Field(default=None)
|
||||
low_energy_fixed_lighting_bulbs_count: Optional[int] = Field(default=None)
|
||||
fixed_lighting_outlets_count: Optional[int] = Field(default=None)
|
||||
low_energy_fixed_lighting_outlets_count: Optional[int] = Field(default=None)
|
||||
number_of_storeys: Optional[int] = Field(default=None)
|
||||
any_unheated_rooms: Optional[bool] = Field(default=None)
|
||||
|
||||
# Misc
|
||||
hydro: Optional[bool] = Field(default=None)
|
||||
photovoltaic_array: Optional[bool] = Field(default=None)
|
||||
waste_water_heat_recovery: Optional[str] = Field(default=None)
|
||||
pressure_test: Optional[int] = Field(default=None)
|
||||
pressure_test_certificate_number: Optional[int] = Field(default=None)
|
||||
percent_draughtproofed: Optional[int] = Field(default=None)
|
||||
insulated_door_u_value: Optional[float] = Field(default=None)
|
||||
multiple_glazed_proportion: Optional[int] = Field(default=None)
|
||||
windows_transmission_u_value: Optional[float] = Field(default=None)
|
||||
windows_transmission_data_source: Optional[int] = Field(default=None)
|
||||
windows_transmission_solar_transmittance: Optional[float] = Field(default=None)
|
||||
|
||||
# Energy source
|
||||
energy_mains_gas: bool
|
||||
energy_meter_type: str
|
||||
energy_pv_battery_count: int
|
||||
energy_wind_turbines_count: int
|
||||
energy_gas_smart_meter_present: bool
|
||||
energy_is_dwelling_export_capable: bool
|
||||
energy_wind_turbines_terrain_type: str
|
||||
energy_electricity_smart_meter_present: bool
|
||||
energy_pv_connection: Optional[str] = Field(default=None)
|
||||
energy_pv_percent_roof_area: Optional[int] = Field(default=None)
|
||||
energy_pv_battery_capacity: Optional[float] = Field(default=None)
|
||||
energy_wind_turbine_hub_height: Optional[float] = Field(default=None)
|
||||
energy_wind_turbine_rotor_diameter: Optional[float] = Field(default=None)
|
||||
|
||||
# Heating config
|
||||
heating_cylinder_size: Optional[str] = Field(default=None)
|
||||
heating_water_heating_code: Optional[int] = Field(default=None)
|
||||
heating_water_heating_fuel: Optional[int] = Field(default=None)
|
||||
heating_immersion_heating_type: Optional[str] = Field(default=None)
|
||||
heating_cylinder_insulation_type: Optional[str] = Field(default=None)
|
||||
heating_cylinder_thermostat: Optional[str] = Field(default=None)
|
||||
heating_secondary_fuel_type: Optional[int] = Field(default=None)
|
||||
heating_secondary_heating_type: Optional[str] = Field(default=None)
|
||||
heating_cylinder_insulation_thickness_mm: Optional[int] = Field(default=None)
|
||||
heating_wwhrs_index_number_1: Optional[int] = Field(default=None)
|
||||
heating_wwhrs_index_number_2: Optional[int] = Field(default=None)
|
||||
heating_shower_outlet_type: Optional[str] = Field(default=None)
|
||||
heating_shower_wwhrs: Optional[int] = Field(default=None)
|
||||
|
||||
# Ventilation
|
||||
ventilation_type: Optional[str] = Field(default=None)
|
||||
ventilation_draught_lobby: Optional[bool] = Field(default=None)
|
||||
ventilation_pressure_test: Optional[str] = Field(default=None)
|
||||
ventilation_open_flues_count: Optional[int] = Field(default=None)
|
||||
ventilation_closed_flues_count: Optional[int] = Field(default=None)
|
||||
ventilation_boiler_flues_count: Optional[int] = Field(default=None)
|
||||
ventilation_other_flues_count: Optional[int] = Field(default=None)
|
||||
ventilation_extract_fans_count: Optional[int] = Field(default=None)
|
||||
ventilation_passive_vents_count: Optional[int] = Field(default=None)
|
||||
ventilation_flueless_gas_fires_count: Optional[int] = Field(default=None)
|
||||
ventilation_in_pcdf_database: Optional[bool] = Field(default=None)
|
||||
mechanical_ventilation: Optional[int] = Field(default=None)
|
||||
mechanical_vent_duct_type: Optional[int] = Field(default=None)
|
||||
mechanical_vent_duct_placement: Optional[int] = Field(default=None)
|
||||
mechanical_vent_duct_insulation: Optional[int] = Field(default=None)
|
||||
mechanical_ventilation_index_number: Optional[int] = Field(default=None)
|
||||
mechanical_vent_measured_installation: Optional[str] = Field(default=None)
|
||||
|
||||
@classmethod
|
||||
def from_epc_property_data(
|
||||
cls,
|
||||
data: EpcPropertyData,
|
||||
property_id: int,
|
||||
portfolio_id: int,
|
||||
) -> EpcPropertyModel:
|
||||
es = data.sap_energy_source
|
||||
h = data.sap_heating
|
||||
v = data.sap_ventilation
|
||||
shower = h.shower_outlets.shower_outlet if h.shower_outlets else None
|
||||
pv = es.photovoltaic_supply
|
||||
wt = es.wind_turbine_details
|
||||
pvb = es.pv_batteries
|
||||
|
||||
return cls(
|
||||
property_id=property_id,
|
||||
portfolio_id=portfolio_id,
|
||||
uprn=data.uprn,
|
||||
uprn_source=data.uprn_source,
|
||||
report_reference=data.report_reference,
|
||||
report_type=data.report_type,
|
||||
assessment_type=data.assessment_type,
|
||||
sap_version=data.sap_version,
|
||||
schema_type=data.schema_type,
|
||||
schema_versions_original=data.schema_versions_original,
|
||||
status=data.status,
|
||||
calculation_software_version=data.calculation_software_version,
|
||||
address_line_1=data.address_line_1,
|
||||
address_line_2=data.address_line_2,
|
||||
post_town=data.post_town,
|
||||
postcode=data.postcode,
|
||||
region_code=data.region_code,
|
||||
country_code=data.country_code,
|
||||
language_code=data.language_code,
|
||||
dwelling_type=data.dwelling_type,
|
||||
property_type=data.property_type,
|
||||
built_form=data.built_form,
|
||||
tenure=data.tenure,
|
||||
transaction_type=data.transaction_type,
|
||||
inspection_date=data.inspection_date.isoformat(),
|
||||
completion_date=(
|
||||
data.completion_date.isoformat() if data.completion_date else None
|
||||
),
|
||||
registration_date=(
|
||||
data.registration_date.isoformat() if data.registration_date else None
|
||||
),
|
||||
total_floor_area_m2=data.total_floor_area_m2,
|
||||
measurement_type=data.measurement_type,
|
||||
solar_water_heating=data.solar_water_heating,
|
||||
has_hot_water_cylinder=data.has_hot_water_cylinder,
|
||||
has_fixed_air_conditioning=data.has_fixed_air_conditioning,
|
||||
has_conservatory=data.has_conservatory,
|
||||
has_heated_separate_conservatory=data.has_heated_separate_conservatory,
|
||||
conservatory_type=data.conservatory_type,
|
||||
door_count=data.door_count,
|
||||
wet_rooms_count=data.wet_rooms_count,
|
||||
extensions_count=data.extensions_count,
|
||||
heated_rooms_count=data.heated_rooms_count,
|
||||
open_chimneys_count=data.open_chimneys_count,
|
||||
habitable_rooms_count=data.habitable_rooms_count,
|
||||
insulated_door_count=data.insulated_door_count,
|
||||
cfl_fixed_lighting_bulbs_count=data.cfl_fixed_lighting_bulbs_count,
|
||||
led_fixed_lighting_bulbs_count=data.led_fixed_lighting_bulbs_count,
|
||||
incandescent_fixed_lighting_bulbs_count=data.incandescent_fixed_lighting_bulbs_count,
|
||||
blocked_chimneys_count=data.blocked_chimneys_count,
|
||||
draughtproofed_door_count=data.draughtproofed_door_count,
|
||||
energy_rating_average=data.energy_rating_average,
|
||||
low_energy_fixed_lighting_bulbs_count=data.low_energy_fixed_lighting_bulbs_count,
|
||||
fixed_lighting_outlets_count=data.fixed_lighting_outlets_count,
|
||||
low_energy_fixed_lighting_outlets_count=data.low_energy_fixed_lighting_outlets_count,
|
||||
number_of_storeys=data.number_of_storeys,
|
||||
any_unheated_rooms=data.any_unheated_rooms,
|
||||
hydro=data.hydro,
|
||||
photovoltaic_array=data.photovoltaic_array,
|
||||
waste_water_heat_recovery=data.waste_water_heat_recovery,
|
||||
pressure_test=data.pressure_test,
|
||||
pressure_test_certificate_number=data.pressure_test_certificate_number,
|
||||
percent_draughtproofed=data.percent_draughtproofed,
|
||||
insulated_door_u_value=data.insulated_door_u_value,
|
||||
multiple_glazed_proportion=data.multiple_glazed_propertion,
|
||||
windows_transmission_u_value=(
|
||||
data.windows_transmission_details.u_value
|
||||
if data.windows_transmission_details
|
||||
else None
|
||||
),
|
||||
windows_transmission_data_source=(
|
||||
data.windows_transmission_details.data_source
|
||||
if data.windows_transmission_details
|
||||
else None
|
||||
),
|
||||
windows_transmission_solar_transmittance=(
|
||||
data.windows_transmission_details.solar_transmittance
|
||||
if data.windows_transmission_details
|
||||
else None
|
||||
),
|
||||
energy_mains_gas=es.mains_gas,
|
||||
energy_meter_type=str(es.meter_type),
|
||||
energy_pv_battery_count=es.pv_battery_count,
|
||||
energy_wind_turbines_count=es.wind_turbines_count,
|
||||
energy_gas_smart_meter_present=es.gas_smart_meter_present,
|
||||
energy_is_dwelling_export_capable=es.is_dwelling_export_capable,
|
||||
energy_wind_turbines_terrain_type=str(es.wind_turbines_terrain_type),
|
||||
energy_electricity_smart_meter_present=es.electricity_smart_meter_present,
|
||||
energy_pv_connection=(
|
||||
str(es.pv_connection) if es.pv_connection is not None else None
|
||||
),
|
||||
energy_pv_percent_roof_area=(
|
||||
pv.none_or_no_details.percent_roof_area if pv else None
|
||||
),
|
||||
energy_pv_battery_capacity=pvb.pv_battery.battery_capacity if pvb else None,
|
||||
energy_wind_turbine_hub_height=wt.hub_height if wt else None,
|
||||
energy_wind_turbine_rotor_diameter=wt.rotor_diameter if wt else None,
|
||||
heating_cylinder_size=(
|
||||
str(h.cylinder_size) if h.cylinder_size is not None else None
|
||||
),
|
||||
heating_water_heating_code=h.water_heating_code,
|
||||
heating_water_heating_fuel=h.water_heating_fuel,
|
||||
heating_immersion_heating_type=(
|
||||
str(h.immersion_heating_type)
|
||||
if h.immersion_heating_type is not None
|
||||
else None
|
||||
),
|
||||
heating_cylinder_insulation_type=(
|
||||
str(h.cylinder_insulation_type)
|
||||
if h.cylinder_insulation_type is not None
|
||||
else None
|
||||
),
|
||||
heating_cylinder_thermostat=h.cylinder_thermostat,
|
||||
heating_secondary_fuel_type=h.secondary_fuel_type,
|
||||
heating_secondary_heating_type=(
|
||||
str(h.secondary_heating_type)
|
||||
if h.secondary_heating_type is not None
|
||||
else None
|
||||
),
|
||||
heating_cylinder_insulation_thickness_mm=h.cylinder_insulation_thickness_mm,
|
||||
heating_wwhrs_index_number_1=h.instantaneous_wwhrs.wwhrs_index_number1,
|
||||
heating_wwhrs_index_number_2=h.instantaneous_wwhrs.wwhrs_index_number2,
|
||||
heating_shower_outlet_type=(
|
||||
str(shower.shower_outlet_type) if shower else None
|
||||
),
|
||||
heating_shower_wwhrs=shower.shower_wwhrs if shower else None,
|
||||
ventilation_type=v.ventilation_type if v else None,
|
||||
ventilation_draught_lobby=v.draught_lobby if v else None,
|
||||
ventilation_pressure_test=v.pressure_test if v else None,
|
||||
ventilation_open_flues_count=v.open_flues_count if v else None,
|
||||
ventilation_closed_flues_count=v.closed_flues_count if v else None,
|
||||
ventilation_boiler_flues_count=v.boiler_flues_count if v else None,
|
||||
ventilation_other_flues_count=v.other_flues_count if v else None,
|
||||
ventilation_extract_fans_count=v.extract_fans_count if v else None,
|
||||
ventilation_passive_vents_count=v.passive_vents_count if v else None,
|
||||
ventilation_flueless_gas_fires_count=(
|
||||
v.flueless_gas_fires_count if v else None
|
||||
),
|
||||
ventilation_in_pcdf_database=v.ventilation_in_pcdf_database if v else None,
|
||||
mechanical_ventilation=data.mechanical_ventilation,
|
||||
mechanical_vent_duct_type=data.mechanical_vent_duct_type,
|
||||
mechanical_vent_duct_placement=data.mechanical_vent_duct_placement,
|
||||
mechanical_vent_duct_insulation=data.mechanical_vent_duct_insulation,
|
||||
mechanical_ventilation_index_number=data.mechanical_ventilation_index_number,
|
||||
mechanical_vent_measured_installation=data.mechanical_vent_measured_installation,
|
||||
)
|
||||
|
||||
|
||||
class EpcPropertyEnergyPerformanceModel(SQLModel, table=True):
|
||||
__tablename__ = "epc_property_energy_performance"
|
||||
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
epc_property_id: int = Field(
|
||||
foreign_key="epc_property.id", nullable=False, unique=True
|
||||
)
|
||||
|
||||
energy_rating_current: Optional[int] = Field(default=None)
|
||||
energy_consumption_current: Optional[int] = Field(default=None)
|
||||
environmental_impact_current: Optional[int] = Field(default=None)
|
||||
heating_cost_current: Optional[float] = Field(default=None)
|
||||
lighting_cost_current: Optional[float] = Field(default=None)
|
||||
hot_water_cost_current: Optional[float] = Field(default=None)
|
||||
co2_emissions_current: Optional[float] = Field(default=None)
|
||||
co2_emissions_current_per_floor_area: Optional[int] = Field(default=None)
|
||||
current_energy_efficiency_band: Optional[str] = Field(default=None)
|
||||
energy_rating_potential: Optional[float] = Field(default=None)
|
||||
energy_consumption_potential: Optional[int] = Field(default=None)
|
||||
environmental_impact_potential: Optional[int] = Field(default=None)
|
||||
heating_cost_potential: Optional[float] = Field(default=None)
|
||||
lighting_cost_potential: Optional[float] = Field(default=None)
|
||||
hot_water_cost_potential: Optional[float] = Field(default=None)
|
||||
co2_emissions_potential: Optional[float] = Field(default=None)
|
||||
potential_energy_efficiency_band: Optional[str] = Field(default=None)
|
||||
|
||||
@classmethod
|
||||
def from_epc_property_data(
|
||||
cls, data: EpcPropertyData, epc_property_id: int
|
||||
) -> EpcPropertyEnergyPerformanceModel:
|
||||
return cls(
|
||||
epc_property_id=epc_property_id,
|
||||
energy_rating_current=data.energy_rating_current,
|
||||
energy_consumption_current=data.energy_consumption_current,
|
||||
environmental_impact_current=data.environmental_impact_current,
|
||||
heating_cost_current=data.heating_cost_current,
|
||||
lighting_cost_current=data.lighting_cost_current,
|
||||
hot_water_cost_current=data.hot_water_cost_current,
|
||||
co2_emissions_current=data.co2_emissions_current,
|
||||
co2_emissions_current_per_floor_area=data.co2_emissions_current_per_floor_area,
|
||||
current_energy_efficiency_band=(
|
||||
data.current_energy_efficiency_band.value
|
||||
if data.current_energy_efficiency_band
|
||||
else None
|
||||
),
|
||||
energy_rating_potential=data.energy_rating_potential,
|
||||
energy_consumption_potential=data.energy_consumption_potential,
|
||||
environmental_impact_potential=data.environmental_impact_potential,
|
||||
heating_cost_potential=data.heating_cost_potential,
|
||||
lighting_cost_potential=data.lighting_cost_potential,
|
||||
hot_water_cost_potential=data.hot_water_cost_potential,
|
||||
co2_emissions_potential=data.co2_emissions_potential,
|
||||
potential_energy_efficiency_band=(
|
||||
data.potential_energy_efficiency_band.value
|
||||
if data.potential_energy_efficiency_band
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class EpcFlatDetailsModel(SQLModel, table=True):
|
||||
__tablename__ = "epc_flat_details"
|
||||
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
epc_property_id: int = Field(
|
||||
foreign_key="epc_property.id", nullable=False, unique=True
|
||||
)
|
||||
|
||||
level: int
|
||||
top_storey: str
|
||||
flat_location: int
|
||||
heat_loss_corridor: int
|
||||
storey_count: Optional[int] = Field(default=None)
|
||||
unheated_corridor_length_m: Optional[int] = Field(default=None)
|
||||
|
||||
@classmethod
|
||||
def from_domain(
|
||||
cls, flat: SapFlatDetails, epc_property_id: int
|
||||
) -> EpcFlatDetailsModel:
|
||||
return cls(
|
||||
epc_property_id=epc_property_id,
|
||||
level=flat.level,
|
||||
top_storey=flat.top_storey,
|
||||
flat_location=flat.flat_location,
|
||||
heat_loss_corridor=flat.heat_loss_corridor,
|
||||
storey_count=flat.storey_count,
|
||||
unheated_corridor_length_m=flat.unheated_corridor_length_m,
|
||||
)
|
||||
|
||||
|
||||
class EpcMainHeatingDetailModel(SQLModel, table=True):
|
||||
__tablename__ = "epc_main_heating_detail"
|
||||
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
epc_property_id: int = Field(foreign_key="epc_property.id", nullable=False)
|
||||
|
||||
has_fghrs: bool
|
||||
main_fuel_type: str
|
||||
heat_emitter_type: str
|
||||
emitter_temperature: str
|
||||
main_heating_control: str
|
||||
fan_flue_present: Optional[bool] = Field(default=None)
|
||||
boiler_flue_type: Optional[int] = Field(default=None)
|
||||
boiler_ignition_type: Optional[int] = Field(default=None)
|
||||
central_heating_pump_age: Optional[int] = Field(default=None)
|
||||
central_heating_pump_age_str: Optional[str] = Field(default=None)
|
||||
main_heating_index_number: Optional[int] = Field(default=None)
|
||||
sap_main_heating_code: Optional[int] = Field(default=None)
|
||||
main_heating_number: Optional[int] = Field(default=None)
|
||||
main_heating_category: Optional[int] = Field(default=None)
|
||||
main_heating_fraction: Optional[int] = Field(default=None)
|
||||
main_heating_data_source: Optional[int] = Field(default=None)
|
||||
condensing: Optional[bool] = Field(default=None)
|
||||
weather_compensator: Optional[bool] = Field(default=None)
|
||||
|
||||
@classmethod
|
||||
def from_domain(
|
||||
cls, detail: MainHeatingDetail, epc_property_id: int
|
||||
) -> EpcMainHeatingDetailModel:
|
||||
return cls(
|
||||
epc_property_id=epc_property_id,
|
||||
has_fghrs=detail.has_fghrs,
|
||||
main_fuel_type=str(detail.main_fuel_type),
|
||||
heat_emitter_type=str(detail.heat_emitter_type),
|
||||
emitter_temperature=str(detail.emitter_temperature),
|
||||
main_heating_control=str(detail.main_heating_control),
|
||||
fan_flue_present=detail.fan_flue_present,
|
||||
boiler_flue_type=detail.boiler_flue_type,
|
||||
boiler_ignition_type=detail.boiler_ignition_type,
|
||||
central_heating_pump_age=detail.central_heating_pump_age,
|
||||
central_heating_pump_age_str=detail.central_heating_pump_age_str,
|
||||
main_heating_index_number=detail.main_heating_index_number,
|
||||
sap_main_heating_code=detail.sap_main_heating_code,
|
||||
main_heating_number=detail.main_heating_number,
|
||||
main_heating_category=detail.main_heating_category,
|
||||
main_heating_fraction=detail.main_heating_fraction,
|
||||
main_heating_data_source=detail.main_heating_data_source,
|
||||
condensing=detail.condensing,
|
||||
weather_compensator=detail.weather_compensator,
|
||||
)
|
||||
|
||||
|
||||
class EpcBuildingPartModel(SQLModel, table=True):
|
||||
__tablename__ = "epc_building_part"
|
||||
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
epc_property_id: int = Field(foreign_key="epc_property.id", nullable=False)
|
||||
|
||||
identifier: str
|
||||
construction_age_band: str
|
||||
wall_construction: str
|
||||
wall_insulation_type: str
|
||||
wall_thickness_measured: bool
|
||||
party_wall_construction: str
|
||||
building_part_number: Optional[int] = Field(default=None)
|
||||
wall_dry_lined: Optional[bool] = Field(default=None)
|
||||
wall_thickness_mm: Optional[int] = Field(default=None)
|
||||
wall_insulation_thickness: Optional[str] = Field(default=None)
|
||||
floor_heat_loss: Optional[int] = Field(default=None)
|
||||
floor_insulation_thickness: Optional[str] = Field(default=None)
|
||||
flat_roof_insulation_thickness: Optional[str] = Field(default=None)
|
||||
floor_type: Optional[str] = Field(default=None)
|
||||
floor_construction_type: Optional[str] = Field(default=None)
|
||||
floor_insulation_type_str: Optional[str] = Field(default=None)
|
||||
floor_u_value_known: Optional[bool] = Field(default=None)
|
||||
roof_construction: Optional[int] = Field(default=None)
|
||||
roof_insulation_location: Optional[str] = Field(default=None)
|
||||
roof_insulation_thickness: Optional[str] = Field(default=None)
|
||||
room_in_roof_floor_area: Optional[float] = Field(default=None)
|
||||
room_in_roof_construction_age_band: Optional[str] = Field(default=None)
|
||||
alt_wall_1_area: Optional[float] = Field(default=None)
|
||||
alt_wall_1_dry_lined: Optional[str] = Field(default=None)
|
||||
alt_wall_1_construction: Optional[int] = Field(default=None)
|
||||
alt_wall_1_insulation_type: Optional[int] = Field(default=None)
|
||||
alt_wall_1_thickness_measured: Optional[str] = Field(default=None)
|
||||
alt_wall_1_insulation_thickness: Optional[str] = Field(default=None)
|
||||
alt_wall_2_area: Optional[float] = Field(default=None)
|
||||
alt_wall_2_dry_lined: Optional[str] = Field(default=None)
|
||||
alt_wall_2_construction: Optional[int] = Field(default=None)
|
||||
alt_wall_2_insulation_type: Optional[int] = Field(default=None)
|
||||
alt_wall_2_thickness_measured: Optional[str] = Field(default=None)
|
||||
alt_wall_2_insulation_thickness: Optional[str] = Field(default=None)
|
||||
|
||||
@classmethod
|
||||
def from_domain(
|
||||
cls, part: SapBuildingPart, epc_property_id: int
|
||||
) -> EpcBuildingPartModel:
|
||||
rir = part.sap_room_in_roof
|
||||
aw1 = part.sap_alternative_wall_1
|
||||
aw2 = part.sap_alternative_wall_2
|
||||
return cls(
|
||||
epc_property_id=epc_property_id,
|
||||
identifier=part.identifier,
|
||||
construction_age_band=part.construction_age_band,
|
||||
wall_construction=str(part.wall_construction),
|
||||
wall_insulation_type=str(part.wall_insulation_type),
|
||||
wall_thickness_measured=part.wall_thickness_measured,
|
||||
party_wall_construction=str(part.party_wall_construction),
|
||||
building_part_number=part.building_part_number,
|
||||
wall_dry_lined=part.wall_dry_lined,
|
||||
wall_thickness_mm=part.wall_thickness_mm,
|
||||
wall_insulation_thickness=part.wall_insulation_thickness,
|
||||
floor_heat_loss=part.floor_heat_loss,
|
||||
floor_insulation_thickness=part.floor_insulation_thickness,
|
||||
flat_roof_insulation_thickness=(
|
||||
str(part.flat_roof_insulation_thickness)
|
||||
if part.flat_roof_insulation_thickness is not None
|
||||
else None
|
||||
),
|
||||
floor_type=part.floor_type,
|
||||
floor_construction_type=part.floor_construction_type,
|
||||
floor_insulation_type_str=part.floor_insulation_type_str,
|
||||
floor_u_value_known=part.floor_u_value_known,
|
||||
roof_construction=part.roof_construction,
|
||||
roof_insulation_location=(
|
||||
str(part.roof_insulation_location)
|
||||
if part.roof_insulation_location is not None
|
||||
else None
|
||||
),
|
||||
roof_insulation_thickness=(
|
||||
str(part.roof_insulation_thickness)
|
||||
if part.roof_insulation_thickness is not None
|
||||
else None
|
||||
),
|
||||
room_in_roof_floor_area=float(rir.floor_area) if rir else None,
|
||||
room_in_roof_construction_age_band=(
|
||||
rir.construction_age_band if rir else None
|
||||
),
|
||||
alt_wall_1_area=aw1.wall_area if aw1 else None,
|
||||
alt_wall_1_dry_lined=aw1.wall_dry_lined if aw1 else None,
|
||||
alt_wall_1_construction=aw1.wall_construction if aw1 else None,
|
||||
alt_wall_1_insulation_type=aw1.wall_insulation_type if aw1 else None,
|
||||
alt_wall_1_thickness_measured=aw1.wall_thickness_measured if aw1 else None,
|
||||
alt_wall_1_insulation_thickness=(
|
||||
aw1.wall_insulation_thickness if aw1 else None
|
||||
),
|
||||
alt_wall_2_area=aw2.wall_area if aw2 else None,
|
||||
alt_wall_2_dry_lined=aw2.wall_dry_lined if aw2 else None,
|
||||
alt_wall_2_construction=aw2.wall_construction if aw2 else None,
|
||||
alt_wall_2_insulation_type=aw2.wall_insulation_type if aw2 else None,
|
||||
alt_wall_2_thickness_measured=aw2.wall_thickness_measured if aw2 else None,
|
||||
alt_wall_2_insulation_thickness=(
|
||||
aw2.wall_insulation_thickness if aw2 else None
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class EpcFloorDimensionModel(SQLModel, table=True):
|
||||
__tablename__ = "epc_floor_dimension"
|
||||
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
epc_building_part_id: int = Field(
|
||||
foreign_key="epc_building_part.id", nullable=False
|
||||
)
|
||||
|
||||
floor: Optional[int] = Field(default=None)
|
||||
room_height_m: float
|
||||
total_floor_area_m2: float
|
||||
party_wall_length_m: float
|
||||
heat_loss_perimeter_m: float
|
||||
floor_insulation: Optional[int] = Field(default=None)
|
||||
floor_construction: Optional[int] = Field(default=None)
|
||||
|
||||
@classmethod
|
||||
def from_domain(
|
||||
cls, dim: SapFloorDimension, epc_building_part_id: int
|
||||
) -> EpcFloorDimensionModel:
|
||||
return cls(
|
||||
epc_building_part_id=epc_building_part_id,
|
||||
floor=dim.floor,
|
||||
room_height_m=dim.room_height_m,
|
||||
total_floor_area_m2=dim.total_floor_area_m2,
|
||||
party_wall_length_m=dim.party_wall_length_m,
|
||||
heat_loss_perimeter_m=dim.heat_loss_perimeter_m,
|
||||
floor_insulation=dim.floor_insulation,
|
||||
floor_construction=dim.floor_construction,
|
||||
)
|
||||
|
||||
|
||||
class EpcWindowModel(SQLModel, table=True):
|
||||
__tablename__ = "epc_window"
|
||||
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
epc_property_id: int = Field(foreign_key="epc_property.id", nullable=False)
|
||||
|
||||
pvc_frame: str
|
||||
glazing_gap: str
|
||||
orientation: str
|
||||
window_type: str
|
||||
glazing_type: str
|
||||
window_width: float
|
||||
window_height: float
|
||||
draught_proofed: bool
|
||||
window_location: str
|
||||
window_wall_type: str
|
||||
permanent_shutters_present: bool
|
||||
frame_factor: Optional[float] = Field(default=None)
|
||||
permanent_shutters_insulated: Optional[str] = Field(default=None)
|
||||
transmission_u_value: Optional[float] = Field(default=None)
|
||||
transmission_data_source: Optional[int] = Field(default=None)
|
||||
transmission_solar_transmittance: Optional[float] = Field(default=None)
|
||||
|
||||
@classmethod
|
||||
def from_domain(cls, window: SapWindow, epc_property_id: int) -> EpcWindowModel:
|
||||
td = window.window_transmission_details
|
||||
return cls(
|
||||
epc_property_id=epc_property_id,
|
||||
pvc_frame=str(window.pvc_frame),
|
||||
glazing_gap=str(window.glazing_gap),
|
||||
orientation=str(window.orientation),
|
||||
window_type=str(window.window_type),
|
||||
glazing_type=str(window.glazing_type),
|
||||
window_width=window.window_width,
|
||||
window_height=window.window_height,
|
||||
draught_proofed=bool(window.draught_proofed),
|
||||
window_location=str(window.window_location),
|
||||
window_wall_type=str(window.window_wall_type),
|
||||
permanent_shutters_present=bool(window.permanent_shutters_present),
|
||||
frame_factor=window.frame_factor,
|
||||
permanent_shutters_insulated=window.permanent_shutters_insulated,
|
||||
transmission_u_value=td.u_value if td else None,
|
||||
transmission_data_source=td.data_source if td else None,
|
||||
transmission_solar_transmittance=td.solar_transmittance if td else None,
|
||||
)
|
||||
|
||||
|
||||
class EpcEnergyElementModel(SQLModel, table=True):
|
||||
__tablename__ = "epc_energy_element"
|
||||
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
epc_property_id: int = Field(foreign_key="epc_property.id", nullable=False)
|
||||
|
||||
element_type: str # roof | wall | floor | main_heating | window | lighting | hot_water | secondary_heating | main_heating_controls
|
||||
description: str
|
||||
energy_efficiency_rating: int
|
||||
environmental_efficiency_rating: int
|
||||
|
||||
@classmethod
|
||||
def from_domain(
|
||||
cls, element: EnergyElement, element_type: str, epc_property_id: int
|
||||
) -> EpcEnergyElementModel:
|
||||
return cls(
|
||||
epc_property_id=epc_property_id,
|
||||
element_type=element_type,
|
||||
description=element.description,
|
||||
energy_efficiency_rating=element.energy_efficiency_rating,
|
||||
environmental_efficiency_rating=element.environmental_efficiency_rating,
|
||||
)
|
||||
|
|
@ -113,8 +113,8 @@ class PropertyModel(Base):
|
|||
Enum(PortfolioStatus, values_callable=lambda x: [e.value for e in x]),
|
||||
nullable=False,
|
||||
)
|
||||
address = Column(Text)
|
||||
postcode = Column(Text)
|
||||
address = Column(Text, nullable=False)
|
||||
postcode = Column(Text, nullable=False)
|
||||
has_pre_condition_report = Column(Boolean)
|
||||
has_recommendations = Column(Boolean)
|
||||
created_at = Column(
|
||||
|
|
|
|||
0
backend/documents_parser/__init__.py
Normal file
0
backend/documents_parser/__init__.py
Normal file
692
backend/documents_parser/extractor.py
Normal file
692
backend/documents_parser/extractor.py
Normal file
|
|
@ -0,0 +1,692 @@
|
|||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
from datatypes.epc.surveys.pashub_rdsap_site_notes import (
|
||||
BuildingConstruction,
|
||||
InspectionMetadata,
|
||||
BuildingMeasurements,
|
||||
Conservatories,
|
||||
CustomerResponse,
|
||||
ExtensionConstruction,
|
||||
ExtensionMeasurements,
|
||||
ExtensionRoofSpace,
|
||||
FloorConstruction,
|
||||
FloorMeasurement,
|
||||
General,
|
||||
HeatingAndHotWater,
|
||||
MainBuildingConstruction,
|
||||
MainBuildingMeasurements,
|
||||
MainHeating,
|
||||
PasHubRdSapSiteNotes,
|
||||
Renewables,
|
||||
RoomCountElements,
|
||||
RoofSpace,
|
||||
RoofSpaceDetail,
|
||||
SecondaryHeating,
|
||||
Shower,
|
||||
SurveyAddendum,
|
||||
Ventilation,
|
||||
WaterHeating,
|
||||
WaterUse,
|
||||
Window,
|
||||
)
|
||||
|
||||
|
||||
class PasHubRdSapSiteNotesExtractor:
|
||||
def __init__(self, text_list: list[str]) -> None:
|
||||
self.text_list = text_list
|
||||
|
||||
# --- generic helpers ---
|
||||
|
||||
def _get_in_doc(self, key: str, offset: int = 1) -> Optional[str]:
|
||||
return self._get_in(self.text_list, key, offset)
|
||||
|
||||
def _bool(self, key: str, offset: int = 1) -> bool:
|
||||
val = self._get_in_doc(key, offset)
|
||||
return val is not None and val.lower() == "yes"
|
||||
|
||||
def _get_in(
|
||||
self, list_to_process: List[str], key: str, offset: int = 1
|
||||
) -> Optional[str]:
|
||||
try:
|
||||
idx = list_to_process.index(key)
|
||||
return list_to_process[idx + offset].strip() or None
|
||||
except (ValueError, IndexError):
|
||||
return None
|
||||
|
||||
def _bool_in(self, list_to_process: List[str], key: str, offset: int = 1) -> bool:
|
||||
val = self._get_in(list_to_process, key, offset)
|
||||
return val is not None and val.lower() == "yes"
|
||||
|
||||
def _optional_bool_in(self, list_to_process: List[str], key: str) -> Optional[bool]:
|
||||
val = self._get_in(list_to_process, key)
|
||||
return None if val is None else val.lower() == "yes"
|
||||
|
||||
def _is_known_in(self, list_to_process: List[str], key: str) -> bool:
|
||||
val = self._get_in(list_to_process, key)
|
||||
return val is not None and val.lower() != "not known"
|
||||
|
||||
def _wall_thickness_in(self, list_to_process: List[str]) -> int:
|
||||
val = self._get_in(list_to_process, "Wall thickness:")
|
||||
return int(val.split()[0]) if val else 0
|
||||
|
||||
def _section(self, start: str, end: str) -> List[str]:
|
||||
try:
|
||||
start_idx = self.text_list.index(start)
|
||||
end_idx = self.text_list.index(end, start_idx)
|
||||
return self.text_list[start_idx:end_idx]
|
||||
except ValueError:
|
||||
return []
|
||||
|
||||
# --- public extract methods ---
|
||||
|
||||
def extract_inspection_metadata(self) -> InspectionMetadata:
|
||||
try:
|
||||
addr_start = self.text_list.index("Property Address:") + 1
|
||||
addr_end = self.text_list.index("Property Photo", addr_start)
|
||||
property_address = ", ".join(
|
||||
t.rstrip(",") for t in self.text_list[addr_start:addr_end]
|
||||
)
|
||||
except ValueError:
|
||||
property_address = ""
|
||||
|
||||
created_on_raw = self._get_in_doc("Created On:")
|
||||
created_on = (
|
||||
datetime.strptime(created_on_raw, "%d %B %Y").strftime("%Y-%m-%d")
|
||||
if created_on_raw
|
||||
else ""
|
||||
)
|
||||
|
||||
date_of_inspection_raw = self._get_in_doc("Date of Inspection:")
|
||||
if not date_of_inspection_raw:
|
||||
raise ValueError("Date of Inspection not found in document")
|
||||
date_of_inspection = datetime.strptime(
|
||||
date_of_inspection_raw, "%d %B %Y"
|
||||
).date()
|
||||
|
||||
return InspectionMetadata(
|
||||
inspection_surveyor=self._get_in_doc("Inspection Surveyor:") or "",
|
||||
email_address=self._get_in_doc("E-Mail Address:") or "",
|
||||
report_reference=self._get_in_doc("Report Reference:") or "",
|
||||
created_on=created_on,
|
||||
date_of_inspection=date_of_inspection,
|
||||
property_address=property_address,
|
||||
property_photo="Property Photo" in self.text_list,
|
||||
)
|
||||
|
||||
def extract(self) -> PasHubRdSapSiteNotes:
|
||||
return PasHubRdSapSiteNotes(
|
||||
inspection_metadata=self.extract_inspection_metadata(),
|
||||
general=self.extract_general(),
|
||||
building_construction=self.extract_building_construction(),
|
||||
building_measurements=self.extract_building_measurements(),
|
||||
roof_space=self.extract_roof_space(),
|
||||
windows=self.extract_windows(),
|
||||
heating_and_hot_water=self.extract_heating_and_hot_water(),
|
||||
ventilation=self.extract_ventilation(),
|
||||
conservatories=self.extract_conservatories(),
|
||||
renewables=self.extract_renewables(),
|
||||
room_count_elements=self.extract_room_count_elements(),
|
||||
water_use=self.extract_water_use(),
|
||||
customer_response=self.extract_customer_response(),
|
||||
addendum=self.extract_addendum(),
|
||||
)
|
||||
|
||||
def extract_general(self) -> General:
|
||||
inspection_date_raw = self._get_in_doc("Inspection Date:")
|
||||
if not inspection_date_raw:
|
||||
raise ValueError("Inspection Date not found in document")
|
||||
inspection_date = datetime.strptime(inspection_date_raw, "%d/%m/%Y").date()
|
||||
|
||||
storeys_raw = self._get_in_doc("Number of storeys:") or "0"
|
||||
extensions_raw = self._get_in_doc("Number of Extensions:") or "0"
|
||||
_extensions_first = extensions_raw.split()[0]
|
||||
extensions_count = int(_extensions_first) if _extensions_first.isdigit() else 0
|
||||
|
||||
return General(
|
||||
epc_checked_before_assessment=self._bool(
|
||||
"Confirm you have checked for the existence of an", offset=2
|
||||
),
|
||||
epc_exists_at_point_of_assessment=self._bool(
|
||||
"Does an EPC exist at the point of carrying out this", offset=2
|
||||
),
|
||||
inspection_date=inspection_date,
|
||||
transaction_type=self._get_in_doc("Transaction Type:") or "",
|
||||
tenure=self._get_in_doc("Tenure:") or "",
|
||||
property_type=self._get_in_doc("Type of Property:") or "",
|
||||
detachment_type=self._get_in_doc("Detachment Type:") or "",
|
||||
number_of_storeys=int(storeys_raw.split()[0]),
|
||||
terrain_type=self._get_in_doc("Terrain Type:") or "",
|
||||
number_of_extensions=extensions_count,
|
||||
electricity_smart_meter=self._bool(
|
||||
"Is an electricity smart meter present?"
|
||||
),
|
||||
electric_meter_type=self._get_in_doc("Electric meter type:") or "",
|
||||
dwelling_export_capable=self._bool("Is the dwelling export-capable?"),
|
||||
mains_gas_available=self._bool("Is mains gas available?"),
|
||||
gas_smart_meter=self._bool("Is there a gas smart meter?"),
|
||||
gas_meter_accessible=self._bool("Is the gas meter accessible?"),
|
||||
measurements_location=self._get_in_doc("Select Measurements Location:")
|
||||
or "",
|
||||
)
|
||||
|
||||
def extract_building_construction(self) -> BuildingConstruction:
|
||||
bc_section = self._section("Building Construction", "Building Measurements")
|
||||
|
||||
# Find extension markers within this section
|
||||
extension_markers = []
|
||||
i = 1
|
||||
while f"Extension {i}" in bc_section:
|
||||
extension_markers.append(f"Extension {i}")
|
||||
i += 1
|
||||
|
||||
# Slice main building data: from "Main Building" to first extension or end
|
||||
main_start = bc_section.index("Main Building")
|
||||
main_end = (
|
||||
bc_section.index(extension_markers[0])
|
||||
if extension_markers
|
||||
else len(bc_section)
|
||||
)
|
||||
main_data = bc_section[main_start:main_end]
|
||||
|
||||
# Slice each extension's data
|
||||
extensions = []
|
||||
for n, marker in enumerate(extension_markers):
|
||||
ext_start = bc_section.index(marker)
|
||||
ext_end = (
|
||||
bc_section.index(extension_markers[n + 1])
|
||||
if n + 1 < len(extension_markers)
|
||||
else len(bc_section)
|
||||
)
|
||||
ext_data = bc_section[ext_start:ext_end]
|
||||
extensions.append(self._parse_extension_construction(n + 1, ext_data))
|
||||
|
||||
return BuildingConstruction(
|
||||
main_building=self._parse_main_building_construction(main_data),
|
||||
floor=self._parse_floor_construction(main_data),
|
||||
extensions=extensions if extensions else None,
|
||||
)
|
||||
|
||||
# --- private parsing helpers ---
|
||||
|
||||
def _parse_main_building_construction(
|
||||
self, data: List[str]
|
||||
) -> MainBuildingConstruction:
|
||||
return MainBuildingConstruction(
|
||||
age_range=self._get_in(data, "Age Range:") or "",
|
||||
age_indicators=self._get_in(data, "Record indicators of property age:")
|
||||
or "",
|
||||
walls_construction_type=self._get_in(data, "Walls - Construction Type:")
|
||||
or "",
|
||||
cavity_construction_indicators=self._get_in(
|
||||
data, "Record external indicators of Cavity Construction:"
|
||||
)
|
||||
or "",
|
||||
walls_insulation_type=self._get_in(data, "Walls - Insulation Type:") or "",
|
||||
filled_cavity_indicators=self._get_in(
|
||||
data, "Record indicators of filled cavity:"
|
||||
),
|
||||
thermal_conductivity_of_wall_insulation=self._get_in(
|
||||
data, "Thermal conductivity of wall insulation:"
|
||||
)
|
||||
or "",
|
||||
wall_u_value_known=self._is_known_in(data, "Wall U-Value known?"),
|
||||
wall_thickness_mm=self._wall_thickness_in(data),
|
||||
party_wall_construction_type=self._get_in(
|
||||
data, "Party wall construction type:"
|
||||
)
|
||||
or "",
|
||||
)
|
||||
|
||||
def _parse_extension_construction(
|
||||
self, ext_id: int, data: List[str]
|
||||
) -> ExtensionConstruction:
|
||||
return ExtensionConstruction(
|
||||
id=ext_id,
|
||||
age_range=self._get_in(data, "Age Range:") or "",
|
||||
age_indicators=self._get_in(data, "Record indicators of property age:")
|
||||
or "",
|
||||
walls_construction_type=self._get_in(data, "Walls - Construction Type:")
|
||||
or "",
|
||||
cavity_construction_indicators=self._get_in(
|
||||
data, "Record external indicators of Cavity Construction:"
|
||||
)
|
||||
or "",
|
||||
walls_insulation_type=self._get_in(data, "Walls - Insulation Type:") or "",
|
||||
filled_cavity_indicators=self._get_in(
|
||||
data, "Record indicators of filled cavity:"
|
||||
),
|
||||
thermal_conductivity_of_wall_insulation=self._get_in(
|
||||
data, "Thermal conductivity of wall insulation:"
|
||||
)
|
||||
or "",
|
||||
wall_u_value_known=self._is_known_in(data, "Wall U-Value known?"),
|
||||
wall_thickness_mm=self._wall_thickness_in(data),
|
||||
party_wall_construction_type=self._get_in(
|
||||
data, "Party wall construction type:"
|
||||
)
|
||||
or "",
|
||||
)
|
||||
|
||||
def extract_building_measurements(self) -> BuildingMeasurements:
|
||||
bm_section = self._section("Building Measurements", "Roof Space")
|
||||
|
||||
extension_markers = []
|
||||
i = 1
|
||||
while f"Extension {i}" in bm_section:
|
||||
extension_markers.append(f"Extension {i}")
|
||||
i += 1
|
||||
|
||||
main_start = bm_section.index("Main Building")
|
||||
main_end = (
|
||||
bm_section.index(extension_markers[0])
|
||||
if extension_markers
|
||||
else len(bm_section)
|
||||
)
|
||||
main_floors = self._parse_floor_measurements(bm_section[main_start:main_end])
|
||||
|
||||
extensions = []
|
||||
for n, marker in enumerate(extension_markers):
|
||||
ext_start = bm_section.index(marker)
|
||||
ext_end = (
|
||||
bm_section.index(extension_markers[n + 1])
|
||||
if n + 1 < len(extension_markers)
|
||||
else len(bm_section)
|
||||
)
|
||||
extensions.append(
|
||||
ExtensionMeasurements(
|
||||
id=n + 1,
|
||||
floors=self._parse_floor_measurements(
|
||||
bm_section[ext_start:ext_end]
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
return BuildingMeasurements(
|
||||
main_building=MainBuildingMeasurements(floors=main_floors),
|
||||
extensions=extensions if extensions else None,
|
||||
)
|
||||
|
||||
def extract_roof_space(self) -> RoofSpace:
|
||||
rs_section = self._section("Roof Space", "Windows")
|
||||
|
||||
extension_markers = []
|
||||
i = 1
|
||||
while f"Extension {i}" in rs_section:
|
||||
extension_markers.append(f"Extension {i}")
|
||||
i += 1
|
||||
|
||||
main_start = rs_section.index("Main Building")
|
||||
main_end = (
|
||||
rs_section.index(extension_markers[0])
|
||||
if extension_markers
|
||||
else len(rs_section)
|
||||
)
|
||||
main_data = rs_section[main_start:main_end]
|
||||
|
||||
extensions = []
|
||||
for n, marker in enumerate(extension_markers):
|
||||
ext_start = rs_section.index(marker)
|
||||
ext_end = (
|
||||
rs_section.index(extension_markers[n + 1])
|
||||
if n + 1 < len(extension_markers)
|
||||
else len(rs_section)
|
||||
)
|
||||
ext_data = rs_section[ext_start:ext_end]
|
||||
extensions.append(self._parse_extension_roof_space(n + 1, ext_data))
|
||||
|
||||
return RoofSpace(
|
||||
main_building=self._parse_roof_space_detail(main_data),
|
||||
extensions=extensions if extensions else None,
|
||||
)
|
||||
|
||||
def extract_windows(self) -> List[Window]:
|
||||
w_section = self._section("Windows", "Heating & Hot Water")
|
||||
|
||||
windows = []
|
||||
n = 1
|
||||
while f"Window {n}" in w_section:
|
||||
start = w_section.index(f"Window {n}")
|
||||
end = (
|
||||
w_section.index(f"Window {n + 1}")
|
||||
if f"Window {n + 1}" in w_section
|
||||
else len(w_section)
|
||||
)
|
||||
windows.append(self._parse_window(n, w_section[start:end]))
|
||||
n += 1
|
||||
|
||||
return windows
|
||||
|
||||
def extract_heating_and_hot_water(self) -> HeatingAndHotWater:
|
||||
hhw_section = self._section("Heating & Hot Water", "Ventilation")
|
||||
return HeatingAndHotWater(
|
||||
main_heating=self._parse_main_heating(hhw_section),
|
||||
secondary_heating=self._parse_secondary_heating(hhw_section),
|
||||
water_heating=self._parse_water_heating(hhw_section),
|
||||
)
|
||||
|
||||
def extract_ventilation(self) -> Ventilation:
|
||||
v_section = self._section("Ventilation", "Conservatories")
|
||||
return Ventilation(
|
||||
ventilation_type=self._get_in(v_section, "Ventilation type:") or "",
|
||||
has_fixed_air_conditioning=self._bool_in(
|
||||
v_section, "Has fixed air conditioning?"
|
||||
),
|
||||
number_of_open_flues=int(
|
||||
self._get_in(v_section, "Number of open flues:") or 0
|
||||
),
|
||||
number_of_closed_flues=int(
|
||||
self._get_in(v_section, "Number of closed flues:") or 0
|
||||
),
|
||||
number_of_boiler_flues=int(
|
||||
self._get_in(v_section, "Number of boiler flues:") or 0
|
||||
),
|
||||
number_of_other_flues=int(
|
||||
self._get_in(v_section, "Number of other flues:") or 0
|
||||
),
|
||||
number_of_extract_fans=int(
|
||||
self._get_in(v_section, "Number of extract fans:") or 0
|
||||
),
|
||||
number_of_passive_vents=int(
|
||||
self._get_in(v_section, "Number of passive vents:") or 0
|
||||
),
|
||||
number_of_flueless_gas_fires=int(
|
||||
self._get_in(v_section, "Number of flueless gas fires:") or 0
|
||||
),
|
||||
pressure_test=self._get_in(v_section, "Pressure test:") or "",
|
||||
draught_lobby=self._bool_in(v_section, "Is there a draught lobby?"),
|
||||
ventilation_in_pcdf_database=self._optional_bool_in(
|
||||
v_section, "Is the ventilation in the PCDF database?"
|
||||
),
|
||||
)
|
||||
|
||||
def extract_conservatories(self) -> Conservatories:
|
||||
c_section = self._section("Conservatories", "Renewables")
|
||||
val = self._get_in(c_section, "Is there conservatory?")
|
||||
return Conservatories(
|
||||
has_conservatory=val is not None and val.lower() != "no conservatory"
|
||||
)
|
||||
|
||||
def extract_renewables(self) -> Renewables:
|
||||
r_section = self._section("Renewables", "Room Count Elements")
|
||||
batteries_raw = self._get_in(r_section, "Number of PV batteries:")
|
||||
batteries = (
|
||||
0
|
||||
if batteries_raw is None or batteries_raw.lower() == "none"
|
||||
else int(batteries_raw)
|
||||
)
|
||||
pv_connection = self._get_in(r_section, "PV Connection:")
|
||||
percent_raw = self._get_in(r_section, "Percentage of roof covered with photovoltaic array?")
|
||||
percent_roof = int(percent_raw.split()[0]) if percent_raw else None
|
||||
return Renewables(
|
||||
wind_turbines=self._bool_in(r_section, "Has wind turbines?"),
|
||||
solar_hot_water=self._bool_in(r_section, "Has solar hot water?"),
|
||||
photovoltaic_array=self._bool_in(r_section, "Has photovoltaic array?"),
|
||||
number_of_pv_batteries=batteries,
|
||||
hydro=self._bool_in(r_section, "Is the dwelling connected to Hydro?"),
|
||||
pv_connection=pv_connection,
|
||||
percent_roof_covered_pv=percent_roof,
|
||||
)
|
||||
|
||||
def extract_room_count_elements(self) -> RoomCountElements:
|
||||
rce_section = self._section("Room Count Elements", "Customer Response")
|
||||
heated_rooms_raw = self._get_in(rce_section, "Number of heated rooms?")
|
||||
return RoomCountElements(
|
||||
number_of_habitable_rooms=int(
|
||||
self._get_in(rce_section, "Number of habitable rooms?") or 0
|
||||
),
|
||||
any_unheated_rooms=self._bool_in(
|
||||
rce_section, "Are any of these rooms unheated?"
|
||||
),
|
||||
number_of_heated_rooms=int(heated_rooms_raw) if heated_rooms_raw else None,
|
||||
number_of_external_doors=int(
|
||||
self._get_in(rce_section, "Number of external doors?") or 0
|
||||
),
|
||||
number_of_insulated_external_doors=int(
|
||||
self._get_in(rce_section, "Number of insulated external doors?") or 0
|
||||
),
|
||||
number_of_draughtproofed_external_doors=int(
|
||||
self._get_in(rce_section, "Number of draughtproofed external doors?")
|
||||
or 0
|
||||
),
|
||||
number_of_open_chimneys=int(
|
||||
self._get_in(rce_section, "Number of open chimneys?") or 0
|
||||
),
|
||||
number_of_blocked_chimneys=int(
|
||||
self._get_in(rce_section, "Number of blocked chimneys?") or 0
|
||||
),
|
||||
number_of_fixed_incandescent_bulbs=int(
|
||||
self._get_in(rce_section, "Number of fixed incandescent bulbs:") or 0
|
||||
),
|
||||
exact_led_cfl_count_known=self._bool_in(
|
||||
rce_section, "Is the exact number of LED and CFL bulbs known?"
|
||||
),
|
||||
number_of_fixed_led_bulbs=int(
|
||||
self._get_in(rce_section, "Number of fixed LED bulbs:") or 0
|
||||
),
|
||||
number_of_fixed_cfl_bulbs=int(
|
||||
self._get_in(rce_section, "Number of fixed CFL bulbs:") or 0
|
||||
),
|
||||
waste_water_heat_recovery=self._get_in(
|
||||
rce_section, "Are there any waste water heat recovery systems?"
|
||||
)
|
||||
or "",
|
||||
)
|
||||
|
||||
def extract_water_use(self) -> WaterUse:
|
||||
wu_section = self._section("Room Count Elements", "Customer Response")
|
||||
baths_raw = self._get_in(wu_section, "Number of baths:") or "0"
|
||||
special_raw = (
|
||||
self._get_in(
|
||||
wu_section, "How many special features are there at the", offset=2
|
||||
)
|
||||
or "0"
|
||||
)
|
||||
|
||||
showers = []
|
||||
n = 1
|
||||
while f"Shower {n}" in wu_section:
|
||||
start = wu_section.index(f"Shower {n}")
|
||||
end = (
|
||||
wu_section.index(f"Shower {n + 1}")
|
||||
if f"Shower {n + 1}" in wu_section
|
||||
else len(wu_section)
|
||||
)
|
||||
shower_data = wu_section[start:end]
|
||||
showers.append(
|
||||
Shower(
|
||||
id=n,
|
||||
outlet_type=self._get_in(shower_data, "Shower outlet type:") or "",
|
||||
)
|
||||
)
|
||||
n += 1
|
||||
|
||||
return WaterUse(
|
||||
number_of_baths=int(baths_raw),
|
||||
number_of_special_features=int(special_raw),
|
||||
showers=showers,
|
||||
)
|
||||
|
||||
def extract_customer_response(self) -> CustomerResponse:
|
||||
cr_section = self._section(
|
||||
"Customer Response", "Addendum + Related Party Disclosure"
|
||||
)
|
||||
return CustomerResponse(
|
||||
customer_present=self._bool_in(cr_section, "Customer present?"),
|
||||
willing_to_answer_satisfaction_survey=self._bool_in(
|
||||
cr_section, "Customer willing to answer satisfaction survey?"
|
||||
),
|
||||
)
|
||||
|
||||
def extract_addendum(self) -> SurveyAddendum:
|
||||
a_section = self._section(
|
||||
"Addendum + Related Party Disclosure", "Photographs Required"
|
||||
)
|
||||
return SurveyAddendum(
|
||||
addendum=self._get_in(a_section, "Addendum") or "",
|
||||
related_party_disclosure=self._get_in(a_section, "Related party disclosure")
|
||||
or "",
|
||||
hard_to_treat_cavity_access_issues=self._bool_in(
|
||||
a_section,
|
||||
"Hard to treat cavity walls: Property has access",
|
||||
offset=2,
|
||||
),
|
||||
hard_to_treat_cavity_high_exposure=self._bool_in(
|
||||
a_section,
|
||||
"Hard to treat cavity walls: Property has high",
|
||||
offset=2,
|
||||
),
|
||||
hard_to_treat_cavity_narrow_cavities=self._bool_in(
|
||||
a_section,
|
||||
"Hard to treat cavity walls: Property has narrow",
|
||||
offset=2,
|
||||
),
|
||||
)
|
||||
|
||||
def _parse_main_heating(self, data: List[str]) -> MainHeating:
|
||||
return MainHeating(
|
||||
selection_method=self._get_in(
|
||||
data, "How would you like to select the Heating System?"
|
||||
)
|
||||
or "",
|
||||
system_type=self._get_in(data, "System type:") or "",
|
||||
product_id=int(self._get_in(data, "Product Id") or 0),
|
||||
manufacturer=self._get_in(data, "Manufacturer") or "",
|
||||
model=self._get_in(data, "Model") or "",
|
||||
orig_manufacturer=self._get_in(data, "Orig Manuf") or "",
|
||||
fuel=self._get_in(data, "Fuel") or "",
|
||||
summer_efficiency=float(self._get_in(data, "S. Efficiency") or 0),
|
||||
type=self._get_in(data, "Type") or "",
|
||||
condensing=self._bool_in(data, "Condensing"),
|
||||
year=self._get_in(data, "Year") or "",
|
||||
mount=self._get_in(data, "Mount") or "",
|
||||
open_flue=self._get_in(data, "Open Flue") or "",
|
||||
fan_assist=self._bool_in(data, "Fan Assist"),
|
||||
status=self._get_in(data, "Status") or "",
|
||||
central_heating_pump_age=self._get_in(data, "Central heating pump age:")
|
||||
or "",
|
||||
controls=self._get_in(data, "Controls:") or "",
|
||||
flue_gas_heat_recovery_system=self._bool_in(
|
||||
data, "Does the boiler have a Flue Gas Heat Recover", offset=2
|
||||
),
|
||||
weather_compensator=self._bool_in(data, "Is there a weather compensator?"),
|
||||
emitter=self._get_in(data, "Emitter:") or "",
|
||||
emitter_temperature=self._get_in(data, "Emitter Temperature:") or "",
|
||||
)
|
||||
|
||||
def _parse_secondary_heating(self, data: List[str]) -> SecondaryHeating:
|
||||
system_raw = self._get_in(data, "Secondary System:")
|
||||
return SecondaryHeating(
|
||||
secondary_fuel=self._get_in(data, "Secondary Fuel") or "",
|
||||
secondary_system=system_raw if system_raw else None,
|
||||
)
|
||||
|
||||
def _parse_water_heating(self, data: List[str]) -> WaterHeating:
|
||||
thickness_raw = self._get_in(data, "Insulation Thickness (mm):") or self._get_in(data, "Thickness:")
|
||||
thickness_mm = int(thickness_raw.split()[0]) if thickness_raw else None
|
||||
return WaterHeating(
|
||||
type=self._get_in(data, "Water Heating Type:") or "",
|
||||
system=self._get_in(data, "Water Heating System:") or "",
|
||||
cylinder_size=self._get_in(data, "Cylinder Size:") or "",
|
||||
cylinder_measured_heat_loss=self._get_in(
|
||||
data, "Cylinder Measured Heat Loss:"
|
||||
),
|
||||
insulation_type=self._get_in(data, "Insulation Type:"),
|
||||
insulation_thickness_mm=thickness_mm,
|
||||
has_thermostat=self._optional_bool_in(data, "Cylinder Thermostat:") or self._optional_bool_in(data, "Has thermostat?"),
|
||||
immersion_type=self._get_in(data, "Immersion:"),
|
||||
)
|
||||
|
||||
def _parse_window(self, window_id: int, data: List[str]) -> Window:
|
||||
height_raw = self._get_in(data, "Window height:")
|
||||
width_raw = self._get_in(data, "Window width:")
|
||||
return Window(
|
||||
id=window_id,
|
||||
location=self._get_in(data, "Window location:") or "",
|
||||
wall_type=self._get_in(data, "Window wall type:") or "",
|
||||
glazing_type=self._get_in(data, "Glazing Type:") or "",
|
||||
window_type=self._get_in(data, "Window type:") or "",
|
||||
frame_type=self._get_in(data, "Window frame type:") or "",
|
||||
glazing_gap=self._get_in(data, "What size is the glazing gap?") or "",
|
||||
draught_proofed=self._bool_in(data, "Is the window draught proofed?"),
|
||||
permanent_shutters=self._bool_in(
|
||||
data, "Are there permanent shutters present?"
|
||||
),
|
||||
height_m=float(height_raw.split()[0]) if height_raw else 0.0,
|
||||
width_m=float(width_raw.split()[0]) if width_raw else 0.0,
|
||||
orientation=self._get_in(data, "Orientation:") or "",
|
||||
)
|
||||
|
||||
def _parse_insulation_thickness(
|
||||
self, val: Optional[str]
|
||||
) -> tuple[Optional[int], Optional[str]]:
|
||||
if val is None:
|
||||
return None, None
|
||||
try:
|
||||
return int(val.split()[0]), None
|
||||
except (ValueError, IndexError):
|
||||
return None, val
|
||||
|
||||
def _parse_roof_space_detail(self, data: List[str]) -> RoofSpaceDetail:
|
||||
thickness_mm, thickness_str = self._parse_insulation_thickness(
|
||||
self._get_in(data, "Roofs - Insulation Thickness:")
|
||||
)
|
||||
return RoofSpaceDetail(
|
||||
construction_type=self._get_in(data, "Roofs - Construction Type:") or "",
|
||||
insulation_at=self._get_in(data, "Roofs - Insulation At:") or "",
|
||||
roof_u_value_known=self._is_known_in(data, "Roof U-Value:"),
|
||||
cavity_wall_construction_indicators=self._get_in(
|
||||
data, "Record indicators of Cavity Wall Construction in roof", offset=2
|
||||
)
|
||||
or "",
|
||||
rooms_in_roof=self._bool_in(data, "Are there rooms in the roof?"),
|
||||
insulation_thickness_mm=thickness_mm,
|
||||
insulation_thickness=thickness_str,
|
||||
)
|
||||
|
||||
def _parse_extension_roof_space(
|
||||
self, ext_id: int, data: List[str]
|
||||
) -> ExtensionRoofSpace:
|
||||
thickness_mm, thickness_str = self._parse_insulation_thickness(
|
||||
self._get_in(data, "Roofs - Insulation Thickness:")
|
||||
)
|
||||
return ExtensionRoofSpace(
|
||||
id=ext_id,
|
||||
construction_type=self._get_in(data, "Roofs - Construction Type:") or "",
|
||||
insulation_at=self._get_in(data, "Roofs - Insulation At:") or "",
|
||||
roof_u_value_known=self._is_known_in(data, "Roof U-Value:"),
|
||||
cavity_wall_construction_indicators=self._get_in(
|
||||
data, "Record indicators of Cavity Wall Construction in roof", offset=2
|
||||
)
|
||||
or "",
|
||||
rooms_in_roof=self._bool_in(data, "Are there rooms in the roof?"),
|
||||
insulation_thickness_mm=thickness_mm,
|
||||
insulation_thickness=thickness_str,
|
||||
)
|
||||
|
||||
def _parse_floor_measurements(self, data: List[str]) -> List[FloorMeasurement]:
|
||||
floors = []
|
||||
i = 0
|
||||
while i < len(data):
|
||||
if data[i].startswith("Floor") and i + 4 < len(data):
|
||||
floors.append(
|
||||
FloorMeasurement(
|
||||
name=data[i],
|
||||
area_m2=float(data[i + 1]),
|
||||
height_m=float(data[i + 2]),
|
||||
heat_loss_perimeter_m=float(data[i + 3]),
|
||||
pwl_m=float(data[i + 4]),
|
||||
)
|
||||
)
|
||||
i += 5
|
||||
else:
|
||||
i += 1
|
||||
return floors
|
||||
|
||||
def _parse_floor_construction(self, data: List[str]) -> FloorConstruction:
|
||||
return FloorConstruction(
|
||||
floor_type=self._get_in(data, "Floor type:") or "",
|
||||
floor_construction=self._get_in(data, "Floor Construction:") or "",
|
||||
floor_insulation_type=self._get_in(data, "Floor Insulation Type:") or "",
|
||||
floor_u_value_known=self._is_known_in(data, "Floor U-Value known?"),
|
||||
)
|
||||
39
backend/documents_parser/handler/handler.py
Normal file
39
backend/documents_parser/handler/handler.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import os
|
||||
from typing import Any, Mapping
|
||||
|
||||
|
||||
from utils.logger import setup_logger
|
||||
from utils.s3 import upload_file_to_s3
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
BUCKET = "retrofit-energy-assessments-dev"
|
||||
PDF_S3_KEY = "example/SiteNotesExample.pdf"
|
||||
PDF_LOCAL_PATH = os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
"..",
|
||||
"..",
|
||||
"tests",
|
||||
"test_data",
|
||||
"SiteNotesExample.pdf",
|
||||
)
|
||||
|
||||
|
||||
def upload_pdf(local_path: str, bucket: str, key: str) -> None:
|
||||
logger.info(f"Uploading {local_path} to s3://{bucket}/{key}")
|
||||
upload_file_to_s3(local_path, bucket, key)
|
||||
logger.info("Upload complete")
|
||||
|
||||
|
||||
def handler(event: Mapping[str, Any], context: Any) -> None:
|
||||
logger.info("Entered handler")
|
||||
|
||||
output_path = os.path.join(os.path.dirname(__file__), "..", "textract_blocks.json")
|
||||
|
||||
upload_pdf(PDF_LOCAL_PATH, BUCKET, PDF_S3_KEY)
|
||||
|
||||
logger.info(f"Uploaded file to {output_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
handler({}, None)
|
||||
12
backend/documents_parser/pdf.py
Normal file
12
backend/documents_parser/pdf.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
from typing import List
|
||||
|
||||
import pymupdf
|
||||
|
||||
|
||||
def pdf_to_text_list(pdf_bytes: bytes) -> List[str]:
|
||||
tokens: List[str] = []
|
||||
with pymupdf.open(stream=pdf_bytes, filetype="pdf") as doc:
|
||||
for page in doc:
|
||||
for line in page.get_text().split("\n"):
|
||||
tokens.append(line)
|
||||
return tokens
|
||||
0
backend/documents_parser/tests/__init__.py
Normal file
0
backend/documents_parser/tests/__init__.py
Normal file
BIN
backend/documents_parser/tests/fixtures/ExampleSiteNotes.pdf
vendored
Normal file
BIN
backend/documents_parser/tests/fixtures/ExampleSiteNotes.pdf
vendored
Normal file
Binary file not shown.
BIN
backend/documents_parser/tests/fixtures/ExampleSiteNotes_2.pdf
vendored
Normal file
BIN
backend/documents_parser/tests/fixtures/ExampleSiteNotes_2.pdf
vendored
Normal file
Binary file not shown.
BIN
backend/documents_parser/tests/fixtures/ExampleSiteNotes_3.pdf
vendored
Normal file
BIN
backend/documents_parser/tests/fixtures/ExampleSiteNotes_3.pdf
vendored
Normal file
Binary file not shown.
BIN
backend/documents_parser/tests/fixtures/ExampleSiteNotes_4.pdf
vendored
Normal file
BIN
backend/documents_parser/tests/fixtures/ExampleSiteNotes_4.pdf
vendored
Normal file
Binary file not shown.
BIN
backend/documents_parser/tests/fixtures/ExampleSiteNotes_5.pdf
vendored
Normal file
BIN
backend/documents_parser/tests/fixtures/ExampleSiteNotes_5.pdf
vendored
Normal file
Binary file not shown.
BIN
backend/documents_parser/tests/fixtures/ExampleSiteNotes_6.pdf
vendored
Normal file
BIN
backend/documents_parser/tests/fixtures/ExampleSiteNotes_6.pdf
vendored
Normal file
Binary file not shown.
737
backend/documents_parser/tests/fixtures/site_notes_example_2_text.json
vendored
Normal file
737
backend/documents_parser/tests/fixtures/site_notes_example_2_text.json
vendored
Normal file
|
|
@ -0,0 +1,737 @@
|
|||
[
|
||||
"SMART EPC: Record of",
|
||||
"Inspection & Site Notes",
|
||||
"Inspection Surveyor:",
|
||||
"Rebecca Mcwilliam",
|
||||
"E-Mail Address:",
|
||||
"rebeccamcdea@gmail.com",
|
||||
"Report Reference:",
|
||||
"ADD992DF-BE9B-47EB-8121-4C6143C6A7C9",
|
||||
"Created On:",
|
||||
"15 October 2025",
|
||||
"Date of Inspection:",
|
||||
"13 October 2025",
|
||||
"Property Address:",
|
||||
"13,",
|
||||
"Sandfield Avenue ,",
|
||||
"Wrenbury,",
|
||||
"CW5 8EU",
|
||||
"Property Photo",
|
||||
"Page 1",
|
||||
"",
|
||||
"Photo of electricity meter:",
|
||||
"Photo of electricity meter:",
|
||||
"Photo of electricity meter:",
|
||||
"Photo of electricity meter:",
|
||||
"RdSAP Assessment",
|
||||
"General",
|
||||
"Confirm you have checked for the existence of an",
|
||||
"EPC before carrying out another energy assessment.",
|
||||
"Yes",
|
||||
"Does an EPC exist at the point of carrying out this",
|
||||
"energy assessment?",
|
||||
"Yes",
|
||||
"Please select why another energy assessment needs",
|
||||
"to be undertaken:",
|
||||
"Assessor instructed to produce a new EPC upon request from building",
|
||||
"owner/tenant/landlord after confirming to the requestor that a valid EPC",
|
||||
"already exists",
|
||||
"Inspection Date:",
|
||||
"13/10/2025",
|
||||
"Transaction Type:",
|
||||
"None of the Above",
|
||||
"Tenure:",
|
||||
"Rented Social",
|
||||
"Type of Property:",
|
||||
"House",
|
||||
"Detachment Type:",
|
||||
"End-terrace",
|
||||
"Number of storeys:",
|
||||
"2 Storeys",
|
||||
"Terrain Type:",
|
||||
"Suburban",
|
||||
"Number of Extensions:",
|
||||
"No Extensions",
|
||||
"Is an electricity smart meter present?",
|
||||
"Yes",
|
||||
"Electric meter type:",
|
||||
"Single",
|
||||
"Page 2",
|
||||
"",
|
||||
"Photo of electricity meter:",
|
||||
"Photo of electricity meter:",
|
||||
"External indicators of Cavity Wall Construction:",
|
||||
"Is the dwelling export-capable?",
|
||||
"Yes",
|
||||
"Is mains gas available?",
|
||||
"No",
|
||||
"Select Measurements Location:",
|
||||
"Internal",
|
||||
"Building Construction",
|
||||
"Main Building",
|
||||
"Age Range:",
|
||||
"1950-1966",
|
||||
"Record indicators of property age:",
|
||||
"local knowledge, enquiries of owner, period building features",
|
||||
"Walls - Construction Type:",
|
||||
"Cavity",
|
||||
"Record external indicators of Cavity Construction:",
|
||||
"stretcher bond, wall thickness over 270 mm",
|
||||
"Walls - Insulation Type:",
|
||||
"Filled Cavity",
|
||||
"Record indicators of filled cavity:",
|
||||
"evidence of cavity fill drill holes, Boroscope",
|
||||
"Page 3",
|
||||
"",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Page 4",
|
||||
"",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Page 5",
|
||||
"",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Page 6",
|
||||
"",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Page 7",
|
||||
"",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Page 8",
|
||||
"",
|
||||
"Photo wall thickness:",
|
||||
"Photo wall thickness:",
|
||||
"Thermal conductivity of wall insulation:",
|
||||
"Unknown",
|
||||
"Wall U-Value known?",
|
||||
"Not Known",
|
||||
"Wall thickness:",
|
||||
"300 mm",
|
||||
"Party wall construction type:",
|
||||
"Unable to determine",
|
||||
"Floor type:",
|
||||
"Ground Floor",
|
||||
"Floor Construction:",
|
||||
"Solid",
|
||||
"Floor Insulation Type:",
|
||||
"As Built",
|
||||
"Floor U-Value known?",
|
||||
"Not Known",
|
||||
"Building Measurements",
|
||||
"Area (m2)",
|
||||
"Height (m)",
|
||||
"Heat Loss Perimeter (m)",
|
||||
"PWL (m)",
|
||||
"Main Building",
|
||||
"Floor 1",
|
||||
"43.29",
|
||||
"2.45",
|
||||
"19.74",
|
||||
"6.58",
|
||||
"Floor 0",
|
||||
"43.29",
|
||||
"2.46",
|
||||
"19.74",
|
||||
"6.58",
|
||||
"Roof Space",
|
||||
"Main Building",
|
||||
"Roofs - Construction Type:",
|
||||
"Pitched roof (Slates or tiles), Access to loft",
|
||||
"Roofs - Insulation At:",
|
||||
"Joists",
|
||||
"Roof U-Value:",
|
||||
"Not Known",
|
||||
"Roofs - Insulation Thickness:",
|
||||
"150 mm",
|
||||
"Page 9",
|
||||
"",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Page 10",
|
||||
"",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Page 11",
|
||||
"",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Page 12",
|
||||
"",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Page 13",
|
||||
"",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Page 14",
|
||||
"",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Page 15",
|
||||
"",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Record indicators of Cavity Wall Construction in roof",
|
||||
"space:",
|
||||
"No indicator of construction visible",
|
||||
"Page 16",
|
||||
"",
|
||||
"Record indicators of party wall construction in roof space:",
|
||||
"Are there rooms in the roof?",
|
||||
"No",
|
||||
"Windows",
|
||||
"Window 1",
|
||||
"Window location:",
|
||||
"Main Building",
|
||||
"Window wall type:",
|
||||
"External wall",
|
||||
"Glazing Type:",
|
||||
"Double glazing installed between 2002 - 2021",
|
||||
"Identify basis of window dating:",
|
||||
"Manufacturing date stamp",
|
||||
"Window type:",
|
||||
"Window",
|
||||
"Window frame type:",
|
||||
"Wooden or PVC",
|
||||
"Is the window draught proofed?",
|
||||
"Yes",
|
||||
"Are there permanent shutters present?",
|
||||
"No",
|
||||
"Window height:",
|
||||
"1.27 m",
|
||||
"Window width:",
|
||||
"1.55 m",
|
||||
"Orientation:",
|
||||
"North West",
|
||||
"Page 17",
|
||||
"",
|
||||
"Photo of glazing type:",
|
||||
"Photo of glazing type:",
|
||||
"Photo of glazing type:",
|
||||
"Photo of glazing type:",
|
||||
"Window 2",
|
||||
"Window location:",
|
||||
"Main Building",
|
||||
"Window wall type:",
|
||||
"External wall",
|
||||
"Glazing Type:",
|
||||
"Double glazing installed between 2002 - 2021",
|
||||
"Identify basis of window dating:",
|
||||
"Manufacturing date stamp",
|
||||
"Window type:",
|
||||
"Window",
|
||||
"Window frame type:",
|
||||
"Wooden or PVC",
|
||||
"Is the window draught proofed?",
|
||||
"Yes",
|
||||
"Are there permanent shutters present?",
|
||||
"No",
|
||||
"Window height:",
|
||||
"0.97 m",
|
||||
"Window width:",
|
||||
"1.04 m",
|
||||
"Orientation:",
|
||||
"North West",
|
||||
"Page 18",
|
||||
"",
|
||||
"Photo of glazing type:",
|
||||
"Photo of glazing type:",
|
||||
"Photo of glazing type:",
|
||||
"Photo of glazing type:",
|
||||
"Window 3",
|
||||
"Window location:",
|
||||
"Main Building",
|
||||
"Window wall type:",
|
||||
"External wall",
|
||||
"Glazing Type:",
|
||||
"Double glazing installed between 2002 - 2021",
|
||||
"Identify basis of window dating:",
|
||||
"Manufacturing date stamp",
|
||||
"Window type:",
|
||||
"Window",
|
||||
"Window frame type:",
|
||||
"Wooden or PVC",
|
||||
"Is the window draught proofed?",
|
||||
"Yes",
|
||||
"Are there permanent shutters present?",
|
||||
"No",
|
||||
"Window height:",
|
||||
"0.97 m",
|
||||
"Window width:",
|
||||
"1.53 m",
|
||||
"Orientation:",
|
||||
"North West",
|
||||
"Window 4",
|
||||
"Window location:",
|
||||
"Main Building",
|
||||
"Window wall type:",
|
||||
"External wall",
|
||||
"Glazing Type:",
|
||||
"Double glazing, Unknown install date",
|
||||
"Window type:",
|
||||
"Window",
|
||||
"Window frame type:",
|
||||
"Wooden or PVC",
|
||||
"Page 19",
|
||||
"",
|
||||
"Photo of glazing type:",
|
||||
"Photo of glazing type:",
|
||||
"Photo of glazing type:",
|
||||
"What size is the glazing gap?",
|
||||
"12 mm",
|
||||
"Is the window draught proofed?",
|
||||
"Yes",
|
||||
"Are there permanent shutters present?",
|
||||
"No",
|
||||
"Window height:",
|
||||
"0.98 m",
|
||||
"Window width:",
|
||||
"0.57 m",
|
||||
"Orientation:",
|
||||
"North West",
|
||||
"Window 5",
|
||||
"Window location:",
|
||||
"Main Building",
|
||||
"Window wall type:",
|
||||
"External wall",
|
||||
"Glazing Type:",
|
||||
"Double glazing installed between 2002 - 2021",
|
||||
"Identify basis of window dating:",
|
||||
"Thermal spacer bar",
|
||||
"Window type:",
|
||||
"Window",
|
||||
"Window frame type:",
|
||||
"Wooden or PVC",
|
||||
"Is the window draught proofed?",
|
||||
"Yes",
|
||||
"Are there permanent shutters present?",
|
||||
"No",
|
||||
"Window height:",
|
||||
"1.29 m",
|
||||
"Window width:",
|
||||
"0.57 m",
|
||||
"Orientation:",
|
||||
"South West",
|
||||
"Page 20",
|
||||
"",
|
||||
"Photo of glazing type:",
|
||||
"Photo of glazing type:",
|
||||
"Window 6",
|
||||
"Window location:",
|
||||
"Main Building",
|
||||
"Window wall type:",
|
||||
"External wall",
|
||||
"Glazing Type:",
|
||||
"Double glazing installed between 2002 - 2021",
|
||||
"Identify basis of window dating:",
|
||||
"Manufacturing date stamp",
|
||||
"Window type:",
|
||||
"Window",
|
||||
"Window frame type:",
|
||||
"Wooden or PVC",
|
||||
"Is the window draught proofed?",
|
||||
"Yes",
|
||||
"Are there permanent shutters present?",
|
||||
"No",
|
||||
"Window height:",
|
||||
"0.98 m",
|
||||
"Window width:",
|
||||
"0.53 m",
|
||||
"Orientation:",
|
||||
"South West",
|
||||
"Window 7",
|
||||
"Window location:",
|
||||
"Main Building",
|
||||
"Window wall type:",
|
||||
"External wall",
|
||||
"Glazing Type:",
|
||||
"Double glazing installed between 2002 - 2021",
|
||||
"Identify basis of window dating:",
|
||||
"Thermal spacer bar",
|
||||
"Window type:",
|
||||
"Window",
|
||||
"Window frame type:",
|
||||
"Wooden or PVC",
|
||||
"Is the window draught proofed?",
|
||||
"Yes",
|
||||
"Are there permanent shutters present?",
|
||||
"No",
|
||||
"Window height:",
|
||||
"0.98 m",
|
||||
"Window width:",
|
||||
"1.09 m",
|
||||
"Orientation:",
|
||||
"South East",
|
||||
"Page 21",
|
||||
"",
|
||||
"Photo of glazing type:",
|
||||
"Photo of glazing type:",
|
||||
"Photo of glazing type:",
|
||||
"Photo of glazing type:",
|
||||
"Window 8",
|
||||
"Window location:",
|
||||
"Main Building",
|
||||
"Window wall type:",
|
||||
"External wall",
|
||||
"Glazing Type:",
|
||||
"Double glazing installed between 2002 - 2021",
|
||||
"Identify basis of window dating:",
|
||||
"Manufacturing date stamp",
|
||||
"Window type:",
|
||||
"Window",
|
||||
"Window frame type:",
|
||||
"Wooden or PVC",
|
||||
"Is the window draught proofed?",
|
||||
"Yes",
|
||||
"Are there permanent shutters present?",
|
||||
"No",
|
||||
"Window height:",
|
||||
"0.96 m",
|
||||
"Window width:",
|
||||
"1.54 m",
|
||||
"Orientation:",
|
||||
"South East",
|
||||
"Page 22",
|
||||
"",
|
||||
"Photo of glazing type:",
|
||||
"Window 9",
|
||||
"Window location:",
|
||||
"Main Building",
|
||||
"Window wall type:",
|
||||
"External wall",
|
||||
"Glazing Type:",
|
||||
"Double glazing installed between 2002 - 2021",
|
||||
"Identify basis of window dating:",
|
||||
"Manufacturing date stamp",
|
||||
"Window type:",
|
||||
"Window",
|
||||
"Window frame type:",
|
||||
"Wooden or PVC",
|
||||
"Is the window draught proofed?",
|
||||
"Yes",
|
||||
"Are there permanent shutters present?",
|
||||
"No",
|
||||
"Window height:",
|
||||
"0.97 m",
|
||||
"Window width:",
|
||||
"1.53 m",
|
||||
"Orientation:",
|
||||
"South East",
|
||||
"Window 10",
|
||||
"Window location:",
|
||||
"Main Building",
|
||||
"Window wall type:",
|
||||
"External wall",
|
||||
"Glazing Type:",
|
||||
"Double glazing, Unknown install date",
|
||||
"Window type:",
|
||||
"Window",
|
||||
"Window frame type:",
|
||||
"Wooden or PVC",
|
||||
"What size is the glazing gap?",
|
||||
"12 mm",
|
||||
"Is the window draught proofed?",
|
||||
"Yes",
|
||||
"Are there permanent shutters present?",
|
||||
"No",
|
||||
"Window height:",
|
||||
"0.95 m",
|
||||
"Window width:",
|
||||
"1.04 m",
|
||||
"Orientation:",
|
||||
"South East",
|
||||
"Page 23",
|
||||
"",
|
||||
"Photo of glazing type:",
|
||||
"Photo of glazing type:",
|
||||
"Photo of glazing type:",
|
||||
"Window 11",
|
||||
"Window location:",
|
||||
"Main Building",
|
||||
"Window wall type:",
|
||||
"External wall",
|
||||
"Glazing Type:",
|
||||
"Double glazing, Unknown install date",
|
||||
"Window type:",
|
||||
"Window",
|
||||
"Window frame type:",
|
||||
"Wooden or PVC",
|
||||
"What size is the glazing gap?",
|
||||
"12 mm",
|
||||
"Is the window draught proofed?",
|
||||
"Yes",
|
||||
"Are there permanent shutters present?",
|
||||
"No",
|
||||
"Window height:",
|
||||
"0.97 m",
|
||||
"Window width:",
|
||||
"1.53 m",
|
||||
"Orientation:",
|
||||
"South East",
|
||||
"Page 24",
|
||||
"",
|
||||
"Photo of heating system:",
|
||||
"Photo of heating system:",
|
||||
"Photo of heating system:",
|
||||
"Photo of heating system:",
|
||||
"Heating & Hot Water",
|
||||
"Main Heating Systems",
|
||||
"Main Heating 1",
|
||||
"How would you like to select the Heating System?",
|
||||
"Manual Entry",
|
||||
"System type:",
|
||||
"Room heaters",
|
||||
"Fuel:",
|
||||
"Dual fuel appliance (mineral and wood)",
|
||||
"Heating System (Other):",
|
||||
"Open fire with back boiler (No radiators)",
|
||||
"Controls:",
|
||||
"No thermostatic control of room temperature",
|
||||
"Secondary Heating System",
|
||||
"Secondary Fuel",
|
||||
"Dual Fuel Appliance (Mineral and Wood)",
|
||||
"Secondary System:",
|
||||
"Open fire in grate",
|
||||
"Page 25",
|
||||
"",
|
||||
"Photo of secondary heating system",
|
||||
"Photo of water heating system:",
|
||||
"Photo of water heating system:",
|
||||
"Water Heating & Cylinder",
|
||||
"Water Heating Type:",
|
||||
"Regular",
|
||||
"Water Heating System:",
|
||||
"From main heating 1",
|
||||
"Page 26",
|
||||
"",
|
||||
"Photo of water heating system:",
|
||||
"Photo of water heating system:",
|
||||
"Photo of water heating system:",
|
||||
"Photo of water heating system:",
|
||||
"Page 27",
|
||||
"",
|
||||
"Photo of water heating system:",
|
||||
"Photo of cylinder and thermostat if present:",
|
||||
"Photo of cylinder and thermostat if present:",
|
||||
"Photo of ventilation type:",
|
||||
"Cylinder Size:",
|
||||
"Normal (90-130 litres)",
|
||||
"What is the cylinder measured heat loss:",
|
||||
"Not known",
|
||||
"Insulation Type:",
|
||||
"Factory fitted",
|
||||
"Thickness:",
|
||||
"38 mm",
|
||||
"Has thermostat?",
|
||||
"No",
|
||||
"Ventilation",
|
||||
"Ventilation type:",
|
||||
"Mechanical Extract - Decentralised",
|
||||
"Has fixed air conditioning?",
|
||||
"No",
|
||||
"Is the ventilation in the PCDF database?",
|
||||
"No",
|
||||
"Page 28",
|
||||
"",
|
||||
"Photo of boiler flues:",
|
||||
"Photo of extract fans:",
|
||||
"Number of open flues:",
|
||||
"0",
|
||||
"Number of closed flues:",
|
||||
"0",
|
||||
"Number of boiler flues:",
|
||||
"1",
|
||||
"Number of other flues:",
|
||||
"0",
|
||||
"Number of extract fans:",
|
||||
"1",
|
||||
"Number of passive vents:",
|
||||
"0",
|
||||
"Number of flueless gas fires:",
|
||||
"0",
|
||||
"Pressure test:",
|
||||
"No test",
|
||||
"Is there a draught lobby?",
|
||||
"No",
|
||||
"Conservatories",
|
||||
"Is there conservatory?",
|
||||
"No conservatory",
|
||||
"Page 29",
|
||||
"",
|
||||
"Photo of open chimneys:",
|
||||
"Renewables",
|
||||
"Wind Turbines",
|
||||
"Has wind turbines?",
|
||||
"No",
|
||||
"Solar hot water",
|
||||
"Has solar hot water?",
|
||||
"No",
|
||||
"Photovoltaics",
|
||||
"Has photovoltaic array?",
|
||||
"No",
|
||||
"Number of PV batteries:",
|
||||
"None",
|
||||
"Hydro",
|
||||
"Is the dwelling connected to Hydro?",
|
||||
"No",
|
||||
"Room Count Elements",
|
||||
"Number of habitable rooms?",
|
||||
"5",
|
||||
"Are any of these rooms unheated?",
|
||||
"Yes",
|
||||
"Please enter the number of HEATED rooms:",
|
||||
"0",
|
||||
"Number of external doors?",
|
||||
"2",
|
||||
"Number of insulated external doors?",
|
||||
"0",
|
||||
"Number of draughtproofed external doors?",
|
||||
"2",
|
||||
"Number of open chimneys?",
|
||||
"1",
|
||||
"Number of blocked chimneys?",
|
||||
"1",
|
||||
"Page 30",
|
||||
"",
|
||||
"Photo of blocked chimneys:",
|
||||
"Photo of blocked chimneys:",
|
||||
"Photo of blocked chimneys:",
|
||||
"Photo of incandescent bulbs:",
|
||||
"Number of fixed incandescent bulbs:",
|
||||
"1",
|
||||
"Is the exact number of LED and CFL bulbs known?",
|
||||
"Yes",
|
||||
"Number of fixed LED bulbs:",
|
||||
"4",
|
||||
"Page 31",
|
||||
"",
|
||||
"Photo of LED bulbs:",
|
||||
"Photo of LED bulbs:",
|
||||
"Photo of LED bulbs:",
|
||||
"Photo of LED bulbs:",
|
||||
"Photo of CFL bulbs:",
|
||||
"Photo of CFL bulbs:",
|
||||
"Number of fixed CFL bulbs:",
|
||||
"4",
|
||||
"Page 32",
|
||||
"",
|
||||
"Photo of CFL bulbs:",
|
||||
"Photo of CFL bulbs:",
|
||||
"Photo of shower:",
|
||||
"Photo of shower:",
|
||||
"Are there any waste water heat recovery systems?",
|
||||
"None",
|
||||
"Number of baths:",
|
||||
"1",
|
||||
"How many special features are there at the",
|
||||
"property?",
|
||||
"0",
|
||||
"Showers",
|
||||
"Shower 1",
|
||||
"Shower outlet type:",
|
||||
"Electric Shower",
|
||||
"Page 33",
|
||||
"",
|
||||
"Photo of shower:",
|
||||
"Customer Response",
|
||||
"Customer present?",
|
||||
"Yes",
|
||||
"Customer willing to answer satisfaction survey?",
|
||||
"No",
|
||||
"Addendum + Related Party Disclosure",
|
||||
"Addendum",
|
||||
"None",
|
||||
"Related party disclosure",
|
||||
"No related party",
|
||||
"Hard to treat cavity walls: Property has access",
|
||||
"issues?",
|
||||
"No",
|
||||
"Hard to treat cavity walls: Property has high",
|
||||
"exposure?",
|
||||
"No",
|
||||
"Hard to treat cavity walls: Property has narrow",
|
||||
"cavities?",
|
||||
"No",
|
||||
"Photographs Required",
|
||||
"Page 34",
|
||||
"",
|
||||
"General Photos:",
|
||||
"General Photos:",
|
||||
"General Photos:",
|
||||
"General Photos:",
|
||||
"External Elevations:",
|
||||
"External Elevations:",
|
||||
"Page 35",
|
||||
"",
|
||||
"External Elevations:",
|
||||
"External Elevations:",
|
||||
"External Elevations:",
|
||||
"External Elevations:",
|
||||
"External Elevations:",
|
||||
"External Elevations:",
|
||||
"External Elevations:",
|
||||
"External Elevations:",
|
||||
"Page 36",
|
||||
"",
|
||||
"External Elevations:",
|
||||
"Page 37",
|
||||
"",
|
||||
"Page 38",
|
||||
"",
|
||||
"Page 39",
|
||||
"",
|
||||
"Page 40",
|
||||
"",
|
||||
"Additional Notes",
|
||||
"Additional Notes",
|
||||
"Over the years Tenant has refused any sort of heating system upgrade so the",
|
||||
"property only has an open fire with back boiler in the living room. There are no",
|
||||
"radiators in habitable rooms. The only radiator is in the bathroom. The hot",
|
||||
"water cylinder is connected to the back boiler and there is also a backup",
|
||||
"emersion.",
|
||||
"Page 41",
|
||||
""
|
||||
]
|
||||
799
backend/documents_parser/tests/fixtures/site_notes_example_3_text.json
vendored
Normal file
799
backend/documents_parser/tests/fixtures/site_notes_example_3_text.json
vendored
Normal file
|
|
@ -0,0 +1,799 @@
|
|||
[
|
||||
"SMART EPC: Record of",
|
||||
"Inspection & Site Notes",
|
||||
"Inspection Surveyor:",
|
||||
"Rebecca Mcwilliam",
|
||||
"E-Mail Address:",
|
||||
"rebeccamcdea@gmail.com",
|
||||
"Report Reference:",
|
||||
"Not Applicable",
|
||||
"Created On:",
|
||||
"14 October 2025",
|
||||
"Date of Inspection:",
|
||||
"14 October 2025",
|
||||
"Property Address:",
|
||||
"19,",
|
||||
"Sandfield ,",
|
||||
"Wrenbury,",
|
||||
"CW5 8EU",
|
||||
"Property Photo",
|
||||
"Page 1",
|
||||
"",
|
||||
"Photo of electricity meter:",
|
||||
"Photo of electricity meter:",
|
||||
"Photo of electricity meter:",
|
||||
"RdSAP Assessment",
|
||||
"General",
|
||||
"Confirm you have checked for the existence of an",
|
||||
"EPC before carrying out another energy assessment.",
|
||||
"Yes",
|
||||
"Does an EPC exist at the point of carrying out this",
|
||||
"energy assessment?",
|
||||
"Yes",
|
||||
"Please select why another energy assessment needs",
|
||||
"to be undertaken:",
|
||||
"Assessor instructed to produce a new EPC upon request from building",
|
||||
"owner/tenant/landlord after confirming to the requestor that a valid EPC",
|
||||
"already exists",
|
||||
"Inspection Date:",
|
||||
"14/10/2025",
|
||||
"Transaction Type:",
|
||||
"None of the Above",
|
||||
"Tenure:",
|
||||
"Rented Social",
|
||||
"Type of Property:",
|
||||
"House",
|
||||
"Detachment Type:",
|
||||
"End-terrace",
|
||||
"Number of storeys:",
|
||||
"2 Storeys",
|
||||
"Terrain Type:",
|
||||
"Suburban",
|
||||
"Number of Extensions:",
|
||||
"No Extensions",
|
||||
"Is an electricity smart meter present?",
|
||||
"Yes",
|
||||
"Electric meter type:",
|
||||
"Dual",
|
||||
"Is the dwelling export-capable?",
|
||||
"Yes",
|
||||
"Is mains gas available?",
|
||||
"No",
|
||||
"Page 2",
|
||||
"",
|
||||
"External indicators of Cavity Wall Construction:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Select Measurements Location:",
|
||||
"Internal",
|
||||
"Building Construction",
|
||||
"Main Building",
|
||||
"Age Range:",
|
||||
"1950-1966",
|
||||
"Record indicators of property age:",
|
||||
"local knowledge, enquiries of owner, period building features",
|
||||
"Walls - Construction Type:",
|
||||
"Cavity",
|
||||
"Record external indicators of Cavity Construction:",
|
||||
"stretcher bond",
|
||||
"Walls - Insulation Type:",
|
||||
"Filled Cavity",
|
||||
"Record indicators of filled cavity:",
|
||||
"evidence of cavity fill drill holes",
|
||||
"Page 3",
|
||||
"",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Page 4",
|
||||
"",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Page 5",
|
||||
"",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Page 6",
|
||||
"",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Page 7",
|
||||
"",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Page 8",
|
||||
"",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Page 9",
|
||||
"",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo wall thickness:",
|
||||
"Thermal conductivity of wall insulation:",
|
||||
"Unknown",
|
||||
"Wall U-Value known?",
|
||||
"Not Known",
|
||||
"Wall thickness:",
|
||||
"300 mm",
|
||||
"Party wall construction type:",
|
||||
"Unable to determine",
|
||||
"Floor type:",
|
||||
"Ground Floor",
|
||||
"Floor Construction:",
|
||||
"Solid",
|
||||
"Floor Insulation Type:",
|
||||
"As Built",
|
||||
"Floor U-Value known?",
|
||||
"Not Known",
|
||||
"Page 10",
|
||||
"",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Building Measurements",
|
||||
"Area (m2)",
|
||||
"Height (m)",
|
||||
"Heat Loss Perimeter (m)",
|
||||
"PWL (m)",
|
||||
"Main Building",
|
||||
"Floor 1",
|
||||
"42.7",
|
||||
"2.45",
|
||||
"19.61",
|
||||
"6.53",
|
||||
"Floor 0",
|
||||
"42.7",
|
||||
"2.45",
|
||||
"19.61",
|
||||
"6.53",
|
||||
"Roof Space",
|
||||
"Main Building",
|
||||
"Roofs - Construction Type:",
|
||||
"Pitched roof (Slates or tiles), Access to loft",
|
||||
"Roofs - Insulation At:",
|
||||
"Joists",
|
||||
"Roof U-Value:",
|
||||
"Not Known",
|
||||
"Roofs - Insulation Thickness:",
|
||||
"200 mm",
|
||||
"Page 11",
|
||||
"",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Page 12",
|
||||
"",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Page 13",
|
||||
"",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Page 14",
|
||||
"",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Page 15",
|
||||
"",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Page 16",
|
||||
"",
|
||||
"Loft insulation:",
|
||||
"Record indicators of party wall construction in roof space:",
|
||||
"Record indicators of Cavity Wall Construction in roof",
|
||||
"space:",
|
||||
"No indicator of construction visible",
|
||||
"Are there rooms in the roof?",
|
||||
"No",
|
||||
"Windows",
|
||||
"Window 1",
|
||||
"Window location:",
|
||||
"Main Building",
|
||||
"Window wall type:",
|
||||
"External wall",
|
||||
"Glazing Type:",
|
||||
"Double glazing installed between 2002 - 2021",
|
||||
"Page 17",
|
||||
"",
|
||||
"Photo of glazing type:",
|
||||
"Photo of glazing type:",
|
||||
"Identify basis of window dating:",
|
||||
"Thermal spacer bar",
|
||||
"Window type:",
|
||||
"Window",
|
||||
"Window frame type:",
|
||||
"Wooden or PVC",
|
||||
"Is the window draught proofed?",
|
||||
"Yes",
|
||||
"Are there permanent shutters present?",
|
||||
"No",
|
||||
"Window height:",
|
||||
"1.26 m",
|
||||
"Window width:",
|
||||
"1.54 m",
|
||||
"Orientation:",
|
||||
"North East",
|
||||
"Window 2",
|
||||
"Window location:",
|
||||
"Main Building",
|
||||
"Window wall type:",
|
||||
"External wall",
|
||||
"Glazing Type:",
|
||||
"Double glazing installed between 2002 - 2021",
|
||||
"Identify basis of window dating:",
|
||||
"Thermal spacer bar",
|
||||
"Window type:",
|
||||
"Window",
|
||||
"Window frame type:",
|
||||
"Wooden or PVC",
|
||||
"Is the window draught proofed?",
|
||||
"Yes",
|
||||
"Are there permanent shutters present?",
|
||||
"No",
|
||||
"Window height:",
|
||||
"0.97 m",
|
||||
"Window width:",
|
||||
"1.04 m",
|
||||
"Orientation:",
|
||||
"North East",
|
||||
"Page 18",
|
||||
"",
|
||||
"Photo of glazing type:",
|
||||
"Photo of glazing type:",
|
||||
"Photo of glazing type:",
|
||||
"Window 3",
|
||||
"Window location:",
|
||||
"Main Building",
|
||||
"Window wall type:",
|
||||
"External wall",
|
||||
"Glazing Type:",
|
||||
"Double glazing installed between 2002 - 2021",
|
||||
"Identify basis of window dating:",
|
||||
"Thermal spacer bar",
|
||||
"Window type:",
|
||||
"Window",
|
||||
"Window frame type:",
|
||||
"Wooden or PVC",
|
||||
"Is the window draught proofed?",
|
||||
"Yes",
|
||||
"Are there permanent shutters present?",
|
||||
"No",
|
||||
"Window height:",
|
||||
"0.97 m",
|
||||
"Window width:",
|
||||
"1.53 m",
|
||||
"Orientation:",
|
||||
"North East",
|
||||
"Window 4",
|
||||
"Window location:",
|
||||
"Main Building",
|
||||
"Window wall type:",
|
||||
"External wall",
|
||||
"Glazing Type:",
|
||||
"Double glazing installed between 2002 - 2021",
|
||||
"Identify basis of window dating:",
|
||||
"Thermal spacer bar",
|
||||
"Window type:",
|
||||
"Window",
|
||||
"Page 19",
|
||||
"",
|
||||
"Photo of glazing type:",
|
||||
"Photo of glazing type:",
|
||||
"Photo of glazing type:",
|
||||
"Window frame type:",
|
||||
"Wooden or PVC",
|
||||
"Is the window draught proofed?",
|
||||
"Yes",
|
||||
"Are there permanent shutters present?",
|
||||
"No",
|
||||
"Window height:",
|
||||
"1.27 m",
|
||||
"Window width:",
|
||||
"0.58 m",
|
||||
"Orientation:",
|
||||
"North East",
|
||||
"Window 5",
|
||||
"Window location:",
|
||||
"Main Building",
|
||||
"Window wall type:",
|
||||
"External wall",
|
||||
"Glazing Type:",
|
||||
"Double glazing installed between 2002 - 2021",
|
||||
"Identify basis of window dating:",
|
||||
"Thermal spacer bar",
|
||||
"Window type:",
|
||||
"Window",
|
||||
"Window frame type:",
|
||||
"Wooden or PVC",
|
||||
"Is the window draught proofed?",
|
||||
"Yes",
|
||||
"Are there permanent shutters present?",
|
||||
"No",
|
||||
"Window height:",
|
||||
"0.98 m",
|
||||
"Window width:",
|
||||
"0.54 m",
|
||||
"Orientation:",
|
||||
"South East",
|
||||
"Page 20",
|
||||
"",
|
||||
"Photo of glazing type:",
|
||||
"Photo of glazing type:",
|
||||
"Window 6",
|
||||
"Window location:",
|
||||
"Main Building",
|
||||
"Window wall type:",
|
||||
"External wall",
|
||||
"Glazing Type:",
|
||||
"Double glazing installed between 2002 - 2021",
|
||||
"Identify basis of window dating:",
|
||||
"Thermal spacer bar",
|
||||
"Window type:",
|
||||
"Window",
|
||||
"Window frame type:",
|
||||
"Wooden or PVC",
|
||||
"Is the window draught proofed?",
|
||||
"Yes",
|
||||
"Are there permanent shutters present?",
|
||||
"No",
|
||||
"Window height:",
|
||||
"1.27 m",
|
||||
"Window width:",
|
||||
"0.58 m",
|
||||
"Orientation:",
|
||||
"South East",
|
||||
"Window 7",
|
||||
"Window location:",
|
||||
"Main Building",
|
||||
"Window wall type:",
|
||||
"External wall",
|
||||
"Glazing Type:",
|
||||
"Double glazing installed between 2002 - 2021",
|
||||
"Identify basis of window dating:",
|
||||
"Thermal spacer bar",
|
||||
"Window type:",
|
||||
"Window",
|
||||
"Window frame type:",
|
||||
"Wooden or PVC",
|
||||
"Is the window draught proofed?",
|
||||
"Yes",
|
||||
"Are there permanent shutters present?",
|
||||
"No",
|
||||
"Window height:",
|
||||
"0.98 m",
|
||||
"Window width:",
|
||||
"1.06 m",
|
||||
"Page 21",
|
||||
"",
|
||||
"Photo of glazing type:",
|
||||
"Photo of glazing type:",
|
||||
"Photo of glazing type:",
|
||||
"Photo of glazing type:",
|
||||
"Orientation:",
|
||||
"South West",
|
||||
"Window 8",
|
||||
"Window location:",
|
||||
"Main Building",
|
||||
"Window wall type:",
|
||||
"External wall",
|
||||
"Glazing Type:",
|
||||
"Double glazing installed between 2002 - 2021",
|
||||
"Identify basis of window dating:",
|
||||
"Thermal spacer bar",
|
||||
"Window type:",
|
||||
"Window",
|
||||
"Window frame type:",
|
||||
"Wooden or PVC",
|
||||
"Is the window draught proofed?",
|
||||
"Yes",
|
||||
"Are there permanent shutters present?",
|
||||
"No",
|
||||
"Window height:",
|
||||
"0.97 m",
|
||||
"Window width:",
|
||||
"1.56 m",
|
||||
"Orientation:",
|
||||
"South East",
|
||||
"Window 9",
|
||||
"Window location:",
|
||||
"Main Building",
|
||||
"Window wall type:",
|
||||
"External wall",
|
||||
"Glazing Type:",
|
||||
"Double glazing installed between 2002 - 2021",
|
||||
"Identify basis of window dating:",
|
||||
"Thermal spacer bar",
|
||||
"Page 22",
|
||||
"",
|
||||
"Photo of glazing type:",
|
||||
"Photo of glazing type:",
|
||||
"Window type:",
|
||||
"Window",
|
||||
"Window frame type:",
|
||||
"Wooden or PVC",
|
||||
"Is the window draught proofed?",
|
||||
"Yes",
|
||||
"Are there permanent shutters present?",
|
||||
"No",
|
||||
"Window height:",
|
||||
"0.96 m",
|
||||
"Window width:",
|
||||
"1.53 m",
|
||||
"Orientation:",
|
||||
"South East",
|
||||
"Window 10",
|
||||
"Window location:",
|
||||
"Main Building",
|
||||
"Window wall type:",
|
||||
"External wall",
|
||||
"Glazing Type:",
|
||||
"Double glazing installed between 2002 - 2021",
|
||||
"Identify basis of window dating:",
|
||||
"Thermal spacer bar",
|
||||
"Window type:",
|
||||
"Window",
|
||||
"Window frame type:",
|
||||
"Wooden or PVC",
|
||||
"Is the window draught proofed?",
|
||||
"Yes",
|
||||
"Are there permanent shutters present?",
|
||||
"No",
|
||||
"Window height:",
|
||||
"0.95 m",
|
||||
"Window width:",
|
||||
"1.04 m",
|
||||
"Orientation:",
|
||||
"South East",
|
||||
"Page 23",
|
||||
"",
|
||||
"Photo of glazing type:",
|
||||
"Photo of glazing type:",
|
||||
"Page 24",
|
||||
"",
|
||||
"Heating & Hot Water",
|
||||
"Main Heating Systems",
|
||||
"Main Heating 1",
|
||||
"How would you like to select the Heating System?",
|
||||
"PCDF Search",
|
||||
"System type:",
|
||||
"Electric storage heaters",
|
||||
"Number of storage heater types:",
|
||||
"3",
|
||||
"Product Id",
|
||||
"230026",
|
||||
"Manufacturer",
|
||||
"Dimplex",
|
||||
"Model",
|
||||
"Quantum",
|
||||
"Orig Manuf",
|
||||
"Dimplex",
|
||||
"S. Efficiency",
|
||||
"0",
|
||||
"Year",
|
||||
"2019 - current",
|
||||
"Open Flue",
|
||||
"No",
|
||||
"Status",
|
||||
"Normal status for an actual product",
|
||||
"Number of heaters of this type:",
|
||||
"1",
|
||||
"Product Id",
|
||||
"230024",
|
||||
"Manufacturer",
|
||||
"Dimplex",
|
||||
"Model",
|
||||
"Quantum",
|
||||
"Orig Manuf",
|
||||
"Dimplex",
|
||||
"S. Efficiency",
|
||||
"0",
|
||||
"Year",
|
||||
"2019 - current",
|
||||
"Open Flue",
|
||||
"No",
|
||||
"Status",
|
||||
"Normal status for an actual product",
|
||||
"Number of heaters of this type:",
|
||||
"2",
|
||||
"Product Id",
|
||||
"230023",
|
||||
"Manufacturer",
|
||||
"Dimplex",
|
||||
"Model",
|
||||
"Quantum",
|
||||
"Orig Manuf",
|
||||
"Dimplex",
|
||||
"S. Efficiency",
|
||||
"0",
|
||||
"Year",
|
||||
"2019 - current",
|
||||
"Open Flue",
|
||||
"No",
|
||||
"Status",
|
||||
"Normal status for an actual product",
|
||||
"Number of heaters of this type:",
|
||||
"3",
|
||||
"Controls:",
|
||||
"Controls for high heat retention storage heaters",
|
||||
"Page 25",
|
||||
"",
|
||||
"Photo of heating system:",
|
||||
"Photo of heating system:",
|
||||
"Photo of heating system:",
|
||||
"Photo of heating system:",
|
||||
"Photo of heating system:",
|
||||
"Photo of heating system:",
|
||||
"Photo of heating system:",
|
||||
"Photo of heating system:",
|
||||
"Page 26",
|
||||
"",
|
||||
"Photo of heating system:",
|
||||
"Photo of heating system:",
|
||||
"Photo of heating system:",
|
||||
"Photo of heating system:",
|
||||
"Photo of heating controls:",
|
||||
"Photo of heating controls:",
|
||||
"Photo of heating controls:",
|
||||
"Page 27",
|
||||
"",
|
||||
"Photo of secondary heating system",
|
||||
"Photo of cylinder and thermostat if present:",
|
||||
"Photo of cylinder and thermostat if present:",
|
||||
"Secondary Heating System",
|
||||
"Secondary Fuel",
|
||||
"Electricity",
|
||||
"Secondary System:",
|
||||
"Panel, convector or radiant heaters",
|
||||
"Water Heating & Cylinder",
|
||||
"Water Heating Type:",
|
||||
"Regular",
|
||||
"Water Heating System:",
|
||||
"Electric immersion",
|
||||
"Immersion:",
|
||||
"Dual",
|
||||
"Cylinder Size:",
|
||||
"Medium (131-170 litres)",
|
||||
"What is the cylinder measured heat loss:",
|
||||
"Not known",
|
||||
"Insulation Type:",
|
||||
"Factory fitted",
|
||||
"Thickness:",
|
||||
"50 mm",
|
||||
"Page 28",
|
||||
"",
|
||||
"Photo of cylinder and thermostat if present:",
|
||||
"Photo of cylinder and thermostat if present:",
|
||||
"Photo of cylinder and thermostat if present:",
|
||||
"Photo of ventilation type:",
|
||||
"Has thermostat?",
|
||||
"Yes",
|
||||
"Ventilation",
|
||||
"Ventilation type:",
|
||||
"Mechanical Extract - Decentralised",
|
||||
"Has fixed air conditioning?",
|
||||
"No",
|
||||
"Page 29",
|
||||
"",
|
||||
"Photo of extract fans:",
|
||||
"Is the ventilation in the PCDF database?",
|
||||
"No",
|
||||
"Number of open flues:",
|
||||
"0",
|
||||
"Number of closed flues:",
|
||||
"0",
|
||||
"Number of boiler flues:",
|
||||
"0",
|
||||
"Number of other flues:",
|
||||
"0",
|
||||
"Number of extract fans:",
|
||||
"1",
|
||||
"Number of passive vents:",
|
||||
"0",
|
||||
"Number of flueless gas fires:",
|
||||
"0",
|
||||
"Pressure test:",
|
||||
"No test",
|
||||
"Is there a draught lobby?",
|
||||
"No",
|
||||
"Conservatories",
|
||||
"Is there conservatory?",
|
||||
"No conservatory",
|
||||
"Page 30",
|
||||
"",
|
||||
"Photo of photovoltaic array:",
|
||||
"Photo of photovoltaic array:",
|
||||
"Photo of photovoltaic array:",
|
||||
"Photo of photovoltaic array:",
|
||||
"Renewables",
|
||||
"Wind Turbines",
|
||||
"Has wind turbines?",
|
||||
"No",
|
||||
"Solar hot water",
|
||||
"Has solar hot water?",
|
||||
"No",
|
||||
"Photovoltaics",
|
||||
"Has photovoltaic array?",
|
||||
"Yes",
|
||||
"Is there a PV diverter?",
|
||||
"No",
|
||||
"PV Connection:",
|
||||
"Connected to dwellings electricity meter",
|
||||
"Photovoltaic array kWp Known?",
|
||||
"No",
|
||||
"Percentage of roof covered with photovoltaic array?",
|
||||
"45 %",
|
||||
"Number of PV batteries:",
|
||||
"None",
|
||||
"Hydro",
|
||||
"Is the dwelling connected to Hydro?",
|
||||
"No",
|
||||
"Room Count Elements",
|
||||
"Number of habitable rooms?",
|
||||
"5",
|
||||
"Are any of these rooms unheated?",
|
||||
"No",
|
||||
"Page 31",
|
||||
"",
|
||||
"Photo of open chimneys:",
|
||||
"Photo of incandescent bulbs:",
|
||||
"Photo of incandescent bulbs:",
|
||||
"Photo of LED bulbs:",
|
||||
"Photo of LED bulbs:",
|
||||
"Number of external doors?",
|
||||
"2",
|
||||
"Number of insulated external doors?",
|
||||
"0",
|
||||
"Number of draughtproofed external doors?",
|
||||
"2",
|
||||
"Number of open chimneys?",
|
||||
"1",
|
||||
"Number of blocked chimneys?",
|
||||
"0",
|
||||
"Number of fixed incandescent bulbs:",
|
||||
"2",
|
||||
"Is the exact number of LED and CFL bulbs known?",
|
||||
"Yes",
|
||||
"Number of fixed LED bulbs:",
|
||||
"9",
|
||||
"Page 32",
|
||||
"",
|
||||
"Photo of LED bulbs:",
|
||||
"Photo of LED bulbs:",
|
||||
"Photo of LED bulbs:",
|
||||
"Number of fixed CFL bulbs:",
|
||||
"0",
|
||||
"Are there any waste water heat recovery systems?",
|
||||
"None",
|
||||
"Number of baths:",
|
||||
"1",
|
||||
"How many special features are there at the",
|
||||
"property?",
|
||||
"0",
|
||||
"Showers",
|
||||
"Shower 1",
|
||||
"Shower outlet type:",
|
||||
"Electric Shower",
|
||||
"Page 33",
|
||||
"",
|
||||
"Photo of shower:",
|
||||
"Photo of shower:",
|
||||
"Customer Response",
|
||||
"Customer present?",
|
||||
"Yes",
|
||||
"Customer willing to answer satisfaction survey?",
|
||||
"No",
|
||||
"Addendum + Related Party Disclosure",
|
||||
"Addendum",
|
||||
"None",
|
||||
"Related party disclosure",
|
||||
"No related party",
|
||||
"Hard to treat cavity walls: Property has access",
|
||||
"issues?",
|
||||
"No",
|
||||
"Hard to treat cavity walls: Property has high",
|
||||
"exposure?",
|
||||
"No",
|
||||
"Hard to treat cavity walls: Property has narrow",
|
||||
"cavities?",
|
||||
"No",
|
||||
"Photographs Required",
|
||||
"Page 34",
|
||||
"",
|
||||
"General Photos:",
|
||||
"General Photos:",
|
||||
"General Photos:",
|
||||
"General Photos:",
|
||||
"General Photos:",
|
||||
"General Photos:",
|
||||
"Page 35",
|
||||
"",
|
||||
"General Photos:",
|
||||
"General Photos:",
|
||||
"External Elevations:",
|
||||
"External Elevations:",
|
||||
"External Elevations:",
|
||||
"External Elevations:",
|
||||
"External Elevations:",
|
||||
"Page 36",
|
||||
"",
|
||||
"External Elevations:",
|
||||
"External Elevations:",
|
||||
"External Elevations:",
|
||||
"External Elevations:",
|
||||
"Page 37",
|
||||
"",
|
||||
"External Elevations:",
|
||||
"External Elevations:",
|
||||
"External Elevations:",
|
||||
"External Elevations:",
|
||||
"Page 38",
|
||||
"",
|
||||
"External Elevations:",
|
||||
"External Elevations:",
|
||||
"External Elevations:",
|
||||
"External Elevations:",
|
||||
"Page 39",
|
||||
"",
|
||||
"Page 40",
|
||||
"",
|
||||
"Page 41",
|
||||
"",
|
||||
"Additional Notes",
|
||||
"Additional Notes",
|
||||
"dMEV in kitchen, IEV in bathroom. Vent on chimney more than 300x100mm so",
|
||||
"recorded as open chimney.",
|
||||
"Page 42",
|
||||
""
|
||||
]
|
||||
480
backend/documents_parser/tests/fixtures/site_notes_example_4_text.json
vendored
Normal file
480
backend/documents_parser/tests/fixtures/site_notes_example_4_text.json
vendored
Normal file
|
|
@ -0,0 +1,480 @@
|
|||
[
|
||||
"SMART EPC: Record of",
|
||||
"Inspection & Site Notes",
|
||||
"Inspection Surveyor:",
|
||||
"Rebecca Mcwilliam",
|
||||
"E-Mail Address:",
|
||||
"rebeccamcdea@gmail.com",
|
||||
"Report Reference:",
|
||||
"DA1A93D9-354C-4B4B-A299-3A681231D4B5",
|
||||
"Created On:",
|
||||
"15 October 2025",
|
||||
"Date of Inspection:",
|
||||
"13 October 2025",
|
||||
"Property Address:",
|
||||
"18,",
|
||||
"Oakfield Close,",
|
||||
"Wrenbury,",
|
||||
"CW5 8ET",
|
||||
"Property Photo",
|
||||
"Page 1",
|
||||
"",
|
||||
"Photo of electricity meter:",
|
||||
"Photo of electricity meter:",
|
||||
"RdSAP Assessment",
|
||||
"General",
|
||||
"Confirm you have checked for the existence of an",
|
||||
"EPC before carrying out another energy assessment.",
|
||||
"Yes",
|
||||
"Does an EPC exist at the point of carrying out this",
|
||||
"energy assessment?",
|
||||
"Yes",
|
||||
"Please select why another energy assessment needs",
|
||||
"to be undertaken:",
|
||||
"Assessor instructed to produce a new EPC upon request from building",
|
||||
"owner/tenant/landlord after confirming to the requestor that a valid EPC",
|
||||
"already exists",
|
||||
"Inspection Date:",
|
||||
"13/10/2025",
|
||||
"Transaction Type:",
|
||||
"None of the Above",
|
||||
"Tenure:",
|
||||
"Rented Social",
|
||||
"Type of Property:",
|
||||
"Bungalow",
|
||||
"Detachment Type:",
|
||||
"End-terrace",
|
||||
"Number of storeys:",
|
||||
"1 Storey",
|
||||
"Terrain Type:",
|
||||
"Suburban",
|
||||
"Number of Extensions:",
|
||||
"No Extensions",
|
||||
"Is an electricity smart meter present?",
|
||||
"Yes",
|
||||
"Electric meter type:",
|
||||
"Single",
|
||||
"Page 2",
|
||||
"",
|
||||
"Photo of electricity meter:",
|
||||
"Photo of electricity meter:",
|
||||
"External indicators of Cavity Wall Construction:",
|
||||
"External indicators of Cavity Wall Construction:",
|
||||
"Is the dwelling export-capable?",
|
||||
"Yes",
|
||||
"Is mains gas available?",
|
||||
"No",
|
||||
"Select Measurements Location:",
|
||||
"Internal",
|
||||
"Building Construction",
|
||||
"Main Building",
|
||||
"Age Range:",
|
||||
"1950-1966",
|
||||
"Record indicators of property age:",
|
||||
"local knowledge, enquiries of owner, period building features",
|
||||
"Walls - Construction Type:",
|
||||
"Cavity",
|
||||
"Record external indicators of Cavity Construction:",
|
||||
"stretcher bond, wall thickness over 270 mm",
|
||||
"Walls - Insulation Type:",
|
||||
"Filled Cavity",
|
||||
"Record indicators of filled cavity:",
|
||||
"evidence of cavity fill drill holes, Boroscope",
|
||||
"Page 3",
|
||||
"",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Page 4",
|
||||
"",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo wall thickness:",
|
||||
"Thermal conductivity of wall insulation:",
|
||||
"Unknown",
|
||||
"Wall U-Value known?",
|
||||
"Not Known",
|
||||
"Wall thickness:",
|
||||
"300 mm",
|
||||
"Party wall construction type:",
|
||||
"Unable to determine",
|
||||
"Floor type:",
|
||||
"Ground Floor",
|
||||
"Floor Construction:",
|
||||
"Solid",
|
||||
"Floor Insulation Type:",
|
||||
"As Built",
|
||||
"Floor U-Value known?",
|
||||
"Not Known",
|
||||
"Page 5",
|
||||
"",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Building Measurements",
|
||||
"Area (m2)",
|
||||
"Height (m)",
|
||||
"Heat Loss Perimeter (m)",
|
||||
"PWL (m)",
|
||||
"Main Building",
|
||||
"Floor 0",
|
||||
"46.08",
|
||||
"2.44",
|
||||
"20.49",
|
||||
"6.67",
|
||||
"Roof Space",
|
||||
"Main Building",
|
||||
"Roofs - Construction Type:",
|
||||
"Pitched roof (Slates or tiles), No access",
|
||||
"Identify the reason for restricted access:",
|
||||
"access hatch blocked",
|
||||
"Roofs - Insulation At:",
|
||||
"Unknown",
|
||||
"Page 6",
|
||||
"",
|
||||
"Loft insulation:",
|
||||
"Record indicators of Cavity Wall Construction in roof",
|
||||
"space:",
|
||||
"No indicator of construction visible",
|
||||
"Are there rooms in the roof?",
|
||||
"No",
|
||||
"Windows",
|
||||
"Window 1",
|
||||
"Window location:",
|
||||
"Main Building",
|
||||
"Window wall type:",
|
||||
"External wall",
|
||||
"Glazing Type:",
|
||||
"Double glazing, Unknown install date",
|
||||
"Window type:",
|
||||
"Window",
|
||||
"Window frame type:",
|
||||
"Wooden or PVC",
|
||||
"What size is the glazing gap?",
|
||||
"12 mm",
|
||||
"Is the window draught proofed?",
|
||||
"Yes",
|
||||
"Are there permanent shutters present?",
|
||||
"No",
|
||||
"Window height:",
|
||||
"0.97 m",
|
||||
"Window width:",
|
||||
"1.54 m",
|
||||
"Orientation:",
|
||||
"North East",
|
||||
"Page 7",
|
||||
"",
|
||||
"Photo of glazing type:",
|
||||
"Photo of glazing type:",
|
||||
"Photo of glazing type:",
|
||||
"Photo of glazing type:",
|
||||
"Window 2",
|
||||
"Window location:",
|
||||
"Main Building",
|
||||
"Window wall type:",
|
||||
"External wall",
|
||||
"Glazing Type:",
|
||||
"Double glazing, Unknown install date",
|
||||
"Window type:",
|
||||
"Window",
|
||||
"Window frame type:",
|
||||
"Wooden or PVC",
|
||||
"What size is the glazing gap?",
|
||||
"12 mm",
|
||||
"Is the window draught proofed?",
|
||||
"Yes",
|
||||
"Are there permanent shutters present?",
|
||||
"No",
|
||||
"Window height:",
|
||||
"1.01 m",
|
||||
"Window width:",
|
||||
"1.04 m",
|
||||
"Orientation:",
|
||||
"North East",
|
||||
"Window 3",
|
||||
"Window location:",
|
||||
"Main Building",
|
||||
"Window wall type:",
|
||||
"External wall",
|
||||
"Glazing Type:",
|
||||
"Double glazing, Unknown install date",
|
||||
"Window type:",
|
||||
"Window",
|
||||
"Window frame type:",
|
||||
"Wooden or PVC",
|
||||
"Page 8",
|
||||
"",
|
||||
"Photo of glazing type:",
|
||||
"Photo of glazing type:",
|
||||
"Photo of glazing type:",
|
||||
"Photo of glazing type:",
|
||||
"What size is the glazing gap?",
|
||||
"12 mm",
|
||||
"Is the window draught proofed?",
|
||||
"Yes",
|
||||
"Are there permanent shutters present?",
|
||||
"No",
|
||||
"Window height:",
|
||||
"0.99 m",
|
||||
"Window width:",
|
||||
"1.07 m",
|
||||
"Orientation:",
|
||||
"South West",
|
||||
"Window 4",
|
||||
"Window location:",
|
||||
"Main Building",
|
||||
"Window wall type:",
|
||||
"External wall",
|
||||
"Glazing Type:",
|
||||
"Double glazing, Unknown install date",
|
||||
"Window type:",
|
||||
"Window",
|
||||
"Window frame type:",
|
||||
"Wooden or PVC",
|
||||
"What size is the glazing gap?",
|
||||
"12 mm",
|
||||
"Is the window draught proofed?",
|
||||
"Yes",
|
||||
"Are there permanent shutters present?",
|
||||
"No",
|
||||
"Window height:",
|
||||
"1.28 m",
|
||||
"Window width:",
|
||||
"2.07 m",
|
||||
"Orientation:",
|
||||
"South",
|
||||
"Page 9",
|
||||
"",
|
||||
"Photo of heating system:",
|
||||
"Photo of heating system:",
|
||||
"Photo of heating system:",
|
||||
"Photo of heating system:",
|
||||
"Heating & Hot Water",
|
||||
"Main Heating Systems",
|
||||
"Main Heating 1",
|
||||
"How would you like to select the Heating System?",
|
||||
"PCDF Search",
|
||||
"System type:",
|
||||
"Heat pump with radiators or underfloor heating",
|
||||
"Product Id",
|
||||
"102421",
|
||||
"Manufacturer",
|
||||
"Daikin Altherma",
|
||||
"Model",
|
||||
"EDLQ05CAV3",
|
||||
"Year",
|
||||
"2014 - current",
|
||||
"Fuel",
|
||||
"Electricity, any tariff",
|
||||
"Status",
|
||||
"Normal status for an actual product",
|
||||
"Central heating pump age:",
|
||||
"Unknown",
|
||||
"MCS installed heat pump:",
|
||||
"Yes",
|
||||
"Controls:",
|
||||
"Programmer, room thermostat and TRVs",
|
||||
"Emitter:",
|
||||
"Radiators",
|
||||
"Emitter Temperature:",
|
||||
"Unknown",
|
||||
"Page 10",
|
||||
"",
|
||||
"Photo of heating controls:",
|
||||
"Photo of heating controls:",
|
||||
"Photo of heating controls:",
|
||||
"Photo of heating controls:",
|
||||
"Photo of heating controls:",
|
||||
"Photo of heating controls:",
|
||||
"Page 11",
|
||||
"",
|
||||
"Photo of heating controls:",
|
||||
"Photo of secondary heating system",
|
||||
"Secondary Heating System",
|
||||
"Secondary Fuel",
|
||||
"Electricity",
|
||||
"Secondary System:",
|
||||
"Panel, convector or radiant heaters",
|
||||
"Water Heating & Cylinder",
|
||||
"Water Heating Type:",
|
||||
"Regular",
|
||||
"Water Heating System:",
|
||||
"From main heating 1",
|
||||
"Page 12",
|
||||
"",
|
||||
"Photo of water heating system:",
|
||||
"Photo of water heating system:",
|
||||
"Photo of water heating system:",
|
||||
"Cylinder Size:",
|
||||
"Medium (131-170 litres)",
|
||||
"What is the cylinder measured heat loss:",
|
||||
"Not known",
|
||||
"Insulation Type:",
|
||||
"Factory fitted",
|
||||
"Thickness:",
|
||||
"50 mm",
|
||||
"Page 13",
|
||||
"",
|
||||
"Photo of cylinder and thermostat if present:",
|
||||
"Photo of cylinder and thermostat if present:",
|
||||
"Photo of ventilation type:",
|
||||
"Photo of ventilation type:",
|
||||
"Has thermostat?",
|
||||
"Yes",
|
||||
"Ventilation",
|
||||
"Ventilation type:",
|
||||
"Mechanical Extract - Decentralised",
|
||||
"Has fixed air conditioning?",
|
||||
"No",
|
||||
"Is the ventilation in the PCDF database?",
|
||||
"No",
|
||||
"Number of open flues:",
|
||||
"0",
|
||||
"Number of closed flues:",
|
||||
"0",
|
||||
"Number of boiler flues:",
|
||||
"0",
|
||||
"Page 14",
|
||||
"",
|
||||
"Photo of extract fans:",
|
||||
"Number of other flues:",
|
||||
"0",
|
||||
"Number of extract fans:",
|
||||
"1",
|
||||
"Number of passive vents:",
|
||||
"0",
|
||||
"Number of flueless gas fires:",
|
||||
"0",
|
||||
"Pressure test:",
|
||||
"No test",
|
||||
"Is there a draught lobby?",
|
||||
"No",
|
||||
"Conservatories",
|
||||
"Is there conservatory?",
|
||||
"No conservatory",
|
||||
"Renewables",
|
||||
"Wind Turbines",
|
||||
"Has wind turbines?",
|
||||
"No",
|
||||
"Solar hot water",
|
||||
"Has solar hot water?",
|
||||
"No",
|
||||
"Photovoltaics",
|
||||
"Has photovoltaic array?",
|
||||
"No",
|
||||
"Number of PV batteries:",
|
||||
"None",
|
||||
"Hydro",
|
||||
"Is the dwelling connected to Hydro?",
|
||||
"No",
|
||||
"Room Count Elements",
|
||||
"Number of habitable rooms?",
|
||||
"2",
|
||||
"Are any of these rooms unheated?",
|
||||
"No",
|
||||
"Page 15",
|
||||
"",
|
||||
"Photo of LED bulbs:",
|
||||
"Photo of LED bulbs:",
|
||||
"Photo of LED bulbs:",
|
||||
"Photo of LED bulbs:",
|
||||
"Photo of LED bulbs:",
|
||||
"Photo of LED bulbs:",
|
||||
"Number of external doors?",
|
||||
"2",
|
||||
"Number of insulated external doors?",
|
||||
"0",
|
||||
"Number of draughtproofed external doors?",
|
||||
"2",
|
||||
"Number of open chimneys?",
|
||||
"0",
|
||||
"Number of blocked chimneys?",
|
||||
"0",
|
||||
"Number of fixed incandescent bulbs:",
|
||||
"0",
|
||||
"Is the exact number of LED and CFL bulbs known?",
|
||||
"Yes",
|
||||
"Number of fixed LED bulbs:",
|
||||
"7",
|
||||
"Page 16",
|
||||
"",
|
||||
"Photo of LED bulbs:",
|
||||
"Photo of shower:",
|
||||
"Photo of shower:",
|
||||
"Number of fixed CFL bulbs:",
|
||||
"0",
|
||||
"Are there any waste water heat recovery systems?",
|
||||
"None",
|
||||
"Number of baths:",
|
||||
"1",
|
||||
"How many special features are there at the",
|
||||
"property?",
|
||||
"0",
|
||||
"Showers",
|
||||
"Shower 1",
|
||||
"Shower outlet type:",
|
||||
"Electric Shower",
|
||||
"Customer Response",
|
||||
"Customer present?",
|
||||
"Yes",
|
||||
"Customer willing to answer satisfaction survey?",
|
||||
"No",
|
||||
"Addendum + Related Party Disclosure",
|
||||
"Addendum",
|
||||
"None",
|
||||
"Page 17",
|
||||
"",
|
||||
"General Photos:",
|
||||
"General Photos:",
|
||||
"External Elevations:",
|
||||
"External Elevations:",
|
||||
"Related party disclosure",
|
||||
"No related party",
|
||||
"Hard to treat cavity walls: Property has access",
|
||||
"issues?",
|
||||
"No",
|
||||
"Hard to treat cavity walls: Property has high",
|
||||
"exposure?",
|
||||
"No",
|
||||
"Hard to treat cavity walls: Property has narrow",
|
||||
"cavities?",
|
||||
"No",
|
||||
"Photographs Required",
|
||||
"Page 18",
|
||||
"",
|
||||
"External Elevations:",
|
||||
"External Elevations:",
|
||||
"External Elevations:",
|
||||
"External Elevations:",
|
||||
"External Elevations:",
|
||||
"External Elevations:",
|
||||
"Page 19",
|
||||
"",
|
||||
"External Elevations:",
|
||||
"External Elevations:",
|
||||
"External Elevations:",
|
||||
"External Elevations:",
|
||||
"Page 20",
|
||||
"",
|
||||
"External Elevations:",
|
||||
"External Elevations:",
|
||||
"External Elevations:",
|
||||
"External Elevations:",
|
||||
"External Elevations:",
|
||||
"Page 21",
|
||||
"",
|
||||
"External Elevations:",
|
||||
"External Elevations:",
|
||||
"Page 22",
|
||||
"",
|
||||
"Additional Notes",
|
||||
"Additional Notes",
|
||||
"The loft hatch is a drop-down hatch with a ladder above. The hatch is broken",
|
||||
"and it could not be opened. She is about to have a new door made so loft will",
|
||||
"be able to be accessed at some point soon but on the day I could not.",
|
||||
"Page 23",
|
||||
""
|
||||
]
|
||||
1
backend/documents_parser/tests/fixtures/site_notes_example_5_text.json
vendored
Normal file
1
backend/documents_parser/tests/fixtures/site_notes_example_5_text.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
backend/documents_parser/tests/fixtures/site_notes_example_6_text.json
vendored
Normal file
1
backend/documents_parser/tests/fixtures/site_notes_example_6_text.json
vendored
Normal file
File diff suppressed because one or more lines are too long
643
backend/documents_parser/tests/fixtures/site_notes_example_text.json
vendored
Normal file
643
backend/documents_parser/tests/fixtures/site_notes_example_text.json
vendored
Normal file
|
|
@ -0,0 +1,643 @@
|
|||
[
|
||||
"SMART EPC: Record of",
|
||||
"Inspection & Site Notes",
|
||||
"Inspection Surveyor:",
|
||||
"Benjamin Burke",
|
||||
"E-Mail Address:",
|
||||
"ben@mbsolutionsgroup.co.uk",
|
||||
"Report Reference:",
|
||||
"6EA2A86D-94CE-4792-8D49-AB495C744EDD",
|
||||
"Created On:",
|
||||
"10 November 2025",
|
||||
"Date of Inspection:",
|
||||
"25 September 2025",
|
||||
"Property Address:",
|
||||
"40,",
|
||||
"Abbey Place,",
|
||||
"Crewe,",
|
||||
"Cheshire,",
|
||||
"CW1 4JR",
|
||||
"Property Photo",
|
||||
"Page 1",
|
||||
"",
|
||||
"Photo of electricity meter:",
|
||||
"Photo of electricity meter:",
|
||||
"Photo of electricity meter:",
|
||||
"Photo of electricity meter:",
|
||||
"RdSAP Assessment",
|
||||
"General",
|
||||
"Confirm you have checked for the existence of an",
|
||||
"EPC before carrying out another energy assessment.",
|
||||
"Yes",
|
||||
"Does an EPC exist at the point of carrying out this",
|
||||
"energy assessment?",
|
||||
"No",
|
||||
"Inspection Date:",
|
||||
"25/09/2025",
|
||||
"Transaction Type:",
|
||||
"Grant-Scheme (ECO, RHI, etc.)",
|
||||
"Tenure:",
|
||||
"Rented Social",
|
||||
"Type of Property:",
|
||||
"House",
|
||||
"Detachment Type:",
|
||||
"Mid-terrace",
|
||||
"Number of storeys:",
|
||||
"2 Storeys",
|
||||
"Terrain Type:",
|
||||
"Suburban",
|
||||
"Number of Extensions:",
|
||||
"1 Extension",
|
||||
"Is an electricity smart meter present?",
|
||||
"Yes",
|
||||
"Electric meter type:",
|
||||
"Single",
|
||||
"Is the dwelling export-capable?",
|
||||
"Yes",
|
||||
"Is mains gas available?",
|
||||
"Yes",
|
||||
"Is there a gas smart meter?",
|
||||
"Yes",
|
||||
"Is the gas meter accessible?",
|
||||
"Yes",
|
||||
"Page 2",
|
||||
"",
|
||||
"Photo of Gas Meter:",
|
||||
"External indicators of Cavity Wall Construction:",
|
||||
"External indicators of Cavity Wall Construction:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Select Measurements Location:",
|
||||
"Internal",
|
||||
"Building Construction",
|
||||
"Main Building",
|
||||
"Age Range:",
|
||||
"1950-1966",
|
||||
"Record indicators of property age:",
|
||||
"local knowledge, enquiries of owner",
|
||||
"Walls - Construction Type:",
|
||||
"Cavity",
|
||||
"Record external indicators of Cavity Construction:",
|
||||
"wall thickness over 270 mm",
|
||||
"Walls - Insulation Type:",
|
||||
"Filled Cavity",
|
||||
"Record indicators of filled cavity:",
|
||||
"evidence of cavity fill drill holes",
|
||||
"Page 3",
|
||||
"",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Page 4",
|
||||
"",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Photo indicators of filled cavity insulation:",
|
||||
"Thermal conductivity of wall insulation:",
|
||||
"Unknown",
|
||||
"Wall U-Value known?",
|
||||
"Not Known",
|
||||
"Wall thickness:",
|
||||
"310 mm",
|
||||
"Page 5",
|
||||
"",
|
||||
"Photo wall thickness:",
|
||||
"External indicators of Cavity Wall Construction:",
|
||||
"Party wall construction type:",
|
||||
"Cavity Masonry, Filled",
|
||||
"Floor type:",
|
||||
"Ground Floor",
|
||||
"Floor Construction:",
|
||||
"Solid",
|
||||
"Floor Insulation Type:",
|
||||
"As Built",
|
||||
"Floor U-Value known?",
|
||||
"Not Known",
|
||||
"Extension 1",
|
||||
"Age Range:",
|
||||
"2003-2006",
|
||||
"Record indicators of property age:",
|
||||
"local knowledge, enquiries of owner",
|
||||
"Walls - Construction Type:",
|
||||
"Cavity",
|
||||
"Record external indicators of Cavity Construction:",
|
||||
"wall thickness over 270 mm",
|
||||
"Walls - Insulation Type:",
|
||||
"As built",
|
||||
"Thermal conductivity of wall insulation:",
|
||||
"Unknown",
|
||||
"Wall U-Value known?",
|
||||
"Not Known",
|
||||
"Wall thickness:",
|
||||
"310 mm",
|
||||
"Page 6",
|
||||
"",
|
||||
"Photo wall thickness:",
|
||||
"Party wall construction type:",
|
||||
"Cavity Masonry, Filled",
|
||||
"Floor type:",
|
||||
"Ground Floor",
|
||||
"Floor Construction:",
|
||||
"Solid",
|
||||
"Floor Insulation Type:",
|
||||
"As Built",
|
||||
"Floor U-Value known?",
|
||||
"Not Known",
|
||||
"Building Measurements",
|
||||
"Area (m2)",
|
||||
"Height (m)",
|
||||
"Heat Loss Perimeter (m)",
|
||||
"PWL (m)",
|
||||
"Main Building",
|
||||
"Floor 1",
|
||||
"35.68",
|
||||
"2.19",
|
||||
"13.44",
|
||||
"10.62",
|
||||
"Floor 0",
|
||||
"35.68",
|
||||
"2.17",
|
||||
"11",
|
||||
"10.62",
|
||||
"Extension 1",
|
||||
"Floor 0",
|
||||
"3.8",
|
||||
"2",
|
||||
"5.7",
|
||||
"0",
|
||||
"Roof Space",
|
||||
"Main Building",
|
||||
"Roofs - Construction Type:",
|
||||
"Pitched roof (Slates or tiles), Access to loft",
|
||||
"Roofs - Insulation At:",
|
||||
"Joists",
|
||||
"Roof U-Value:",
|
||||
"Not Known",
|
||||
"Roofs - Insulation Thickness:",
|
||||
"100 mm",
|
||||
"Page 7",
|
||||
"",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Page 8",
|
||||
"",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Loft insulation:",
|
||||
"Page 9",
|
||||
"",
|
||||
"Loft insulation:",
|
||||
"Indicators of Cavity Wall Construction in roof space:",
|
||||
"Indicators of Cavity Wall Construction in roof space:",
|
||||
"Record indicators of party wall construction in roof space:",
|
||||
"Record indicators of party wall construction in roof space:",
|
||||
"Record indicators of Cavity Wall Construction in roof",
|
||||
"space:",
|
||||
"cavity visible in roof space",
|
||||
"Are there rooms in the roof?",
|
||||
"No",
|
||||
"Extension 1",
|
||||
"Roofs - Construction Type:",
|
||||
"Pitched roof, Sloping ceiling",
|
||||
"Page 10",
|
||||
"",
|
||||
"Photo of glazing type:",
|
||||
"Photo of glazing type:",
|
||||
"Roofs - Insulation At:",
|
||||
"Sloping ceiling insulation",
|
||||
"Roof U-Value:",
|
||||
"Not Known",
|
||||
"Roofs - Insulation Thickness:",
|
||||
"As built",
|
||||
"Record indicators of Cavity Wall Construction in roof",
|
||||
"space:",
|
||||
"No indicator of construction visible",
|
||||
"Are there rooms in the roof?",
|
||||
"No",
|
||||
"Windows",
|
||||
"Window 1",
|
||||
"Window location:",
|
||||
"Main Building",
|
||||
"Window wall type:",
|
||||
"External wall",
|
||||
"Glazing Type:",
|
||||
"Double glazing, Unknown install date",
|
||||
"Window type:",
|
||||
"Window",
|
||||
"Window frame type:",
|
||||
"Wooden or PVC",
|
||||
"What size is the glazing gap?",
|
||||
"16 mm or more",
|
||||
"Is the window draught proofed?",
|
||||
"Yes",
|
||||
"Are there permanent shutters present?",
|
||||
"No",
|
||||
"Window height:",
|
||||
"1.2 m",
|
||||
"Window width:",
|
||||
"2.3 m",
|
||||
"Orientation:",
|
||||
"North West",
|
||||
"Window 2",
|
||||
"Window location:",
|
||||
"Main Building",
|
||||
"Window wall type:",
|
||||
"External wall",
|
||||
"Glazing Type:",
|
||||
"Double glazing, Unknown install date",
|
||||
"Window type:",
|
||||
"Window",
|
||||
"Window frame type:",
|
||||
"Wooden or PVC",
|
||||
"What size is the glazing gap?",
|
||||
"16 mm or more",
|
||||
"Is the window draught proofed?",
|
||||
"Yes",
|
||||
"Are there permanent shutters present?",
|
||||
"No",
|
||||
"Page 11",
|
||||
"",
|
||||
"Photo of glazing type:",
|
||||
"Photo of glazing type:",
|
||||
"Photo of glazing type:",
|
||||
"Photo of glazing type:",
|
||||
"Window height:",
|
||||
"1.2 m",
|
||||
"Window width:",
|
||||
"1 m",
|
||||
"Orientation:",
|
||||
"North West",
|
||||
"Window 3",
|
||||
"Window location:",
|
||||
"Main Building",
|
||||
"Window wall type:",
|
||||
"External wall",
|
||||
"Glazing Type:",
|
||||
"Double glazing, Unknown install date",
|
||||
"Window type:",
|
||||
"Window",
|
||||
"Window frame type:",
|
||||
"Wooden or PVC",
|
||||
"What size is the glazing gap?",
|
||||
"16 mm or more",
|
||||
"Is the window draught proofed?",
|
||||
"Yes",
|
||||
"Are there permanent shutters present?",
|
||||
"No",
|
||||
"Window height:",
|
||||
"0.9 m",
|
||||
"Window width:",
|
||||
"1 m",
|
||||
"Orientation:",
|
||||
"North East",
|
||||
"Window 4",
|
||||
"Window location:",
|
||||
"Extension 1",
|
||||
"Window wall type:",
|
||||
"External wall",
|
||||
"Page 12",
|
||||
"",
|
||||
"Photo of glazing type:",
|
||||
"Photo of glazing type:",
|
||||
"Glazing Type:",
|
||||
"Double glazing, Unknown install date",
|
||||
"Window type:",
|
||||
"Window",
|
||||
"Window frame type:",
|
||||
"Wooden or PVC",
|
||||
"What size is the glazing gap?",
|
||||
"16 mm or more",
|
||||
"Is the window draught proofed?",
|
||||
"Yes",
|
||||
"Are there permanent shutters present?",
|
||||
"No",
|
||||
"Window height:",
|
||||
"0.9 m",
|
||||
"Window width:",
|
||||
"1 m",
|
||||
"Orientation:",
|
||||
"North",
|
||||
"Window 5",
|
||||
"Window location:",
|
||||
"Extension 1",
|
||||
"Window wall type:",
|
||||
"External wall",
|
||||
"Glazing Type:",
|
||||
"Double glazing, Unknown install date",
|
||||
"Window type:",
|
||||
"Window",
|
||||
"Window frame type:",
|
||||
"Wooden or PVC",
|
||||
"What size is the glazing gap?",
|
||||
"16 mm or more",
|
||||
"Is the window draught proofed?",
|
||||
"Yes",
|
||||
"Are there permanent shutters present?",
|
||||
"No",
|
||||
"Window height:",
|
||||
"0.9 m",
|
||||
"Window width:",
|
||||
"1.7 m",
|
||||
"Orientation:",
|
||||
"North East",
|
||||
"Page 13",
|
||||
"",
|
||||
"Photo of glazing type:",
|
||||
"Photo of glazing type:",
|
||||
"Photo of glazing type:",
|
||||
"Photo of glazing type:",
|
||||
"Window 6",
|
||||
"Window location:",
|
||||
"Extension 1",
|
||||
"Window wall type:",
|
||||
"External wall",
|
||||
"Glazing Type:",
|
||||
"Double glazing, Unknown install date",
|
||||
"Window type:",
|
||||
"Window",
|
||||
"Window frame type:",
|
||||
"Wooden or PVC",
|
||||
"What size is the glazing gap?",
|
||||
"16 mm or more",
|
||||
"Is the window draught proofed?",
|
||||
"Yes",
|
||||
"Are there permanent shutters present?",
|
||||
"No",
|
||||
"Window height:",
|
||||
"0.9 m",
|
||||
"Window width:",
|
||||
"2.3 m",
|
||||
"Orientation:",
|
||||
"North West",
|
||||
"Window 7",
|
||||
"Window location:",
|
||||
"Extension 1",
|
||||
"Window wall type:",
|
||||
"External wall",
|
||||
"Glazing Type:",
|
||||
"Double glazing, Unknown install date",
|
||||
"Window type:",
|
||||
"Window",
|
||||
"Window frame type:",
|
||||
"Wooden or PVC",
|
||||
"Page 14",
|
||||
"",
|
||||
"Photo of glazing type:",
|
||||
"Photo of glazing type:",
|
||||
"Photo of glazing type:",
|
||||
"Photo of glazing type:",
|
||||
"What size is the glazing gap?",
|
||||
"16 mm or more",
|
||||
"Is the window draught proofed?",
|
||||
"Yes",
|
||||
"Are there permanent shutters present?",
|
||||
"No",
|
||||
"Window height:",
|
||||
"1 m",
|
||||
"Window width:",
|
||||
"1.2 m",
|
||||
"Orientation:",
|
||||
"North West",
|
||||
"Window 8",
|
||||
"Window location:",
|
||||
"Extension 1",
|
||||
"Window wall type:",
|
||||
"External wall",
|
||||
"Glazing Type:",
|
||||
"Double glazing, Unknown install date",
|
||||
"Window type:",
|
||||
"Window",
|
||||
"Window frame type:",
|
||||
"Wooden or PVC",
|
||||
"What size is the glazing gap?",
|
||||
"16 mm or more",
|
||||
"Is the window draught proofed?",
|
||||
"Yes",
|
||||
"Are there permanent shutters present?",
|
||||
"No",
|
||||
"Window height:",
|
||||
"0.9 m",
|
||||
"Window width:",
|
||||
"1 m",
|
||||
"Orientation:",
|
||||
"North East",
|
||||
"Page 15",
|
||||
"",
|
||||
"Photo of heating system:",
|
||||
"Heating & Hot Water",
|
||||
"Main Heating Systems",
|
||||
"Main Heating 1",
|
||||
"How would you like to select the Heating System?",
|
||||
"PCDF Search",
|
||||
"System type:",
|
||||
"Boiler with radiators or underfloor heating",
|
||||
"Product Id",
|
||||
"16839",
|
||||
"Manufacturer",
|
||||
"Vaillant",
|
||||
"Model",
|
||||
"ecoTEC pro 28",
|
||||
"Orig Manuf",
|
||||
"Vaillant",
|
||||
"Fuel",
|
||||
"Mains gas",
|
||||
"S. Efficiency",
|
||||
"0",
|
||||
"Type",
|
||||
"Combi",
|
||||
"Condensing",
|
||||
"Yes",
|
||||
"Year",
|
||||
"2005 - 2015",
|
||||
"Mount",
|
||||
"Wall",
|
||||
"Open Flue",
|
||||
"Room-sealed",
|
||||
"Fan Assist",
|
||||
"Yes",
|
||||
"Status",
|
||||
"Normal status for an actual product",
|
||||
"Central heating pump age:",
|
||||
"Unknown",
|
||||
"Controls:",
|
||||
"Programmer, room thermostat and TRVs",
|
||||
"Does the boiler have a Flue Gas Heat Recover",
|
||||
"System (FGHRS)?",
|
||||
"No",
|
||||
"Is there a weather compensator?",
|
||||
"No",
|
||||
"Emitter:",
|
||||
"Radiators",
|
||||
"Emitter Temperature:",
|
||||
"Unknown",
|
||||
"Page 16",
|
||||
"",
|
||||
"Photo of heating system:",
|
||||
"Photo of heating system:",
|
||||
"Photo of heating system:",
|
||||
"Photo of heating system:",
|
||||
"Photo of heating system:",
|
||||
"Photo of heating system:",
|
||||
"Page 17",
|
||||
"",
|
||||
"Photo of heating system:",
|
||||
"Photo of heating system:",
|
||||
"Photo of heating system:",
|
||||
"Photo of heating system:",
|
||||
"Photo of heating controls:",
|
||||
"Photo of heating controls:",
|
||||
"Secondary Heating System",
|
||||
"Secondary Fuel",
|
||||
"No Secondary Heating",
|
||||
"Water Heating & Cylinder",
|
||||
"Water Heating Type:",
|
||||
"Regular",
|
||||
"Water Heating System:",
|
||||
"From main heating 1",
|
||||
"Cylinder Size:",
|
||||
"No Cylinder",
|
||||
"Ventilation",
|
||||
"Ventilation type:",
|
||||
"Mechanical Extract - Decentralised",
|
||||
"Page 18",
|
||||
"",
|
||||
"Photo of ventilation type:",
|
||||
"Has fixed air conditioning?",
|
||||
"No",
|
||||
"Is the ventilation in the PCDF database?",
|
||||
"No",
|
||||
"Number of open flues:",
|
||||
"0",
|
||||
"Number of closed flues:",
|
||||
"0",
|
||||
"Number of boiler flues:",
|
||||
"0",
|
||||
"Number of other flues:",
|
||||
"0",
|
||||
"Number of extract fans:",
|
||||
"0",
|
||||
"Number of passive vents:",
|
||||
"0",
|
||||
"Number of flueless gas fires:",
|
||||
"0",
|
||||
"Pressure test:",
|
||||
"No test",
|
||||
"Is there a draught lobby?",
|
||||
"No",
|
||||
"Conservatories",
|
||||
"Is there conservatory?",
|
||||
"No conservatory",
|
||||
"Page 19",
|
||||
"",
|
||||
"Photo of incandescent bulbs:",
|
||||
"Photo of incandescent bulbs:",
|
||||
"Renewables",
|
||||
"Wind Turbines",
|
||||
"Has wind turbines?",
|
||||
"No",
|
||||
"Solar hot water",
|
||||
"Has solar hot water?",
|
||||
"No",
|
||||
"Photovoltaics",
|
||||
"Has photovoltaic array?",
|
||||
"No",
|
||||
"Number of PV batteries:",
|
||||
"None",
|
||||
"Hydro",
|
||||
"Is the dwelling connected to Hydro?",
|
||||
"No",
|
||||
"Room Count Elements",
|
||||
"Number of habitable rooms?",
|
||||
"3",
|
||||
"Are any of these rooms unheated?",
|
||||
"No",
|
||||
"Number of external doors?",
|
||||
"2",
|
||||
"Number of insulated external doors?",
|
||||
"0",
|
||||
"Number of draughtproofed external doors?",
|
||||
"2",
|
||||
"Number of open chimneys?",
|
||||
"0",
|
||||
"Number of blocked chimneys?",
|
||||
"0",
|
||||
"Number of fixed incandescent bulbs:",
|
||||
"4",
|
||||
"Page 20",
|
||||
"",
|
||||
"Photo of incandescent bulbs:",
|
||||
"Photo of incandescent bulbs:",
|
||||
"Photo of CFL bulbs:",
|
||||
"Is the exact number of LED and CFL bulbs known?",
|
||||
"Yes",
|
||||
"Number of fixed LED bulbs:",
|
||||
"0",
|
||||
"Number of fixed CFL bulbs:",
|
||||
"1",
|
||||
"Are there any waste water heat recovery systems?",
|
||||
"None",
|
||||
"Number of baths:",
|
||||
"1",
|
||||
"How many special features are there at the",
|
||||
"property?",
|
||||
"0",
|
||||
"Showers",
|
||||
"Shower 1",
|
||||
"Shower outlet type:",
|
||||
"Non-Electric Shower",
|
||||
"Page 21",
|
||||
"",
|
||||
"Photo of shower:",
|
||||
"Photo of shower:",
|
||||
"General Photos:",
|
||||
"Customer Response",
|
||||
"Customer present?",
|
||||
"Yes",
|
||||
"Customer willing to answer satisfaction survey?",
|
||||
"No",
|
||||
"Addendum + Related Party Disclosure",
|
||||
"Addendum",
|
||||
"None",
|
||||
"Related party disclosure",
|
||||
"No related party",
|
||||
"Hard to treat cavity walls: Property has access",
|
||||
"issues?",
|
||||
"No",
|
||||
"Hard to treat cavity walls: Property has high",
|
||||
"exposure?",
|
||||
"No",
|
||||
"Hard to treat cavity walls: Property has narrow",
|
||||
"cavities?",
|
||||
"No",
|
||||
"Photographs Required",
|
||||
"Page 22",
|
||||
"",
|
||||
"External Elevations:",
|
||||
"External Elevations:",
|
||||
"External Elevations:",
|
||||
"External Elevations:",
|
||||
"External Elevations:",
|
||||
"External Elevations:",
|
||||
"External Elevations:",
|
||||
"Page 23",
|
||||
"",
|
||||
"Page 24",
|
||||
"",
|
||||
"Page 25",
|
||||
"",
|
||||
"Page 26",
|
||||
"",
|
||||
"Page 27",
|
||||
""
|
||||
]
|
||||
422
backend/documents_parser/tests/test_end_to_end.py
Normal file
422
backend/documents_parser/tests/test_end_to_end.py
Normal file
|
|
@ -0,0 +1,422 @@
|
|||
import os
|
||||
from datetime import date
|
||||
|
||||
import pytest
|
||||
|
||||
from backend.documents_parser.extractor import PasHubRdSapSiteNotesExtractor
|
||||
from backend.documents_parser.pdf import pdf_to_text_list
|
||||
from datatypes.epc.domain.epc_property_data import (
|
||||
EpcPropertyData,
|
||||
InstantaneousWwhrs,
|
||||
MainHeatingDetail,
|
||||
SapBuildingPart,
|
||||
SapEnergySource,
|
||||
SapFloorDimension,
|
||||
SapHeating,
|
||||
SapVentilation,
|
||||
SapWindow,
|
||||
ShowerOutlet,
|
||||
ShowerOutlets,
|
||||
)
|
||||
from datatypes.epc.domain.mapper import EpcPropertyDataMapper
|
||||
|
||||
PDF_PATH = os.path.join(os.path.dirname(__file__), "fixtures", "ExampleSiteNotes.pdf")
|
||||
PDF_PATH_2 = os.path.join(
|
||||
os.path.dirname(__file__), "fixtures", "ExampleSiteNotes_2.pdf"
|
||||
)
|
||||
|
||||
|
||||
class TestPdfToEpcPropertyData:
|
||||
@pytest.fixture
|
||||
def result(self) -> EpcPropertyData:
|
||||
with open(PDF_PATH, "rb") as f:
|
||||
pdf_bytes = f.read()
|
||||
site_notes = PasHubRdSapSiteNotesExtractor(
|
||||
pdf_to_text_list(pdf_bytes)
|
||||
).extract()
|
||||
return EpcPropertyDataMapper.from_site_notes(site_notes)
|
||||
|
||||
def test_full_epc_property_data(self, result: EpcPropertyData) -> None:
|
||||
assert result == EpcPropertyData(
|
||||
dwelling_type="Mid-terrace house",
|
||||
inspection_date=date(2025, 9, 25),
|
||||
tenure="Rented Social",
|
||||
transaction_type="Grant-Scheme (ECO, RHI, etc.)",
|
||||
roofs=[],
|
||||
walls=[],
|
||||
floors=[],
|
||||
main_heating=[],
|
||||
door_count=2,
|
||||
sap_heating=SapHeating(
|
||||
instantaneous_wwhrs=InstantaneousWwhrs(),
|
||||
main_heating_details=[
|
||||
MainHeatingDetail(
|
||||
has_fghrs=False,
|
||||
main_fuel_type="Mains gas",
|
||||
heat_emitter_type="Radiators",
|
||||
emitter_temperature="Unknown",
|
||||
main_heating_control="Programmer, room thermostat and TRVs",
|
||||
fan_flue_present=True,
|
||||
condensing=True,
|
||||
weather_compensator=False,
|
||||
central_heating_pump_age_str="Unknown",
|
||||
)
|
||||
],
|
||||
has_fixed_air_conditioning=False,
|
||||
shower_outlets=ShowerOutlets(
|
||||
shower_outlet=ShowerOutlet(
|
||||
shower_outlet_type="Non-Electric Shower"
|
||||
),
|
||||
),
|
||||
),
|
||||
sap_windows=[
|
||||
SapWindow(
|
||||
pvc_frame="Wooden or PVC",
|
||||
glazing_gap="16 mm or more",
|
||||
orientation="North West",
|
||||
window_type="Window",
|
||||
glazing_type="Double glazing, Unknown install date",
|
||||
window_width=2.3,
|
||||
window_height=1.2,
|
||||
draught_proofed=True,
|
||||
window_location="Main Building",
|
||||
window_wall_type="External wall",
|
||||
permanent_shutters_present=False,
|
||||
),
|
||||
SapWindow(
|
||||
pvc_frame="Wooden or PVC",
|
||||
glazing_gap="16 mm or more",
|
||||
orientation="North West",
|
||||
window_type="Window",
|
||||
glazing_type="Double glazing, Unknown install date",
|
||||
window_width=1.0,
|
||||
window_height=1.2,
|
||||
draught_proofed=True,
|
||||
window_location="Main Building",
|
||||
window_wall_type="External wall",
|
||||
permanent_shutters_present=False,
|
||||
),
|
||||
SapWindow(
|
||||
pvc_frame="Wooden or PVC",
|
||||
glazing_gap="16 mm or more",
|
||||
orientation="North East",
|
||||
window_type="Window",
|
||||
glazing_type="Double glazing, Unknown install date",
|
||||
window_width=1.0,
|
||||
window_height=0.9,
|
||||
draught_proofed=True,
|
||||
window_location="Main Building",
|
||||
window_wall_type="External wall",
|
||||
permanent_shutters_present=False,
|
||||
),
|
||||
SapWindow(
|
||||
pvc_frame="Wooden or PVC",
|
||||
glazing_gap="16 mm or more",
|
||||
orientation="North",
|
||||
window_type="Window",
|
||||
glazing_type="Double glazing, Unknown install date",
|
||||
window_width=1.0,
|
||||
window_height=0.9,
|
||||
draught_proofed=True,
|
||||
window_location="Extension 1",
|
||||
window_wall_type="External wall",
|
||||
permanent_shutters_present=False,
|
||||
),
|
||||
SapWindow(
|
||||
pvc_frame="Wooden or PVC",
|
||||
glazing_gap="16 mm or more",
|
||||
orientation="North East",
|
||||
window_type="Window",
|
||||
glazing_type="Double glazing, Unknown install date",
|
||||
window_width=1.7,
|
||||
window_height=0.9,
|
||||
draught_proofed=True,
|
||||
window_location="Extension 1",
|
||||
window_wall_type="External wall",
|
||||
permanent_shutters_present=False,
|
||||
),
|
||||
SapWindow(
|
||||
pvc_frame="Wooden or PVC",
|
||||
glazing_gap="16 mm or more",
|
||||
orientation="North West",
|
||||
window_type="Window",
|
||||
glazing_type="Double glazing, Unknown install date",
|
||||
window_width=2.3,
|
||||
window_height=0.9,
|
||||
draught_proofed=True,
|
||||
window_location="Extension 1",
|
||||
window_wall_type="External wall",
|
||||
permanent_shutters_present=False,
|
||||
),
|
||||
SapWindow(
|
||||
pvc_frame="Wooden or PVC",
|
||||
glazing_gap="16 mm or more",
|
||||
orientation="North West",
|
||||
window_type="Window",
|
||||
glazing_type="Double glazing, Unknown install date",
|
||||
window_width=1.2,
|
||||
window_height=1.0,
|
||||
draught_proofed=True,
|
||||
window_location="Extension 1",
|
||||
window_wall_type="External wall",
|
||||
permanent_shutters_present=False,
|
||||
),
|
||||
SapWindow(
|
||||
pvc_frame="Wooden or PVC",
|
||||
glazing_gap="16 mm or more",
|
||||
orientation="North East",
|
||||
window_type="Window",
|
||||
glazing_type="Double glazing, Unknown install date",
|
||||
window_width=1.0,
|
||||
window_height=0.9,
|
||||
draught_proofed=True,
|
||||
window_location="Extension 1",
|
||||
window_wall_type="External wall",
|
||||
permanent_shutters_present=False,
|
||||
),
|
||||
],
|
||||
sap_energy_source=SapEnergySource(
|
||||
mains_gas=True,
|
||||
meter_type="Single",
|
||||
pv_battery_count=0,
|
||||
wind_turbines_count=0,
|
||||
gas_smart_meter_present=True,
|
||||
is_dwelling_export_capable=True,
|
||||
wind_turbines_terrain_type="Suburban",
|
||||
electricity_smart_meter_present=True,
|
||||
),
|
||||
sap_building_parts=[
|
||||
SapBuildingPart(
|
||||
identifier="main",
|
||||
construction_age_band="1950-1966",
|
||||
wall_construction="Cavity",
|
||||
wall_insulation_type="Filled Cavity",
|
||||
wall_thickness_measured=True,
|
||||
party_wall_construction="Cavity Masonry, Filled",
|
||||
sap_floor_dimensions=[
|
||||
SapFloorDimension(
|
||||
room_height_m=2.19,
|
||||
total_floor_area_m2=35.68,
|
||||
party_wall_length_m=10.62,
|
||||
heat_loss_perimeter_m=13.44,
|
||||
floor=1,
|
||||
),
|
||||
SapFloorDimension(
|
||||
room_height_m=2.17,
|
||||
total_floor_area_m2=35.68,
|
||||
party_wall_length_m=10.62,
|
||||
heat_loss_perimeter_m=11.0,
|
||||
floor=0,
|
||||
),
|
||||
],
|
||||
wall_thickness_mm=310,
|
||||
roof_insulation_location="Joists",
|
||||
roof_insulation_thickness=100,
|
||||
floor_type="Ground Floor",
|
||||
floor_construction_type="Solid",
|
||||
floor_insulation_type_str="As Built",
|
||||
floor_u_value_known=False,
|
||||
),
|
||||
SapBuildingPart(
|
||||
identifier="extension_1",
|
||||
construction_age_band="2003-2006",
|
||||
wall_construction="Cavity",
|
||||
wall_insulation_type="As built",
|
||||
wall_thickness_measured=True,
|
||||
party_wall_construction="Cavity Masonry, Filled",
|
||||
sap_floor_dimensions=[
|
||||
SapFloorDimension(
|
||||
room_height_m=2.0,
|
||||
total_floor_area_m2=3.8,
|
||||
party_wall_length_m=0.0,
|
||||
heat_loss_perimeter_m=5.7,
|
||||
floor=0,
|
||||
),
|
||||
],
|
||||
wall_thickness_mm=310,
|
||||
roof_insulation_location="Sloping ceiling insulation",
|
||||
roof_insulation_thickness="As built",
|
||||
),
|
||||
],
|
||||
solar_water_heating=False,
|
||||
has_hot_water_cylinder=False,
|
||||
has_fixed_air_conditioning=False,
|
||||
wet_rooms_count=0,
|
||||
extensions_count=1,
|
||||
heated_rooms_count=0,
|
||||
open_chimneys_count=0,
|
||||
habitable_rooms_count=3,
|
||||
insulated_door_count=0,
|
||||
cfl_fixed_lighting_bulbs_count=1,
|
||||
led_fixed_lighting_bulbs_count=0,
|
||||
incandescent_fixed_lighting_bulbs_count=4,
|
||||
total_floor_area_m2=75.16,
|
||||
built_form="Mid-terrace",
|
||||
property_type="House",
|
||||
has_conservatory=False,
|
||||
blocked_chimneys_count=0,
|
||||
draughtproofed_door_count=2,
|
||||
address_line_1="40, Abbey Place",
|
||||
post_town="Crewe",
|
||||
postcode="CW1 4JR",
|
||||
report_reference="6EA2A86D-94CE-4792-8D49-AB495C744EDD",
|
||||
number_of_storeys=2,
|
||||
any_unheated_rooms=False,
|
||||
waste_water_heat_recovery="None",
|
||||
hydro=False,
|
||||
photovoltaic_array=False,
|
||||
sap_ventilation=SapVentilation(
|
||||
ventilation_type="Mechanical Extract - Decentralised",
|
||||
draught_lobby=False,
|
||||
pressure_test="No test",
|
||||
open_flues_count=0,
|
||||
closed_flues_count=0,
|
||||
boiler_flues_count=0,
|
||||
other_flues_count=0,
|
||||
extract_fans_count=0,
|
||||
passive_vents_count=0,
|
||||
flueless_gas_fires_count=0,
|
||||
ventilation_in_pcdf_database=False,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class TestPdfToEpcPropertyDataFixture2:
|
||||
@pytest.fixture
|
||||
def result(self) -> EpcPropertyData:
|
||||
with open(PDF_PATH_2, "rb") as f:
|
||||
pdf_bytes = f.read()
|
||||
site_notes = PasHubRdSapSiteNotesExtractor(
|
||||
pdf_to_text_list(pdf_bytes)
|
||||
).extract()
|
||||
return EpcPropertyDataMapper.from_site_notes(site_notes)
|
||||
|
||||
def test_cylinder_insulation_thickness(self, result: EpcPropertyData) -> None:
|
||||
assert result.sap_heating.cylinder_insulation_thickness_mm == 38
|
||||
|
||||
def test_cylinder_size(self, result: EpcPropertyData) -> None:
|
||||
assert result.sap_heating.cylinder_size == "Normal (90-130 litres)"
|
||||
|
||||
def test_secondary_heating_type(self, result: EpcPropertyData) -> None:
|
||||
assert result.sap_heating.secondary_heating_type == "Open fire in grate"
|
||||
|
||||
|
||||
PDF_PATH_3 = os.path.join(
|
||||
os.path.dirname(__file__), "fixtures", "ExampleSiteNotes_3.pdf"
|
||||
)
|
||||
|
||||
|
||||
class TestPdfToEpcPropertyDataFixture3:
|
||||
@pytest.fixture
|
||||
def result(self) -> EpcPropertyData:
|
||||
with open(PDF_PATH_3, "rb") as f:
|
||||
pdf_bytes = f.read()
|
||||
site_notes = PasHubRdSapSiteNotesExtractor(
|
||||
pdf_to_text_list(pdf_bytes)
|
||||
).extract()
|
||||
return EpcPropertyDataMapper.from_site_notes(site_notes)
|
||||
|
||||
def test_immersion_heating_type(self, result: EpcPropertyData) -> None:
|
||||
assert result.sap_heating.immersion_heating_type == "Dual"
|
||||
|
||||
def test_pv_connection(self, result: EpcPropertyData) -> None:
|
||||
assert (
|
||||
result.sap_energy_source.pv_connection
|
||||
== "Connected to dwellings electricity meter"
|
||||
)
|
||||
|
||||
def test_photovoltaic_supply_percent_roof(self, result: EpcPropertyData) -> None:
|
||||
assert result.sap_energy_source.photovoltaic_supply is not None
|
||||
assert (
|
||||
result.sap_energy_source.photovoltaic_supply.none_or_no_details.percent_roof_area
|
||||
== 45
|
||||
)
|
||||
|
||||
def test_electric_storage_heater_fuel_type(self, result: EpcPropertyData) -> None:
|
||||
assert (
|
||||
result.sap_heating.main_heating_details[0].main_fuel_type == "Electricity"
|
||||
)
|
||||
|
||||
|
||||
PDF_PATH_4 = os.path.join(
|
||||
os.path.dirname(__file__), "fixtures", "ExampleSiteNotes_4.pdf"
|
||||
)
|
||||
|
||||
|
||||
class TestPdfToEpcPropertyDataFixture4:
|
||||
@pytest.fixture
|
||||
def result(self) -> EpcPropertyData:
|
||||
with open(PDF_PATH_4, "rb") as f:
|
||||
pdf_bytes = f.read()
|
||||
site_notes = PasHubRdSapSiteNotesExtractor(
|
||||
pdf_to_text_list(pdf_bytes)
|
||||
).extract()
|
||||
return EpcPropertyDataMapper.from_site_notes(site_notes)
|
||||
|
||||
def test_cylinder_insulation_type(self, result: EpcPropertyData) -> None:
|
||||
assert result.sap_heating.cylinder_insulation_type == "Factory fitted"
|
||||
|
||||
def test_heat_pump_fuel_type(self, result: EpcPropertyData) -> None:
|
||||
assert (
|
||||
result.sap_heating.main_heating_details[0].main_fuel_type == "Electricity"
|
||||
)
|
||||
|
||||
def test_roof_insulation_location_unknown(self, result: EpcPropertyData) -> None:
|
||||
assert result.sap_building_parts[0].roof_insulation_location == "Unknown"
|
||||
|
||||
def test_roof_insulation_thickness_none(self, result: EpcPropertyData) -> None:
|
||||
assert result.sap_building_parts[0].roof_insulation_thickness is None
|
||||
|
||||
|
||||
PDF_PATH_5 = os.path.join(
|
||||
os.path.dirname(__file__), "fixtures", "ExampleSiteNotes_5.pdf"
|
||||
)
|
||||
|
||||
|
||||
class TestPdfToEpcPropertyDataFixture5:
|
||||
@pytest.fixture
|
||||
def result(self) -> EpcPropertyData:
|
||||
with open(PDF_PATH_5, "rb") as f:
|
||||
pdf_bytes = f.read()
|
||||
site_notes = PasHubRdSapSiteNotesExtractor(
|
||||
pdf_to_text_list(pdf_bytes)
|
||||
).extract()
|
||||
return EpcPropertyDataMapper.from_site_notes(site_notes)
|
||||
|
||||
def test_cfl_bulb_count(self, result: EpcPropertyData) -> None:
|
||||
assert result.cfl_fixed_lighting_bulbs_count == 2
|
||||
|
||||
def test_secondary_heating_type(self, result: EpcPropertyData) -> None:
|
||||
assert (
|
||||
result.sap_heating.secondary_heating_type
|
||||
== "Panel, convector or radiant heaters"
|
||||
)
|
||||
|
||||
def test_electric_shower_outlet_type(self, result: EpcPropertyData) -> None:
|
||||
assert result.sap_heating.shower_outlets is not None
|
||||
assert (
|
||||
result.sap_heating.shower_outlets.shower_outlet.shower_outlet_type
|
||||
== "Electric Shower"
|
||||
)
|
||||
|
||||
|
||||
PDF_PATH_6 = os.path.join(
|
||||
os.path.dirname(__file__), "fixtures", "ExampleSiteNotes_6.pdf"
|
||||
)
|
||||
|
||||
|
||||
class TestPdfToEpcPropertyDataFixture6:
|
||||
@pytest.fixture
|
||||
def result(self) -> EpcPropertyData:
|
||||
with open(PDF_PATH_6, "rb") as f:
|
||||
pdf_bytes = f.read()
|
||||
site_notes = PasHubRdSapSiteNotesExtractor(
|
||||
pdf_to_text_list(pdf_bytes)
|
||||
).extract()
|
||||
return EpcPropertyDataMapper.from_site_notes(site_notes)
|
||||
|
||||
def test_party_wall_construction(self, result: EpcPropertyData) -> None:
|
||||
assert (
|
||||
result.sap_building_parts[0].party_wall_construction
|
||||
== "Solid Masonry, Timber Frame, or System Built"
|
||||
)
|
||||
799
backend/documents_parser/tests/test_extractor.py
Normal file
799
backend/documents_parser/tests/test_extractor.py
Normal file
|
|
@ -0,0 +1,799 @@
|
|||
import json
|
||||
import os
|
||||
from datetime import date
|
||||
|
||||
import pytest
|
||||
|
||||
from backend.documents_parser.extractor import PasHubRdSapSiteNotesExtractor
|
||||
from datatypes.epc.surveys.pashub_rdsap_site_notes import (
|
||||
BuildingConstruction,
|
||||
BuildingMeasurements,
|
||||
Conservatories,
|
||||
CustomerResponse,
|
||||
ExtensionConstruction,
|
||||
ExtensionMeasurements,
|
||||
ExtensionRoofSpace,
|
||||
FloorConstruction,
|
||||
FloorMeasurement,
|
||||
General,
|
||||
HeatingAndHotWater,
|
||||
InspectionMetadata,
|
||||
MainBuildingConstruction,
|
||||
MainBuildingMeasurements,
|
||||
MainHeating,
|
||||
Renewables,
|
||||
RoomCountElements,
|
||||
RoofSpace,
|
||||
RoofSpaceDetail,
|
||||
SecondaryHeating,
|
||||
Shower,
|
||||
SurveyAddendum,
|
||||
Ventilation,
|
||||
WaterHeating,
|
||||
WaterUse,
|
||||
)
|
||||
|
||||
FIXTURES = os.path.join(os.path.dirname(__file__), "fixtures")
|
||||
|
||||
|
||||
def load_text_fixture() -> list[str]:
|
||||
with open(os.path.join(FIXTURES, "site_notes_example_text.json")) as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def load_text_fixture_2() -> list[str]:
|
||||
with open(os.path.join(FIXTURES, "site_notes_example_2_text.json")) as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def load_text_fixture_3() -> list[str]:
|
||||
with open(os.path.join(FIXTURES, "site_notes_example_3_text.json")) as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def load_text_fixture_4() -> list[str]:
|
||||
with open(os.path.join(FIXTURES, "site_notes_example_4_text.json")) as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def load_text_fixture_5() -> list[str]:
|
||||
with open(os.path.join(FIXTURES, "site_notes_example_5_text.json")) as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def load_text_fixture_6() -> list[str]:
|
||||
with open(os.path.join(FIXTURES, "site_notes_example_6_text.json")) as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
class TestInspectionMetadata:
|
||||
def test_full_inspection_metadata(self) -> None:
|
||||
result = PasHubRdSapSiteNotesExtractor(load_text_fixture()).extract_inspection_metadata()
|
||||
assert result == InspectionMetadata(
|
||||
inspection_surveyor="Benjamin Burke",
|
||||
email_address="ben@mbsolutionsgroup.co.uk",
|
||||
report_reference="6EA2A86D-94CE-4792-8D49-AB495C744EDD",
|
||||
created_on="2025-11-10",
|
||||
date_of_inspection=date(2025, 9, 25),
|
||||
property_address="40, Abbey Place, Crewe, Cheshire, CW1 4JR",
|
||||
property_photo=True,
|
||||
)
|
||||
|
||||
|
||||
class TestGeneral:
|
||||
@pytest.fixture
|
||||
def general(self) -> General:
|
||||
return PasHubRdSapSiteNotesExtractor(load_text_fixture()).extract_general()
|
||||
|
||||
def test_epc_checked_before_assessment(self, general: General) -> None:
|
||||
assert general.epc_checked_before_assessment is True
|
||||
|
||||
def test_epc_exists_at_point_of_assessment(self, general: General) -> None:
|
||||
assert general.epc_exists_at_point_of_assessment is False
|
||||
|
||||
def test_inspection_date(self, general: General) -> None:
|
||||
assert general.inspection_date == date(2025, 9, 25)
|
||||
|
||||
def test_transaction_type(self, general: General) -> None:
|
||||
assert general.transaction_type == "Grant-Scheme (ECO, RHI, etc.)"
|
||||
|
||||
def test_tenure(self, general: General) -> None:
|
||||
assert general.tenure == "Rented Social"
|
||||
|
||||
def test_property_type(self, general: General) -> None:
|
||||
assert general.property_type == "House"
|
||||
|
||||
def test_detachment_type(self, general: General) -> None:
|
||||
assert general.detachment_type == "Mid-terrace"
|
||||
|
||||
def test_number_of_storeys(self, general: General) -> None:
|
||||
assert general.number_of_storeys == 2
|
||||
|
||||
def test_number_of_extensions(self, general: General) -> None:
|
||||
assert general.number_of_extensions == 1
|
||||
|
||||
def test_electricity_smart_meter(self, general: General) -> None:
|
||||
assert general.electricity_smart_meter is True
|
||||
|
||||
def test_mains_gas_available(self, general: General) -> None:
|
||||
assert general.mains_gas_available is True
|
||||
|
||||
def test_measurements_location(self, general: General) -> None:
|
||||
assert general.measurements_location == "Internal"
|
||||
|
||||
def test_full_general(self, general: General) -> None:
|
||||
assert general == General(
|
||||
epc_checked_before_assessment=True,
|
||||
epc_exists_at_point_of_assessment=False,
|
||||
inspection_date=date(2025, 9, 25),
|
||||
transaction_type="Grant-Scheme (ECO, RHI, etc.)",
|
||||
tenure="Rented Social",
|
||||
property_type="House",
|
||||
detachment_type="Mid-terrace",
|
||||
number_of_storeys=2,
|
||||
terrain_type="Suburban",
|
||||
number_of_extensions=1,
|
||||
electricity_smart_meter=True,
|
||||
electric_meter_type="Single",
|
||||
dwelling_export_capable=True,
|
||||
mains_gas_available=True,
|
||||
gas_smart_meter=True,
|
||||
gas_meter_accessible=True,
|
||||
measurements_location="Internal",
|
||||
)
|
||||
|
||||
|
||||
class TestGeneralNoExtensions:
|
||||
@pytest.fixture
|
||||
def general(self) -> General:
|
||||
return PasHubRdSapSiteNotesExtractor(load_text_fixture_2()).extract_general()
|
||||
|
||||
def test_number_of_extensions_when_no_extensions(self, general: General) -> None:
|
||||
assert general.number_of_extensions == 0
|
||||
|
||||
|
||||
class TestBuildingConstruction:
|
||||
@pytest.fixture
|
||||
def construction(self) -> BuildingConstruction:
|
||||
return PasHubRdSapSiteNotesExtractor(
|
||||
load_text_fixture()
|
||||
).extract_building_construction()
|
||||
|
||||
def test_main_building_wall_u_value_known_is_false(
|
||||
self, construction: BuildingConstruction
|
||||
) -> None:
|
||||
assert construction.main_building.wall_u_value_known is False
|
||||
|
||||
def test_main_building_wall_thickness_mm(
|
||||
self, construction: BuildingConstruction
|
||||
) -> None:
|
||||
assert construction.main_building.wall_thickness_mm == 310
|
||||
|
||||
def test_main_building_filled_cavity_indicators_present(
|
||||
self, construction: BuildingConstruction
|
||||
) -> None:
|
||||
assert (
|
||||
construction.main_building.filled_cavity_indicators
|
||||
== "evidence of cavity fill drill holes"
|
||||
)
|
||||
|
||||
def test_extension_filled_cavity_indicators_absent(
|
||||
self, construction: BuildingConstruction
|
||||
) -> None:
|
||||
assert construction.extensions is not None
|
||||
assert construction.extensions[0].filled_cavity_indicators is None
|
||||
|
||||
def test_one_extension(self, construction: BuildingConstruction) -> None:
|
||||
assert construction.extensions is not None
|
||||
assert len(construction.extensions) == 1
|
||||
|
||||
def test_extension_id(self, construction: BuildingConstruction) -> None:
|
||||
assert construction.extensions is not None
|
||||
assert construction.extensions[0].id == 1
|
||||
|
||||
def test_full_building_construction(
|
||||
self, construction: BuildingConstruction
|
||||
) -> None:
|
||||
assert construction == BuildingConstruction(
|
||||
main_building=MainBuildingConstruction(
|
||||
age_range="1950-1966",
|
||||
age_indicators="local knowledge, enquiries of owner",
|
||||
walls_construction_type="Cavity",
|
||||
cavity_construction_indicators="wall thickness over 270 mm",
|
||||
walls_insulation_type="Filled Cavity",
|
||||
filled_cavity_indicators="evidence of cavity fill drill holes",
|
||||
thermal_conductivity_of_wall_insulation="Unknown",
|
||||
wall_u_value_known=False,
|
||||
wall_thickness_mm=310,
|
||||
party_wall_construction_type="Cavity Masonry, Filled",
|
||||
),
|
||||
floor=FloorConstruction(
|
||||
floor_type="Ground Floor",
|
||||
floor_construction="Solid",
|
||||
floor_insulation_type="As Built",
|
||||
floor_u_value_known=False,
|
||||
),
|
||||
extensions=[
|
||||
ExtensionConstruction(
|
||||
id=1,
|
||||
age_range="2003-2006",
|
||||
age_indicators="local knowledge, enquiries of owner",
|
||||
walls_construction_type="Cavity",
|
||||
cavity_construction_indicators="wall thickness over 270 mm",
|
||||
walls_insulation_type="As built",
|
||||
thermal_conductivity_of_wall_insulation="Unknown",
|
||||
wall_u_value_known=False,
|
||||
wall_thickness_mm=310,
|
||||
party_wall_construction_type="Cavity Masonry, Filled",
|
||||
filled_cavity_indicators=None,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class TestBuildingMeasurements:
|
||||
@pytest.fixture
|
||||
def measurements(self) -> BuildingMeasurements:
|
||||
return PasHubRdSapSiteNotesExtractor(
|
||||
load_text_fixture()
|
||||
).extract_building_measurements()
|
||||
|
||||
def test_main_building_has_two_floors(
|
||||
self, measurements: BuildingMeasurements
|
||||
) -> None:
|
||||
assert len(measurements.main_building.floors) == 2
|
||||
|
||||
def test_main_building_floor_area(
|
||||
self, measurements: BuildingMeasurements
|
||||
) -> None:
|
||||
assert measurements.main_building.floors[0].area_m2 == 35.68
|
||||
|
||||
def test_integer_token_parses_to_float(
|
||||
self, measurements: BuildingMeasurements
|
||||
) -> None:
|
||||
# "11" in the PDF (no decimal) should parse to 11.0
|
||||
assert measurements.main_building.floors[1].heat_loss_perimeter_m == 11.0
|
||||
|
||||
def test_extension_measurements_present(
|
||||
self, measurements: BuildingMeasurements
|
||||
) -> None:
|
||||
assert measurements.extensions is not None
|
||||
assert len(measurements.extensions) == 1
|
||||
|
||||
def test_extension_id(self, measurements: BuildingMeasurements) -> None:
|
||||
assert measurements.extensions is not None
|
||||
assert measurements.extensions[0].id == 1
|
||||
|
||||
def test_full_building_measurements(
|
||||
self, measurements: BuildingMeasurements
|
||||
) -> None:
|
||||
assert measurements == BuildingMeasurements(
|
||||
main_building=MainBuildingMeasurements(
|
||||
floors=[
|
||||
FloorMeasurement(
|
||||
name="Floor 1",
|
||||
area_m2=35.68,
|
||||
height_m=2.19,
|
||||
heat_loss_perimeter_m=13.44,
|
||||
pwl_m=10.62,
|
||||
),
|
||||
FloorMeasurement(
|
||||
name="Floor 0",
|
||||
area_m2=35.68,
|
||||
height_m=2.17,
|
||||
heat_loss_perimeter_m=11.0,
|
||||
pwl_m=10.62,
|
||||
),
|
||||
]
|
||||
),
|
||||
extensions=[
|
||||
ExtensionMeasurements(
|
||||
id=1,
|
||||
floors=[
|
||||
FloorMeasurement(
|
||||
name="Floor 0",
|
||||
area_m2=3.8,
|
||||
height_m=2.0,
|
||||
heat_loss_perimeter_m=5.7,
|
||||
pwl_m=0.0,
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class TestRoofSpace:
|
||||
@pytest.fixture
|
||||
def roof_space(self) -> RoofSpace:
|
||||
return PasHubRdSapSiteNotesExtractor(load_text_fixture()).extract_roof_space()
|
||||
|
||||
def test_main_building_insulation_thickness_mm(
|
||||
self, roof_space: RoofSpace
|
||||
) -> None:
|
||||
assert roof_space.main_building.insulation_thickness_mm == 100
|
||||
|
||||
def test_main_building_insulation_thickness_string_absent(
|
||||
self, roof_space: RoofSpace
|
||||
) -> None:
|
||||
assert roof_space.main_building.insulation_thickness is None
|
||||
|
||||
def test_main_building_rooms_in_roof(self, roof_space: RoofSpace) -> None:
|
||||
assert roof_space.main_building.rooms_in_roof is False
|
||||
|
||||
def test_main_building_roof_u_value_known(self, roof_space: RoofSpace) -> None:
|
||||
assert roof_space.main_building.roof_u_value_known is False
|
||||
|
||||
def test_extension_uses_string_thickness(self, roof_space: RoofSpace) -> None:
|
||||
assert roof_space.extensions is not None
|
||||
assert roof_space.extensions[0].insulation_thickness == "As built"
|
||||
assert roof_space.extensions[0].insulation_thickness_mm is None
|
||||
|
||||
def test_full_roof_space(self, roof_space: RoofSpace) -> None:
|
||||
assert roof_space == RoofSpace(
|
||||
main_building=RoofSpaceDetail(
|
||||
construction_type="Pitched roof (Slates or tiles), Access to loft",
|
||||
insulation_at="Joists",
|
||||
roof_u_value_known=False,
|
||||
cavity_wall_construction_indicators="cavity visible in roof space",
|
||||
rooms_in_roof=False,
|
||||
insulation_thickness_mm=100,
|
||||
insulation_thickness=None,
|
||||
),
|
||||
extensions=[
|
||||
ExtensionRoofSpace(
|
||||
id=1,
|
||||
construction_type="Pitched roof, Sloping ceiling",
|
||||
insulation_at="Sloping ceiling insulation",
|
||||
roof_u_value_known=False,
|
||||
cavity_wall_construction_indicators="No indicator of construction visible",
|
||||
rooms_in_roof=False,
|
||||
insulation_thickness_mm=None,
|
||||
insulation_thickness="As built",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class TestWindows:
|
||||
@pytest.fixture
|
||||
def windows(self) -> list:
|
||||
return PasHubRdSapSiteNotesExtractor(load_text_fixture()).extract_windows()
|
||||
|
||||
def test_window_count(self, windows: list) -> None:
|
||||
assert len(windows) == 8
|
||||
|
||||
def test_ids_are_sequential(self, windows: list) -> None:
|
||||
assert [w.id for w in windows] == list(range(1, 9))
|
||||
|
||||
def test_first_window_location(self, windows: list) -> None:
|
||||
assert windows[0].location == "Main Building"
|
||||
|
||||
def test_extension_window_location(self, windows: list) -> None:
|
||||
assert windows[3].location == "Extension 1"
|
||||
|
||||
def test_height_parses_to_float(self, windows: list) -> None:
|
||||
assert windows[0].height_m == 1.2
|
||||
|
||||
def test_draught_proofed_true(self, windows: list) -> None:
|
||||
assert windows[0].draught_proofed is True
|
||||
|
||||
def test_permanent_shutters_false(self, windows: list) -> None:
|
||||
assert windows[0].permanent_shutters is False
|
||||
|
||||
def test_first_window_full(self, windows: list) -> None:
|
||||
from datatypes.epc.surveys.pashub_rdsap_site_notes import Window
|
||||
assert windows[0] == Window(
|
||||
id=1,
|
||||
location="Main Building",
|
||||
wall_type="External wall",
|
||||
glazing_type="Double glazing, Unknown install date",
|
||||
window_type="Window",
|
||||
frame_type="Wooden or PVC",
|
||||
glazing_gap="16 mm or more",
|
||||
draught_proofed=True,
|
||||
permanent_shutters=False,
|
||||
height_m=1.2,
|
||||
width_m=2.3,
|
||||
orientation="North West",
|
||||
)
|
||||
|
||||
|
||||
class TestWaterHeatingCylinderThickness:
|
||||
@pytest.fixture
|
||||
def hhw(self) -> HeatingAndHotWater:
|
||||
return PasHubRdSapSiteNotesExtractor(
|
||||
load_text_fixture_2()
|
||||
).extract_heating_and_hot_water()
|
||||
|
||||
@pytest.fixture
|
||||
def hhw_no_cylinder(self) -> HeatingAndHotWater:
|
||||
return PasHubRdSapSiteNotesExtractor(
|
||||
load_text_fixture()
|
||||
).extract_heating_and_hot_water()
|
||||
|
||||
def test_cylinder_insulation_thickness_mm(self, hhw: HeatingAndHotWater) -> None:
|
||||
assert hhw.water_heating.insulation_thickness_mm == 38
|
||||
|
||||
def test_cylinder_insulation_thickness_mm_absent(self, hhw_no_cylinder: HeatingAndHotWater) -> None:
|
||||
assert hhw_no_cylinder.water_heating.insulation_thickness_mm is None
|
||||
|
||||
def test_cylinder_size(self, hhw: HeatingAndHotWater) -> None:
|
||||
assert hhw.water_heating.cylinder_size == "Normal (90-130 litres)"
|
||||
|
||||
|
||||
class TestImmersionType:
|
||||
@pytest.fixture
|
||||
def hhw(self) -> HeatingAndHotWater:
|
||||
return PasHubRdSapSiteNotesExtractor(
|
||||
load_text_fixture_3()
|
||||
).extract_heating_and_hot_water()
|
||||
|
||||
def test_immersion_type(self, hhw: HeatingAndHotWater) -> None:
|
||||
assert hhw.water_heating.immersion_type == "Dual"
|
||||
|
||||
|
||||
class TestCylinderThermostat:
|
||||
@pytest.fixture
|
||||
def hhw(self) -> HeatingAndHotWater:
|
||||
return PasHubRdSapSiteNotesExtractor(
|
||||
load_text_fixture_3()
|
||||
).extract_heating_and_hot_water()
|
||||
|
||||
def test_has_thermostat_true(self, hhw: HeatingAndHotWater) -> None:
|
||||
assert hhw.water_heating.has_thermostat is True
|
||||
|
||||
|
||||
class TestSecondaryHeating:
|
||||
@pytest.fixture
|
||||
def hhw(self) -> HeatingAndHotWater:
|
||||
return PasHubRdSapSiteNotesExtractor(
|
||||
load_text_fixture_2()
|
||||
).extract_heating_and_hot_water()
|
||||
|
||||
@pytest.fixture
|
||||
def hhw_no_secondary(self) -> HeatingAndHotWater:
|
||||
return PasHubRdSapSiteNotesExtractor(
|
||||
load_text_fixture()
|
||||
).extract_heating_and_hot_water()
|
||||
|
||||
def test_secondary_system(self, hhw: HeatingAndHotWater) -> None:
|
||||
assert hhw.secondary_heating.secondary_system == "Open fire in grate"
|
||||
|
||||
def test_secondary_system_absent(self, hhw_no_secondary: HeatingAndHotWater) -> None:
|
||||
assert hhw_no_secondary.secondary_heating.secondary_system is None
|
||||
|
||||
|
||||
class TestHeatingAndHotWater:
|
||||
@pytest.fixture
|
||||
def hhw(self) -> HeatingAndHotWater:
|
||||
return PasHubRdSapSiteNotesExtractor(
|
||||
load_text_fixture()
|
||||
).extract_heating_and_hot_water()
|
||||
|
||||
def test_product_id_parses_to_int(self, hhw: HeatingAndHotWater) -> None:
|
||||
assert hhw.main_heating.product_id == 16839
|
||||
|
||||
def test_summer_efficiency_parses_to_float(self, hhw: HeatingAndHotWater) -> None:
|
||||
assert hhw.main_heating.summer_efficiency == 0.0
|
||||
|
||||
def test_condensing_true(self, hhw: HeatingAndHotWater) -> None:
|
||||
assert hhw.main_heating.condensing is True
|
||||
|
||||
def test_fghrs_false(self, hhw: HeatingAndHotWater) -> None:
|
||||
# multi-line label
|
||||
assert hhw.main_heating.flue_gas_heat_recovery_system is False
|
||||
|
||||
def test_secondary_fuel(self, hhw: HeatingAndHotWater) -> None:
|
||||
assert hhw.secondary_heating.secondary_fuel == "No Secondary Heating"
|
||||
|
||||
def test_water_heating_no_cylinder(self, hhw: HeatingAndHotWater) -> None:
|
||||
assert hhw.water_heating.cylinder_size == "No Cylinder"
|
||||
assert hhw.water_heating.insulation_type is None
|
||||
assert hhw.water_heating.has_thermostat is None
|
||||
|
||||
def test_full_heating_and_hot_water(self, hhw: HeatingAndHotWater) -> None:
|
||||
assert hhw == HeatingAndHotWater(
|
||||
main_heating=MainHeating(
|
||||
selection_method="PCDF Search",
|
||||
system_type="Boiler with radiators or underfloor heating",
|
||||
product_id=16839,
|
||||
manufacturer="Vaillant",
|
||||
model="ecoTEC pro 28",
|
||||
orig_manufacturer="Vaillant",
|
||||
fuel="Mains gas",
|
||||
summer_efficiency=0.0,
|
||||
type="Combi",
|
||||
condensing=True,
|
||||
year="2005 - 2015",
|
||||
mount="Wall",
|
||||
open_flue="Room-sealed",
|
||||
fan_assist=True,
|
||||
status="Normal status for an actual product",
|
||||
central_heating_pump_age="Unknown",
|
||||
controls="Programmer, room thermostat and TRVs",
|
||||
flue_gas_heat_recovery_system=False,
|
||||
weather_compensator=False,
|
||||
emitter="Radiators",
|
||||
emitter_temperature="Unknown",
|
||||
),
|
||||
secondary_heating=SecondaryHeating(
|
||||
secondary_fuel="No Secondary Heating",
|
||||
),
|
||||
water_heating=WaterHeating(
|
||||
type="Regular",
|
||||
system="From main heating 1",
|
||||
cylinder_size="No Cylinder",
|
||||
cylinder_measured_heat_loss=None,
|
||||
insulation_type=None,
|
||||
insulation_thickness_mm=None,
|
||||
has_thermostat=None,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class TestVentilation:
|
||||
@pytest.fixture
|
||||
def ventilation(self) -> Ventilation:
|
||||
return PasHubRdSapSiteNotesExtractor(
|
||||
load_text_fixture()
|
||||
).extract_ventilation()
|
||||
|
||||
def test_ventilation_type(self, ventilation: Ventilation) -> None:
|
||||
assert ventilation.ventilation_type == "Mechanical Extract - Decentralised"
|
||||
|
||||
def test_number_of_open_flues(self, ventilation: Ventilation) -> None:
|
||||
assert ventilation.number_of_open_flues == 0
|
||||
|
||||
def test_ventilation_in_pcdf_database(self, ventilation: Ventilation) -> None:
|
||||
assert ventilation.ventilation_in_pcdf_database is False
|
||||
|
||||
def test_full_ventilation(self, ventilation: Ventilation) -> None:
|
||||
assert ventilation == Ventilation(
|
||||
ventilation_type="Mechanical Extract - Decentralised",
|
||||
has_fixed_air_conditioning=False,
|
||||
number_of_open_flues=0,
|
||||
number_of_closed_flues=0,
|
||||
number_of_boiler_flues=0,
|
||||
number_of_other_flues=0,
|
||||
number_of_extract_fans=0,
|
||||
number_of_passive_vents=0,
|
||||
number_of_flueless_gas_fires=0,
|
||||
pressure_test="No test",
|
||||
draught_lobby=False,
|
||||
ventilation_in_pcdf_database=False,
|
||||
)
|
||||
|
||||
|
||||
class TestConservatories:
|
||||
def test_full_conservatories(self) -> None:
|
||||
result = PasHubRdSapSiteNotesExtractor(
|
||||
load_text_fixture()
|
||||
).extract_conservatories()
|
||||
assert result == Conservatories(has_conservatory=False)
|
||||
|
||||
|
||||
class TestRenewables:
|
||||
def test_number_of_pv_batteries_none_string_becomes_zero(self) -> None:
|
||||
result = PasHubRdSapSiteNotesExtractor(load_text_fixture()).extract_renewables()
|
||||
assert result.number_of_pv_batteries == 0
|
||||
|
||||
def test_full_renewables(self) -> None:
|
||||
result = PasHubRdSapSiteNotesExtractor(load_text_fixture()).extract_renewables()
|
||||
assert result == Renewables(
|
||||
wind_turbines=False,
|
||||
solar_hot_water=False,
|
||||
photovoltaic_array=False,
|
||||
number_of_pv_batteries=0,
|
||||
hydro=False,
|
||||
)
|
||||
|
||||
|
||||
class TestRenewablesPvConnection:
|
||||
@pytest.fixture
|
||||
def renewables(self) -> Renewables:
|
||||
return PasHubRdSapSiteNotesExtractor(
|
||||
load_text_fixture_3()
|
||||
).extract_renewables()
|
||||
|
||||
def test_pv_connection(self, renewables: Renewables) -> None:
|
||||
assert renewables.pv_connection == "Connected to dwellings electricity meter"
|
||||
|
||||
def test_percent_roof_covered_pv(self, renewables: Renewables) -> None:
|
||||
assert renewables.percent_roof_covered_pv == 45
|
||||
|
||||
|
||||
class TestRoomCountElements:
|
||||
@pytest.fixture
|
||||
def rce(self) -> RoomCountElements:
|
||||
return PasHubRdSapSiteNotesExtractor(
|
||||
load_text_fixture()
|
||||
).extract_room_count_elements()
|
||||
|
||||
def test_habitable_rooms(self, rce: RoomCountElements) -> None:
|
||||
assert rce.number_of_habitable_rooms == 3
|
||||
|
||||
def test_heated_rooms_null(self, rce: RoomCountElements) -> None:
|
||||
assert rce.number_of_heated_rooms is None
|
||||
|
||||
def test_full_room_count_elements(self, rce: RoomCountElements) -> None:
|
||||
assert rce == RoomCountElements(
|
||||
number_of_habitable_rooms=3,
|
||||
any_unheated_rooms=False,
|
||||
number_of_heated_rooms=None,
|
||||
number_of_external_doors=2,
|
||||
number_of_insulated_external_doors=0,
|
||||
number_of_draughtproofed_external_doors=2,
|
||||
number_of_open_chimneys=0,
|
||||
number_of_blocked_chimneys=0,
|
||||
number_of_fixed_incandescent_bulbs=4,
|
||||
exact_led_cfl_count_known=True,
|
||||
number_of_fixed_led_bulbs=0,
|
||||
number_of_fixed_cfl_bulbs=1,
|
||||
waste_water_heat_recovery="None",
|
||||
)
|
||||
|
||||
|
||||
class TestWaterUse:
|
||||
def test_full_water_use(self) -> None:
|
||||
result = PasHubRdSapSiteNotesExtractor(load_text_fixture()).extract_water_use()
|
||||
assert result == WaterUse(
|
||||
number_of_baths=1,
|
||||
number_of_special_features=0,
|
||||
showers=[Shower(id=1, outlet_type="Non-Electric Shower")],
|
||||
)
|
||||
|
||||
|
||||
class TestCustomerResponse:
|
||||
def test_full_customer_response(self) -> None:
|
||||
result = PasHubRdSapSiteNotesExtractor(
|
||||
load_text_fixture()
|
||||
).extract_customer_response()
|
||||
assert result == CustomerResponse(
|
||||
customer_present=True,
|
||||
willing_to_answer_satisfaction_survey=False,
|
||||
)
|
||||
|
||||
|
||||
class TestExtract:
|
||||
def test_full_extract(self) -> None:
|
||||
result = PasHubRdSapSiteNotesExtractor(load_text_fixture()).extract()
|
||||
assert result.inspection_metadata.inspection_surveyor == "Benjamin Burke"
|
||||
assert result.general.inspection_date == date(2025, 9, 25)
|
||||
assert result.building_construction.main_building.wall_thickness_mm == 310
|
||||
assert result.building_measurements.main_building.floors[0].area_m2 == 35.68
|
||||
assert result.roof_space.main_building.insulation_thickness_mm == 100
|
||||
assert len(result.windows) == 8
|
||||
assert result.heating_and_hot_water.main_heating.product_id == 16839
|
||||
assert result.ventilation.ventilation_type == "Mechanical Extract - Decentralised"
|
||||
assert result.conservatories.has_conservatory is False
|
||||
assert result.renewables.number_of_pv_batteries == 0
|
||||
assert result.room_count_elements.number_of_habitable_rooms == 3
|
||||
assert result.water_use.number_of_baths == 1
|
||||
assert result.customer_response.customer_present is True
|
||||
assert result.addendum.addendum == "None"
|
||||
|
||||
|
||||
class TestSurveyAddendum:
|
||||
def test_hard_to_treat_flags(self) -> None:
|
||||
result = PasHubRdSapSiteNotesExtractor(load_text_fixture()).extract_addendum()
|
||||
assert result.hard_to_treat_cavity_access_issues is False
|
||||
assert result.hard_to_treat_cavity_high_exposure is False
|
||||
assert result.hard_to_treat_cavity_narrow_cavities is False
|
||||
|
||||
def test_full_addendum(self) -> None:
|
||||
result = PasHubRdSapSiteNotesExtractor(load_text_fixture()).extract_addendum()
|
||||
assert result == SurveyAddendum(
|
||||
addendum="None",
|
||||
related_party_disclosure="No related party",
|
||||
hard_to_treat_cavity_access_issues=False,
|
||||
hard_to_treat_cavity_high_exposure=False,
|
||||
hard_to_treat_cavity_narrow_cavities=False,
|
||||
)
|
||||
|
||||
|
||||
# --- fixture 4: heat pump, factory-fitted cylinder, blocked loft ---
|
||||
|
||||
|
||||
class TestCylinderInsulationType:
|
||||
@pytest.fixture
|
||||
def hhw(self) -> HeatingAndHotWater:
|
||||
return PasHubRdSapSiteNotesExtractor(
|
||||
load_text_fixture_4()
|
||||
).extract_heating_and_hot_water()
|
||||
|
||||
def test_insulation_type_extracted(self, hhw: HeatingAndHotWater) -> None:
|
||||
assert hhw.water_heating.insulation_type == "Factory fitted"
|
||||
|
||||
def test_insulation_thickness_mm(self, hhw: HeatingAndHotWater) -> None:
|
||||
assert hhw.water_heating.insulation_thickness_mm == 50
|
||||
|
||||
def test_cylinder_size(self, hhw: HeatingAndHotWater) -> None:
|
||||
assert hhw.water_heating.cylinder_size == "Medium (131-170 litres)"
|
||||
|
||||
|
||||
class TestHeatPumpFuelExtraction:
|
||||
@pytest.fixture
|
||||
def hhw(self) -> HeatingAndHotWater:
|
||||
return PasHubRdSapSiteNotesExtractor(
|
||||
load_text_fixture_4()
|
||||
).extract_heating_and_hot_water()
|
||||
|
||||
def test_fuel_raw_value(self, hhw: HeatingAndHotWater) -> None:
|
||||
assert hhw.main_heating.fuel == "Electricity, any tariff"
|
||||
|
||||
def test_system_type(self, hhw: HeatingAndHotWater) -> None:
|
||||
assert hhw.main_heating.system_type == "Heat pump with radiators or underfloor heating"
|
||||
|
||||
|
||||
class TestRoofSpaceUnknownInsulation:
|
||||
@pytest.fixture
|
||||
def roof_space(self) -> RoofSpace:
|
||||
return PasHubRdSapSiteNotesExtractor(
|
||||
load_text_fixture_4()
|
||||
).extract_roof_space()
|
||||
|
||||
def test_insulation_at_unknown(self, roof_space: RoofSpace) -> None:
|
||||
assert roof_space.main_building.insulation_at == "Unknown"
|
||||
|
||||
def test_insulation_thickness_mm_none(self, roof_space: RoofSpace) -> None:
|
||||
assert roof_space.main_building.insulation_thickness_mm is None
|
||||
|
||||
def test_insulation_thickness_str_none(self, roof_space: RoofSpace) -> None:
|
||||
assert roof_space.main_building.insulation_thickness is None
|
||||
|
||||
|
||||
class TestCflBulbCount:
|
||||
@pytest.fixture
|
||||
def rce(self) -> RoomCountElements:
|
||||
return PasHubRdSapSiteNotesExtractor(
|
||||
load_text_fixture_5()
|
||||
).extract_room_count_elements()
|
||||
|
||||
def test_cfl_count(self, rce: RoomCountElements) -> None:
|
||||
assert rce.number_of_fixed_cfl_bulbs == 2
|
||||
|
||||
def test_led_count(self, rce: RoomCountElements) -> None:
|
||||
assert rce.number_of_fixed_led_bulbs == 7
|
||||
|
||||
def test_incandescent_count(self, rce: RoomCountElements) -> None:
|
||||
assert rce.number_of_fixed_incandescent_bulbs == 1
|
||||
|
||||
|
||||
class TestSecondaryHeatingPanel:
|
||||
@pytest.fixture
|
||||
def hhw(self) -> HeatingAndHotWater:
|
||||
return PasHubRdSapSiteNotesExtractor(
|
||||
load_text_fixture_5()
|
||||
).extract_heating_and_hot_water()
|
||||
|
||||
def test_secondary_system(self, hhw: HeatingAndHotWater) -> None:
|
||||
assert hhw.secondary_heating.secondary_system == "Panel, convector or radiant heaters"
|
||||
|
||||
def test_secondary_fuel(self, hhw: HeatingAndHotWater) -> None:
|
||||
assert hhw.secondary_heating.secondary_fuel == "Electricity"
|
||||
|
||||
|
||||
class TestElectricShowerExtraction:
|
||||
@pytest.fixture
|
||||
def wu(self) -> WaterUse:
|
||||
return PasHubRdSapSiteNotesExtractor(
|
||||
load_text_fixture_5()
|
||||
).extract_water_use()
|
||||
|
||||
def test_shower_outlet_type(self, wu: WaterUse) -> None:
|
||||
assert wu.showers[0].outlet_type == "Electric Shower"
|
||||
|
||||
|
||||
class TestSolidMasonryPartyWall:
|
||||
@pytest.fixture
|
||||
def bc(self) -> BuildingConstruction:
|
||||
return PasHubRdSapSiteNotesExtractor(
|
||||
load_text_fixture_6()
|
||||
).extract_building_construction()
|
||||
|
||||
def test_party_wall_construction_type(self, bc: BuildingConstruction) -> None:
|
||||
assert (
|
||||
bc.main_building.party_wall_construction_type
|
||||
== "Solid Masonry, Timber Frame, or System Built"
|
||||
)
|
||||
31
backend/documents_parser/tests/test_pdf.py
Normal file
31
backend/documents_parser/tests/test_pdf.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import json
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from backend.documents_parser.pdf import pdf_to_text_list
|
||||
|
||||
PDF_PATH = os.path.join(os.path.dirname(__file__), "fixtures", "ExampleSiteNotes.pdf")
|
||||
FIXTURE_PATH = os.path.join(os.path.dirname(__file__), "fixtures", "site_notes_example_text.json")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pdf_bytes() -> bytes:
|
||||
with open(PDF_PATH, "rb") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
class TestPdfToTextList:
|
||||
def test_returns_list(self, pdf_bytes: bytes) -> None:
|
||||
result = pdf_to_text_list(pdf_bytes)
|
||||
assert isinstance(result, list)
|
||||
|
||||
def test_all_elements_are_strings(self, pdf_bytes: bytes) -> None:
|
||||
result = pdf_to_text_list(pdf_bytes)
|
||||
assert all(isinstance(t, str) for t in result)
|
||||
|
||||
def test_matches_fixture(self, pdf_bytes: bytes) -> None:
|
||||
with open(FIXTURE_PATH) as f:
|
||||
expected = json.load(f)
|
||||
result = pdf_to_text_list(pdf_bytes)
|
||||
assert result == expected
|
||||
|
|
@ -29,18 +29,21 @@ class MainHeatingDetail:
|
|||
boiler_flue_type: Optional[int] = None # TODO: make enum?
|
||||
boiler_ignition_type: Optional[int] = None # TODO: make enum?
|
||||
central_heating_pump_age: Optional[int] = None
|
||||
central_heating_pump_age_str: Optional[str] = None # str from site notes e.g. "Unknown", "Pre 2013"
|
||||
main_heating_index_number: Optional[int] = None
|
||||
sap_main_heating_code: Optional[int] = None # TODO: make enum?
|
||||
main_heating_number: Optional[int] = None
|
||||
main_heating_category: Optional[int] = None
|
||||
main_heating_fraction: Optional[int] = None
|
||||
main_heating_data_source: Optional[int] = None
|
||||
condensing: Optional[bool] = None
|
||||
weather_compensator: Optional[bool] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ShowerOutlet:
|
||||
shower_wwhrs: int
|
||||
shower_outlet_type: int
|
||||
shower_outlet_type: Union[int, str]
|
||||
shower_wwhrs: Optional[int] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -54,18 +57,33 @@ class SapHeating:
|
|||
instantaneous_wwhrs: InstantaneousWwhrs
|
||||
main_heating_details: List[MainHeatingDetail]
|
||||
has_fixed_air_conditioning: bool
|
||||
cylinder_size: Optional[int] = (
|
||||
None # int code from API; not directly available from site notes
|
||||
cylinder_size: Optional[Union[int, str]] = (
|
||||
None # int code from API; str (e.g. "Normal (90-130 litres)") from site notes
|
||||
)
|
||||
water_heating_code: Optional[int] = None # TODO: make enum?
|
||||
water_heating_fuel: Optional[int] = None # TODO: make enum?
|
||||
immersion_heating_type: Optional[Union[int, str]] = None # TODO: make enum?
|
||||
shower_outlets: Optional[ShowerOutlets] = None
|
||||
cylinder_insulation_type: Optional[int] = None
|
||||
cylinder_insulation_type: Optional[Union[int, str]] = None
|
||||
cylinder_thermostat: Optional[str] = None
|
||||
secondary_fuel_type: Optional[int] = None
|
||||
secondary_heating_type: Optional[int] = None
|
||||
cylinder_insulation_thickness: Optional[int] = None
|
||||
secondary_heating_type: Optional[Union[int, str]] = None # int from API; str from site notes
|
||||
cylinder_insulation_thickness_mm: Optional[int] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class SapVentilation:
|
||||
ventilation_type: Optional[str] = None
|
||||
draught_lobby: Optional[bool] = None
|
||||
pressure_test: Optional[str] = None # str from site notes e.g. "No test"; int in API via mechanical_ventilation
|
||||
open_flues_count: Optional[int] = None
|
||||
closed_flues_count: Optional[int] = None
|
||||
boiler_flues_count: Optional[int] = None
|
||||
other_flues_count: Optional[int] = None
|
||||
extract_fans_count: Optional[int] = None
|
||||
passive_vents_count: Optional[int] = None
|
||||
flueless_gas_fires_count: Optional[int] = None
|
||||
ventilation_in_pcdf_database: Optional[bool] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -130,7 +148,7 @@ class SapEnergySource:
|
|||
wind_turbines_terrain_type: str # int in API, str (e.g. "Suburban") in site notes
|
||||
electricity_smart_meter_present: bool
|
||||
|
||||
pv_connection: Optional[int] = None
|
||||
pv_connection: Optional[Union[int, str]] = None # int from API; str from site notes
|
||||
photovoltaic_supply: Optional[PhotovoltaicSupply] = None
|
||||
wind_turbine_details: Optional[WindTurbineDetails] = None
|
||||
pv_batteries: Optional[PvBatteries] = None
|
||||
|
|
@ -200,6 +218,10 @@ class SapBuildingPart:
|
|||
flat_roof_insulation_thickness: Optional[Union[str, int]] = (
|
||||
None # TODO: make enum/mapping?
|
||||
)
|
||||
floor_type: Optional[str] = None # str from site notes e.g. "Ground Floor"
|
||||
floor_construction_type: Optional[str] = None # str from site notes; distinct from floor_construction: int in SapFloorDimension
|
||||
floor_insulation_type_str: Optional[str] = None # str from site notes e.g. "As Built"
|
||||
floor_u_value_known: Optional[bool] = None
|
||||
|
||||
roof_construction: Optional[int] = None
|
||||
roof_insulation_location: Optional[Union[int, str]] = (
|
||||
|
|
@ -235,6 +257,9 @@ class EpcPropertyData:
|
|||
inspection_date: date
|
||||
tenure: str # str in site notes; stringified int (e.g. "1") from API
|
||||
transaction_type: str # str in site notes; stringified int from API
|
||||
address_line_1: str
|
||||
postcode: str
|
||||
post_town: str
|
||||
|
||||
# Elements
|
||||
roofs: List[EnergyElement]
|
||||
|
|
@ -270,9 +295,6 @@ class EpcPropertyData:
|
|||
assessment_type: Optional[str] = None # not available from site notes
|
||||
sap_version: Optional[float] = None # not available from site notes
|
||||
uprn: Optional[int] = None # not available from site notes
|
||||
address_line_1: Optional[str] = None # not available from site notes
|
||||
postcode: Optional[str] = None # not available from site notes
|
||||
post_town: Optional[str] = None # not available from site notes
|
||||
status: Optional[str] = None # not available from site notes
|
||||
window: Optional[EnergyElement] = None # not available from site notes
|
||||
lighting: Optional[EnergyElement] = None # not available from site notes
|
||||
|
|
@ -280,6 +302,7 @@ class EpcPropertyData:
|
|||
schema_type: Optional[str] = None
|
||||
schema_versions_original: Optional[str] = None
|
||||
report_type: Optional[str] = None # TODO: make enum?
|
||||
report_reference: Optional[str] = None
|
||||
uprn_source: Optional[str] = None
|
||||
address_line_2: Optional[str] = None
|
||||
region_code: Optional[str] = None # TODO: make enum?
|
||||
|
|
@ -346,3 +369,10 @@ class EpcPropertyData:
|
|||
# survey_addendum: Optional[Any] = None # not sure how to handle, skip for now
|
||||
fixed_lighting_outlets_count: Optional[int] = None
|
||||
low_energy_fixed_lighting_outlets_count: Optional[int] = None
|
||||
# Site-notes-only fields
|
||||
sap_ventilation: Optional[SapVentilation] = None
|
||||
number_of_storeys: Optional[int] = None
|
||||
any_unheated_rooms: Optional[bool] = None
|
||||
waste_water_heat_recovery: Optional[str] = None
|
||||
hydro: Optional[bool] = None
|
||||
photovoltaic_array: Optional[bool] = None
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from datetime import date
|
||||
from typing import List, Sequence, Union
|
||||
from typing import List, Optional, Sequence, Union
|
||||
|
||||
from datatypes.epc.domain.epc_property_data import (
|
||||
EnergyElement,
|
||||
|
|
@ -16,6 +16,7 @@ from datatypes.epc.domain.epc_property_data import (
|
|||
SapFloorDimension,
|
||||
SapHeating,
|
||||
SapRoomInRoof,
|
||||
SapVentilation,
|
||||
SapWindow,
|
||||
ShowerOutlet,
|
||||
ShowerOutlets,
|
||||
|
|
@ -55,10 +56,14 @@ from datatypes.epc.surveys.pashub_rdsap_site_notes import (
|
|||
BuildingMeasurements,
|
||||
ExtensionConstruction,
|
||||
ExtensionMeasurements,
|
||||
ExtensionRoofSpace,
|
||||
FloorConstruction,
|
||||
FloorMeasurement,
|
||||
HeatingAndHotWater,
|
||||
PasHubRdSapSiteNotes,
|
||||
RoofSpaceDetail,
|
||||
Ventilation,
|
||||
WaterUse,
|
||||
Window,
|
||||
)
|
||||
|
||||
|
|
@ -78,6 +83,24 @@ class EpcPropertyDataMapper:
|
|||
@staticmethod
|
||||
def from_site_notes(survey: PasHubRdSapSiteNotes) -> EpcPropertyData:
|
||||
general = survey.general
|
||||
metadata = survey.inspection_metadata
|
||||
address_parts = [p.strip() for p in metadata.property_address.split(", ")]
|
||||
postcode = address_parts[-1] if address_parts else ""
|
||||
post_town = (
|
||||
address_parts[-3]
|
||||
if len(address_parts) >= 4
|
||||
else (address_parts[-2] if len(address_parts) >= 3 else "")
|
||||
)
|
||||
address_line_1 = (
|
||||
", ".join(address_parts[:-3])
|
||||
if len(address_parts) >= 4
|
||||
else (
|
||||
", ".join(address_parts[:-2])
|
||||
if len(address_parts) >= 3
|
||||
else address_parts[0] if address_parts else ""
|
||||
)
|
||||
)
|
||||
|
||||
construction = survey.building_construction
|
||||
measurements = survey.building_measurements
|
||||
heating = survey.heating_and_hot_water
|
||||
|
|
@ -85,13 +108,23 @@ class EpcPropertyDataMapper:
|
|||
renewables = survey.renewables
|
||||
room_counts = survey.room_count_elements
|
||||
|
||||
sap_building_parts = [_map_main_building_part(construction, measurements)]
|
||||
roof_space = survey.roof_space
|
||||
sap_building_parts = [
|
||||
_map_main_building_part(
|
||||
construction, measurements, roof_space.main_building
|
||||
)
|
||||
]
|
||||
if construction.extensions and measurements.extensions:
|
||||
for ext_c in construction.extensions:
|
||||
matching = [m for m in measurements.extensions if m.id == ext_c.id]
|
||||
if matching:
|
||||
matching_m = [m for m in measurements.extensions if m.id == ext_c.id]
|
||||
matching_r = [
|
||||
r for r in (roof_space.extensions or []) if r.id == ext_c.id
|
||||
]
|
||||
if matching_m:
|
||||
sap_building_parts.append(
|
||||
_map_extension_building_part(ext_c, matching[0])
|
||||
_map_extension_building_part(
|
||||
ext_c, matching_m[0], matching_r[0] if matching_r else None
|
||||
)
|
||||
)
|
||||
|
||||
total_floor_area = round(
|
||||
|
|
@ -101,11 +134,11 @@ class EpcPropertyDataMapper:
|
|||
for floor in part.sap_floor_dimensions
|
||||
),
|
||||
2,
|
||||
)
|
||||
) # TODO: verify that is the correct approach
|
||||
|
||||
return EpcPropertyData(
|
||||
dwelling_type=f"{general.detachment_type} {general.property_type.lower()}",
|
||||
inspection_date=date.fromisoformat(general.inspection_date),
|
||||
inspection_date=general.inspection_date,
|
||||
tenure=general.tenure,
|
||||
transaction_type=general.transaction_type,
|
||||
roofs=[],
|
||||
|
|
@ -113,7 +146,7 @@ class EpcPropertyDataMapper:
|
|||
floors=[],
|
||||
main_heating=[],
|
||||
door_count=room_counts.number_of_external_doors,
|
||||
sap_heating=_map_sap_heating(heating, ventilation),
|
||||
sap_heating=_map_sap_heating(heating, ventilation, survey.water_use),
|
||||
sap_windows=[_map_sap_window(w) for w in survey.windows],
|
||||
sap_energy_source=SapEnergySource(
|
||||
mains_gas=general.mains_gas_available,
|
||||
|
|
@ -124,6 +157,16 @@ class EpcPropertyDataMapper:
|
|||
is_dwelling_export_capable=general.dwelling_export_capable,
|
||||
wind_turbines_terrain_type=general.terrain_type,
|
||||
electricity_smart_meter_present=general.electricity_smart_meter,
|
||||
pv_connection=renewables.pv_connection,
|
||||
photovoltaic_supply=(
|
||||
PhotovoltaicSupply(
|
||||
none_or_no_details=PhotovoltaicSupplyNoneOrNoDetails(
|
||||
percent_roof_area=renewables.percent_roof_covered_pv,
|
||||
)
|
||||
)
|
||||
if renewables.percent_roof_covered_pv is not None
|
||||
else None
|
||||
),
|
||||
),
|
||||
sap_building_parts=sap_building_parts,
|
||||
solar_water_heating=renewables.solar_hot_water,
|
||||
|
|
@ -145,6 +188,16 @@ class EpcPropertyDataMapper:
|
|||
has_conservatory=survey.conservatories.has_conservatory,
|
||||
blocked_chimneys_count=room_counts.number_of_blocked_chimneys,
|
||||
draughtproofed_door_count=room_counts.number_of_draughtproofed_external_doors,
|
||||
address_line_1=address_line_1,
|
||||
post_town=post_town,
|
||||
postcode=postcode,
|
||||
report_reference=metadata.report_reference,
|
||||
number_of_storeys=general.number_of_storeys,
|
||||
any_unheated_rooms=room_counts.any_unheated_rooms,
|
||||
waste_water_heat_recovery=room_counts.waste_water_heat_recovery,
|
||||
hydro=renewables.hydro,
|
||||
photovoltaic_array=renewables.photovoltaic_array,
|
||||
sap_ventilation=_map_sap_ventilation(ventilation),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -227,7 +280,7 @@ class EpcPropertyDataMapper:
|
|||
cylinder_thermostat=None,
|
||||
secondary_fuel_type=None,
|
||||
secondary_heating_type=None,
|
||||
cylinder_insulation_thickness=None,
|
||||
cylinder_insulation_thickness_mm=None,
|
||||
),
|
||||
sap_windows=[],
|
||||
sap_energy_source=SapEnergySource(
|
||||
|
|
@ -368,7 +421,7 @@ class EpcPropertyDataMapper:
|
|||
cylinder_thermostat=schema.sap_heating.cylinder_thermostat,
|
||||
secondary_fuel_type=schema.sap_heating.secondary_fuel_type,
|
||||
secondary_heating_type=schema.sap_heating.secondary_heating_type,
|
||||
cylinder_insulation_thickness=schema.sap_heating.cylinder_insulation_thickness,
|
||||
cylinder_insulation_thickness_mm=schema.sap_heating.cylinder_insulation_thickness,
|
||||
),
|
||||
sap_windows=[],
|
||||
sap_energy_source=SapEnergySource(
|
||||
|
|
@ -509,7 +562,7 @@ class EpcPropertyDataMapper:
|
|||
cylinder_thermostat=schema.sap_heating.cylinder_thermostat,
|
||||
secondary_fuel_type=schema.sap_heating.secondary_fuel_type,
|
||||
secondary_heating_type=schema.sap_heating.secondary_heating_type,
|
||||
cylinder_insulation_thickness=schema.sap_heating.cylinder_insulation_thickness,
|
||||
cylinder_insulation_thickness_mm=schema.sap_heating.cylinder_insulation_thickness,
|
||||
),
|
||||
sap_windows=[],
|
||||
sap_energy_source=SapEnergySource(
|
||||
|
|
@ -657,7 +710,7 @@ class EpcPropertyDataMapper:
|
|||
cylinder_thermostat=schema.sap_heating.cylinder_thermostat,
|
||||
secondary_fuel_type=schema.sap_heating.secondary_fuel_type,
|
||||
secondary_heating_type=schema.sap_heating.secondary_heating_type,
|
||||
cylinder_insulation_thickness=schema.sap_heating.cylinder_insulation_thickness,
|
||||
cylinder_insulation_thickness_mm=schema.sap_heating.cylinder_insulation_thickness,
|
||||
),
|
||||
# 19.0 has no per-window list; individual window fields are at schema root
|
||||
sap_windows=[],
|
||||
|
|
@ -808,7 +861,7 @@ class EpcPropertyDataMapper:
|
|||
cylinder_thermostat=schema.sap_heating.cylinder_thermostat,
|
||||
secondary_fuel_type=schema.sap_heating.secondary_fuel_type,
|
||||
secondary_heating_type=schema.sap_heating.secondary_heating_type,
|
||||
cylinder_insulation_thickness=schema.sap_heating.cylinder_insulation_thickness,
|
||||
cylinder_insulation_thickness_mm=schema.sap_heating.cylinder_insulation_thickness,
|
||||
),
|
||||
# 20.0.0 SapWindow lacks frame/gap/draught fields present in later schemas
|
||||
sap_windows=[
|
||||
|
|
@ -986,7 +1039,7 @@ class EpcPropertyDataMapper:
|
|||
cylinder_thermostat=schema.sap_heating.cylinder_thermostat,
|
||||
secondary_fuel_type=schema.sap_heating.secondary_fuel_type,
|
||||
secondary_heating_type=schema.sap_heating.secondary_heating_type,
|
||||
cylinder_insulation_thickness=schema.sap_heating.cylinder_insulation_thickness,
|
||||
cylinder_insulation_thickness_mm=schema.sap_heating.cylinder_insulation_thickness,
|
||||
),
|
||||
sap_windows=[
|
||||
SapWindow(
|
||||
|
|
@ -1219,7 +1272,7 @@ class EpcPropertyDataMapper:
|
|||
cylinder_thermostat=schema.sap_heating.cylinder_thermostat,
|
||||
secondary_fuel_type=schema.sap_heating.secondary_fuel_type,
|
||||
secondary_heating_type=schema.sap_heating.secondary_heating_type,
|
||||
cylinder_insulation_thickness=schema.sap_heating.cylinder_insulation_thickness,
|
||||
cylinder_insulation_thickness_mm=schema.sap_heating.cylinder_insulation_thickness,
|
||||
),
|
||||
# SAP windows
|
||||
sap_windows=[
|
||||
|
|
@ -1412,16 +1465,33 @@ def _map_floor_dimensions(floors: List[FloorMeasurement]) -> List[SapFloorDimens
|
|||
total_floor_area_m2=floor.area_m2,
|
||||
party_wall_length_m=floor.pwl_m,
|
||||
heat_loss_perimeter_m=floor.heat_loss_perimeter_m,
|
||||
floor=int(floor.name.split()[-1]),
|
||||
)
|
||||
for floor in floors
|
||||
]
|
||||
|
||||
|
||||
def _map_roof(
|
||||
roof: Optional[Union[RoofSpaceDetail, ExtensionRoofSpace]],
|
||||
) -> tuple[Optional[str], Optional[Union[str, int]]]:
|
||||
if roof is None:
|
||||
return None, None
|
||||
thickness: Optional[Union[str, int]] = (
|
||||
roof.insulation_thickness_mm
|
||||
if roof.insulation_thickness_mm is not None
|
||||
else roof.insulation_thickness
|
||||
)
|
||||
return roof.insulation_at or None, thickness
|
||||
|
||||
|
||||
def _map_main_building_part(
|
||||
construction: BuildingConstruction,
|
||||
measurements: BuildingMeasurements,
|
||||
roof: RoofSpaceDetail,
|
||||
) -> SapBuildingPart:
|
||||
main = construction.main_building
|
||||
floor = construction.floor
|
||||
roof_location, roof_thickness = _map_roof(roof)
|
||||
return SapBuildingPart(
|
||||
identifier="main",
|
||||
construction_age_band=_extract_age_band(main.age_range),
|
||||
|
|
@ -1431,13 +1501,21 @@ def _map_main_building_part(
|
|||
party_wall_construction=main.party_wall_construction_type,
|
||||
sap_floor_dimensions=_map_floor_dimensions(measurements.main_building.floors),
|
||||
wall_thickness_mm=main.wall_thickness_mm,
|
||||
roof_insulation_location=roof_location,
|
||||
roof_insulation_thickness=roof_thickness,
|
||||
floor_type=floor.floor_type,
|
||||
floor_construction_type=floor.floor_construction,
|
||||
floor_insulation_type_str=floor.floor_insulation_type,
|
||||
floor_u_value_known=floor.floor_u_value_known,
|
||||
)
|
||||
|
||||
|
||||
def _map_extension_building_part(
|
||||
ext_c: ExtensionConstruction,
|
||||
ext_m: ExtensionMeasurements,
|
||||
roof: Optional[ExtensionRoofSpace],
|
||||
) -> SapBuildingPart:
|
||||
roof_location, roof_thickness = _map_roof(roof)
|
||||
return SapBuildingPart(
|
||||
identifier=f"extension_{ext_c.id}",
|
||||
construction_age_band=_extract_age_band(ext_c.age_range),
|
||||
|
|
@ -1447,6 +1525,8 @@ def _map_extension_building_part(
|
|||
party_wall_construction=ext_c.party_wall_construction_type,
|
||||
sap_floor_dimensions=_map_floor_dimensions(ext_m.floors),
|
||||
wall_thickness_mm=ext_c.wall_thickness_mm,
|
||||
roof_insulation_location=roof_location,
|
||||
roof_insulation_thickness=roof_thickness,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -1467,7 +1547,7 @@ def _map_sap_window(window: Window) -> SapWindow:
|
|||
|
||||
|
||||
def _map_sap_heating(
|
||||
heating: HeatingAndHotWater, ventilation: Ventilation
|
||||
heating: HeatingAndHotWater, ventilation: Ventilation, water_use: WaterUse
|
||||
) -> SapHeating:
|
||||
main = heating.main_heating
|
||||
secondary = heating.secondary_heating
|
||||
|
|
@ -1479,18 +1559,61 @@ def _map_sap_heating(
|
|||
None if secondary.secondary_fuel == "No Secondary Heating" else None
|
||||
)
|
||||
|
||||
shower_outlets = (
|
||||
ShowerOutlets(
|
||||
shower_outlet=ShowerOutlet(
|
||||
shower_outlet_type=water_use.showers[0].outlet_type,
|
||||
)
|
||||
)
|
||||
if water_use.showers
|
||||
else None
|
||||
)
|
||||
|
||||
_ELECTRIC_SYSTEM_TYPES = {"electric storage heaters", "electric underfloor heating"}
|
||||
_raw_fuel = main.fuel.split(", ")[0] if main.fuel else ""
|
||||
fuel_type = (
|
||||
_raw_fuel
|
||||
if _raw_fuel
|
||||
else ("Electricity" if main.system_type.lower() in _ELECTRIC_SYSTEM_TYPES else _raw_fuel)
|
||||
)
|
||||
|
||||
return SapHeating(
|
||||
instantaneous_wwhrs=InstantaneousWwhrs(),
|
||||
main_heating_details=[
|
||||
MainHeatingDetail(
|
||||
has_fghrs=main.flue_gas_heat_recovery_system,
|
||||
main_fuel_type=main.fuel,
|
||||
main_fuel_type=fuel_type,
|
||||
heat_emitter_type=main.emitter,
|
||||
emitter_temperature=main.emitter_temperature,
|
||||
fan_flue_present=main.fan_assist,
|
||||
main_heating_control=main.controls,
|
||||
condensing=main.condensing,
|
||||
weather_compensator=main.weather_compensator,
|
||||
central_heating_pump_age_str=main.central_heating_pump_age,
|
||||
)
|
||||
],
|
||||
has_fixed_air_conditioning=ventilation.has_fixed_air_conditioning,
|
||||
secondary_fuel_type=secondary_fuel_type,
|
||||
secondary_heating_type=heating.secondary_heating.secondary_system,
|
||||
shower_outlets=shower_outlets,
|
||||
cylinder_size=heating.water_heating.cylinder_size if heating.water_heating.cylinder_size != "No Cylinder" else None,
|
||||
cylinder_insulation_type=heating.water_heating.insulation_type,
|
||||
cylinder_insulation_thickness_mm=heating.water_heating.insulation_thickness_mm,
|
||||
immersion_heating_type=heating.water_heating.immersion_type,
|
||||
)
|
||||
|
||||
|
||||
def _map_sap_ventilation(ventilation: Ventilation) -> SapVentilation:
|
||||
return SapVentilation(
|
||||
ventilation_type=ventilation.ventilation_type,
|
||||
draught_lobby=ventilation.draught_lobby,
|
||||
pressure_test=ventilation.pressure_test,
|
||||
open_flues_count=ventilation.number_of_open_flues,
|
||||
closed_flues_count=ventilation.number_of_closed_flues,
|
||||
boiler_flues_count=ventilation.number_of_boiler_flues,
|
||||
other_flues_count=ventilation.number_of_other_flues,
|
||||
extract_fans_count=ventilation.number_of_extract_fans,
|
||||
passive_vents_count=ventilation.number_of_passive_vents,
|
||||
flueless_gas_fires_count=ventilation.number_of_flueless_gas_fires,
|
||||
ventilation_in_pcdf_database=ventilation.ventilation_in_pcdf_database,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,10 @@ from datatypes.epc.domain.epc_property_data import (
|
|||
SapEnergySource,
|
||||
SapFloorDimension,
|
||||
SapHeating,
|
||||
SapVentilation,
|
||||
SapWindow,
|
||||
ShowerOutlet,
|
||||
ShowerOutlets,
|
||||
)
|
||||
from datatypes.epc.domain.mapper import EpcPropertyDataMapper
|
||||
from datatypes.epc.schema.tests.helpers import from_dict
|
||||
|
|
@ -330,14 +333,14 @@ class TestFromSiteNotesExample1:
|
|||
def test_uprn_absent(self, result: EpcPropertyData) -> None:
|
||||
assert result.uprn is None
|
||||
|
||||
def test_address_absent(self, result: EpcPropertyData) -> None:
|
||||
assert result.address_line_1 is None
|
||||
def test_address_line_1(self, result: EpcPropertyData) -> None:
|
||||
assert result.address_line_1 == "1, Test Street"
|
||||
|
||||
def test_postcode_absent(self, result: EpcPropertyData) -> None:
|
||||
assert result.postcode is None
|
||||
def test_postcode(self, result: EpcPropertyData) -> None:
|
||||
assert result.postcode == "TE1 1ST"
|
||||
|
||||
def test_post_town_absent(self, result: EpcPropertyData) -> None:
|
||||
assert result.post_town is None
|
||||
def test_post_town(self, result: EpcPropertyData) -> None:
|
||||
assert result.post_town == "Test Town"
|
||||
|
||||
def test_status_absent(self, result: EpcPropertyData) -> None:
|
||||
assert result.status is None
|
||||
|
|
@ -352,9 +355,9 @@ class TestFromSiteNotesExample1:
|
|||
sap_version=None,
|
||||
dwelling_type="Mid-terrace house",
|
||||
uprn=None,
|
||||
address_line_1=None,
|
||||
postcode=None,
|
||||
post_town=None,
|
||||
address_line_1="1, Test Street",
|
||||
postcode="TE1 1ST",
|
||||
post_town="Test Town",
|
||||
inspection_date=date(2026, 3, 31),
|
||||
status=None,
|
||||
tenure="Rented Social",
|
||||
|
|
@ -379,9 +382,18 @@ class TestFromSiteNotesExample1:
|
|||
emitter_temperature="Unknown",
|
||||
fan_flue_present=True,
|
||||
main_heating_control="Programmer, room thermostat and TRVs",
|
||||
condensing=True,
|
||||
weather_compensator=False,
|
||||
central_heating_pump_age_str="Unknown",
|
||||
)
|
||||
],
|
||||
has_fixed_air_conditioning=False,
|
||||
cylinder_size="Normal (90-130 litres)",
|
||||
cylinder_insulation_type="Factory fitted",
|
||||
cylinder_insulation_thickness_mm=12,
|
||||
shower_outlets=ShowerOutlets(
|
||||
shower_outlet=ShowerOutlet(shower_outlet_type="Non-Electric Shower"),
|
||||
),
|
||||
),
|
||||
# Windows
|
||||
sap_windows=[
|
||||
|
|
@ -464,15 +476,23 @@ class TestFromSiteNotesExample1:
|
|||
total_floor_area_m2=24.78,
|
||||
party_wall_length_m=6.15,
|
||||
heat_loss_perimeter_m=14.21,
|
||||
floor=1,
|
||||
),
|
||||
SapFloorDimension(
|
||||
room_height_m=2.35,
|
||||
total_floor_area_m2=24.78,
|
||||
party_wall_length_m=6.15,
|
||||
heat_loss_perimeter_m=14.21,
|
||||
floor=0,
|
||||
),
|
||||
],
|
||||
wall_thickness_mm=280,
|
||||
roof_insulation_location="Joists",
|
||||
roof_insulation_thickness=100,
|
||||
floor_type="Ground Floor",
|
||||
floor_construction_type="Suspended, not timber",
|
||||
floor_insulation_type_str="As Built",
|
||||
floor_u_value_known=False,
|
||||
)
|
||||
],
|
||||
solar_water_heating=False,
|
||||
|
|
@ -495,5 +515,182 @@ class TestFromSiteNotesExample1:
|
|||
has_conservatory=False,
|
||||
blocked_chimneys_count=0,
|
||||
draughtproofed_door_count=2,
|
||||
report_reference="49D422A9-0779-44DD-9665-464D35DFF1A8",
|
||||
number_of_storeys=2,
|
||||
any_unheated_rooms=True,
|
||||
waste_water_heat_recovery="None",
|
||||
hydro=False,
|
||||
photovoltaic_array=False,
|
||||
sap_ventilation=SapVentilation(
|
||||
ventilation_type="Natural",
|
||||
open_flues_count=0,
|
||||
closed_flues_count=0,
|
||||
boiler_flues_count=0,
|
||||
other_flues_count=0,
|
||||
extract_fans_count=2,
|
||||
passive_vents_count=0,
|
||||
flueless_gas_fires_count=0,
|
||||
pressure_test="No test",
|
||||
draught_lobby=False,
|
||||
),
|
||||
)
|
||||
assert result == expected
|
||||
|
||||
|
||||
class TestFromSiteNotesVentilation:
|
||||
"""
|
||||
Fixture: pashub_rdsap_site_notes_example1.json
|
||||
Ventilation: Natural, 2 extract fans, no flues, no test, no draught lobby.
|
||||
"""
|
||||
|
||||
@pytest.fixture
|
||||
def survey(self) -> PasHubRdSapSiteNotes:
|
||||
return from_dict(
|
||||
PasHubRdSapSiteNotes, load("pashub_rdsap_site_notes_example1.json")
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def result(self, survey: PasHubRdSapSiteNotes) -> EpcPropertyData:
|
||||
return EpcPropertyDataMapper.from_site_notes(survey)
|
||||
|
||||
def test_sap_ventilation_present(self, result: EpcPropertyData) -> None:
|
||||
assert result.sap_ventilation is not None
|
||||
|
||||
def test_ventilation_type(self, result: EpcPropertyData) -> None:
|
||||
# ventilation.ventilation_type: "Natural"
|
||||
assert result.sap_ventilation.ventilation_type == "Natural"
|
||||
|
||||
def test_open_flues_count(self, result: EpcPropertyData) -> None:
|
||||
# ventilation.number_of_open_flues: 0
|
||||
assert result.sap_ventilation.open_flues_count == 0
|
||||
|
||||
def test_closed_flues_count(self, result: EpcPropertyData) -> None:
|
||||
# ventilation.number_of_closed_flues: 0
|
||||
assert result.sap_ventilation.closed_flues_count == 0
|
||||
|
||||
def test_boiler_flues_count(self, result: EpcPropertyData) -> None:
|
||||
# ventilation.number_of_boiler_flues: 0
|
||||
assert result.sap_ventilation.boiler_flues_count == 0
|
||||
|
||||
def test_other_flues_count(self, result: EpcPropertyData) -> None:
|
||||
# ventilation.number_of_other_flues: 0
|
||||
assert result.sap_ventilation.other_flues_count == 0
|
||||
|
||||
def test_extract_fans_count(self, result: EpcPropertyData) -> None:
|
||||
# ventilation.number_of_extract_fans: 2
|
||||
assert result.sap_ventilation.extract_fans_count == 2
|
||||
|
||||
def test_passive_vents_count(self, result: EpcPropertyData) -> None:
|
||||
# ventilation.number_of_passive_vents: 0
|
||||
assert result.sap_ventilation.passive_vents_count == 0
|
||||
|
||||
def test_flueless_gas_fires_count(self, result: EpcPropertyData) -> None:
|
||||
# ventilation.number_of_flueless_gas_fires: 0
|
||||
assert result.sap_ventilation.flueless_gas_fires_count == 0
|
||||
|
||||
def test_pressure_test(self, result: EpcPropertyData) -> None:
|
||||
# ventilation.pressure_test: "No test"
|
||||
assert result.sap_ventilation.pressure_test == "No test"
|
||||
|
||||
def test_draught_lobby(self, result: EpcPropertyData) -> None:
|
||||
# ventilation.draught_lobby: false
|
||||
assert result.sap_ventilation.draught_lobby is False
|
||||
|
||||
|
||||
class TestFromSiteNotesFloorConstruction:
|
||||
"""
|
||||
Fixture: pashub_rdsap_site_notes_example1.json
|
||||
Floor: Suspended not timber, As Built insulation, Ground Floor type.
|
||||
"""
|
||||
|
||||
@pytest.fixture
|
||||
def survey(self) -> PasHubRdSapSiteNotes:
|
||||
return from_dict(
|
||||
PasHubRdSapSiteNotes, load("pashub_rdsap_site_notes_example1.json")
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def result(self, survey: PasHubRdSapSiteNotes) -> EpcPropertyData:
|
||||
return EpcPropertyDataMapper.from_site_notes(survey)
|
||||
|
||||
def test_floor_type(self, result: EpcPropertyData) -> None:
|
||||
# building_construction.floor.floor_type: "Ground Floor"
|
||||
assert result.sap_building_parts[0].floor_type == "Ground Floor"
|
||||
|
||||
def test_floor_construction_type(self, result: EpcPropertyData) -> None:
|
||||
# building_construction.floor.floor_construction: "Suspended, not timber"
|
||||
assert result.sap_building_parts[0].floor_construction_type == "Suspended, not timber"
|
||||
|
||||
def test_floor_insulation_type_str(self, result: EpcPropertyData) -> None:
|
||||
# building_construction.floor.floor_insulation_type: "As Built"
|
||||
assert result.sap_building_parts[0].floor_insulation_type_str == "As Built"
|
||||
|
||||
def test_floor_u_value_known(self, result: EpcPropertyData) -> None:
|
||||
# building_construction.floor.floor_u_value_known: false
|
||||
assert result.sap_building_parts[0].floor_u_value_known is False
|
||||
|
||||
|
||||
class TestFromSiteNotesHeatingBoiler:
|
||||
"""
|
||||
Fixture: pashub_rdsap_site_notes_example1.json
|
||||
Boiler: Vaillant ecoFIT sustain, condensing, no weather compensator, pump age unknown.
|
||||
"""
|
||||
|
||||
@pytest.fixture
|
||||
def survey(self) -> PasHubRdSapSiteNotes:
|
||||
return from_dict(
|
||||
PasHubRdSapSiteNotes, load("pashub_rdsap_site_notes_example1.json")
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def result(self, survey: PasHubRdSapSiteNotes) -> EpcPropertyData:
|
||||
return EpcPropertyDataMapper.from_site_notes(survey)
|
||||
|
||||
def test_condensing(self, result: EpcPropertyData) -> None:
|
||||
# heating_and_hot_water.main_heating.condensing: true
|
||||
assert result.sap_heating.main_heating_details[0].condensing is True
|
||||
|
||||
def test_weather_compensator(self, result: EpcPropertyData) -> None:
|
||||
# heating_and_hot_water.main_heating.weather_compensator: false
|
||||
assert result.sap_heating.main_heating_details[0].weather_compensator is False
|
||||
|
||||
def test_central_heating_pump_age_str(self, result: EpcPropertyData) -> None:
|
||||
# heating_and_hot_water.main_heating.central_heating_pump_age: "Unknown"
|
||||
assert result.sap_heating.main_heating_details[0].central_heating_pump_age_str == "Unknown"
|
||||
|
||||
|
||||
class TestFromSiteNotesMiscTopLevel:
|
||||
"""
|
||||
Fixture: pashub_rdsap_site_notes_example1.json
|
||||
Misc fields: 2 storeys, unheated rooms present, no hydro, no PV array, no WWHR.
|
||||
"""
|
||||
|
||||
@pytest.fixture
|
||||
def survey(self) -> PasHubRdSapSiteNotes:
|
||||
return from_dict(
|
||||
PasHubRdSapSiteNotes, load("pashub_rdsap_site_notes_example1.json")
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def result(self, survey: PasHubRdSapSiteNotes) -> EpcPropertyData:
|
||||
return EpcPropertyDataMapper.from_site_notes(survey)
|
||||
|
||||
def test_number_of_storeys(self, result: EpcPropertyData) -> None:
|
||||
# general.number_of_storeys: 2
|
||||
assert result.number_of_storeys == 2
|
||||
|
||||
def test_any_unheated_rooms(self, result: EpcPropertyData) -> None:
|
||||
# room_count_elements.any_unheated_rooms: true
|
||||
assert result.any_unheated_rooms is True
|
||||
|
||||
def test_waste_water_heat_recovery(self, result: EpcPropertyData) -> None:
|
||||
# room_count_elements.waste_water_heat_recovery: "None"
|
||||
assert result.waste_water_heat_recovery == "None"
|
||||
|
||||
def test_hydro(self, result: EpcPropertyData) -> None:
|
||||
# renewables.hydro: false
|
||||
assert result.hydro is False
|
||||
|
||||
def test_photovoltaic_array(self, result: EpcPropertyData) -> None:
|
||||
# renewables.photovoltaic_array: false
|
||||
assert result.photovoltaic_array is False
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import dataclasses
|
||||
import typing
|
||||
from datetime import date
|
||||
from typing import Any, Dict, Type, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
|
@ -70,4 +71,7 @@ def _coerce(value: Any, hint: Any) -> Any:
|
|||
if dataclasses.is_dataclass(hint) and isinstance(value, dict):
|
||||
return _from_dict_impl(hint, value)
|
||||
|
||||
if hint is date and isinstance(value, str):
|
||||
return date.fromisoformat(value)
|
||||
|
||||
return value
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from dataclasses import dataclass
|
||||
from datetime import date
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
|
|
@ -8,7 +9,7 @@ class InspectionMetadata:
|
|||
email_address: str
|
||||
report_reference: str
|
||||
created_on: str
|
||||
date_of_inspection: str
|
||||
date_of_inspection: date
|
||||
property_address: str
|
||||
property_photo: Optional[bool] = None
|
||||
|
||||
|
|
@ -17,7 +18,7 @@ class InspectionMetadata:
|
|||
class General:
|
||||
epc_checked_before_assessment: bool
|
||||
epc_exists_at_point_of_assessment: bool
|
||||
inspection_date: str
|
||||
inspection_date: date
|
||||
transaction_type: str
|
||||
tenure: str
|
||||
property_type: str
|
||||
|
|
@ -178,6 +179,7 @@ class MainHeating:
|
|||
@dataclass
|
||||
class SecondaryHeating:
|
||||
secondary_fuel: str
|
||||
secondary_system: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -189,6 +191,7 @@ class WaterHeating:
|
|||
insulation_type: Optional[str] = None
|
||||
insulation_thickness_mm: Optional[int] = None
|
||||
has_thermostat: Optional[bool] = None
|
||||
immersion_type: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -226,6 +229,8 @@ class Renewables:
|
|||
photovoltaic_array: bool
|
||||
number_of_pv_batteries: int
|
||||
hydro: bool
|
||||
pv_connection: Optional[str] = None
|
||||
percent_roof_covered_pv: Optional[int] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
"report_reference": "49D422A9-0779-44DD-9665-464D35DFF1A8",
|
||||
"created_on": "2026-03-31",
|
||||
"date_of_inspection": "2026-03-31",
|
||||
"property_address": "test"
|
||||
"property_address": "1, Test Street, Test Town, Test County, TE1 1ST"
|
||||
},
|
||||
"general": {
|
||||
"epc_checked_before_assessment": true,
|
||||
|
|
@ -229,4 +229,4 @@
|
|||
"hard_to_treat_cavity_high_exposure": false,
|
||||
"hard_to_treat_cavity_narrow_cavities": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import json
|
||||
import os
|
||||
from datetime import date
|
||||
from typing import Any, Dict
|
||||
|
||||
import pytest
|
||||
|
|
@ -224,7 +225,7 @@ class TestExample2:
|
|||
self, survey: PasHubRdSapSiteNotes
|
||||
) -> None:
|
||||
assert survey.inspection_metadata.created_on == "2025-11-10"
|
||||
assert survey.inspection_metadata.date_of_inspection == "2025-09-25"
|
||||
assert survey.inspection_metadata.date_of_inspection == date(2025, 9, 25)
|
||||
|
||||
# --- general ---
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@ pythonpath = .
|
|||
log_cli = true
|
||||
log_cli_level = INFO
|
||||
addopts = --cov-report term-missing --cov=etl/epc --cov=recommendations --cov=backend --cov=etl/epc_clean --cov=etl/spatial
|
||||
testpaths = recommendations/tests backend/tests etl/epc/tests etl/epc_clean/tests etl/spatial/tests backend/condition/tests backend/address2UPRN/tests backend/onboarders/tests backend/categorisation/tests backend/export/tests etl/hubspot/tests backend/hubspot_trigger_orchestrator/tests datatypes/epc/schema/tests datatypes/epc/surveys/tests datatypes/epc/domain/tests backend/ecmk_fetcher/tests/
|
||||
testpaths = recommendations/tests backend/tests etl/epc/tests etl/epc_clean/tests etl/spatial/tests backend/condition/tests backend/address2UPRN/tests backend/onboarders/tests backend/categorisation/tests backend/export/tests etl/hubspot/tests backend/hubspot_trigger_orchestrator/tests datatypes/epc/schema/tests datatypes/epc/surveys/tests datatypes/epc/domain/tests backend/ecmk_fetcher/tests/ backend/documents_parser/tests
|
||||
markers =
|
||||
integration: mark a test as an integration test
|
||||
|
|
|
|||
|
|
@ -6,4 +6,5 @@ dotenv
|
|||
psycopg[binary]
|
||||
pytest-postgresql
|
||||
hubspot-api-client
|
||||
fuzzywuzzy
|
||||
fuzzywuzzy
|
||||
pymupdf
|
||||
Loading…
Add table
Reference in a new issue