kitchen/backend/models/product_definition.py
jtricerolph 8d688b459d Initial kitchen scaffold — Phase 1 kitchen port (build-verified 2026-07-11)
FastAPI backend (Python 3.11, MSSQL ODBC for SambaPOS, Azure DI OCR),
kitchen_db on central PG. React/TS/Vite frontend with navy sidebar layout.

Backend: auth.py (APP_SLUG=kitchen, SimpleNamespace — archive routes use
.kitchen_id/.is_admin without modification), main.py (51 migrations, scheduler,
internal router for KDS bookings feed), api/internal.py, full archive API
(31 routers: invoices, recipes, menus, sambapos, resos, newbook, disputes,
purchase_orders, etc.), models, migrations, OCR pipeline.
kitchen_id pinned to 1 (B1 — single hotel).

Frontend: AuthGate (app=kitchen, token shim for archive compat — B5b pending),
Layout (navy sidebar, 6 sections, Lucide icons, teal --app-primary),
App.tsx (Outlet pattern, UploadApp outside Layout), index.css (full :root block).
strict: false — archive components have type issues; build clean.

Note: 45 archive components call fetch('/api/...') without /kitchen/ prefix
(B5b). Runtime 404s; deferred until after initial testing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-12 12:15:39 +00:00

70 lines
2.8 KiB
Python

from datetime import datetime
from decimal import Decimal
from typing import Optional
from sqlalchemy import String, DateTime, ForeignKey, Numeric, Integer, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship
from database import Base
class ProductDefinition(Base):
"""
Stores persistent portion/pack size definitions for products.
When a product appears on a new invoice, these definitions are auto-applied
so staff don't need to re-enter portions_per_unit every time.
"""
__tablename__ = "product_definitions"
id: Mapped[int] = mapped_column(primary_key=True, index=True)
kitchen_id: Mapped[int] = mapped_column(
ForeignKey("kitchens.id"),
nullable=False,
index=True
)
supplier_id: Mapped[int] = mapped_column(
ForeignKey("suppliers.id"),
nullable=True, # Can be null for kitchen-wide definitions
index=True
)
# Product identification
product_code: Mapped[str] = mapped_column(String(100), nullable=True)
description_pattern: Mapped[str] = mapped_column(String(255), nullable=True) # For fuzzy matching
# Pack size info (can override OCR-parsed values)
pack_quantity: Mapped[int] = mapped_column(Integer, nullable=True)
unit_size: Mapped[Decimal] = mapped_column(Numeric(10, 3), nullable=True)
unit_size_type: Mapped[str] = mapped_column(String(10), nullable=True) # g, kg, ml, ltr, oz, cl
# Portion info (the key persistent data)
portions_per_unit: Mapped[int] = mapped_column(Integer, nullable=True)
portion_description: Mapped[str] = mapped_column(String(100), nullable=True) # e.g., "250ml glass", "50g serving"
# Saved by metadata - who saved this definition and from which invoice
saved_by_user_id: Mapped[Optional[int]] = mapped_column(
ForeignKey("users.id"),
nullable=True
)
source_invoice_id: Mapped[Optional[int]] = mapped_column(
ForeignKey("invoices.id", ondelete="SET NULL"),
nullable=True
)
# Timestamps
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
kitchen: Mapped["Kitchen"] = relationship("Kitchen")
supplier: Mapped["Supplier"] = relationship("Supplier")
saved_by_user: Mapped[Optional["User"]] = relationship("User", foreign_keys=[saved_by_user_id])
source_invoice: Mapped[Optional["Invoice"]] = relationship("Invoice", foreign_keys=[source_invoice_id])
# Unique constraint: one definition per product_code per supplier per kitchen
__table_args__ = (
UniqueConstraint('kitchen_id', 'supplier_id', 'product_code', name='uix_product_definition'),
)
from .user import Kitchen, User
from .supplier import Supplier
from .invoice import Invoice