iOS-Parität zum Web: Item-Modell + API, Einzelstueck-Liste in der Produkt- Detailansicht (anlegen, je-Stueck Lagerort/Kaufdatum/Garantie/Bezugsquelle/Notiz aendern, entfernen mit Grund, QR je Stueck via CoreImage). Anlegen-Formular hat den Schalter "Einzelstuecke". Der Scanner erkennt Einzelstueck-QR (…/i/<UID>) und oeffnet direkt das Stueck. Seed: "Kaufdatum"/"Garantie bis" nicht mehr als Modell-Felder auf Elektronik - diese Angaben gehoeren je Einzelstueck ans Item, nicht ans Modell. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
200 lines
6.6 KiB
Python
200 lines
6.6 KiB
Python
"""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, 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]] = [
|
||
("Stück", UnitKind.count, 1.0),
|
||
("Gramm", UnitKind.weight, 1.0),
|
||
("Kilogramm", UnitKind.weight, 1000.0),
|
||
("Milliliter", UnitKind.volume, 1.0),
|
||
("Liter", UnitKind.volume, 1000.0),
|
||
]
|
||
|
||
|
||
# Gebinde in Einzahl und Mehrzahl. Bewusst nur die gaengigen - eigene legt man
|
||
# unter Verwaltung > Gebinde an. Wo Einzahl und Mehrzahl gleich lauten
|
||
# ("2 Becher"), steht das hier ausdruecklich, damit es niemand fuer vergessen
|
||
# haelt und "Bechers" daraus macht.
|
||
BUILTIN_PACKAGE_TYPES: list[tuple[str, str]] = [
|
||
("Packung", "Packungen"),
|
||
("Glas", "Gläser"),
|
||
("Flasche", "Flaschen"),
|
||
("Dose", "Dosen"),
|
||
("Tüte", "Tüten"),
|
||
("Beutel", "Beutel"),
|
||
("Becher", "Becher"),
|
||
("Tube", "Tuben"),
|
||
("Karton", "Kartons"),
|
||
("Riegel", "Riegel"),
|
||
("Rolle", "Rollen"),
|
||
("Sack", "Säcke"),
|
||
("Kiste", "Kisten"),
|
||
("Bund", "Bund"),
|
||
("Stück", "Stück"),
|
||
]
|
||
|
||
|
||
def ensure_builtin_package_types(db: Session) -> None:
|
||
changed = False
|
||
for singular, plural in BUILTIN_PACKAGE_TYPES:
|
||
if not db.query(PackageType).filter(PackageType.singular == singular).first():
|
||
db.add(PackageType(singular=singular, plural=plural, is_builtin=True))
|
||
changed = True
|
||
if changed:
|
||
db.commit()
|
||
|
||
|
||
def ensure_builtin_units(db: Session) -> None:
|
||
changed = False
|
||
for name, kind, factor in BUILTIN_UNITS:
|
||
if not db.query(Unit).filter(Unit.name == name).first():
|
||
db.add(Unit(name=name, kind=kind, factor=factor, is_builtin=True))
|
||
changed = True
|
||
if changed:
|
||
db.commit()
|
||
|
||
|
||
# Startliste für den Überblick über den Vorrat. Zweistufig, damit die
|
||
# Verschachtelung von Anfang an sichtbar ist. Alles frei änderbar –
|
||
# umbenennen, löschen und eigene ergänzen ist ausdrücklich vorgesehen.
|
||
BUILTIN_CATEGORIES: list[tuple[str, list[str]]] = [
|
||
("Obst & Gemüse", []),
|
||
("Molkereiprodukte", ["Käse", "Joghurt & Quark"]),
|
||
("Fleisch & Fisch", []),
|
||
("Brot & Backwaren", []),
|
||
("Trockenwaren & Teigwaren", ["Mehl & Zucker", "Nudeln & Reis"]),
|
||
("Konserven & Gläser", []),
|
||
("Tiefkühl", []),
|
||
("Süßwaren & Snacks", ["Schokolade", "Chips & Salziges"]),
|
||
("Getränke", ["Kaffee & Tee"]),
|
||
("Gewürze & Saucen", []),
|
||
("Frühstück", []),
|
||
("Haushalt & Hygiene", []),
|
||
]
|
||
|
||
|
||
def ensure_builtin_categories(db: Session) -> None:
|
||
"""Legt nur an, was fehlt – gelöschte Kategorien kommen nicht zurück.
|
||
|
||
Anders als bei den Einheiten prüfen wir zusätzlich, ob überhaupt schon
|
||
Kategorien existieren: Wer die Startliste bewusst leergeräumt hat, soll sie
|
||
beim nächsten Start nicht wiederfinden.
|
||
"""
|
||
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, tracking=food)
|
||
db.add(parent)
|
||
db.flush()
|
||
for child in children:
|
||
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"}]},
|
||
],
|
||
},
|
||
{
|
||
# Kaufdatum/Garantie sind je Einzelstück (Item), nicht am Modell – deshalb
|
||
# hier bewusst keine solchen Felder. Modell-Felder = gemeinsame Merkmale.
|
||
"name": "Elektronik",
|
||
"fields": [],
|
||
"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()
|
||
|
||
|
||
def ensure_first_admin(db: Session) -> None:
|
||
settings = get_settings()
|
||
has_users = db.query(User).first() is not None
|
||
if has_users:
|
||
return
|
||
admin = User(
|
||
username=settings.admin_username,
|
||
password_hash=hash_password(settings.admin_password),
|
||
role=Role.admin,
|
||
)
|
||
db.add(admin)
|
||
db.commit()
|