Public Access
fix: step_finalize — resolve ingredient names and prices in shopping list email
Preloads Ingredient names from the DB (ingredients JSONB has no name field), deduplicates by ingredient name, looks up top-confidence IngredientGroceryMatch per ingredient, and renders a rich 4-column HTML table (Ingredient | Qty | Unit | At Lucky | Price) with an estimated total. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -321,6 +321,8 @@ def step_finalize(run: "WeeklyRun", db: "Session") -> None:
|
|||||||
logger.info("step_finalize: already done for %s", run.week_start_date)
|
logger.info("step_finalize: already done for %s", run.week_start_date)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
import uuid as _uuid_fin
|
||||||
|
|
||||||
plan = (
|
plan = (
|
||||||
db.query(MealPlan)
|
db.query(MealPlan)
|
||||||
.filter(
|
.filter(
|
||||||
@@ -345,20 +347,77 @@ def step_finalize(run: "WeeklyRun", db: "Session") -> None:
|
|||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Preload ingredient names for approved items
|
||||||
|
_fin_ids: set = set()
|
||||||
|
for _item in approved_items:
|
||||||
|
if _item.recipe:
|
||||||
|
for _ing in (_item.recipe.ingredients or []):
|
||||||
|
if "ingredient_id" in _ing:
|
||||||
|
_fin_ids.add(_ing["ingredient_id"])
|
||||||
|
_fin_names: dict = {}
|
||||||
|
if _fin_ids:
|
||||||
|
_fin_rows = db.query(Ingredient).filter(
|
||||||
|
Ingredient.id.in_([_uuid_fin.UUID(str(i)) for i in _fin_ids])
|
||||||
|
).all()
|
||||||
|
_fin_names = {str(r.id): r.name for r in _fin_rows}
|
||||||
|
|
||||||
if approved_items:
|
if approved_items:
|
||||||
rows_html = "".join(
|
total_cost = 0.0
|
||||||
f"<tr><td>{html.escape(item.recipe.name)}</td>"
|
rows_html = ""
|
||||||
f"<td>{html.escape(str(ing.get('name', '')))}</td>"
|
seen_ingredients: set = set()
|
||||||
f"<td>{html.escape(str(ing.get('qty', '')))} {html.escape(str(ing.get('unit', '')))}</td></tr>"
|
for item in approved_items:
|
||||||
for item in approved_items
|
if not item.recipe:
|
||||||
if item.recipe
|
continue
|
||||||
for ing in (item.recipe.ingredients or [])
|
for ing in (item.recipe.ingredients or []):
|
||||||
)
|
ing_id = str(ing.get("ingredient_id", ""))
|
||||||
|
ing_name = _fin_names.get(ing_id, "")
|
||||||
|
if not ing_name or ing_name in seen_ingredients:
|
||||||
|
continue
|
||||||
|
seen_ingredients.add(ing_name)
|
||||||
|
qty = ing.get("qty", "")
|
||||||
|
unit = ing.get("unit", "")
|
||||||
|
|
||||||
|
# Find Lucky CA match
|
||||||
|
match = (
|
||||||
|
db.query(IngredientGroceryMatch)
|
||||||
|
.join(Ingredient, IngredientGroceryMatch.ingredient_id == Ingredient.id)
|
||||||
|
.filter(Ingredient.id == _uuid_fin.UUID(ing_id))
|
||||||
|
.order_by(IngredientGroceryMatch.confidence.desc())
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if match and match.grocery_item:
|
||||||
|
lucky_name = html.escape(match.grocery_item.name or "")
|
||||||
|
price = float(match.grocery_item.price or 0)
|
||||||
|
total_cost += price
|
||||||
|
price_str = f"${price:.2f}"
|
||||||
|
else:
|
||||||
|
lucky_name = "—"
|
||||||
|
price_str = "—"
|
||||||
|
|
||||||
|
rows_html += (
|
||||||
|
f"<tr>"
|
||||||
|
f"<td style='padding:6px 8px'>{html.escape(ing_name)}</td>"
|
||||||
|
f"<td style='padding:6px 8px;color:#555'>{html.escape(str(qty))} {html.escape(str(unit))}</td>"
|
||||||
|
f"<td style='padding:6px 8px;color:#555'>{lucky_name}</td>"
|
||||||
|
f"<td style='padding:6px 8px;font-weight:bold'>{price_str}</td>"
|
||||||
|
f"</tr>"
|
||||||
|
)
|
||||||
|
|
||||||
email_html = (
|
email_html = (
|
||||||
|
f"<div style='font-family:sans-serif;max-width:600px;margin:0 auto'>"
|
||||||
f"<h2>Shopping list — week of {run.week_start_date}</h2>"
|
f"<h2>Shopping list — week of {run.week_start_date}</h2>"
|
||||||
f"<p>{len(approved_items)} meal(s) approved.</p>"
|
f"<p>{len(approved_items)} meal(s) approved.</p>"
|
||||||
f"<table><thead><tr><th>Recipe</th><th>Ingredient</th><th>Qty</th></tr></thead>"
|
f"<table style='border-collapse:collapse;width:100%;font-size:14px'>"
|
||||||
f"<tbody>{rows_html}</tbody></table>"
|
f"<thead><tr style='background:#f3f4f6'>"
|
||||||
|
f"<th style='text-align:left;padding:6px 8px;border-bottom:2px solid #e5e7eb'>Ingredient</th>"
|
||||||
|
f"<th style='text-align:left;padding:6px 8px;border-bottom:2px solid #e5e7eb'>Qty</th>"
|
||||||
|
f"<th style='text-align:left;padding:6px 8px;border-bottom:2px solid #e5e7eb'>At Lucky</th>"
|
||||||
|
f"<th style='text-align:left;padding:6px 8px;border-bottom:2px solid #e5e7eb'>Price</th>"
|
||||||
|
f"</tr></thead>"
|
||||||
|
f"<tbody>{rows_html}</tbody>"
|
||||||
|
f"</table>"
|
||||||
|
f"<p style='margin-top:16px;font-weight:bold'>Estimated total: ${total_cost:.2f}</p>"
|
||||||
|
f"</div>"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
email_html = f"<p>No meals were approved for week of {run.week_start_date}.</p>"
|
email_html = f"<p>No meals were approved for week of {run.week_start_date}.</p>"
|
||||||
|
|||||||
Reference in New Issue
Block a user