Adds seven digital-menu-specific columns to the products table so
managers can control the public Xenia Connect menu independently of POS
data:
- digital_visible (INTEGER, default 1) — hide product from menu
- digital_available (INTEGER, default 1) — show as out-of-stock
- digital_name (VARCHAR, nullable) — override display name
- digital_description (VARCHAR, nullable)
- digital_price (REAL, nullable) — override price
- digital_discount (REAL, default 0.0) — percentage discount
- digital_image_url (VARCHAR, nullable) — menu-specific image
Changes:
- main.py: 7 additive ALTER TABLE migrations (safe on existing DBs)
- models/product.py: matching SQLAlchemy columns on Product
- schemas/product.py: ProductBase carries defaults (visible/available
default True); ProductUpdate has all fields Optional; ProductOut
inherits everything via ProductBase
No router changes needed — PUT /api/products/{id} picks up the new
fields from ProductUpdate automatically.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
133 lines
6.3 KiB
Python
133 lines
6.3 KiB
Python
from sqlalchemy import Column, Integer, String, Boolean, Float, ForeignKey, Text
|
|
from sqlalchemy.orm import relationship
|
|
from database import Base
|
|
|
|
|
|
class Category(Base):
|
|
__tablename__ = "categories"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
name = Column(String, nullable=False)
|
|
color = Column(String, nullable=True)
|
|
sort_order = Column(Integer, default=0)
|
|
# self-referential: null = top-level, non-null = sub-category of parent
|
|
parent_id = Column(Integer, ForeignKey("categories.id"), nullable=True)
|
|
# position of the "General" group (direct products) among sub-categories
|
|
general_sort_order = Column(Integer, default=0, nullable=False)
|
|
# sub-categories only: if True, the accordion section is expanded by default on the PWA
|
|
auto_expanded = Column(Boolean, default=False, nullable=False)
|
|
|
|
products = relationship("Product", back_populates="category")
|
|
subcategories = relationship("Category", back_populates="parent", foreign_keys="Category.parent_id")
|
|
parent = relationship("Category", back_populates="subcategories", remote_side="Category.id", foreign_keys="Category.parent_id")
|
|
|
|
|
|
class Product(Base):
|
|
__tablename__ = "products"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
name = Column(String, nullable=False)
|
|
category_id = Column(Integer, ForeignKey("categories.id"), nullable=True)
|
|
base_price = Column(Float, nullable=False)
|
|
is_available = Column(Boolean, default=True, nullable=False)
|
|
# "active" | "archived" — archived products are kept for order history but hidden from active use
|
|
lifecycle_status = Column(String, default="active", nullable=False)
|
|
printer_zone_id = Column(Integer, ForeignKey("printers.id"), nullable=True)
|
|
image_url = Column(String, nullable=True)
|
|
sort_order = Column(Integer, default=0, nullable=False)
|
|
|
|
# Xenia Connect — digital menu overrides
|
|
digital_visible = Column(Integer, default=1, nullable=False)
|
|
digital_available = Column(Integer, default=1, nullable=False)
|
|
digital_name = Column(String, nullable=True)
|
|
digital_description = Column(String, nullable=True)
|
|
digital_price = Column(Float, nullable=True)
|
|
digital_discount = Column(Float, default=0.0, nullable=False)
|
|
digital_image_url = Column(String, nullable=True)
|
|
|
|
category = relationship("Category", back_populates="products")
|
|
printer_zone = relationship("Printer", back_populates="products")
|
|
quick_options = relationship("ProductQuickOption", back_populates="product", cascade="all, delete-orphan")
|
|
options = relationship("ProductOption", back_populates="product", cascade="all, delete-orphan")
|
|
ingredients = relationship("ProductIngredient", back_populates="product", cascade="all, delete-orphan")
|
|
preference_sets = relationship("ProductPreferenceSet", back_populates="product", cascade="all, delete-orphan")
|
|
order_items = relationship("OrderItem", back_populates="product")
|
|
|
|
|
|
class ProductOption(Base):
|
|
__tablename__ = "product_options"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
product_id = Column(Integer, ForeignKey("products.id"), nullable=False)
|
|
name = Column(String, nullable=False)
|
|
extra_cost = Column(Float, default=0.0)
|
|
allow_multiple = Column(Boolean, default=False, nullable=False)
|
|
# JSON array [{name, extra_cost, is_default}] — sub-options shown when this option is checked
|
|
sub_choices = Column(Text, nullable=True)
|
|
is_favorite = Column(Boolean, default=False, nullable=False)
|
|
favorite_sort_order = Column(Integer, default=0, nullable=False)
|
|
|
|
product = relationship("Product", back_populates="options")
|
|
|
|
|
|
class ProductQuickOption(Base):
|
|
__tablename__ = "product_quick_options"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
product_id = Column(Integer, ForeignKey("products.id"), nullable=False)
|
|
name = Column(String, nullable=False)
|
|
price = Column(Float, default=0.0, nullable=False)
|
|
allow_multiple = Column(Boolean, default=False, nullable=False)
|
|
sort_order = Column(Integer, default=0, nullable=False)
|
|
is_favorite = Column(Boolean, default=False, nullable=False)
|
|
favorite_sort_order = Column(Integer, default=0, nullable=False)
|
|
is_compact = Column(Boolean, default=False, nullable=False)
|
|
|
|
product = relationship("Product", back_populates="quick_options")
|
|
|
|
|
|
class ProductIngredient(Base):
|
|
__tablename__ = "product_ingredients"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
product_id = Column(Integer, ForeignKey("products.id"), nullable=False)
|
|
name = Column(String, nullable=False)
|
|
extra_cost = Column(Float, default=0.0)
|
|
is_favorite = Column(Boolean, default=False, nullable=False)
|
|
favorite_sort_order = Column(Integer, default=0, nullable=False)
|
|
|
|
product = relationship("Product", back_populates="ingredients")
|
|
|
|
|
|
class ProductPreferenceSet(Base):
|
|
__tablename__ = "product_preference_sets"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
product_id = Column(Integer, ForeignKey("products.id"), nullable=False)
|
|
name = Column(String, nullable=False)
|
|
default_choice_id = Column(Integer, nullable=True)
|
|
# JSON: {name, default_choice_index, choices:[{name,extra_cost,is_default}]}
|
|
# Shared sub-set shown for all choices that don't have disables_subset=True
|
|
shared_subset = Column(Text, nullable=True)
|
|
is_favorite = Column(Boolean, default=False, nullable=False)
|
|
favorite_sort_order = Column(Integer, default=0, nullable=False)
|
|
|
|
product = relationship("Product", back_populates="preference_sets")
|
|
choices = relationship("ProductPreferenceChoice", back_populates="set", cascade="all, delete-orphan")
|
|
|
|
|
|
class ProductPreferenceChoice(Base):
|
|
__tablename__ = "product_preference_choices"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
set_id = Column(Integer, ForeignKey("product_preference_sets.id"), nullable=False)
|
|
name = Column(String, nullable=False)
|
|
extra_cost = Column(Float, default=0.0)
|
|
# JSON array of sub-choice objects: [{name, extra_cost, is_default}]
|
|
# Per-choice inline sub-preference shown only when this choice is selected.
|
|
sub_choices = Column(Text, nullable=True)
|
|
# When True this choice hides the set-level shared_subset on the PWA.
|
|
disables_subset = Column(Boolean, default=False, nullable=False)
|
|
|
|
set = relationship("ProductPreferenceSet", back_populates="choices")
|