""" Real Estate data models for Architektur-Planungs-App. Implements a general Swiss architecture planning data model. (PEK is one example implementation, but the model is general-purpose) """ from typing import List, Dict, Any, Optional, ForwardRef from enum import Enum from pydantic import BaseModel, Field from modules.datamodels.datamodelBase import PowerOnModel from modules.shared.attributeUtils import registerModelLabels from modules.shared.timeUtils import getUtcTimestamp import uuid # ===== Enums ===== class StatusProzess(str, Enum): """Project process status""" EINGANG = "Eingang" ANALYSE = "Analyse" STUDIE = "Studie" PLANUNG = "Planung" BAURECHTSVERFAHREN = "Baurechtsverfahren" UMSETZUNG = "Umsetzung" ARCHIV = "Archiv" class DokumentTyp(str, Enum): """Document type for categorization""" KANTON_BAUREGLEMENT_AKTUELL = "kantonBaureglementAktuell" KANTON_BAUREGLEMENT_REVISION = "kantonBaureglementRevision" KANTON_BAUVERORDNUNG_AKTUELL = "kantonBauverordnungAktuell" KANTON_BAUVERORDNUNG_REVISION = "kantonBauverordnungRevision" GEMEINDE_BZO_AKTUELL = "gemeindeBzoAktuell" GEMEINDE_BZO_REVISION = "gemeindeBzoRevision" class JaNein(str, Enum): """Three-valued state for optional yes/no questions""" UNBEKANNT = "" # Empty string for unknown/not captured JA = "Ja" NEIN = "Nein" class GeoTag(str, Enum): """Geopoint categories""" K1 = "K1" # Fixpunkt höchster Genauigkeit K2 = "K2" # Fixpunkt mittlerer Genauigkeit K3 = "K3" # Fixpunkt niedriger Genauigkeit GEOMETER = "Geometer" # Vom Geometer vermessener Punkt # ===== Helper Models (must be defined before main models) ===== class GeoPunkt(BaseModel): """Represents a 3D point with reference.""" koordinatensystem: str = Field( description="Coordinate system (e.g. 'LV95', 'EPSG:2056')", frontend_type="text", frontend_readonly=False, frontend_required=True, ) x: float = Field( description="East value (E) [m], typically 2'480'000 - 2'840'000", frontend_type="number", frontend_readonly=False, frontend_required=True, ) y: float = Field( description="North value (N) [m], typically 1'070'000 - 1'300'000", frontend_type="number", frontend_readonly=False, frontend_required=True, ) z: Optional[float] = Field( None, description="Height above sea level [m]", frontend_type="number", frontend_readonly=False, frontend_required=False, ) referenz: Optional[GeoTag] = Field( None, description="Point categorization", frontend_type="select", frontend_readonly=False, frontend_required=False, ) class GeoPolylinie(BaseModel): """Represents a line or polygon from multiple GeoPunkte.""" id: str = Field( default_factory=lambda: str(uuid.uuid4()), description="Primary key", ) closed: bool = Field( description="Is the GeoPolylinie closed (polygon)?", frontend_type="boolean", frontend_readonly=False, frontend_required=True, ) punkte: List[GeoPunkt] = Field( default_factory=list, description="List of GeoPunkte forming the GeoPolylinie", frontend_type="json", frontend_readonly=False, frontend_required=True, ) class Dokument(BaseModel): """Supporting data object for file and URL management with versioning.""" id: str = Field( default_factory=lambda: str(uuid.uuid4()), description="Primary key", frontend_type="text", frontend_readonly=True, frontend_required=False, ) mandateId: str = Field( description="ID of the mandate this document belongs to", frontend_type="text", frontend_readonly=True, frontend_required=False, ) featureInstanceId: str = Field( description="ID of the feature instance this document belongs to", frontend_type="text", frontend_readonly=True, frontend_required=False, ) label: str = Field( description="Document label", frontend_type="text", frontend_readonly=False, frontend_required=True, ) versionsbezeichnung: Optional[str] = Field( None, description="Version number or designation (e.g. 'v1.0', 'Rev. A')", frontend_type="text", frontend_readonly=False, frontend_required=False, ) dokumentTyp: Optional[DokumentTyp] = Field( None, description="Document type", frontend_type="select", frontend_readonly=False, frontend_required=False, ) dokumentReferenz: str = Field( description="File path or URL", frontend_type="text", frontend_readonly=False, frontend_required=True, ) quelle: Optional[str] = Field( None, description="Source of the document", frontend_type="text", frontend_readonly=False, frontend_required=False, ) mimeType: Optional[str] = Field( None, description="MIME type of the document (e.g. 'application/pdf', 'image/png')", frontend_type="text", frontend_readonly=False, frontend_required=False, ) kategorienTags: List[str] = Field( default_factory=list, description="Document categorization tags", frontend_type="json", frontend_readonly=False, frontend_required=False, ) class Kontext(PowerOnModel): """Supporting data object for flexible additional information.""" id: str = Field( default_factory=lambda: str(uuid.uuid4()), description="Primary key", ) thema: str = Field( description="Theme designation", frontend_type="text", frontend_readonly=False, frontend_required=True, ) inhalt: str = Field( description="Detailed information (text)", frontend_type="textarea", frontend_readonly=False, frontend_required=True, ) class Land(BaseModel): """National level administrative entity.""" id: str = Field( default_factory=lambda: str(uuid.uuid4()), description="Primary key", frontend_type="text", frontend_readonly=True, frontend_required=False, ) mandateId: str = Field( description="ID of the mandate", frontend_type="text", frontend_readonly=True, frontend_required=False, ) featureInstanceId: str = Field( description="ID of the feature instance", frontend_type="text", frontend_readonly=True, frontend_required=False, ) label: str = Field( description="Country name (e.g. 'Schweiz')", frontend_type="text", frontend_readonly=False, frontend_required=True, ) abk: Optional[str] = Field( None, description="Abbreviation (e.g. 'CH')", frontend_type="text", frontend_readonly=False, frontend_required=False, ) dokumente: List[Dokument] = Field( default_factory=list, description="National laws/documents", frontend_type="json", frontend_readonly=False, frontend_required=False, ) kontextInformationen: List[Kontext] = Field( default_factory=list, description="National context information", frontend_type="json", frontend_readonly=False, frontend_required=False, ) class Kanton(PowerOnModel): """Cantonal level administrative entity.""" id: str = Field( default_factory=lambda: str(uuid.uuid4()), description="Primary key", frontend_type="text", frontend_readonly=True, frontend_required=False, ) mandateId: str = Field( description="ID of the mandate", frontend_type="text", frontend_readonly=True, frontend_required=False, ) featureInstanceId: str = Field( description="ID of the feature instance", frontend_type="text", frontend_readonly=True, frontend_required=False, ) label: str = Field( description="Canton name (e.g. 'Zürich')", frontend_type="text", frontend_readonly=False, frontend_required=True, ) id_land: Optional[str] = Field( None, description="Land ID (Foreign Key) - eindeutiger Link zum Land, in welchem Land der Kanton liegt", frontend_type="text", frontend_readonly=False, frontend_required=False, ) abk: Optional[str] = Field( None, description="Abbreviation (e.g. 'ZH')", frontend_type="text", frontend_readonly=False, frontend_required=False, ) dokumente: List[Dokument] = Field( default_factory=list, description="Cantonal documents", frontend_type="json", frontend_readonly=False, frontend_required=False, ) kontextInformationen: List[Kontext] = Field( default_factory=list, description="Canton-specific context information", frontend_type="json", frontend_readonly=False, frontend_required=False, ) class Gemeinde(BaseModel): """Municipal level administrative entity.""" id: str = Field( default_factory=lambda: str(uuid.uuid4()), description="Primary key", frontend_type="text", frontend_readonly=True, frontend_required=False, ) mandateId: str = Field( description="ID of the mandate", frontend_type="text", frontend_readonly=True, frontend_required=False, ) featureInstanceId: str = Field( description="ID of the feature instance", frontend_type="text", frontend_readonly=True, frontend_required=False, ) label: str = Field( description="Municipality name (e.g. 'Zürich')", frontend_type="text", frontend_readonly=False, frontend_required=True, ) id_kanton: Optional[str] = Field( None, description="Kanton ID (Foreign Key) - eindeutiger Link zum Kanton, in welchem Kanton die Gemeinde liegt", frontend_type="text", frontend_readonly=False, frontend_required=False, ) plz: Optional[str] = Field( None, description="Postal code (for municipalities with multiple PLZ, this can be a main PLZ). Bei Gemeinden mit mehreren Postleitzahlen wird die konkrete PLZ der Parzelle im Attribut `plz` der Parzelle erfasst.", frontend_type="text", frontend_readonly=False, frontend_required=False, ) dokumente: List[Dokument] = Field( default_factory=list, description="Municipal documents", frontend_type="json", frontend_readonly=False, frontend_required=False, ) kontextInformationen: List[Kontext] = Field( default_factory=list, description="Municipality-specific context information", frontend_type="json", frontend_readonly=False, frontend_required=False, ) # ===== Main Models (use ForwardRef for circular references) ===== # Forward references for circular dependencies ParzelleRef = ForwardRef('Parzelle') class Parzelle(PowerOnModel): """Represents a plot with all building law properties.""" id: str = Field( default_factory=lambda: str(uuid.uuid4()), description="Primary key", frontend_type="text", frontend_readonly=True, frontend_required=False, ) mandateId: str = Field( description="ID of the mandate", frontend_type="text", frontend_readonly=True, frontend_required=False, ) featureInstanceId: str = Field( description="ID of the feature instance", frontend_type="text", frontend_readonly=True, frontend_required=False, ) # Grunddaten label: str = Field( description="Plot designation", frontend_type="text", frontend_readonly=False, frontend_required=True, ) parzellenAliasTags: List[str] = Field( default_factory=list, description="Additional plot names or field names", frontend_type="json", frontend_readonly=False, frontend_required=False, ) eigentuemerschaft: Optional[str] = Field( None, description="Owner of the plot", frontend_type="text", frontend_readonly=False, frontend_required=False, ) strasseNr: Optional[str] = Field( None, description="Street and house number", frontend_type="text", frontend_readonly=False, frontend_required=False, ) plz: Optional[str] = Field( None, description="Postal code of the plot", frontend_type="text", frontend_readonly=False, frontend_required=False, ) # Geografischer Kontext perimeter: Optional[GeoPolylinie] = Field( None, description="Plot boundary as closed GeoPolylinie", frontend_type="json", frontend_readonly=False, frontend_required=False, ) baulinie: Optional[GeoPolylinie] = Field( None, description="Building line of the plot", frontend_type="json", frontend_readonly=False, frontend_required=False, ) kontextGemeinde: Optional[str] = Field( None, description="Municipality ID (Foreign Key)", frontend_type="text", frontend_readonly=False, frontend_required=False, ) # Bebauungsparameter bauzone: Optional[str] = Field( None, description="Building zone designation (e.g. W3, WG2, etc.)", frontend_type="text", frontend_readonly=False, frontend_required=False, ) az: Optional[float] = Field( None, description="Ausnützungsziffer", frontend_type="number", frontend_readonly=False, frontend_required=False, ) bz: Optional[float] = Field( None, description="Bebauungsziffer", frontend_type="number", frontend_readonly=False, frontend_required=False, ) vollgeschossZahl: Optional[int] = Field( None, description="Number of allowed full floors", frontend_type="number", frontend_readonly=False, frontend_required=False, ) anrechenbarDachgeschoss: Optional[float] = Field( None, description="Accountable portion of attic (0.0 - 1.0)", frontend_type="number", frontend_readonly=False, frontend_required=False, ) anrechenbarUntergeschoss: Optional[float] = Field( None, description="Accountable portion of basement (0.0 - 1.0)", frontend_type="number", frontend_readonly=False, frontend_required=False, ) gebaeudehoeheMax: Optional[float] = Field( None, description="Maximum building height in meters", frontend_type="number", frontend_readonly=False, frontend_required=False, ) # Abstandsregelungen regelnGrenzabstand: List[str] = Field( default_factory=list, description="Regulations for boundary distance", frontend_type="json", frontend_readonly=False, frontend_required=False, ) regelnMehrlaengenzuschlag: List[str] = Field( default_factory=list, description="Regulations for additional length surcharge", frontend_type="json", frontend_readonly=False, frontend_required=False, ) regelnMehrhoehenzuschlag: List[str] = Field( default_factory=list, description="Regulations for additional height surcharge", frontend_type="json", frontend_readonly=False, frontend_required=False, ) # Eigenschaften (Ja/Nein) parzelleBebaut: Optional[JaNein] = Field( None, description="Is the plot built?", frontend_type="select", frontend_readonly=False, frontend_required=False, ) parzelleErschlossen: Optional[JaNein] = Field( None, description="Is the plot developed?", frontend_type="select", frontend_readonly=False, frontend_required=False, ) parzelleHanglage: Optional[JaNein] = Field( None, description="Is the plot on a slope?", frontend_type="select", frontend_readonly=False, frontend_required=False, ) # Schutzzonen laermschutzzone: Optional[str] = Field( None, description="Noise protection zone (e.g. 'II')", frontend_type="text", frontend_readonly=False, frontend_required=False, ) hochwasserschutzzone: Optional[str] = Field( None, description="Flood protection zone (e.g. 'tief')", frontend_type="text", frontend_readonly=False, frontend_required=False, ) grundwasserschutzzone: Optional[str] = Field( None, description="Groundwater protection zone", frontend_type="text", frontend_readonly=False, frontend_required=False, ) # Beziehungen (stored as JSONB in database) parzellenNachbarschaft: List[Dict[str, Any]] = Field( default_factory=list, description="Neighboring plots (stored as list of Parzelle IDs or full objects)", frontend_type="json", frontend_readonly=False, frontend_required=False, ) dokumente: List[Dokument] = Field( default_factory=list, description="Plot-specific documents", frontend_type="json", frontend_readonly=False, frontend_required=False, ) kontextInformationen: List[Kontext] = Field( default_factory=list, description="Plot-specific context information", frontend_type="json", frontend_readonly=False, frontend_required=False, ) class Projekt(PowerOnModel): """Core object representing a construction project.""" id: str = Field( default_factory=lambda: str(uuid.uuid4()), description="Primary key", frontend_type="text", frontend_readonly=True, frontend_required=False, ) mandateId: str = Field( description="ID of the mandate", frontend_type="text", frontend_readonly=True, frontend_required=False, ) featureInstanceId: str = Field( description="ID of the feature instance", frontend_type="text", frontend_readonly=True, frontend_required=False, ) label: str = Field( description="Project designation", frontend_type="text", frontend_readonly=False, frontend_required=True, ) statusProzess: Optional[StatusProzess] = Field( None, description="Project status", frontend_type="select", frontend_readonly=False, frontend_required=False, ) perimeter: Optional[GeoPolylinie] = Field( None, description="Envelope of all plots in the project", frontend_type="json", frontend_readonly=False, frontend_required=False, ) baulinie: Optional[GeoPolylinie] = Field( None, description="Building line of the project", frontend_type="json", frontend_readonly=False, frontend_required=False, ) parzellen: List[Parzelle] = Field( default_factory=list, description="All plots of the project", frontend_type="json", frontend_readonly=False, frontend_required=False, ) dokumente: List[Dokument] = Field( default_factory=list, description="Project-specific documents", frontend_type="json", frontend_readonly=False, frontend_required=False, ) kontextInformationen: List[Kontext] = Field( default_factory=list, description="Project-specific context information", frontend_type="json", frontend_readonly=False, frontend_required=False, ) # Resolve forward references Parzelle.model_rebuild() Projekt.model_rebuild() # Register labels for frontend registerModelLabels( "Projekt", {"en": "Project", "fr": "Projet", "de": "Projekt"}, { "id": {"en": "ID", "fr": "ID", "de": "ID"}, "label": {"en": "Label", "fr": "Libellé", "de": "Bezeichnung"}, "statusProzess": {"en": "Process Status", "fr": "Statut du processus", "de": "Prozessstatus"}, "mandateId": {"en": "Mandate ID", "fr": "ID du mandat", "de": "Mandats-ID"}, "featureInstanceId": {"en": "Feature Instance ID", "fr": "ID de l'instance", "de": "Feature-Instanz-ID"}, }, ) registerModelLabels( "Parzelle", {"en": "Plot", "fr": "Parcelle", "de": "Parzelle"}, { "id": {"en": "ID", "fr": "ID", "de": "ID"}, "label": {"en": "Label", "fr": "Libellé", "de": "Bezeichnung"}, "mandateId": {"en": "Mandate ID", "fr": "ID du mandat", "de": "Mandats-ID"}, "featureInstanceId": {"en": "Feature Instance ID", "fr": "ID de l'instance", "de": "Feature-Instanz-ID"}, }, ) registerModelLabels( "Dokument", {"en": "Document", "fr": "Document", "de": "Dokument"}, { "id": {"en": "ID", "fr": "ID", "de": "ID"}, "label": {"en": "Label", "fr": "Libellé", "de": "Bezeichnung"}, "mandateId": {"en": "Mandate ID", "fr": "ID du mandat", "de": "Mandats-ID"}, "featureInstanceId": {"en": "Feature Instance ID", "fr": "ID de l'instance", "de": "Feature-Instanz-ID"}, }, )