v0.1.30 - Throughput overview today-only mix cards; mix formula save 500 fix

Throughput Overview: Horse Mix and Grain Mix are now the first two cards and
show TODAY's output only. Removed the 7d/4w/6w/12w range selector; the cards
are fixed to Horse mix today, Grain mix today, Today, This week, 4-week average.

Mix Editor formula save: fix HTTP 500 on PUT /editor/mixes/{id}/formula. The
audit-diff path read the resolved formula by attribute, but the resolver returns
dicts -> AttributeError. Read by key and expire stale ORM state so the response
reflects the just-saved rows. Adds regression tests for both save branches.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-18 14:04:49 +12:00
co-authored by Claude Opus 4.8
parent e7a7b11589
commit 1062c038e8
8 changed files with 133 additions and 90 deletions
+13 -7
View File
@@ -27,7 +27,6 @@ from app.schemas.editor import (
EditorProductRow,
EditorProductUpdate,
EditorResolvedMixFormula,
EditorResolvedMixIngredient,
)
from app.services.change_log import (
ENTITY_INGREDIENT,
@@ -162,12 +161,16 @@ def _format_kg(value: float) -> str:
def _formula_deltas(
before: list[EditorResolvedMixIngredient] | list,
after: list[EditorResolvedMixIngredient] | list,
before: list[dict],
after: list[dict],
) -> list[dict]:
"""Per-ingredient before/after deltas between two resolved formulas."""
before_map = {row.raw_material_name: row.quantity_kg for row in before}
after_map = {row.raw_material_name: row.quantity_kg for row in after}
"""Per-ingredient before/after deltas between two resolved formulas.
`resolve_editor_mix_formula` returns plain dicts (ingredients are dicts too),
so read the rows by key, not attribute.
"""
before_map = {row["raw_material_name"]: row["quantity_kg"] for row in before}
after_map = {row["raw_material_name"]: row["quantity_kg"] for row in after}
deltas: list[dict] = []
for name in sorted(set(before_map) | set(after_map)):
old = before_map.get(name)
@@ -640,9 +643,12 @@ def replace_editor_mix_formula(
)
db.flush()
# Drop now-stale ORM state so the re-resolve reads the rows we just wrote
# rather than the formerly-loaded ingredient collections from the identity map.
db.expire_all()
mix = _load_editor_mix_formula(db, mix_id=mix_id, tenant_id=tenant_id)
after_formula = resolve_editor_mix_formula(db, tenant_id=tenant_id, mix=mix)
deltas = _formula_deltas(before_formula.ingredients, after_formula.ingredients)
deltas = _formula_deltas(before_formula["ingredients"], after_formula["ingredients"])
if deltas:
record_change(
db,