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)