299 lines
8.8 KiB
Python
299 lines
8.8 KiB
Python
import json
|
|
from pydantic import BaseModel, model_validator, field_validator
|
|
from typing import Optional, List, Any
|
|
|
|
|
|
class CategoryCreate(BaseModel):
|
|
name: str
|
|
color: Optional[str] = None
|
|
sort_order: int = 0
|
|
parent_id: Optional[int] = None
|
|
general_sort_order: int = 0
|
|
auto_expanded: bool = False
|
|
|
|
|
|
class CategoryUpdate(BaseModel):
|
|
name: Optional[str] = None
|
|
color: Optional[str] = None
|
|
sort_order: Optional[int] = None
|
|
parent_id: Optional[int] = None
|
|
general_sort_order: Optional[int] = None
|
|
auto_expanded: Optional[bool] = None
|
|
|
|
|
|
class CategoryOut(BaseModel):
|
|
id: int
|
|
name: str
|
|
color: Optional[str] = None
|
|
sort_order: int = 0
|
|
parent_id: Optional[int] = None
|
|
general_sort_order: int = 0
|
|
auto_expanded: bool = False
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class CategoryReorderItem(BaseModel):
|
|
id: int
|
|
sort_order: int
|
|
|
|
|
|
class SubcategoryReorderItem(BaseModel):
|
|
id: int
|
|
sort_order: int # position among subcategories within the parent
|
|
|
|
|
|
class ParentGeneralReorderItem(BaseModel):
|
|
id: int # parent category id
|
|
general_sort_order: int
|
|
|
|
|
|
# ── Quick Options ─────────────────────────────────────────────────────────────
|
|
|
|
class ProductQuickOptionCreate(BaseModel):
|
|
name: str
|
|
price: float = 0.0
|
|
allow_multiple: bool = False
|
|
sort_order: int = 0
|
|
is_favorite: bool = False
|
|
favorite_sort_order: int = 0
|
|
is_compact: bool = False
|
|
|
|
|
|
class ProductQuickOptionOut(BaseModel):
|
|
id: int
|
|
product_id: int
|
|
name: str
|
|
price: float = 0.0
|
|
allow_multiple: bool = False
|
|
sort_order: int = 0
|
|
is_favorite: bool = False
|
|
favorite_sort_order: int = 0
|
|
is_compact: bool = False
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
# ── Options ──────────────────────────────────────────────────────────────────
|
|
|
|
class OptionSubChoice(BaseModel):
|
|
name: str
|
|
extra_cost: float = 0.0
|
|
is_default: bool = False
|
|
|
|
|
|
class ProductOptionBase(BaseModel):
|
|
name: str
|
|
extra_cost: float = 0.0
|
|
allow_multiple: bool = False
|
|
is_favorite: bool = False
|
|
favorite_sort_order: int = 0
|
|
|
|
|
|
class ProductOptionCreate(ProductOptionBase):
|
|
sub_choices: List[OptionSubChoice] = []
|
|
|
|
|
|
class ProductOptionOut(ProductOptionBase):
|
|
id: int
|
|
product_id: int
|
|
sub_choices: List[OptionSubChoice] = []
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
@model_validator(mode='before')
|
|
@classmethod
|
|
def parse_option_sub_choices(cls, data: Any) -> Any:
|
|
if hasattr(data, 'sub_choices'):
|
|
raw = data.sub_choices
|
|
parsed = json.loads(raw) if isinstance(raw, str) else []
|
|
return {
|
|
'id': data.id,
|
|
'product_id': data.product_id,
|
|
'name': data.name,
|
|
'extra_cost': data.extra_cost,
|
|
'allow_multiple': getattr(data, 'allow_multiple', False) or False,
|
|
'sub_choices': parsed,
|
|
'is_favorite': getattr(data, 'is_favorite', False) or False,
|
|
'favorite_sort_order': getattr(data, 'favorite_sort_order', 0) or 0,
|
|
}
|
|
return data
|
|
|
|
|
|
# ── Ingredients ───────────────────────────────────────────────────────────────
|
|
|
|
class ProductIngredientBase(BaseModel):
|
|
name: str
|
|
extra_cost: float = 0.0
|
|
is_favorite: bool = False
|
|
favorite_sort_order: int = 0
|
|
|
|
|
|
class ProductIngredientCreate(ProductIngredientBase):
|
|
pass
|
|
|
|
|
|
class ProductIngredientOut(ProductIngredientBase):
|
|
id: int
|
|
product_id: int
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
# ── Sub-choices (nested under a preference choice) ────────────────────────────
|
|
|
|
class SubChoice(BaseModel):
|
|
name: str
|
|
extra_cost: float = 0.0
|
|
is_default: bool = False
|
|
|
|
|
|
# ── Shared subset (set-level, shown for all non-disabling choices) ─────────────
|
|
|
|
class SharedSubsetChoice(BaseModel):
|
|
name: str
|
|
extra_cost: float = 0.0
|
|
is_default: bool = False
|
|
|
|
|
|
class SharedSubset(BaseModel):
|
|
name: str
|
|
choices: List[SharedSubsetChoice] = []
|
|
|
|
|
|
# ── Preferences ───────────────────────────────────────────────────────────────
|
|
|
|
class PreferenceChoiceCreate(BaseModel):
|
|
name: str
|
|
extra_cost: float = 0.0
|
|
sub_choices: List[SubChoice] = []
|
|
disables_subset: bool = False
|
|
|
|
|
|
class PreferenceChoiceOut(BaseModel):
|
|
id: int
|
|
set_id: int
|
|
name: str
|
|
extra_cost: float = 0.0
|
|
sub_choices: List[SubChoice] = []
|
|
disables_subset: bool = False
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
@model_validator(mode='before')
|
|
@classmethod
|
|
def parse_sub_choices(cls, data: Any) -> Any:
|
|
if hasattr(data, 'sub_choices'):
|
|
raw = data.sub_choices
|
|
if isinstance(raw, str):
|
|
try:
|
|
parsed = json.loads(raw)
|
|
except Exception:
|
|
parsed = []
|
|
else:
|
|
parsed = []
|
|
return {
|
|
'id': data.id,
|
|
'set_id': data.set_id,
|
|
'name': data.name,
|
|
'extra_cost': data.extra_cost,
|
|
'sub_choices': parsed,
|
|
'disables_subset': data.disables_subset or False,
|
|
}
|
|
return data
|
|
|
|
|
|
class PreferenceSetCreate(BaseModel):
|
|
name: str
|
|
choices: List[PreferenceChoiceCreate] = []
|
|
default_choice_index: Optional[int] = None # index into choices (0-based)
|
|
shared_subset: Optional[SharedSubset] = None
|
|
is_favorite: bool = False
|
|
favorite_sort_order: int = 0
|
|
|
|
|
|
class PreferenceSetOut(BaseModel):
|
|
id: int
|
|
product_id: int
|
|
name: str
|
|
choices: List[PreferenceChoiceOut] = []
|
|
default_choice_id: Optional[int] = None
|
|
shared_subset: Optional[SharedSubset] = None
|
|
is_favorite: bool = False
|
|
favorite_sort_order: int = 0
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
@model_validator(mode='before')
|
|
@classmethod
|
|
def parse_shared_subset(cls, data: Any) -> Any:
|
|
if hasattr(data, 'shared_subset'):
|
|
raw = data.shared_subset
|
|
if isinstance(raw, str):
|
|
try:
|
|
parsed = json.loads(raw)
|
|
except Exception:
|
|
parsed = None
|
|
else:
|
|
parsed = None
|
|
return {
|
|
'id': data.id,
|
|
'product_id': data.product_id,
|
|
'name': data.name,
|
|
'choices': list(data.choices),
|
|
'default_choice_id': data.default_choice_id,
|
|
'shared_subset': parsed,
|
|
'is_favorite': getattr(data, 'is_favorite', False) or False,
|
|
'favorite_sort_order': getattr(data, 'favorite_sort_order', 0) or 0,
|
|
}
|
|
return data
|
|
|
|
|
|
# ── Products ──────────────────────────────────────────────────────────────────
|
|
|
|
class ProductBase(BaseModel):
|
|
name: str
|
|
category_id: Optional[int] = None
|
|
base_price: float
|
|
is_available: bool = True
|
|
lifecycle_status: str = "active"
|
|
printer_zone_id: Optional[int] = None
|
|
sort_order: int = 0
|
|
|
|
|
|
class ProductCreate(ProductBase):
|
|
quick_options: List[ProductQuickOptionCreate] = []
|
|
options: List[ProductOptionCreate] = []
|
|
ingredients: List[ProductIngredientCreate] = []
|
|
preference_sets: List[PreferenceSetCreate] = []
|
|
|
|
|
|
class ProductUpdate(BaseModel):
|
|
name: Optional[str] = None
|
|
category_id: Optional[int] = None
|
|
base_price: Optional[float] = None
|
|
is_available: Optional[bool] = None
|
|
lifecycle_status: Optional[str] = None
|
|
printer_zone_id: Optional[int] = None
|
|
sort_order: Optional[int] = None
|
|
quick_options: Optional[List[ProductQuickOptionCreate]] = None
|
|
options: Optional[List[ProductOptionCreate]] = None
|
|
ingredients: Optional[List[ProductIngredientCreate]] = None
|
|
preference_sets: Optional[List[PreferenceSetCreate]] = None
|
|
|
|
|
|
class ProductReorderItem(BaseModel):
|
|
id: int
|
|
sort_order: int
|
|
|
|
|
|
class ProductOut(ProductBase):
|
|
id: int
|
|
quick_options: List[ProductQuickOptionOut] = []
|
|
options: List[ProductOptionOut] = []
|
|
ingredients: List[ProductIngredientOut] = []
|
|
preference_sets: List[PreferenceSetOut] = []
|
|
image_url: Optional[str] = None
|
|
|
|
model_config = {"from_attributes": True}
|