gateway/modules/datamodels/datamodelRealEstate.py

667 lines
20 KiB
Python

"""
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,
)
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,
)
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,
)
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,
)
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,
)
# 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,
)
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"},
},
)
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"},
},
)
registerModelLabels(
"Dokument",
{"en": "Document", "fr": "Document", "de": "Dokument"},
{
"id": {"en": "ID", "fr": "ID", "de": "ID"},
"label": {"en": "Label", "fr": "Libellé", "de": "Bezeichnung"},
},
)