Schritt 1: Fundament der Lebensmittel-Lagerverwaltung (Pantry)
Backend (FastAPI + PostgreSQL): Chargen mit MHD, FEFO-Auslagern, Einheiten-Umrechnung (Stueck/g/ml + Packungen), Open-Food-Facts-Lookup mit lokalem Fallback, JWT-Auth mit Rollen (Admin/Nutzer), erster Admin beim Setup, Einkaufsliste, Ablaufwarnung, Lagerorte, pytest fuer FEFO. Web-UI (React/Vite): Login, Dashboard, Ein-/Auslagern, Produkte, Lagerorte, Benutzerverwaltung, Einkaufsliste - rollenabhaengig. Deploy: docker-compose + install.sh (Docker-Autoinstall, Secrets), README und Roadmap fuer Schritt 2 (iOS) und Schritt 3. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
64
backend/app/services/units.py
Normal file
64
backend/app/services/units.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""Einheiten-Umrechnung.
|
||||
|
||||
Der Bestand einer Charge (Lot) wird immer in der Basiseinheit des Produkts
|
||||
gespeichert (Stück, Gramm oder Milliliter). Nutzer geben Mengen entweder in der
|
||||
Basiseinheit oder in "Packungen" (package) an. Eine Packung entspricht
|
||||
``product.package_size`` Basiseinheiten.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ..models import BaseUnit, Product
|
||||
|
||||
# Aliase, die als "Basiseinheit" akzeptiert werden.
|
||||
_BASE_ALIASES: dict[str, BaseUnit] = {
|
||||
"piece": BaseUnit.piece,
|
||||
"pieces": BaseUnit.piece,
|
||||
"stück": BaseUnit.piece,
|
||||
"stueck": BaseUnit.piece,
|
||||
"st": BaseUnit.piece,
|
||||
"g": BaseUnit.gram,
|
||||
"gram": BaseUnit.gram,
|
||||
"gramm": BaseUnit.gram,
|
||||
"ml": BaseUnit.milliliter,
|
||||
"milliliter": BaseUnit.milliliter,
|
||||
}
|
||||
|
||||
PACKAGE_UNITS = {"package", "packung", "pkg", "pack"}
|
||||
|
||||
|
||||
class UnitError(ValueError):
|
||||
"""Wird geworfen, wenn eine Einheit nicht zum Produkt passt."""
|
||||
|
||||
|
||||
def to_base_quantity(product: Product, quantity: float, unit: str) -> float:
|
||||
"""Rechnet eine vom Nutzer angegebene Menge in Basiseinheiten um."""
|
||||
if quantity <= 0:
|
||||
raise UnitError("Menge muss größer als 0 sein")
|
||||
|
||||
unit_norm = unit.strip().lower()
|
||||
|
||||
if unit_norm in PACKAGE_UNITS:
|
||||
if not product.package_size or product.package_size <= 0:
|
||||
raise UnitError(
|
||||
"Für dieses Produkt ist keine Packungsgröße hinterlegt, "
|
||||
"Packungen können nicht umgerechnet werden"
|
||||
)
|
||||
return quantity * product.package_size
|
||||
|
||||
mapped = _BASE_ALIASES.get(unit_norm)
|
||||
if mapped is None:
|
||||
raise UnitError(f"Unbekannte Einheit: {unit}")
|
||||
if mapped != product.base_unit:
|
||||
raise UnitError(
|
||||
f"Einheit '{unit}' passt nicht zur Basiseinheit "
|
||||
f"'{product.base_unit.value}' des Produkts"
|
||||
)
|
||||
return float(quantity)
|
||||
|
||||
|
||||
def base_to_packages(product: Product, quantity_base: float) -> float | None:
|
||||
"""Rechnet Basiseinheiten in (ggf. gebrochene) Packungen um, falls möglich."""
|
||||
if not product.package_size or product.package_size <= 0:
|
||||
return None
|
||||
return quantity_base / product.package_size
|
||||
Reference in New Issue
Block a user