from datetime import datetime from pydantic import BaseModel, ConfigDict, Field class EditorProductRow(BaseModel): id: int tenant_id: str client_name: str item_id: str | None name: str mix_id: int mix_client_name: str mix_name: str sale_type: str unit_of_measure: str visible: bool product_notes: str | None mix_notes: str | None class EditorProductUpdate(BaseModel): model_config = ConfigDict(extra="forbid") client_name: str | None = Field(default=None, min_length=1, max_length=255) name: str | None = Field(default=None, min_length=1, max_length=255) item_id: str | None = Field(default=None, max_length=128) mix_id: int | None = None sale_type: str | None = Field(default=None, min_length=1, max_length=64) unit_of_measure: str | None = Field(default=None, min_length=1, max_length=64) visible: bool | None = None notes: str | None = Field(default=None, max_length=2000) class EditorMixCreate(BaseModel): model_config = ConfigDict(extra="forbid") client_name: str = Field(min_length=1, max_length=255) name: str = Field(min_length=1, max_length=255) notes: str | None = Field(default=None, max_length=2000) class EditorMixUpdate(BaseModel): model_config = ConfigDict(extra="forbid") client_name: str | None = Field(default=None, min_length=1, max_length=255) name: str | None = Field(default=None, min_length=1, max_length=255) notes: str | None = Field(default=None, max_length=2000) # Toggling a mix's status fans out to the visibility of every product under it. visible: bool | None = None class EditorMixRow(BaseModel): id: int tenant_id: str client_name: str name: str # A mix reads as "Active" when at least one of its products is visible. visible: bool product_count: int visible_product_count: int notes: str | None class EditorMixIngredientRead(BaseModel): id: int raw_material_id: int raw_material_name: str quantity_kg: float notes: str | None class EditorMixFormulaRead(BaseModel): id: int tenant_id: str client_name: str name: str ingredients: list[EditorMixIngredientRead] total_kg: float class EditorResolvedMixIngredient(BaseModel): raw_material_id: int raw_material_name: str quantity_kg: float # This row's share of the mix total, matching the Mix Calculator. mix_percentage: float unit: str notes: str | None class EditorResolvedMixFormula(BaseModel): """A mix formula resolved the way the Mix Calculator reads it. `source` is `product` when the numbers come from a representative product's own formula, or `mix` when they come from the shared mix master fallback. The PUT endpoint writes back to whichever source produced these rows. """ id: int tenant_id: str client_name: str name: str source: str product_id: int | None ingredients: list[EditorResolvedMixIngredient] total_kg: float class EditorMixFormulaRowInput(BaseModel): model_config = ConfigDict(extra="forbid") raw_material_id: int quantity_kg: float = Field(gt=0) notes: str | None = Field(default=None, max_length=1000) class EditorMixFormulaReplace(BaseModel): """Full replacement of a mix's formula in one save. The frontend keeps kilograms as the canonical value (percentages are an entry aid that resolve back to kg against the total), so the API only needs the resolved kg per row. """ model_config = ConfigDict(extra="forbid") rows: list[EditorMixFormulaRowInput] = Field(min_length=1) class EditorMixIngredientCreate(BaseModel): model_config = ConfigDict(extra="forbid") raw_material_id: int quantity_kg: float = Field(gt=0) notes: str | None = Field(default=None, max_length=1000) class EditorMixIngredientUpdate(BaseModel): model_config = ConfigDict(extra="forbid") quantity_kg: float | None = Field(default=None, gt=0) notes: str | None = Field(default=None, max_length=1000) class EditorProductIngredientCreate(BaseModel): model_config = ConfigDict(extra="forbid") raw_material_id: int quantity_kg: float = Field(gt=0) notes: str | None = Field(default=None, max_length=1000) class EditorProductIngredientUpdate(BaseModel): model_config = ConfigDict(extra="forbid") quantity_kg: float | None = Field(default=None, gt=0) notes: str | None = Field(default=None, max_length=1000) class EditorProductIngredientRead(BaseModel): id: int raw_material_id: int raw_material_name: str quantity_kg: float sort_order: int notes: str | None class EditorProductFormulaRead(BaseModel): id: int tenant_id: str client_name: str name: str mix_id: int mix_name: str ingredients: list[EditorProductIngredientRead] total_kg: float # --- Ingredients (raw materials) catalogue ----------------------------------- class EditorIngredientRow(BaseModel): id: int name: str supplier: str | None unit_of_measure: str kg_per_unit: float status: str # Decimal places this ingredient is rounded to in the mix calculator output. rounding_decimals: int notes: str | None cost_per_kg: float | None # How many product/mix formulas currently reference this ingredient. usage_count: int created_at: datetime class EditorIngredientCreate(BaseModel): model_config = ConfigDict(extra="forbid") name: str = Field(min_length=1, max_length=255) supplier: str | None = Field(default=None, max_length=255) unit_of_measure: str = Field(min_length=1, max_length=64) kg_per_unit: float = Field(gt=0) status: str = Field(default="active", max_length=32) rounding_decimals: int = Field(default=2, ge=0, le=6) notes: str | None = Field(default=None, max_length=2000) class EditorIngredientUpdate(BaseModel): model_config = ConfigDict(extra="forbid") name: str | None = Field(default=None, min_length=1, max_length=255) supplier: str | None = Field(default=None, max_length=255) unit_of_measure: str | None = Field(default=None, min_length=1, max_length=64) kg_per_unit: float | None = Field(default=None, gt=0) status: str | None = Field(default=None, max_length=32) rounding_decimals: int | None = Field(default=None, ge=0, le=6) notes: str | None = Field(default=None, max_length=2000)