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:
147
backend/app/models.py
Normal file
147
backend/app/models.py
Normal file
@@ -0,0 +1,147 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
from datetime import date, datetime, timezone
|
||||
|
||||
from sqlalchemy import (
|
||||
Date,
|
||||
DateTime,
|
||||
Enum,
|
||||
Float,
|
||||
ForeignKey,
|
||||
Integer,
|
||||
String,
|
||||
Text,
|
||||
UniqueConstraint,
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from .database import Base
|
||||
|
||||
|
||||
def _now() -> datetime:
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
class Role(str, enum.Enum):
|
||||
admin = "admin"
|
||||
user = "user"
|
||||
|
||||
|
||||
class BaseUnit(str, enum.Enum):
|
||||
piece = "piece"
|
||||
gram = "gram"
|
||||
milliliter = "milliliter"
|
||||
|
||||
|
||||
class MovementType(str, enum.Enum):
|
||||
in_ = "in"
|
||||
out = "out"
|
||||
adjust = "adjust"
|
||||
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
username: Mapped[str] = mapped_column(String(64), unique=True, nullable=False)
|
||||
password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
role: Mapped[Role] = mapped_column(Enum(Role), nullable=False, default=Role.user)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now)
|
||||
|
||||
|
||||
class Group(Base):
|
||||
__tablename__ = "groups"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(120), unique=True, nullable=False)
|
||||
# Group-level minimum stock is a Schritt-3 feature; column kept for forward-compat.
|
||||
min_stock: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
|
||||
products: Mapped[list[Product]] = relationship(back_populates="group")
|
||||
|
||||
|
||||
class Location(Base):
|
||||
__tablename__ = "locations"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(120), nullable=False)
|
||||
# Sub-locations (Regal/Fach) are a Schritt-3 feature; parent_id kept for forward-compat.
|
||||
parent_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("locations.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
|
||||
|
||||
class Product(Base):
|
||||
__tablename__ = "products"
|
||||
__table_args__ = (UniqueConstraint("barcode", name="uq_products_barcode"),)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
barcode: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
brand: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||
image_url: Mapped[str | None] = mapped_column(String(1024), nullable=True)
|
||||
|
||||
base_unit: Mapped[BaseUnit] = mapped_column(
|
||||
Enum(BaseUnit), nullable=False, default=BaseUnit.piece
|
||||
)
|
||||
# Number of base units contained in one package (e.g. 500 g per package).
|
||||
package_size: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
|
||||
group_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("groups.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
min_stock: Mapped[float | None] = mapped_column(Float, nullable=True) # in base units
|
||||
|
||||
source: Mapped[str] = mapped_column(String(16), nullable=False, default="manual")
|
||||
off_raw: Mapped[str | None] = mapped_column(Text, nullable=True) # JSON blob from OFF
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now)
|
||||
|
||||
group: Mapped[Group | None] = relationship(back_populates="products")
|
||||
lots: Mapped[list[Lot]] = relationship(
|
||||
back_populates="product", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
|
||||
class Lot(Base):
|
||||
__tablename__ = "lots"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
product_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("products.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
quantity: Mapped[float] = mapped_column(Float, nullable=False) # in base units
|
||||
best_before: Mapped[date | None] = mapped_column(Date, nullable=True)
|
||||
location_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("locations.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now)
|
||||
|
||||
product: Mapped[Product] = relationship(back_populates="lots")
|
||||
|
||||
|
||||
class Movement(Base):
|
||||
__tablename__ = "movements"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
product_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("products.id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
lot_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("lots.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
user_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("users.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
type: Mapped[MovementType] = mapped_column(Enum(MovementType), nullable=False)
|
||||
quantity: Mapped[float] = mapped_column(Float, nullable=False) # in base units
|
||||
unit_used: Mapped[str] = mapped_column(String(32), nullable=False) # what user entered
|
||||
note: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now)
|
||||
|
||||
|
||||
class Setting(Base):
|
||||
__tablename__ = "settings"
|
||||
|
||||
key: Mapped[str] = mapped_column(String(64), primary_key=True)
|
||||
value: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
Reference in New Issue
Block a user