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"
|