""" 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.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(BaseModel): """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(BaseModel): """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(BaseModel): """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(BaseModel): """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"}, }, )