from __future__ import annotations from datetime import date, datetime from pydantic import BaseModel, ConfigDict, Field from .models import BaseUnit, DatePrecision, Role, UnitKind # ---- Units ---- class UnitOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: int name: str kind: UnitKind factor: float is_builtin: bool class UnitCreate(BaseModel): name: str = Field(min_length=1, max_length=64) kind: UnitKind factor: float = Field(gt=0) # ---- API-Tokens (externe Zugriffe, z.B. Home Assistant) ---- class ApiTokenOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: int name: str created_at: datetime last_used_at: datetime | None = None class ApiTokenCreate(BaseModel): name: str = Field(min_length=1, max_length=120) class ApiTokenCreated(ApiTokenOut): token: str # nur einmalig beim Anlegen # ---- Barcodes ---- class BarcodeOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: int code: str note: str | None = None # Nur bei Gruppen-Codes gefüllt: der Artikel, über den der Code hier steht. product_name: str | None = None class ProductBarcodeOut(BaseModel): """Code, der über einen Artikel dieser Kategorie angehört. Rein informativ: Er steht am Artikel, nicht an der Kategorie. Beim Scannen findet die Suche immer zuerst den Artikel (siehe routers/products.lookup), weshalb ein gleichlautender Kategorie-Eintrag nie greifen könnte. """ code: str product_id: int product_name: str # ---- Auth / Users ---- class Token(BaseModel): access_token: str token_type: str = "bearer" role: Role username: str class UserOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: int username: str role: Role created_at: datetime class UserCreate(BaseModel): username: str = Field(min_length=1, max_length=64) password: str = Field(min_length=4, max_length=255) role: Role = Role.user class UserUpdate(BaseModel): password: str | None = Field(default=None, min_length=4, max_length=255) role: Role | None = None # ---- Groups ---- class GroupOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: int name: str min_stock: float | None = None min_stock_unit_id: int | None = None # angereichert: product_count: int = 0 stock: float = 0.0 # Bestand in Basiseinheiten min_stock_unit_name: str | None = None kind: str | None = None # Art der Mindestbestand-Einheit barcodes: list[BarcodeOut] = [] # EANs, die dieser Gruppe zugeordnet sind # EANs der Artikel in dieser Gruppe - nur zur Anzeige, nicht bearbeitbar. product_barcodes: list[ProductBarcodeOut] = [] class GroupCreate(BaseModel): name: str = Field(min_length=1, max_length=120) min_stock: float | None = Field(default=None, ge=0) min_stock_unit_id: int | None = None class GroupUpdate(BaseModel): name: str | None = Field(default=None, min_length=1, max_length=120) min_stock: float | None = Field(default=None, ge=0) min_stock_unit_id: int | None = None # ---- Categories ---- class CategoryOut(BaseModel): """Reine Ordnungshilfe – kein Bestand, kein Mindestbestand, keine EAN-Codes.""" model_config = ConfigDict(from_attributes=True) id: int name: str parent_id: int | None = None is_builtin: bool = False product_count: int = 0 class CategoryCreate(BaseModel): name: str = Field(min_length=1, max_length=120) parent_id: int | None = None class CategoryUpdate(BaseModel): name: str | None = Field(default=None, min_length=1, max_length=120) parent_id: int | None = None # ---- Locations ---- class LocationOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: int name: str parent_id: int | None = None class LocationCreate(BaseModel): name: str = Field(min_length=1, max_length=120) parent_id: int | None = None # ---- Products ---- class ProductBase(BaseModel): barcode: str | None = None name: str = Field(min_length=1, max_length=255) brand: str | None = None image_url: str | None = None base_unit: BaseUnit = BaseUnit.piece # Optionale verwaltete Einheit; setzt base_unit anhand ihrer Art und die Anzeigeeinheit. unit_id: int | None = None package_size: float | None = Field(default=None, gt=0) # Bezeichnung eines Gebindes ("Packung", "Glas", "Tüte", …) package_label: str | None = Field(default=None, max_length=32) # Voreingestellte MHD-Genauigkeit dieses Produkts (z.B. Konserven: nur Monat). date_precision: DatePrecision = DatePrecision.day group_id: int | None = None # Kategorie: nur fuer den Ueberblick, unabhaengig von der Gruppe. category_id: int | None = None min_stock: float | None = Field(default=None, ge=0) # immer in Basiseinheiten # Nur für die Anzeige: in welcher Einheit der Mindestbestand erfasst wurde. min_stock_unit_id: int | None = None min_stock_in_packages: bool = False class ProductCreate(ProductBase): pass class ProductUpdate(BaseModel): barcode: str | None = None name: str | None = Field(default=None, min_length=1, max_length=255) brand: str | None = None image_url: str | None = None base_unit: BaseUnit | None = None unit_id: int | None = None package_size: float | None = Field(default=None, gt=0) package_label: str | None = Field(default=None, max_length=32) date_precision: DatePrecision | None = None group_id: int | None = None category_id: int | None = None min_stock: float | None = Field(default=None, ge=0) min_stock_unit_id: int | None = None min_stock_in_packages: bool | None = None class ProductOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: int barcode: str | None name: str brand: str | None image_url: str | None base_unit: BaseUnit display_unit_id: int | None = None package_size: float | None package_label: str | None = None date_precision: DatePrecision = DatePrecision.day group_id: int | None category_id: int | None = None category_name: str | None = None min_stock: float | None min_stock_unit_id: int | None = None min_stock_in_packages: bool = False source: str created_at: datetime # angereichert: stock: float = 0.0 expired_count: int = 0 kind: str = "" unit_name: str = "" unit_factor: float = 1.0 # Mindestbestand in der erfassten Einheit (für die Anzeige): min_stock_display: float | None = None min_stock_unit_label: str = "" # Zusätzliche EAN-Codes (neben dem Haupt-Barcode): barcodes: list[BarcodeOut] = [] class LookupResult(BaseModel): found: bool existing_product: ProductOut | None = None suggestion: dict | None = None # Ist der Code einer Gruppe zugeordnet (z.B. "Mehl"), wird sie hier gemeldet. group_id: int | None = None group_name: str | None = None # Aus der Open-Food-Facts-Kategorie abgeleiteter Vorschlag (nur Vorschlag). category_id: int | None = None category_name: str | None = None class BarcodeCreate(BaseModel): code: str = Field(min_length=4, max_length=64) note: str | None = Field(default=None, max_length=120) class BarcodeNoteUpdate(BaseModel): """Notiz zu einem Code nachtragen oder ändern.""" note: str | None = Field(default=None, max_length=120) # ---- Stock movements ---- class CheckInRequest(BaseModel): product_id: int | None = None barcode: str | None = None quantity: float = Field(gt=0) unit: str best_before: date | None = None # "month" legt das MHD auf den Monatsletzten (siehe services/dates.py). best_before_precision: DatePrecision = DatePrecision.day location_id: int | None = None note: str | None = None class CheckOutRequest(BaseModel): product_id: int | None = None barcode: str | None = None quantity: float = Field(gt=0) unit: str # Optional: gezielt aus dieser Charge abbuchen (sonst automatisch FEFO). lot_id: int | None = None note: str | None = None class LotOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: int product_id: int quantity: float best_before: date | None best_before_precision: DatePrecision = DatePrecision.day location_id: int | None created_at: datetime class LotUpdate(BaseModel): """Korrektur einer Charge (Vertipper beim Einlagern o.ä.).""" quantity: float | None = Field(default=None, gt=0) best_before: date | None = None best_before_precision: DatePrecision | None = None location_id: int | None = None class CheckInResponse(BaseModel): lot: LotOut product_stock: float class CheckInLine(BaseModel): """Eine Charge innerhalb eines Sammel-Einlagerns (Menge + eigenes MHD).""" quantity: float = Field(gt=0) best_before: date | None = None best_before_precision: DatePrecision = DatePrecision.day location_id: int | None = None class BatchCheckInRequest(BaseModel): product_id: int | None = None barcode: str | None = None unit: str lines: list[CheckInLine] = Field(min_length=1) note: str | None = None class BatchCheckInResponse(BaseModel): lots: list[LotOut] product_stock: float class CheckOutResponse(BaseModel): affected_lots: list[dict] product_stock: float # ---- Views ---- class ShoppingItem(BaseModel): product_id: int name: str base_unit: BaseUnit package_size: float | None = None stock: float min_stock: float deficit: float class ExpiringItem(BaseModel): lot_id: int product_id: int product_name: str quantity: float # in Basiseinheiten base_unit: BaseUnit best_before: date best_before_precision: DatePrecision = DatePrecision.day days_left: int # Für die Anzeige in Artikeleinheiten: package_size: float | None = None package_label: str | None = None unit_name: str = "" unit_factor: float = 1.0 class GroupShoppingItem(BaseModel): group_id: int name: str stock: float min_stock: float deficit: float unit_name: str = "" product_count: int class MovementOut(BaseModel): id: int product_id: int product_name: str type: str quantity: float # in Basiseinheiten base_unit: BaseUnit unit_used: str username: str | None note: str | None created_at: datetime # Für die Anzeige in Artikeleinheiten: package_size: float | None = None package_label: str | None = None unit_name: str = "" unit_factor: float = 1.0 class SettingOut(BaseModel): key: str value: str