Vorrat: eigene Läden mit Produktzuordnung
- Läden pro Liste (Tabelle stores): anlegen, umbenennen, sortieren, löschen; je Laden eigene Reihenfolge der Abteilungen - Produkte einem Laden zuordnen (Tabelle store_products, pro Liste, bleibt nach dem Einkauf gemerkt); gelöschter Laden → Produkte ohne Laden - Einkaufsliste "Nach Laden": nach Laden gruppiert, darin nach Abteilung; Filter "Alle | Laden | Ohne Laden", im Filter Hinzugefügtes wird diesem Laden zugeordnet - Läden fremder privater Listen sind nicht nutzbar (Tests) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
bbf740603e
commit
fef9c8f16e
9 files changed
+393
-33
No files matched your search
+112
-2
@@ -25,6 +25,8 @@ MAX_QUANTITY = 999
|
||||
MAX_PRODUCTS = 2000
|
||||
MAX_LIST_NAME = 40
|
||||
MAX_LISTS = 50
|
||||
MAX_STORES = 30
|
||||
MAX_STORE_NAME = 40
|
||||
|
||||
|
||||
def _item(row) -> dict:
|
||||
@@ -37,6 +39,7 @@ def _item(row) -> dict:
|
||||
"unit": row["unit"],
|
||||
"checked": bool(row["checked"]),
|
||||
"category": row["category"] or guess(row["name"]),
|
||||
"storeId": row["store_id"],
|
||||
"updatedAt": row["updated_at"],
|
||||
"updatedBy": {"id": row["updated_by"], "name": row["updated_by_name"]} if row["updated_by"] else None,
|
||||
}
|
||||
@@ -187,6 +190,10 @@ def _shopping_list(conn, row, user: dict) -> dict:
|
||||
"createdBy": {"id": row["created_by"], "name": row["created_by_name"]} if row["created_by"] else None,
|
||||
"canManage": _can_manage(row, user),
|
||||
"categoryOrder": _category_order(row["category_order"]),
|
||||
"stores": [
|
||||
{"id": s["id"], "name": s["name"], "categoryOrder": _category_order(s["category_order"] or row["category_order"])}
|
||||
for s in conn.execute("SELECT * FROM stores WHERE list_id = ? ORDER BY position, id", (row["id"],))
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -234,9 +241,10 @@ def _state(conn, user: dict) -> dict:
|
||||
ids = [r["id"] for r in lists]
|
||||
marks = ",".join("?" * len(ids))
|
||||
items = conn.execute(
|
||||
f"""SELECT i.*, u.display_name AS updated_by_name, p.category FROM household_items i
|
||||
f"""SELECT i.*, u.display_name AS updated_by_name, p.category, sp.store_id FROM household_items i
|
||||
LEFT JOIN users u ON u.id = i.updated_by
|
||||
LEFT JOIN products p ON p.key = i.key
|
||||
LEFT JOIN store_products sp ON sp.list_id = i.list_id AND sp.key = i.key
|
||||
WHERE i.list_id IN ({marks})""",
|
||||
ids,
|
||||
).fetchall()
|
||||
@@ -295,7 +303,11 @@ def add_item(data: dict = Body(...), user: dict = Depends(current_user)):
|
||||
quantity = _quantity(data.get("quantity", 1), unit)
|
||||
with transaction() as conn:
|
||||
list_id = _get_list(conn, data.get("listId"), user)["id"]
|
||||
_add(conn, _canonical_name(conn, name, list_id), list_, list_id, quantity, unit, user["id"])
|
||||
name = _canonical_name(conn, name, list_id)
|
||||
_add(conn, name, list_, list_id, quantity, unit, user["id"])
|
||||
# Im Laden-Filter hinzugefügt: Produkt gleich diesem Laden zuordnen
|
||||
if data.get("storeId") is not None:
|
||||
_assign_store(conn, list_id, _key(name), _get_store(conn, data["storeId"], user, list_id)["id"])
|
||||
return _state(conn, user)
|
||||
|
||||
|
||||
@@ -488,3 +500,101 @@ def set_category_order(list_id: int, data: dict = Body(...), user: dict = Depend
|
||||
_get_list(conn, list_id, user)
|
||||
conn.execute("UPDATE shopping_lists SET category_order = ? WHERE id = ?", (json.dumps(order), list_id))
|
||||
return _state(conn, user)
|
||||
|
||||
|
||||
# ---------- Läden ----------
|
||||
|
||||
|
||||
def _get_store(conn, store_id, user: dict, list_id=None):
|
||||
"""Laden einer Liste, die das Konto sieht (optional: muss zu list_id gehören)."""
|
||||
if isinstance(store_id, bool) or not isinstance(store_id, int):
|
||||
raise ValidationError("Ungültiger Laden.")
|
||||
row = conn.execute("SELECT * FROM stores WHERE id = ?", (store_id,)).fetchone()
|
||||
if not row or (list_id is not None and row["list_id"] != list_id):
|
||||
raise HTTPException(404, "Laden nicht gefunden.")
|
||||
_get_list(conn, row["list_id"], user)
|
||||
return row
|
||||
|
||||
|
||||
def _assign_store(conn, list_id: int, key: str, store_id) -> None:
|
||||
if store_id is None:
|
||||
conn.execute("DELETE FROM store_products WHERE list_id = ? AND key = ?", (list_id, key))
|
||||
else:
|
||||
conn.execute(
|
||||
"""INSERT INTO store_products (list_id, key, store_id) VALUES (?, ?, ?)
|
||||
ON CONFLICT (list_id, key) DO UPDATE SET store_id = excluded.store_id""",
|
||||
(list_id, key, store_id),
|
||||
)
|
||||
|
||||
|
||||
@router.post("/api/haushalt/listen/{list_id}/laeden", status_code=201)
|
||||
def create_store(list_id: int, data: dict = Body(...), user: dict = Depends(current_user)):
|
||||
"""Laden anlegen (alle, die die Liste sehen, dürfen Läden verwalten)."""
|
||||
name = clean_text(data.get("name"), "Name des Ladens", MAX_STORE_NAME)
|
||||
with transaction() as conn:
|
||||
_get_list(conn, list_id, user)
|
||||
if conn.execute("SELECT COUNT(*) FROM stores WHERE list_id = ?", (list_id,)).fetchone()[0] >= MAX_STORES:
|
||||
raise ValidationError(f"Maximal {MAX_STORES} Läden pro Liste.")
|
||||
if conn.execute("SELECT 1 FROM stores WHERE list_id = ? AND name = ? COLLATE NOCASE", (list_id, name)).fetchone():
|
||||
raise ValidationError("Diesen Laden gibt es in der Liste schon.")
|
||||
position = conn.execute("SELECT COALESCE(MAX(position), 0) + 1 FROM stores WHERE list_id = ?", (list_id,)).fetchone()[0]
|
||||
store_id = conn.execute(
|
||||
"INSERT INTO stores (list_id, name, position) VALUES (?, ?, ?)", (list_id, name, position)
|
||||
).lastrowid
|
||||
return {**_state(conn, user), "created": store_id}
|
||||
|
||||
|
||||
@router.patch("/api/haushalt/laeden/{store_id}")
|
||||
def update_store(store_id: int, data: dict = Body(...), user: dict = Depends(current_user)):
|
||||
"""Laden umbenennen ({name}) oder seine Abteilungs-Reihenfolge setzen ({categoryOrder}, null = wie die Liste)."""
|
||||
with transaction() as conn:
|
||||
row = _get_store(conn, store_id, user)
|
||||
if "name" in data:
|
||||
name = clean_text(data.get("name"), "Name des Ladens", MAX_STORE_NAME)
|
||||
if conn.execute(
|
||||
"SELECT 1 FROM stores WHERE list_id = ? AND name = ? COLLATE NOCASE AND id != ?", (row["list_id"], name, store_id)
|
||||
).fetchone():
|
||||
raise ValidationError("Diesen Laden gibt es in der Liste schon.")
|
||||
conn.execute("UPDATE stores SET name = ? WHERE id = ?", (name, store_id))
|
||||
if "categoryOrder" in data:
|
||||
order = data["categoryOrder"]
|
||||
if order is not None and (not isinstance(order, list) or sorted(order) != sorted(CATEGORY_KEYS)):
|
||||
raise ValidationError("Die Reihenfolge muss jede Kategorie genau einmal enthalten.")
|
||||
conn.execute("UPDATE stores SET category_order = ? WHERE id = ?", (json.dumps(order) if order else None, store_id))
|
||||
return _state(conn, user)
|
||||
|
||||
|
||||
@router.put("/api/haushalt/listen/{list_id}/laeden-reihenfolge")
|
||||
def order_stores(list_id: int, data: dict = Body(...), user: dict = Depends(current_user)):
|
||||
"""Reihenfolge der Läden ({order: [ids]}) – so erscheinen sie in der Einkaufsliste."""
|
||||
with transaction() as conn:
|
||||
_get_list(conn, list_id, user)
|
||||
ids = [r["id"] for r in conn.execute("SELECT id FROM stores WHERE list_id = ?", (list_id,))]
|
||||
order = data.get("order")
|
||||
if not isinstance(order, list) or sorted(order) != sorted(ids):
|
||||
raise ValidationError("Die Reihenfolge muss jeden Laden genau einmal enthalten.")
|
||||
conn.executemany("UPDATE stores SET position = ? WHERE id = ?", [(i, s) for i, s in enumerate(order)])
|
||||
return _state(conn, user)
|
||||
|
||||
|
||||
@router.delete("/api/haushalt/laeden/{store_id}")
|
||||
def delete_store(store_id: int, user: dict = Depends(current_user)):
|
||||
"""Laden löschen; seine Produkte bleiben auf der Liste, nur ohne Laden."""
|
||||
with transaction() as conn:
|
||||
_get_store(conn, store_id, user)
|
||||
conn.execute("DELETE FROM stores WHERE id = ?", (store_id,))
|
||||
return _state(conn, user)
|
||||
|
||||
|
||||
@router.put("/api/haushalt/listen/{list_id}/produkt-laden")
|
||||
def set_product_store(list_id: int, data: dict = Body(...), user: dict = Depends(current_user)):
|
||||
"""Produkt einem Laden zuordnen ({name, storeId}); storeId null = kein Laden. Gilt für diese Liste."""
|
||||
name = _clean_name(data.get("name"))
|
||||
with transaction() as conn:
|
||||
_get_list(conn, list_id, user)
|
||||
store_id = data.get("storeId")
|
||||
if store_id is not None:
|
||||
store_id = _get_store(conn, store_id, user, list_id)["id"]
|
||||
_assign_store(conn, list_id, _key(name), store_id)
|
||||
return _state(conn, user)
|
||||
|
||||
Reference in new issue
Block a user