This commit is contained in:
2026-05-31 20:19:44 +12:00
parent 2f2466ecac
commit 84792c0947
59 changed files with 5412 additions and 898 deletions
+85 -3
View File
@@ -8,7 +8,7 @@ from sqlalchemy.orm import Session, selectinload
from app.models.assumption import FreightCostRule, PackagingCostRule, ProcessCostRule
from app.models.mix import Mix, MixIngredient
from app.models.product import Product
from app.models.product import Product, ProductIngredient
from app.models.raw_material import RawMaterial, RawMaterialPriceVersion
@@ -119,6 +119,78 @@ def calculate_mix_cost(db: Session, mix_id: int, overrides: dict | None = None)
}
def _calculate_formula_cost_from_product_ingredients(
product_ingredients: list[ProductIngredient],
overrides: dict | None = None,
) -> dict:
overrides = overrides or {}
total_mix_kg = 0.0
total_mix_cost = 0.0
warnings: list[str] = []
lines: list[dict] = []
for ingredient in product_ingredients:
raw_material = ingredient.raw_material
active_price = get_active_price(raw_material)
if active_price is None:
warnings.append(f"{raw_material.name} has no active price")
lines.append(
{
"id": ingredient.id,
"raw_material_id": raw_material.id,
"raw_material_name": raw_material.name,
"quantity_kg": ingredient.quantity_kg,
"cost_per_kg": None,
"line_cost": None,
"notes": ingredient.notes,
}
)
total_mix_kg += ingredient.quantity_kg
continue
market_value = overrides.get("raw_material_market_values", {}).get(str(raw_material.id), active_price.market_value)
waste_percentage = overrides.get("raw_material_waste_percentages", {}).get(str(raw_material.id), active_price.waste_percentage)
price_stub = RawMaterialPriceVersion(
raw_material_id=raw_material.id,
market_value=market_value,
waste_percentage=waste_percentage,
effective_date=active_price.effective_date,
status=active_price.status,
)
price_comp = calculate_raw_material_cost(raw_material, price_stub)
line_cost = round(ingredient.quantity_kg * price_comp.cost_per_kg, 4)
total_mix_kg += ingredient.quantity_kg
total_mix_cost += line_cost
lines.append(
{
"id": ingredient.id,
"raw_material_id": raw_material.id,
"raw_material_name": raw_material.name,
"quantity_kg": ingredient.quantity_kg,
"cost_per_kg": price_comp.cost_per_kg,
"line_cost": line_cost,
"notes": ingredient.notes,
}
)
if total_mix_kg == 0:
warnings.append("Mix total kg is zero")
mix_cost_per_kg = None
else:
mix_cost_per_kg = round(total_mix_cost / total_mix_kg, 4)
if not product_ingredients:
warnings.append("Mix has no ingredients")
return {
"ingredients": lines,
"total_mix_kg": round(total_mix_kg, 4),
"total_mix_cost": round(total_mix_cost, 4),
"mix_cost_per_kg": mix_cost_per_kg,
"warnings": warnings,
}
def _get_process_costs(db: Session, process_name: str | None, overrides: dict) -> tuple[float, float, float, list[str]]:
if not process_name:
return 0.0, 0.0, 0.0, ["Missing bagging process"]
@@ -192,12 +264,22 @@ def extract_unit_quantity_kg(unit_of_measure: str) -> float:
def calculate_product_cost(db: Session, product_id: int, overrides: dict | None = None) -> dict:
overrides = overrides or {}
overrides = {**overrides, "tenant_id": overrides.get("tenant_id")}
product = db.scalar(select(Product).where(Product.id == product_id).options(selectinload(Product.mix)))
product = db.scalar(
select(Product)
.where(Product.id == product_id)
.options(
selectinload(Product.mix),
selectinload(Product.ingredients).selectinload(ProductIngredient.raw_material).selectinload(RawMaterial.price_versions),
)
)
if product is None:
raise ValueError(f"Product {product_id} not found")
overrides["tenant_id"] = product.tenant_id
mix_result = calculate_mix_cost(db, product.mix_id, overrides=overrides)
if product.ingredients:
mix_result = _calculate_formula_cost_from_product_ingredients(product.ingredients, overrides=overrides)
else:
mix_result = calculate_mix_cost(db, product.mix_id, overrides=overrides)
warnings = list(mix_result["warnings"])
sale_unit_kg = extract_unit_quantity_kg(product.unit_of_measure)