Gegenstands-Verwaltung (Non-Food) neben Lebensmitteln

Die Kategorie bestimmt die Verwaltungsart (food/object). Gegenstaende:
Menge je Lagerort statt Chargen/MHD, Umlagern (ohne Grund) und Entfernen
mit Pflicht-Grund samt Entnahme-Statistik. Beliebig viele eigene Felder
je Kategorie (vererbt an Unterkategorien), "gekauft bei" ueber eine
verwaltbare Shop-Liste und ein Produktlink. Barcode-Lookup zusaetzlich
ueber Open Products Facts.

Der Lebensmittel-Teil (Chargen/MHD/FEFO) bleibt unveraendert. Umgesetzt in
Backend (FastAPI, +Tests gruen), Web (React) und iOS (SwiftUI).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-25 15:33:56 +02:00
parent 580afcc133
commit 5b952524d7
29 changed files with 3116 additions and 115 deletions

View File

@@ -1,10 +1,13 @@
"""Startup-Seeds: erster Admin-Benutzer und die eingebauten Einheiten."""
import json
from sqlalchemy.orm import Session
from .config import get_settings
from .models import Category, PackageType, Role, Unit, UnitKind, User
from .models import Category, CategoryTracking, FieldDefinition, PackageType, Role, Unit, UnitKind, User
from .security import hash_password
from .services.fields import slugify
# (Name, Art, Faktor zur kanonischen Basiseinheit)
BUILTIN_UNITS: list[tuple[str, UnitKind, float]] = [
@@ -87,12 +90,99 @@ def ensure_builtin_categories(db: Session) -> None:
"""
if db.query(Category).first() is not None:
return
food = CategoryTracking.food.value
for name, children in BUILTIN_CATEGORIES:
parent = Category(name=name, is_builtin=True)
parent = Category(name=name, is_builtin=True, tracking=food)
db.add(parent)
db.flush()
for child in children:
db.add(Category(name=child, parent_id=parent.id, is_builtin=True))
db.add(Category(name=child, parent_id=parent.id, is_builtin=True, tracking=food))
db.commit()
# Beispiel-Kategorien für Gegenstände (Non-Food) mit ein paar sinnvollen Feldern.
# Frei änderbar; wer sie löscht, bekommt sie nicht zurück (siehe Guard unten).
OBJECT_EXAMPLES: list[dict] = [
{
"name": "Kleidung",
"fields": [
{"label": "Größe", "type": "select",
"options": ["XS", "S", "M", "L", "XL", "XXL"]},
{"label": "Farbe", "type": "text"},
],
"children": [
{"name": "Schuhe",
"fields": [{"label": "Schuhgröße", "type": "number", "unit": "EU"}]},
],
},
{
"name": "Elektronik",
"fields": [
{"label": "Kaufdatum", "type": "date"},
{"label": "Garantie bis", "type": "date"},
],
"children": [
{"name": "Powerbank",
"fields": [{"label": "Kapazität", "type": "number", "unit": "mAh"}]},
{"name": "Kabel", "fields": [
{"label": "Länge", "type": "number", "unit": "cm"},
{"label": "Anschluss", "type": "text"},
]},
],
},
{"name": "Werkzeug", "fields": []},
{"name": "Bücher & Medien", "fields": []},
]
def _add_fields(db: Session, category_id: int, felder: list[dict]) -> None:
for pos, f in enumerate(felder):
optionen = f.get("options")
db.add(
FieldDefinition(
category_id=category_id,
label=f["label"],
key=slugify(f["label"]),
field_type=f.get("type", "text"),
unit=f.get("unit"),
options=json.dumps(optionen) if optionen else None,
required=bool(f.get("required")),
position=pos,
is_builtin=True,
)
)
def ensure_example_object_categories(db: Session) -> None:
"""Legt Beispiel-Gegenstandskategorien mit Feldern an nur, wenn es noch keine gibt.
So sieht man das Gegenstands-Feature gleich in Aktion; wer die Beispiele
bewusst entfernt, bekommt sie beim nächsten Start nicht zurück.
"""
if (
db.query(Category)
.filter(Category.tracking == CategoryTracking.object.value)
.first()
is not None
):
return
for top in OBJECT_EXAMPLES:
parent = Category(
name=top["name"], tracking=CategoryTracking.object.value, is_builtin=True
)
db.add(parent)
db.flush()
_add_fields(db, parent.id, top.get("fields", []))
for child in top.get("children", []):
node = Category(
name=child["name"],
parent_id=parent.id,
tracking=CategoryTracking.object.value,
is_builtin=True,
)
db.add(node)
db.flush()
_add_fields(db, node.id, child.get("fields", []))
db.commit()