Export:
- GET /export/stock.csv - eine Zeile je Charge, Semikolon-getrennt mit BOM,
direkt in Excel/LibreOffice bearbeitbar; Mengen in der Artikeleinheit.
- GET /export/backup.json - vollstaendiges Backup (Einheiten, Gruppen,
Lagerorte, Produkte, Chargen), Referenzen ueber Namen statt IDs.
Import:
- POST /import/stock (Admin, Datei-Upload). Erkennt CSV oder JSON automatisch.
Arbeitet ausschliesslich additiv: unbekannte Produkte/Gruppen/Lagerorte/
Einheiten werden angelegt, Chargen ergaenzt - nichts wird geloescht.
Mengen koennen in Gebinde ("Glas") oder Einheiten ("Gramm") angegeben werden,
Datum als YYYY-MM-DD oder TT.MM.JJJJ. Fehlerhafte Zeilen werden uebersprungen
und im Ergebnis gemeldet; jeder Import wird als Bewegung protokolliert.
Frontend: neue Seite "Import / Export" (Verwaltung) mit Download-Buttons,
Datei-Upload, Ergebniszusammenfassung und einer Beschreibung der CSV-Spalten.
api.js kann jetzt Datei-Downloads mit Auth-Header und FormData-Uploads.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
95 lines
2.8 KiB
Python
95 lines
2.8 KiB
Python
from contextlib import asynccontextmanager
|
||
|
||
from fastapi import FastAPI
|
||
from fastapi.middleware.cors import CORSMiddleware
|
||
from sqlalchemy import text
|
||
|
||
from .config import get_settings
|
||
from .database import Base, SessionLocal, engine
|
||
from .routers import (
|
||
auth,
|
||
groups,
|
||
locations,
|
||
products,
|
||
settings as settings_router,
|
||
stock,
|
||
transfer,
|
||
units,
|
||
users,
|
||
views,
|
||
)
|
||
from .seed import ensure_builtin_units, ensure_first_admin
|
||
|
||
settings = get_settings()
|
||
|
||
|
||
def _ensure_schema() -> None:
|
||
"""Schonende Migration: fehlende Spalten auf bestehenden Tabellen nachziehen.
|
||
|
||
create_all() legt nur fehlende Tabellen an, ändert aber keine bestehenden.
|
||
Auf Postgres holen wir neue nullable-Spalten per ADD COLUMN IF NOT EXISTS nach,
|
||
damit vorhandene Installationen ihre Daten behalten. Auf SQLite (Tests) sind
|
||
die Spalten bereits durch create_all vorhanden.
|
||
"""
|
||
if engine.dialect.name != "postgresql":
|
||
return
|
||
stmts = [
|
||
"ALTER TABLE products ADD COLUMN IF NOT EXISTS display_unit_id INTEGER "
|
||
"REFERENCES units(id) ON DELETE SET NULL",
|
||
"ALTER TABLE groups ADD COLUMN IF NOT EXISTS min_stock_unit_id INTEGER "
|
||
"REFERENCES units(id) ON DELETE SET NULL",
|
||
"ALTER TABLE products ADD COLUMN IF NOT EXISTS min_stock_unit_id INTEGER "
|
||
"REFERENCES units(id) ON DELETE SET NULL",
|
||
"ALTER TABLE products ADD COLUMN IF NOT EXISTS min_stock_in_packages BOOLEAN "
|
||
"NOT NULL DEFAULT FALSE",
|
||
"ALTER TABLE products ADD COLUMN IF NOT EXISTS package_label VARCHAR(32)",
|
||
]
|
||
with engine.begin() as conn:
|
||
for stmt in stmts:
|
||
conn.execute(text(stmt))
|
||
|
||
|
||
@asynccontextmanager
|
||
async def lifespan(app: FastAPI):
|
||
# Tabellen anlegen (MVP: create_all statt Alembic-Migrationen).
|
||
Base.metadata.create_all(bind=engine)
|
||
_ensure_schema()
|
||
db = SessionLocal()
|
||
try:
|
||
ensure_builtin_units(db)
|
||
ensure_first_admin(db)
|
||
finally:
|
||
db.close()
|
||
yield
|
||
|
||
|
||
app = FastAPI(title="Project-Good – Lebensmittel-Lagerverwaltung", version="1.0.0", lifespan=lifespan)
|
||
|
||
origins = ["*"] if settings.cors_origins.strip() == "*" else [
|
||
o.strip() for o in settings.cors_origins.split(",") if o.strip()
|
||
]
|
||
app.add_middleware(
|
||
CORSMiddleware,
|
||
allow_origins=origins,
|
||
allow_credentials=True,
|
||
allow_methods=["*"],
|
||
allow_headers=["*"],
|
||
)
|
||
|
||
|
||
@app.get("/health", tags=["meta"])
|
||
def health() -> dict:
|
||
return {"status": "ok"}
|
||
|
||
|
||
app.include_router(auth.router)
|
||
app.include_router(users.router)
|
||
app.include_router(products.router)
|
||
app.include_router(stock.router)
|
||
app.include_router(locations.router)
|
||
app.include_router(groups.router)
|
||
app.include_router(units.router)
|
||
app.include_router(views.router)
|
||
app.include_router(transfer.router)
|
||
app.include_router(settings_router.router)
|