Files
patrickandClaude Sonnet 5 f7367f6df3
CI / backend-tests (push) Successful in 1m56s
CI / frontend-build (push) Successful in 18s
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
2026-09-05 21:42:37 +02:00

87 lines
3.0 KiB
Python

"""Warehouse-Epic Kern (WH-001..004+007): Lagerort-Hierarchie, Lagerplätze,
Bestand je Lagerplatz, Materialbewegungsprotokoll. Additiv, ersetzt nicht
`standort`/`Lagerbewegung` (Objekt-Ebene bleibt unverändert).
Revision ID: 0019_lager
Revises: 0018_funkkenner
Create Date: 2026-09-05
"""
from typing import Sequence, Union
from alembic import op
revision: str = "0019_lager"
down_revision: Union[str, None] = "0018_funkkenner"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.execute(
"CREATE TYPE lagerort_typ AS ENUM ('organisation', 'standort', 'gebaeude', 'raum', 'lager')"
)
op.execute(
"""
CREATE TABLE lagerort (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
parent_id INTEGER REFERENCES lagerort(id) ON DELETE SET NULL,
typ lagerort_typ NOT NULL
)
"""
)
op.execute("CREATE INDEX idx_lagerort_parent ON lagerort (parent_id)")
op.execute(
"""
CREATE TABLE lagerplatz (
id SERIAL PRIMARY KEY,
lagerort_id INTEGER NOT NULL REFERENCES lagerort(id),
code TEXT UNIQUE NOT NULL,
bezeichnung TEXT
)
"""
)
op.execute("CREATE INDEX idx_lagerplatz_lagerort ON lagerplatz (lagerort_id)")
op.execute(
"""
CREATE TABLE bestand (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
lagerplatz_id INTEGER NOT NULL REFERENCES lagerplatz(id),
material_id INTEGER NOT NULL REFERENCES material(id),
menge NUMERIC NOT NULL DEFAULT 0,
CONSTRAINT bestand_lagerplatz_material_uq UNIQUE (lagerplatz_id, material_id)
)
"""
)
op.execute("CREATE INDEX idx_bestand_material ON bestand (material_id)")
op.execute(
"""
CREATE TABLE materialbewegung (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
material_id INTEGER NOT NULL REFERENCES material(id),
menge NUMERIC NOT NULL,
von_lagerplatz_id INTEGER REFERENCES lagerplatz(id),
nach_lagerplatz_id INTEGER REFERENCES lagerplatz(id),
benutzer_id INTEGER NOT NULL REFERENCES benutzer(id),
zeitpunkt TIMESTAMPTZ NOT NULL,
grund TEXT,
CONSTRAINT materialbewegung_von_oder_nach
CHECK (von_lagerplatz_id IS NOT NULL OR nach_lagerplatz_id IS NOT NULL)
)
"""
)
op.execute("CREATE INDEX idx_materialbewegung_material ON materialbewegung (material_id)")
op.execute("CREATE INDEX idx_materialbewegung_von ON materialbewegung (von_lagerplatz_id)")
op.execute("CREATE INDEX idx_materialbewegung_nach ON materialbewegung (nach_lagerplatz_id)")
def downgrade() -> None:
op.execute("DROP TABLE materialbewegung")
op.execute("DROP TABLE bestand")
op.execute("DROP TABLE lagerplatz")
op.execute("DROP TABLE lagerort")
op.execute("DROP TYPE lagerort_typ")