tweaks
This commit is contained in:
@@ -8,7 +8,7 @@ from sqlalchemy.orm import Session, joinedload, selectinload
|
||||
from app.api.deps import AuthSession
|
||||
from app.models.mix import Mix, MixIngredient
|
||||
from app.models.mix_calculator import MixCalculatorSession, MixCalculatorSessionLine
|
||||
from app.models.product import Product
|
||||
from app.models.product import Product, ProductIngredient
|
||||
from app.schemas.mix_calculator import MixCalculatorSessionCreate, MixCalculatorSessionUpdate
|
||||
from app.services.costing_engine import extract_unit_quantity_kg
|
||||
|
||||
@@ -28,10 +28,44 @@ def _load_product_for_calculation(db: Session, tenant_id: str, product_id: int)
|
||||
return db.scalar(
|
||||
select(Product)
|
||||
.where(Product.id == product_id, Product.tenant_id == tenant_id, Product.visible.is_(True))
|
||||
.options(selectinload(Product.mix).selectinload(Mix.ingredients).selectinload(MixIngredient.raw_material))
|
||||
.options(
|
||||
selectinload(Product.ingredients).selectinload(ProductIngredient.raw_material),
|
||||
selectinload(Product.mix).selectinload(Mix.ingredients).selectinload(MixIngredient.raw_material),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _resolved_formula_rows(product: Product) -> tuple[list[dict], float]:
|
||||
if product.ingredients:
|
||||
rows = [
|
||||
{
|
||||
"raw_material_id": ingredient.raw_material_id,
|
||||
"raw_material_name": ingredient.raw_material.name,
|
||||
"quantity_kg": ingredient.quantity_kg,
|
||||
"unit": ingredient.raw_material.unit_of_measure,
|
||||
"sort_order": ingredient.sort_order,
|
||||
}
|
||||
for ingredient in product.ingredients
|
||||
if ingredient.raw_material is not None
|
||||
]
|
||||
elif product.mix is not None:
|
||||
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,
|
||||
}
|
||||
for index, ingredient in enumerate(product.mix.ingredients, start=1)
|
||||
]
|
||||
else:
|
||||
rows = []
|
||||
|
||||
rows.sort(key=lambda row: (row["sort_order"], row["raw_material_name"]))
|
||||
return rows, round(sum(row["quantity_kg"] for row in rows), 4)
|
||||
|
||||
|
||||
def _fractional_bag_warning(batch_size_kg: float, total_bags: float, unit_of_measure: str) -> str | None:
|
||||
rounded_bags = round(total_bags)
|
||||
if abs(total_bags - rounded_bags) < 1e-9:
|
||||
@@ -54,12 +88,9 @@ def calculate_mix_calculator_preview(
|
||||
raise ValueError("Product not found")
|
||||
if product.client_name != values["client_name"]:
|
||||
raise ValueError("Selected product does not belong to the chosen client")
|
||||
if product.mix is None:
|
||||
raise ValueError("Product mix is not configured")
|
||||
|
||||
source_total_kg = round(sum(ingredient.quantity_kg for ingredient in product.mix.ingredients), 4)
|
||||
formula_rows, source_total_kg = _resolved_formula_rows(product)
|
||||
if source_total_kg <= 0:
|
||||
raise ValueError("Product mix has no source kilograms to scale")
|
||||
raise ValueError("Product has no source kilograms to scale")
|
||||
|
||||
batch_size_kg = float(values["batch_size_kg"])
|
||||
scale_factor = batch_size_kg / source_total_kg
|
||||
@@ -72,18 +103,17 @@ def calculate_mix_calculator_preview(
|
||||
warnings.append(bag_warning)
|
||||
|
||||
lines = []
|
||||
for index, ingredient in enumerate(product.mix.ingredients, start=1):
|
||||
mix_percentage = round((ingredient.quantity_kg / source_total_kg) * 100, 4)
|
||||
required_kg = round(ingredient.quantity_kg * scale_factor, 4)
|
||||
raw_material = ingredient.raw_material
|
||||
for index, ingredient in enumerate(formula_rows, start=1):
|
||||
mix_percentage = round((ingredient["quantity_kg"] / source_total_kg) * 100, 4)
|
||||
required_kg = round(ingredient["quantity_kg"] * scale_factor, 4)
|
||||
lines.append(
|
||||
{
|
||||
"raw_material_id": raw_material.id if raw_material is not None else ingredient.raw_material_id,
|
||||
"raw_material_name": raw_material.name if raw_material is not None else f"Raw material {ingredient.raw_material_id}",
|
||||
"raw_material_id": ingredient["raw_material_id"],
|
||||
"raw_material_name": ingredient["raw_material_name"],
|
||||
"required_kg": required_kg,
|
||||
"mix_percentage": mix_percentage,
|
||||
"unit": raw_material.unit_of_measure if raw_material is not None else "kg",
|
||||
"sort_order": index,
|
||||
"unit": ingredient["unit"],
|
||||
"sort_order": ingredient["sort_order"] or index,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -92,7 +122,7 @@ def calculate_mix_calculator_preview(
|
||||
"product_id": product.id,
|
||||
"product_name": product.name,
|
||||
"mix_id": product.mix_id,
|
||||
"mix_name": product.mix.name,
|
||||
"mix_name": product.mix.name if product.mix else product.name,
|
||||
"mix_date": values["mix_date"],
|
||||
"batch_size_kg": round(batch_size_kg, 4),
|
||||
"total_bags": total_bags,
|
||||
@@ -108,10 +138,16 @@ def calculate_mix_calculator_preview(
|
||||
|
||||
|
||||
def build_mix_calculator_options(db: Session, *, tenant_id: str) -> dict:
|
||||
# Aggregate mix totals in a single query instead of loading every
|
||||
# ingredient row for every product. The previous implementation was the
|
||||
# main slow path on first Mix Calculator open — it streamed the entire
|
||||
# tenant's recipe table just to compute one sum per product.
|
||||
# Prefer product-specific formulas where present; fall back to the shared
|
||||
# mix master for legacy rows that have not been migrated yet.
|
||||
product_totals_rows = db.execute(
|
||||
select(ProductIngredient.product_id, func.coalesce(func.sum(ProductIngredient.quantity_kg), 0.0))
|
||||
.join(Product, Product.id == ProductIngredient.product_id)
|
||||
.where(Product.tenant_id == tenant_id)
|
||||
.group_by(ProductIngredient.product_id)
|
||||
).all()
|
||||
product_totals: dict[int, float] = {product_id: round(total or 0.0, 4) for product_id, total in product_totals_rows}
|
||||
|
||||
mix_totals_rows = db.execute(
|
||||
select(MixIngredient.mix_id, func.coalesce(func.sum(MixIngredient.quantity_kg), 0.0))
|
||||
.join(Mix, Mix.id == MixIngredient.mix_id)
|
||||
@@ -137,7 +173,7 @@ def build_mix_calculator_options(db: Session, *, tenant_id: str) -> dict:
|
||||
"mix_name": product.mix.name if product.mix else "",
|
||||
"unit_of_measure": product.unit_of_measure,
|
||||
"unit_size_kg": round(extract_unit_quantity_kg(product.unit_of_measure), 4),
|
||||
"mix_total_kg": mix_totals.get(product.mix_id, 0.0),
|
||||
"mix_total_kg": product_totals.get(product.id, mix_totals.get(product.mix_id, 0.0)),
|
||||
}
|
||||
for product in products
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user