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
103 lines
4.1 KiB
Python
103 lines
4.1 KiB
Python
"""The Mix Editor must resolve and edit the SAME formula the Mix Calculator reads.
|
|
|
|
These cover `resolve_editor_mix_formula` / `resolve_representative_product`: a mix
|
|
whose product carries its own formula resolves to that product (not the shared
|
|
mix master), percentages are computed against the total, and the representative
|
|
product is chosen the way the calculator chooses it.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import Session, sessionmaker
|
|
|
|
from app.db.session import Base
|
|
from app.models.mix import Mix, MixIngredient
|
|
from app.models.product import Product, ProductIngredient
|
|
from app.models.raw_material import RawMaterial
|
|
from app.services.mix_calculator_service import (
|
|
resolve_editor_mix_formula,
|
|
resolve_representative_product,
|
|
)
|
|
|
|
TENANT = "hunter-premium-produce"
|
|
|
|
|
|
def _session() -> Session:
|
|
engine = create_engine("sqlite:///:memory:")
|
|
Base.metadata.create_all(bind=engine)
|
|
return sessionmaker(bind=engine, expire_on_commit=False)()
|
|
|
|
|
|
def _raw(db: Session, name: str) -> RawMaterial:
|
|
material = RawMaterial(tenant_id=TENANT, name=name, unit_of_measure="kg", kg_per_unit=1, status="active")
|
|
db.add(material)
|
|
db.flush()
|
|
return material
|
|
|
|
|
|
def test_resolves_product_formula_not_mix_master():
|
|
db = _session()
|
|
bayley = _raw(db, "Bayley")
|
|
filler = _raw(db, "Filler")
|
|
|
|
mix = Mix(tenant_id=TENANT, client_name="Hunter", name="Pigeon Mix")
|
|
db.add(mix)
|
|
db.flush()
|
|
|
|
# Shared mix master says something different from the product formula.
|
|
db.add(MixIngredient(tenant_id=TENANT, mix_id=mix.id, raw_material_id=bayley.id, quantity_kg=100))
|
|
db.add(MixIngredient(tenant_id=TENANT, mix_id=mix.id, raw_material_id=filler.id, quantity_kg=100))
|
|
|
|
product = Product(tenant_id=TENANT, client_name="Hunter", name="Pigeon 20kg", mix_id=mix.id, unit_of_measure="20kg bag", visible=True)
|
|
db.add(product)
|
|
db.flush()
|
|
# The calculator's real numbers live here: 787.5 / 1320.41 ~ 59.6%.
|
|
db.add(ProductIngredient(tenant_id=TENANT, product_id=product.id, raw_material_id=bayley.id, quantity_kg=787.5, sort_order=1))
|
|
db.add(ProductIngredient(tenant_id=TENANT, product_id=product.id, raw_material_id=filler.id, quantity_kg=532.91, sort_order=2))
|
|
db.commit()
|
|
|
|
formula = resolve_editor_mix_formula(db, tenant_id=TENANT, mix=mix)
|
|
|
|
assert formula["source"] == "product"
|
|
assert formula["product_id"] == product.id
|
|
assert formula["total_kg"] == 1320.41
|
|
by_name = {row["raw_material_name"]: row for row in formula["ingredients"]}
|
|
assert by_name["Bayley"]["quantity_kg"] == 787.5
|
|
# Percentage matches the worked example (787.5 / 1320.41 * 100).
|
|
assert abs(by_name["Bayley"]["mix_percentage"] - 59.6406) < 0.001
|
|
|
|
|
|
def test_falls_back_to_mix_master_when_no_product_formula():
|
|
db = _session()
|
|
maize = _raw(db, "Maize")
|
|
mix = Mix(tenant_id=TENANT, client_name="Hunter", name="Plain Mix")
|
|
db.add(mix)
|
|
db.flush()
|
|
db.add(MixIngredient(tenant_id=TENANT, mix_id=mix.id, raw_material_id=maize.id, quantity_kg=50))
|
|
db.commit()
|
|
|
|
formula = resolve_editor_mix_formula(db, tenant_id=TENANT, mix=mix)
|
|
assert formula["source"] == "mix"
|
|
assert formula["total_kg"] == 50
|
|
assert formula["ingredients"][0]["mix_percentage"] == 100.0
|
|
|
|
|
|
def test_representative_product_prefers_20kg_bag():
|
|
db = _session()
|
|
maize = _raw(db, "Maize")
|
|
mix = Mix(tenant_id=TENANT, client_name="Hunter", name="Dual Mix")
|
|
db.add(mix)
|
|
db.flush()
|
|
|
|
bulka = Product(tenant_id=TENANT, client_name="Hunter", name="Dual Bulka", mix_id=mix.id, unit_of_measure="500kg bulka", visible=True)
|
|
bag = Product(tenant_id=TENANT, client_name="Hunter", name="Dual 20kg", mix_id=mix.id, unit_of_measure="20kg bag", visible=True)
|
|
db.add_all([bulka, bag])
|
|
db.flush()
|
|
for product in (bulka, bag):
|
|
db.add(ProductIngredient(tenant_id=TENANT, product_id=product.id, raw_material_id=maize.id, quantity_kg=20, sort_order=1))
|
|
db.commit()
|
|
|
|
representative = resolve_representative_product(db, tenant_id=TENANT, mix_id=mix.id)
|
|
assert representative is not None
|
|
assert representative.unit_of_measure == "20kg bag"
|