Lagerort-Hierarchie (organisation/standort/gebaeude/raum/lager, self-referenzierend), Lagerplätze mit eindeutigem Code, Bestand je Lagerplatz+Material, Ein-/Auslagerung mit lückenlosem Materialbewegungsprotokoll (append-only, analog Lagerbewegung auf Objekt-Ebene, hier aber Material-/Lagerplatz-fein). - Migration 0019: 4 neue Tabellen (lagerort, lagerplatz, bestand, materialbewegung) - Backend: CRUD Lagerort/Lagerplatz, Bestandsabfrage, Ein-/Auslagern-Endpunkte, Negativbestand vorab abgefangen (kein DB-Constraint-Exception-Pfad, gleiches Muster wie objekte.py-Fix vom 2026-09-05) - Frontend: neuer Admin-Tab "Lager" (Lagerort/-platz-Verwaltung, Bestand+Buchung je Lagerplatz) - 2 neue Tests, 139 Tests weiterhin grün WH-005 (Umlagerung) und WH-006 (Inventur) bewusst zurückgestellt - Kern zuerst, laut Nutzer-Entscheidung. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV
86 lines
3.6 KiB
Python
86 lines
3.6 KiB
Python
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)
|