Add shopping entry notes

This commit is contained in:
2026-04-26 13:18:14 +02:00
parent d3c58c5dd2
commit 1034ea72a8
5 changed files with 255 additions and 75 deletions
+122 -20
View File
@@ -1001,6 +1001,10 @@ def should_activate_shopping_need(needed_date: date, today: date | None = None)
return (today or date.today()) >= shopping_activation_date_for(needed_date)
def normalize_shopping_note(value: str | None) -> str:
return " ".join((value or "").strip().split())[:80]
def schedule_shopping_need(
*,
item_id: int,
@@ -1082,14 +1086,16 @@ def add_to_shopping_list(
visibility_override: str | None = None,
needed_for_date: str | None = None,
needed_for_daypart_id: int | None = None,
shopping_note: str | None = None,
) -> bool:
item = get_item(item_id)
normalized_note = normalize_shopping_note(shopping_note)
existing = get_db().execute(
"""
SELECT id FROM shopping_entries
WHERE item_id = ? AND is_checked = 0
WHERE item_id = ? AND shopping_note = ? AND is_checked = 0
""",
(item_id,),
(item_id, normalized_note),
).fetchone()
if existing:
return False
@@ -1099,15 +1105,16 @@ def add_to_shopping_list(
get_db().execute(
"""
INSERT INTO shopping_entries (
household_id, owner_user_id, visibility, item_id, added_by, needed_for_date, needed_for_daypart_id
household_id, owner_user_id, visibility, item_id, shopping_note, added_by, needed_for_date, needed_for_daypart_id
)
VALUES (?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
(
current_household_id(),
owner_user_id,
visibility,
item_id,
normalized_note,
user_id,
needed_for_date,
needed_for_daypart_id,
@@ -1150,6 +1157,7 @@ def ensure_item_or_missing_components_are_shopped(
needed_for_date: str | None = None,
needed_for_daypart_id: int | None = None,
source_item_id: int | None = None,
shopping_note: str | None = None,
) -> dict:
item = get_item(item_id)
if item["kind"] == "meal":
@@ -1219,6 +1227,7 @@ def ensure_item_or_missing_components_are_shopped(
visibility_override=visibility,
needed_for_date=needed_for_date,
needed_for_daypart_id=needed_for_daypart_id,
shopping_note=shopping_note,
)
return {
"added": added,
@@ -1289,6 +1298,39 @@ def fetch_items_by_ids(item_ids: list[int]) -> list[dict]:
return [items_by_id[item_id] for item_id in normalized_ids if item_id in items_by_id]
def find_shopping_food_by_name(name: str) -> dict | None:
normalized_name = name.strip().lower()
if not normalized_name:
return None
row = get_db().execute(
f"""
SELECT items.*,
owner.display_name AS owner_display_name,
owner.username AS owner_username,
target.display_name AS target_display_name,
target.username AS target_username,
EXISTS(
SELECT 1
FROM shopping_entries
WHERE shopping_entries.item_id = items.id AND shopping_entries.is_checked = 0
) AS is_on_shopping_list
FROM items
LEFT JOIN users AS owner ON owner.id = items.owner_user_id
LEFT JOIN users AS target ON target.id = items.target_user_id
WHERE items.kind = 'food'
AND items.is_archived = 0
AND LOWER(items.name) = ?
AND {visible_clause('items')}
ORDER BY LOWER(items.name), items.id
LIMIT 1
""",
[normalized_name, *visible_params()],
).fetchone()
if row is None:
return None
return attach_builder_keys(attach_dayparts(describe_records([row])))[0]
def fetch_shopping_entries():
rows = get_db().execute(
f"""
@@ -4385,6 +4427,56 @@ def remove_shopping_entry(entry_id: int) -> None:
get_db().commit()
@main_bp.post("/shopping/<int:entry_id>/note")
@login_required
def shopping_update_note(entry_id: int):
entry = get_db().execute(
f"""
SELECT shopping_entries.*,
owner.display_name AS owner_display_name,
owner.username AS owner_username
FROM shopping_entries
LEFT JOIN users AS owner ON owner.id = shopping_entries.owner_user_id
WHERE shopping_entries.id = ? AND {visible_clause('shopping_entries')}
""",
[entry_id, *visible_params()],
).fetchone()
if entry is None:
flash("Der Einkaufseintrag wurde nicht gefunden.", "error")
return redirect(url_with_scroll_position(url_for("main.shopping_list")))
try:
ensure_can_edit(describe_record(dict(entry)), "Diesen Einkaufseintrag kannst du gerade nicht ändern.")
except PermissionError as exc:
flash(str(exc), "error")
return redirect(url_with_scroll_position(url_for("main.shopping_list")))
shopping_note = normalize_shopping_note(request.form.get("shopping_note"))
duplicate = get_db().execute(
"""
SELECT id
FROM shopping_entries
WHERE item_id = ?
AND shopping_note = ?
AND is_checked = 0
AND id != ?
LIMIT 1
""",
(entry["item_id"], shopping_note, entry_id),
).fetchone()
if duplicate:
flash("Dieser Hinweis steht für das Lebensmittel schon auf der Einkaufsliste.", "info")
return redirect(url_with_scroll_position(url_for("main.shopping_list")))
get_db().execute(
"UPDATE shopping_entries SET shopping_note = ? WHERE id = ?",
(shopping_note, entry_id),
)
get_db().commit()
flash("Der Einkaufshinweis wurde gespeichert.", "success")
return redirect(url_with_scroll_position(url_for("main.shopping_list")))
@main_bp.post("/items/<int:item_id>/shopping/bought")
@login_required
def item_mark_bought(item_id: int):
@@ -4436,30 +4528,40 @@ def item_remove_from_shopping(item_id: int):
def shopping_list():
if request.method == "POST":
selected_item_id = request.form.get("item_id", "").strip()
if not selected_item_id.isdigit():
flash("Bitte zuerst etwas auswählen.", "error")
else:
item_search = request.form.get("item_search", "").strip()
shopping_note = normalize_shopping_note(request.form.get("shopping_note"))
item = None
if selected_item_id.isdigit():
try:
item = get_item(int(selected_item_id))
result = ensure_item_or_missing_components_are_shopped(
item["id"],
g.user["id"],
item["visibility"],
)
if result["count"]:
flash(f"Die Einkaufsliste wurde ergänzt: {', '.join(result['names'][:4])}.", "success")
elif result["scheduled_count"]:
flash("Ein paar Dinge sind für einen späteren Einkauf vorgemerkt.", "info")
else:
flash("Dafür ist gerade nichts zusätzlich nötig.", "info")
except ValueError as exc:
flash(str(exc), "error")
elif item_search:
item = find_shopping_food_by_name(item_search)
if item is None:
flash("Bitte ein Lebensmittel aus der Suche auswählen.", "error")
else:
flash("Bitte zuerst etwas auswählen.", "error")
if item is not None:
result = ensure_item_or_missing_components_are_shopped(
item["id"],
g.user["id"],
item["visibility"],
shopping_note=shopping_note,
)
if result["count"]:
note_suffix = f" ({shopping_note})" if shopping_note else ""
flash(f"Die Einkaufsliste wurde ergänzt: {', '.join(result['names'][:4])}{note_suffix}.", "success")
elif result["scheduled_count"]:
flash("Ein paar Dinge sind für einen späteren Einkauf vorgemerkt.", "info")
else:
flash("Dieser Einkaufseintrag steht so schon auf der Liste.", "info")
return redirect(url_with_scroll_position(url_for("main.shopping_list")))
entries = fetch_shopping_entries()
upcoming_entries = fetch_upcoming_shopping_needs()
addable_items = fetch_items(include_archived=True, include_quick_added=True)
addable_items = [item for item in addable_items if item["kind"] == "food" and not item["is_on_shopping_list"]]
addable_items = fetch_items(kind="food", include_archived=False, include_quick_added=True)
household_settings = get_household_settings()
shopping_weekday_label = dict(WEEKDAY_OPTIONS).get(household_settings["shopping_weekday"], "gesetzt")
return render_template(