976 lines
34 KiB
Markdown
976 lines
34 KiB
Markdown
# Schritt 1: Datenmodell erstellen
|
|
|
|
[← Zurück zur Übersicht](README.md) | [Weiter: Interface erstellen →](03-interfaces.md)
|
|
|
|
## Real Estate-Datenmodelle
|
|
|
|
**Datei:** `modules/datamodels/datamodelRealEstate.py`
|
|
|
|
Das Feature arbeitet **stateless** ohne Session-Management. Die Datenmodelle definieren die Struktur der Real Estate-Entitäten, die über die API verwaltet werden können.
|
|
|
|
**Hinweis:** Die Real Estate-Datenmodell-Entitäten (Projekt, Parzelle, Dokument, etc.) werden in `datamodelRealEstate.py` definiert. Diese werden direkt über CRUD-Operationen verwaltet, ohne zusätzliche Chat-Interface-Modelle.
|
|
|
|
### Warum keine Chat-Interface-Modelle?
|
|
|
|
Das Feature arbeitet **stateless** ohne Session-Management. Alle Operationen arbeiten direkt auf den Real Estate-Datenmodellen:
|
|
|
|
#### Stateless Design
|
|
|
|
- **Keine Session-Modelle**: Keine `RealEstateChatSession` notwendig
|
|
- **Keine Query-History**: Queries werden nicht gespeichert (kann optional später hinzugefügt werden)
|
|
- **Direkte CRUD-Operationen**: User-Input → AI-Analyse → CRUD → Ergebnis
|
|
- **Einfache Architektur**: Weniger Komplexität, bessere Performance
|
|
|
|
#### Real Estate-Modelle
|
|
|
|
Die Real Estate-Modelle (`Projekt`, `Parzelle`, `Dokument`, etc.):
|
|
- Repräsentieren die **tatsächlichen Geschäftsdaten** der Immobilien-Projekte
|
|
- Werden über lange Zeiträume gepflegt und verändert
|
|
- Haben komplexe Beziehungen zueinander (Projekt → Parzellen → Dokumente)
|
|
- Werden direkt über CRUD-Operationen verwaltet
|
|
|
|
#### Datenfluss (stateless)
|
|
|
|
```
|
|
User Input (natürliche Sprache)
|
|
↓
|
|
AI-Analyse (Intent-Erkennung)
|
|
↓
|
|
CRUD-Operation identifizieren
|
|
↓
|
|
Real Estate-Modelle
|
|
↓
|
|
Datenbank-Operation
|
|
↓
|
|
Ergebnis zurückgeben
|
|
(keine Session, keine History)
|
|
```
|
|
|
|
---
|
|
|
|
### Stateless vs. Session-basiert
|
|
|
|
**Real Estate Feature (stateless):**
|
|
- Direkte CRUD-Operationen auf Real Estate-Modellen
|
|
- Keine Session-Modelle notwendig
|
|
- Keine Query-History
|
|
- Einfacher und schneller
|
|
|
|
**Chat-System (session-basiert):**
|
|
- Verwendet `ChatWorkflow` für komplexe AI-Workflows
|
|
- Verwendet `ChatDocument` für Datei-Verknüpfungen
|
|
- Session-Management für Multi-Step-Operationen
|
|
- Für komplexe Workflows mit Planung und Review
|
|
|
|
**Unterschied:**
|
|
- Real Estate Feature ist für **einfache CRUD-Operationen** optimiert
|
|
- Chat-System ist für **komplexe AI-Workflows** optimiert
|
|
- Beide können parallel existieren und für verschiedene Use Cases genutzt werden
|
|
|
|
---
|
|
|
|
### Warum nicht das bestehende `ChatWorkflow` verwenden?
|
|
|
|
Sie fragen sich vielleicht: **Kann ich nicht einfach das bestehende `ChatWorkflow` aus `datamodelChat.py` verwenden?**
|
|
|
|
Die kurze Antwort: **Für stateless CRUD-Operationen ist `ChatWorkflow` zu komplex**. Das Real Estate Feature arbeitet ohne Session-Management und nutzt direkt die Real Estate-Modelle.
|
|
|
|
#### Unterschiedliche Anwendungsfälle
|
|
|
|
| **Aspekt** | **ChatWorkflow (bestehend)** | **Real Estate Feature (stateless)** |
|
|
|------------|------------------------------|-------------------------------------|
|
|
| **Zweck** | Komplexe AI-gesteuerte Workflows mit mehreren Tasks/Actions | Einfache CRUD-Operationen |
|
|
| **Komplexität** | Hoch: Tasks, Actions, Rounds, Workflow-Modi, Retries | Niedrig: Direkte CRUD-Operationen |
|
|
| **Session** | Session-Management für Multi-Step-Workflows | Keine Session, stateless |
|
|
| **Verarbeitung** | Multi-Step AI-Workflows mit Planung, Review, Iteration | Direkte CRUD: User-Input → AI-Analyse → CRUD → Ergebnis |
|
|
| **Ergebnisse** | `ChatMessage` mit `documents`, `ActionResult` | Direkte CRUD-Ergebnisse (Projekt, Parzelle, etc.) |
|
|
|
|
#### Warum `ChatWorkflow` nicht passt:
|
|
|
|
1. **Zu komplex**: `ChatWorkflow` hat viele Felder, die für einfache CRUD-Operationen nicht relevant sind
|
|
2. **Session-basiert**: `ChatWorkflow` benötigt Session-Management, das wir nicht brauchen
|
|
3. **Falsches Abstraktionsniveau**: `ChatWorkflow` ist für komplexe AI-Workflows, Real Estate braucht einfache CRUD-Operationen
|
|
|
|
#### Die richtige Lösung: Direkte CRUD-Operationen
|
|
|
|
Stattdessen arbeiten wir **direkt** mit den Real Estate-Modellen:
|
|
|
|
```python
|
|
# Stateless CRUD-Operationen
|
|
User Input → AI-Analyse → CRUD-Operation → Ergebnis
|
|
# Keine Session, keine History, einfach und schnell
|
|
```
|
|
|
|
#### Wann könnte man `ChatWorkflow` verwenden?
|
|
|
|
Sie könnten `ChatWorkflow` verwenden, wenn Sie:
|
|
- ✅ **Komplexe AI-Workflows** für Real Estate implementieren wollen (z.B. "Analysiere alle Projekte und erstelle einen Bericht")
|
|
- ✅ **Multi-Step-Verarbeitung** benötigen (z.B. "Lade Daten → Transformiere → Erstelle Visualisierung")
|
|
- ✅ **Planung und Review** brauchen (z.B. "Prüfe alle Parzellen auf Konformität")
|
|
|
|
Aber für **einfache CRUD-Operationen** ist der stateless Ansatz die bessere Wahl.
|
|
|
|
---
|
|
|
|
## Real Estate-Datenmodell-Implementierung:
|
|
|
|
Die Real Estate-Datenmodell-Entitäten müssen separat in `modules/datamodels/datamodelRealEstate.py` implementiert werden.
|
|
Siehe `../PEK_datamodel_desc.md` für die vollständige Spezifikation aller Felder und Beziehungen (PEK ist ein Beispiel für eine Real Estate-Firma, das Modell ist aber allgemein verwendbar).
|
|
|
|
### Wichtige Hinweise zum Datenmodell
|
|
|
|
**Objektmodell vs. Datenbank-Repräsentation:**
|
|
|
|
Dieses Dokument beschreibt ein **Objektmodell** für die Arbeit im Code. Es handelt sich **NICHT** um ein relationales Datenbankmodell mit Junction Tables.
|
|
|
|
- **Im Code-Modell**: Alle Beziehungen werden als Objektreferenzen oder Listen von Objekten dargestellt (z.B. `dokumente: list[Dokument]`, `parzellen: list[Parzelle]`).
|
|
- **Für die Datenbank-Serialisierung**: Bei der Persistierung können Junction Tables verwendet werden, um n:m-Beziehungen in der Datenbank abzubilden. Dies ist jedoch ein Implementierungsdetail der Datenbank-Schicht und gehört nicht zum Hauptmodell.
|
|
|
|
**Systemattribute:**
|
|
|
|
Alle Datenobjekte haben automatisch die folgenden Systemattribute:
|
|
- `_createdAt`: Float (Timestamp UTC)
|
|
- `_createdBy`: String (User-ID)
|
|
- `_modifiedAt`: Float (Timestamp UTC)
|
|
- `_modifiedBy`: String (User-ID)
|
|
|
|
**Timestamps:**
|
|
- Alle Timestamps sind im Float-Format UTC im Datenmodell gespeichert.
|
|
- Die Darstellung im UI erfolgt mit der lokalen Zeitzone des Benutzers.
|
|
|
|
**Wichtige Punkte für die Implementierung:**
|
|
- Objektbeziehungen wie `parzellen: list[Parzelle]` werden als JSONB in PostgreSQL gespeichert
|
|
- Einzelne Objektreferenzen wie `kontextKanton: Optional[str]` werden als String-ID (Foreign Key) gespeichert
|
|
- Administrative Hierarchie: `Kanton` benötigt `id_land` (Foreign Key zu Land), `Gemeinde` benötigt `id_kanton` (Foreign Key zu Kanton)
|
|
- Alle Entitäten benötigen `mandateId` für Mandaten-Isolation
|
|
- Systemattribute (`_createdAt`, `_createdBy`, etc.) werden automatisch vom DatabaseConnector hinzugefügt
|
|
|
|
### Datenfluss-Diagramm
|
|
|
|
```mermaid
|
|
---
|
|
title: Hauptflüsse - Architektur-Planungs-App
|
|
---
|
|
flowchart LR
|
|
subgraph Admin[Administrative Ebene]
|
|
Land[Land<br/>Schweiz]
|
|
Kanton[Kanton<br/>z.B. Zürich]
|
|
Gemeinde[Gemeinde<br/>z.B. Zürich Stadt]
|
|
Land --> Kanton
|
|
Kanton --> Gemeinde
|
|
end
|
|
|
|
subgraph Geo[Geografische Daten]
|
|
GeoPolylinie[GeoPolylinie<br/>Linie/Polygon]
|
|
GeoPunkt[GeoPunkt<br/>Koordinaten]
|
|
GeoPolylinie --> GeoPunkt
|
|
end
|
|
|
|
subgraph Core[Kern-Business-Logik]
|
|
Projekt[Projekt<br/>Bauprojekt]
|
|
Parzelle[Parzelle<br/>Grundstück mit<br/>Bauparametern]
|
|
Gemeinde --> Parzelle
|
|
Projekt --> Parzelle
|
|
Projekt --> GeoPolylinie
|
|
Parzelle --> GeoPolylinie
|
|
end
|
|
|
|
subgraph Support[Unterstützende Daten]
|
|
Dokument[Dokument<br/>Dateien & URLs]
|
|
Kontext[Kontext<br/>Zusatzinfos]
|
|
end
|
|
```
|
|
|
|
### Übersichtstabelle aller Entitäten
|
|
|
|
| Objekt | Typ | Beschreibung | Hauptfelder |
|
|
|--------|-----|--------------|-------------|
|
|
| **Projekt** | Kernentität | Bauprojekt mit Status und Perimeter | id, label, statusProzess, perimeter, baulinie, parzellen |
|
|
| **Parzelle** | Hauptentität | Grundstück mit Bauparametern | id, label, plz, bauzone, AZ, BZ, perimeter, baulinie, laermschutzzone, hochwasserschutzzone, grundwasserschutzzone |
|
|
| **Dokument** | Unterstützend | Dateien und URLs mit Versionierung | id, label, dokumentTyp, quelle, mimeType, kategorienTags |
|
|
| **Kontext** | Unterstützend | Flexible Zusatzinformationen | id, thema, inhalt |
|
|
| **GeoPolylinie** | Hilfsobjekt | Geometrische Linie/Polygon | id, closed, punkte |
|
|
| **Land** | Admin | Nationale Ebene | id, label, abk |
|
|
| **Kanton** | Admin | Kantonale Ebene mit Baurecht | id, label, id_land, abk |
|
|
| **Gemeinde** | Admin | Gemeinde-Ebene mit BZO | id, label, id_kanton, plz |
|
|
| **GeoPunkt** | Hilfsobjekt | 3D-Koordinate | koordinatensystem, x, y, z, referenz |
|
|
| **GeoTag** | Enum | Geopunkt-Kategorien | K1, K2, K3, Geometer |
|
|
| **JaNein** | Enum | Drei-wertiger Status | "", "Ja", "Nein" |
|
|
| **StatusProzess** | Enum | Projektstatus | 7 Werte (Eingang, Analyse, Studie, Planung, Baurechtsverfahren, Umsetzung, Archiv) |
|
|
| **DokumentTyp** | Enum | Dokumenttyp | 6 Werte (kantonBaureglementAktuell, kantonBaureglementRevision, etc.) |
|
|
|
|
### Beispiel: Vollständige Pydantic-Modelle für Real Estate-Entitäten
|
|
|
|
Hier ist ein Beispiel, wie die Pydantic-Modelle für die Real Estate-Entitäten aussehen sollten:
|
|
|
|
```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,
|
|
)
|
|
|
|
|
|
# Beispiel-Kategorien für Dokumente (nicht abschließend):
|
|
# - "Kataster Objekte" - Amtliche Vermessung
|
|
# - "Kataster Werkeleitungen" - Leitungskataster
|
|
# - "Kataster Belastete Standorte" - Altlasten
|
|
# - "Kataster Bäume" - Baumkataster
|
|
# - "Zonenplan" - Zonenpläne
|
|
# - "Planungs- und Baugesetz (PGB)" - Kantonale Baugesetze
|
|
# - "Bau- und Zonenordnung (BZO)" - Gemeinde BZO
|
|
# - "Parkplatzverordnung" - Parkplatzregelungen
|
|
# - "Eigentümerauskunft" - Grundbuch-Auszüge Eigentümer
|
|
# - "Grundbuchauszug" - Vollständige Grundbuch-Auszüge
|
|
# - "Bauherrschaft" - Dokumente von der Bauherrschaft
|
|
# - "Planung" - Planungsdokumente
|
|
#
|
|
# Hinweis: Aktuelle Dokumente (z.B. aktuelle Baureglemente, BZO) können anhand des
|
|
# `dokumentTyp`-Attributs identifiziert werden. Die entsprechenden Dokumente finden sich
|
|
# in der `dokumente`-Liste der jeweiligen Entität (Kanton, Gemeinde).
|
|
|
|
|
|
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,
|
|
)
|
|
|
|
|
|
# Beispielthemen für Kontext (nicht abschließend):
|
|
# - "Nutzung" - Vorgaben zur Erdgeschossnutzung (Wohnen erlaubt oder Pflicht für Gewerbe)
|
|
# - "Rechte" - Dienstbarkeiten (Wegrechte, Nähebaurechte, etc.)
|
|
# - "Parkierung" - Anforderung Parkplätze (Berechnung / Reduktionsfaktoren)
|
|
# - "Ausnützung" - Ausnützungsübertragungen
|
|
# - "Umwelt" - Schadstoffbelastungen auf Parzellen
|
|
# - "Planung" - Aktive Gestaltungspläne
|
|
# - "Lärm" - Lärmempfindlichkeitsstufen
|
|
# - "Energie" - Mögliche Wärmenutzung (Wärmeverbundnetze; Fernwärme, Anergie)
|
|
# - "Natur" - Baumbestand auf privaten Grundstücken
|
|
# - "Schutz" - Isos (Ortsbild, Schutzstatus, Denkmalschutz, Weilergebiet, etc.)
|
|
# - "Gefahren" - Naturgefahren (z.B. Objektschutzmassnahmen (Hochwasser))
|
|
# - "Revision" - Verweis auf aktuell in oder zukünftig in Revision befindlichen Normen/Gesetze
|
|
#
|
|
# Verwendung: Kontext-Objekte werden als Listen in den jeweiligen Entitäten gespeichert:
|
|
# - projekt.kontextInformationen: list[Kontext]
|
|
# - parzelle.kontextInformationen: list[Kontext]
|
|
# - land.kontextInformationen: list[Kontext]
|
|
# - kanton.kontextInformationen: list[Kontext]
|
|
# - gemeinde.kontextInformationen: list[Kontext]
|
|
#
|
|
# Design-Rationale: Das Kontext-Objekt ermöglicht flexibles Hinzufügen von projektspezifischen,
|
|
# parzellen-spezifischen oder regionalen Informationen ohne Schemaänderungen.
|
|
|
|
|
|
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,
|
|
)
|
|
kontextLand: Optional[str] = Field(
|
|
None,
|
|
description="Land ID (Foreign Key)",
|
|
frontend_type="text",
|
|
frontend_readonly=False,
|
|
frontend_required=False,
|
|
)
|
|
kontextKanton: Optional[str] = Field(
|
|
None,
|
|
description="Canton ID (Foreign Key)",
|
|
frontend_type="text",
|
|
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"},
|
|
# ... more labels
|
|
},
|
|
)
|
|
|
|
# Similar registerModelLabels calls for all other models...
|
|
```
|
|
|
|
**Wichtige Hinweise zur Implementierung:**
|
|
|
|
1. **Forward References**: Für zirkuläre Referenzen (z.B. `Parzelle` → `parzellenNachbarschaft: list[Parzelle]`) verwenden Sie `ForwardRef` oder speichern Sie nur IDs als Strings.
|
|
|
|
2. **JSONB-Speicherung**: Listen von Objekten (`list[Parzelle]`, `list[Dokument]`) werden automatisch als JSONB gespeichert. Der DatabaseConnector erkennt `List`-Typen automatisch.
|
|
|
|
3. **Foreign Keys**: Einzelne Objektreferenzen wie `kontextKanton: Optional[str]` werden als String-ID gespeichert. Sie können später im Interface die vollständigen Objekte laden.
|
|
|
|
4. **MandateId**: Alle Entitäten benötigen `mandateId` für Mandaten-Isolation.
|
|
|
|
5. **Systemattribute**: `_createdAt`, `_createdBy`, `_modifiedAt`, `_modifiedBy` werden automatisch vom DatabaseConnector hinzugefügt - Sie müssen sie nicht im Modell definieren.
|
|
|
|
---
|
|
|
|
**WICHTIG:** Die obigen Real Estate-Modelle (`Projekt`, `Parzelle`, etc.) sind die **tatsächlichen Datenmodelle**, die Sie implementieren müssen. Diese werden in `modules/datamodels/datamodelRealEstate.py` erstellt.
|
|
|
|
**Keine Chat-Interface-Modelle notwendig:**
|
|
- Das Feature arbeitet **stateless** ohne Session-Management
|
|
- Alle Operationen arbeiten direkt auf den Real Estate-Modellen
|
|
- Keine `RealEstateChatSession`, `RealEstateQuery` oder `RealEstateQueryResult` notwendig
|
|
- CRUD-Operationen werden direkt ausgeführt und Ergebnisse direkt zurückgegeben
|
|
|
|
### Wichtige Punkte:
|
|
|
|
1. **UUID als ID**: Alle Modelle verwenden `uuid.uuid4()` für eindeutige IDs
|
|
2. **MandateId**: Jedes Modell benötigt `mandateId` für Mandaten-Isolation
|
|
3. **Frontend-Metadaten**: `frontend_type`, `frontend_readonly`, `frontend_required` für UI-Generierung
|
|
4. **registerModelLabels**: Registriert Labels für Mehrsprachigkeit
|
|
5. **JSONB-Felder**: `Dict[str, Any]` und `List[...]` werden automatisch als JSONB in PostgreSQL gespeichert
|
|
6. **Foreign Keys**: Administrative Hierarchie wird über Foreign Keys abgebildet:
|
|
- `Kanton.id_land` → `Land.id`
|
|
- `Gemeinde.id_kanton` → `Kanton.id`
|
|
- `Parzelle.kontextLand` → `Land.id` (Optional)
|
|
- `Parzelle.kontextKanton` → `Kanton.id` (Optional)
|
|
- `Parzelle.kontextGemeinde` → `Gemeinde.id` (Optional)
|
|
|
|
---
|
|
|
|
## Q & A - Häufige Fragen
|
|
|
|
1. **Versionierung**: Sollen Änderungen an Parzellen historisiert werden?
|
|
→ Vorerst nicht
|
|
|
|
2. **Mehrsprachigkeit**: Labels in DE/FR/IT?
|
|
→ Wird im Pydantic Model über `registerModelLabels` umgesetzt
|
|
|
|
3. **Benutzer & Rollen**: Wer darf was bearbeiten?
|
|
→ In der App über Roles und Permissions gesteuert (UAM-System)
|
|
|
|
4. **Workflow-Engine**: Für Statusübergänge und Genehmigungen?
|
|
→ In der App über Workflow-Engine gesteuert (optional, kann später integriert werden)
|
|
|
|
5. **Integration**: Anbindung an amtliche Geodaten (z.B. Swisstopo API)?
|
|
→ In der App über Integrationen gesteuert (optional)
|
|
|
|
6. **Berechnungen**: Sollen Ausnützungsberechnungen automatisiert werden?
|
|
→ In der App über Berechnungen gesteuert (optional)
|
|
|
|
---
|
|
|
|
[← Zurück zur Übersicht](README.md) | [Weiter: Interface erstellen →](03-interfaces.md)
|
|
|