import enum import uuid from datetime import datetime from decimal import Decimal from sqlalchemy import CheckConstraint, ForeignKey, Numeric, String, UniqueConstraint from sqlalchemy.dialects.postgresql import ENUM as PgEnum, TIMESTAMP, UUID from sqlalchemy.orm import Mapped, mapped_column from app.db.base import Base class LagerortTyp(str, enum.Enum): organisation = "organisation" standort = "standort" gebaeude = "gebaeude" raum = "raum" lager = "lager" lagerort_typ_pg = PgEnum(LagerortTyp, name="lagerort_typ", create_type=False) class Lagerort(Base): """WH-001: mehrstufige Hierarchie statt flachem `standort`-Feld (Objekt/ Zustaendigkeit bleiben unverändert an `standort` hängen - Lagerort ist ein zusätzliches, feineres Konzept für das Warehouse-Epic, kein Ersatz).""" __tablename__ = "lagerort" id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column(String, nullable=False) parent_id: Mapped[int | None] = mapped_column(ForeignKey("lagerort.id", ondelete="SET NULL")) typ: Mapped[LagerortTyp] = mapped_column(lagerort_typ_pg, nullable=False) class Lagerplatz(Base): """WH-002: unterste Hierarchieebene (Regal/Fach/Box) innerhalb eines Lagerorts, mit eindeutigem Code als QR-Ziel (analog Objekt-Code, Karte 10).""" __tablename__ = "lagerplatz" id: Mapped[int] = mapped_column(primary_key=True) lagerort_id: Mapped[int] = mapped_column(ForeignKey("lagerort.id"), nullable=False) code: Mapped[str] = mapped_column(String, unique=True, nullable=False) bezeichnung: Mapped[str | None] = mapped_column(String) class Bestand(Base): """WH-003: Menge eines Materials an einem konkreten Lagerplatz - feiner als INV-003 (Standort), ersetzt es nicht (Objekte werden weiterhin über Lagerbewegung zwischen Standorten verschoben, dies hier ist reine Verbrauchsmaterial-/Stückzahl-Bestandsführung je Lagerplatz).""" __tablename__ = "bestand" __table_args__ = (UniqueConstraint("lagerplatz_id", "material_id", name="bestand_lagerplatz_material_uq"),) id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) lagerplatz_id: Mapped[int] = mapped_column(ForeignKey("lagerplatz.id"), nullable=False) material_id: Mapped[int] = mapped_column(ForeignKey("material.id"), nullable=False) menge: Mapped[Decimal] = mapped_column(Numeric, nullable=False, default=0) class Materialbewegung(Base): """WH-007: lückenloses, append-only Bewegungsprotokoll auf Material-/ Lagerplatz-Ebene (feiner als `Lagerbewegung`, die ganze Objekte zwischen Standorten verschiebt). Ein-/Auslagerung/Umlagerung erzeugen je genau einen Eintrag hier.""" __tablename__ = "materialbewegung" __table_args__ = ( CheckConstraint( "von_lagerplatz_id IS NOT NULL OR nach_lagerplatz_id IS NOT NULL", name="materialbewegung_von_oder_nach", ), ) id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) material_id: Mapped[int] = mapped_column(ForeignKey("material.id"), nullable=False) menge: Mapped[Decimal] = mapped_column(Numeric, nullable=False) von_lagerplatz_id: Mapped[int | None] = mapped_column(ForeignKey("lagerplatz.id")) nach_lagerplatz_id: Mapped[int | None] = mapped_column(ForeignKey("lagerplatz.id")) benutzer_id: Mapped[int] = mapped_column(ForeignKey("benutzer.id"), nullable=False) zeitpunkt: Mapped[datetime] = mapped_column(TIMESTAMP(timezone=True), nullable=False) grund: Mapped[str | None] = mapped_column(String)