feat(lager): Warehouse-Epic Kern (WH-001..004+007)
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
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user