v0.1.27
Fix: Throughput API v1 available - Details posted to Irving. POWERBI_KEY was missing from the .ENV file, so was not live. Add: Editor now supports editing a mix's resolved formula directly, with % and kg dual entry on ingredient rows Fix: Mix Editor should bring through correct ingredients. New resolved formula (same logic we use in Mix Calculator). Fix: Security headers on all API responses (hardening) Add: New mix button available on the Mix Editor. Add: New ingredient button available on the Ingredient Editor
This commit is contained in:
@@ -93,6 +93,96 @@ def _mix_calculator_option_rank(product: Product) -> tuple[int, int, float, int]
|
||||
)
|
||||
|
||||
|
||||
def resolve_representative_product(db: Session, *, tenant_id: str, mix_id: int) -> Product | None:
|
||||
"""The single product the Mix Calculator surfaces for a given mix.
|
||||
|
||||
The calculator lists one representative product per (client, mix) and reads
|
||||
its formula. The Mix Editor reuses this so it edits exactly what the
|
||||
calculator shows. Preference order mirrors `build_mix_calculator_options`:
|
||||
visible products that already have a product-specific formula, ranked by
|
||||
`_mix_calculator_option_rank`; then any visible product; then any product.
|
||||
"""
|
||||
products = db.scalars(
|
||||
select(Product)
|
||||
.where(Product.tenant_id == tenant_id, Product.mix_id == mix_id)
|
||||
.options(
|
||||
selectinload(Product.ingredients).selectinload(ProductIngredient.raw_material),
|
||||
selectinload(Product.mix).selectinload(Mix.ingredients).selectinload(MixIngredient.raw_material),
|
||||
)
|
||||
).all()
|
||||
if not products:
|
||||
return None
|
||||
with_formula = [product for product in products if product.visible and product.ingredients]
|
||||
pool = with_formula or [product for product in products if product.visible] or list(products)
|
||||
return min(pool, key=_mix_calculator_option_rank)
|
||||
|
||||
|
||||
def resolve_editor_mix_formula(db: Session, *, tenant_id: str, mix: Mix) -> dict:
|
||||
"""Resolve a mix's formula the way the calculator does, for the editor.
|
||||
|
||||
Returns the resolved ingredient rows (with each row's share of the total as
|
||||
`mix_percentage`), the total kg, and where the formula lives:
|
||||
`source='product'` (a representative product's own formula) or `source='mix'`
|
||||
(the shared mix master fallback). `product_id` names the product that owns the
|
||||
formula when `source='product'`. The save path writes back to that same source.
|
||||
"""
|
||||
product = resolve_representative_product(db, tenant_id=tenant_id, mix_id=mix.id)
|
||||
if product is not None and product.ingredients:
|
||||
rows, total_kg = _resolved_formula_rows(product)
|
||||
# Carry each ingredient's note through so saving doesn't wipe it.
|
||||
notes_by_raw_material = {
|
||||
ingredient.raw_material_id: ingredient.notes for ingredient in product.ingredients
|
||||
}
|
||||
for row in rows:
|
||||
row["notes"] = notes_by_raw_material.get(row["raw_material_id"])
|
||||
source = "product"
|
||||
product_id = product.id
|
||||
else:
|
||||
# No product-specific formula: the calculator reads the shared mix master,
|
||||
# so the editor shows and edits that.
|
||||
rows = [
|
||||
{
|
||||
"raw_material_id": ingredient.raw_material_id,
|
||||
"raw_material_name": ingredient.raw_material.name
|
||||
if ingredient.raw_material is not None
|
||||
else f"Raw material {ingredient.raw_material_id}",
|
||||
"quantity_kg": ingredient.quantity_kg,
|
||||
"unit": ingredient.raw_material.unit_of_measure if ingredient.raw_material is not None else "kg",
|
||||
"sort_order": index,
|
||||
"notes": ingredient.notes,
|
||||
}
|
||||
for index, ingredient in enumerate(
|
||||
sorted(mix.ingredients, key=lambda item: item.raw_material.name if item.raw_material else ""),
|
||||
start=1,
|
||||
)
|
||||
]
|
||||
total_kg = round(sum(row["quantity_kg"] for row in rows), 4)
|
||||
source = "mix"
|
||||
product_id = product.id if product is not None else None
|
||||
|
||||
ingredients = [
|
||||
{
|
||||
"raw_material_id": row["raw_material_id"],
|
||||
"raw_material_name": row["raw_material_name"],
|
||||
"quantity_kg": round(row["quantity_kg"], 4),
|
||||
"mix_percentage": round((row["quantity_kg"] / total_kg) * 100, 4) if total_kg > 0 else 0.0,
|
||||
"unit": row["unit"],
|
||||
"notes": row.get("notes"),
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
return {
|
||||
"id": mix.id,
|
||||
"tenant_id": mix.tenant_id,
|
||||
"client_name": mix.client_name,
|
||||
"name": mix.name,
|
||||
"source": source,
|
||||
"product_id": product_id,
|
||||
"ingredients": ingredients,
|
||||
"total_kg": total_kg,
|
||||
}
|
||||
|
||||
|
||||
def calculate_mix_calculator_preview(
|
||||
db: Session,
|
||||
*,
|
||||
|
||||
Reference in New Issue
Block a user