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
87 lines
3.0 KiB
Python
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")
|