diff --git a/backend/app/routers/dashboard.py b/backend/app/routers/dashboard.py index 5c46878..db44ebd 100644 --- a/backend/app/routers/dashboard.py +++ b/backend/app/routers/dashboard.py @@ -158,13 +158,20 @@ def expense_share( label = _label(db, user) start, end = budget.period_bounds(label, _start_day(db, user)) actual = budget.actual_expense_by_category(db, user, start, end, base) + # Das Geplante kommt mit, damit die Karte zeigen kann, ob ein Anteil noch + # im Rahmen liegt. Ohne Plan wird nichts behauptet: „ueber Budget" waere + # sonst die Aussage ueber jede Kategorie, fuer die nie eine gemacht wurde. + planned = budget.effective_planned(db, user, label) by_category = group_by != "group" rows = [] for name, color, ids in _units(_expense_groups(db, user), by_category): val = sum(actual.get(i, 0) for i in ids) if val > 0: - rows.append({"name": name, "color": color, "amount": val}) + geplant = sum(planned.get(i, 0) for i in ids) + rows.append({"name": name, "color": color, "amount": val, + "planned": geplant, + "over": geplant > 0 and val > geplant}) rows.sort(key=lambda r: r["amount"], reverse=True) return { "rows": rows, diff --git a/backend/tests/test_budget.py b/backend/tests/test_budget.py index 37f5132..89ca057 100644 --- a/backend/tests/test_budget.py +++ b/backend/tests/test_budget.py @@ -167,3 +167,45 @@ def test_budget_monate_stossen_lueckenlos_aneinander(): f"Starttag {start_day}: {label} endet {bis}, " f"{naechstes} beginnt {von_naechstes}") label = naechstes + + +def test_ausgabenanteile_nennen_das_geplante(admin): + """Die Karte soll zeigen, ob ein Anteil noch im Rahmen liegt. Ohne Plan + wird nichts behauptet – 'ueber Budget' waere sonst die Aussage ueber jede + Kategorie, fuer die nie eine Vorgabe gemacht wurde.""" + import datetime + + leb = _lebensmittel(admin) + name = next(c["name"] for g in admin.get("/api/categories").json() + for c in g["categories"] if c["id"] == leb) + konto = admin.post("/api/accounts", json={ + "name": "Anteile", "type": "girokonto", "currency": "EUR", + "opening_balance_cents": 0}).json() + admin.post("/api/transactions", json={ + "type": "expense", "date": datetime.date.today().isoformat(), + "account_id": konto["id"], "amount_cents": 60000, "category_id": leb}) + + # Ueber den MONATS-Wert, nicht die Vorlage: Ein vorhandener Monats-Wert + # aus einem anderen Test uebersteuert die Vorlage sonst. + label = admin.get("/api/budget/current").json()["label"] + + def plane(cents): + r = admin.put(f"/api/budget/{label}/category/{leb}", + params={"planned_cents": cents}) + assert r.status_code == 200, r.text + + def zeile(): + rows = admin.get("/api/dashboard/expense-share").json()["rows"] + return next(r for r in rows if r["name"] == name) + + ist = zeile()["amount"] + + plane(0) + assert zeile()["planned"] == 0 + assert zeile()["over"] is False, "ohne Plan darf nichts als ueberschritten gelten" + + plane(ist + 10000) + assert zeile()["over"] is False, "im Rahmen" + + plane(max(1, ist - 10000)) + assert zeile()["over"] is True, "ueberschritten" diff --git a/web/src/dashboard/cards.jsx b/web/src/dashboard/cards.jsx index 54d0e37..55fb723 100644 --- a/web/src/dashboard/cards.jsx +++ b/web/src/dashboard/cards.jsx @@ -178,7 +178,13 @@ function ExpenseShare({ props }) { ● {r.name} {pct.toFixed(1).replace(".", ",")} % - {formatMoney(r.amount, data.currency)} + {/* Grün im geplanten Rahmen, rot darüber – dieselben Farben wie + überall sonst für Einnahme und Ausgabe. Ohne Plan bleibt der + Betrag gedeckt: Da gibt es nichts zu überschreiten. */} + 0 ? (r.over ? "neg" : "pos") : "muted"}`}> + {formatMoney(r.amount, data.currency)} +