feat: Eine Ausgabe, die kein Verlust ist
Sparen zaehlt im Budget wie eine Ausgabe – zu Recht, das Geld ist diesen Monat gebunden. Es stand danach aber in Rot da, und wer tausend zuruecklegte statt achthundert, bekam eine Warnung fuer etwas Gutes. Ein Haken an der Kategorie stellt das ab. Zwei Folgen: Der Betrag steht neutral. NICHT gruen: Gruen heisst in Certain ueberall "Geld kommt rein", und CHF 1'010 in Gruen liest sich als Einnahme. Grau sagt genau das Richtige – kein Verlust, kein Gewinn. Und mehr als geplant ist keine Ueberschreitung mehr. Das gilt auch eine Ebene hoeher: Eine Gruppe misst sich an dem, was WIRKLICH zu viel war. Sonst faerbt sich "Finanzen" rot, weil jemand gespart hat, und die echte Ueberschreitung daneben faellt nicht mehr auf. Angezeigt werden weiter die vollen Summen; nur das Urteil laesst das Sparen aus. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
15c2a164ec
commit
ff621984dc
@@ -170,12 +170,26 @@ def _build_view(db: Session, user: User, label: str) -> dict:
|
||||
cats.append({
|
||||
"category_id": c.id, "name": c.name, "icon": c.icon, "color": c.color,
|
||||
"planned_cents": p, "actual_cents": a, "delta_cents": p - a, "over": a > p,
|
||||
# "over" bleibt die nackte Tatsache; ob sie eine Warnung IST,
|
||||
# entscheidet die Kategorie. Wer mehr zuruecklegt als geplant,
|
||||
# hat nichts ueberzogen.
|
||||
"good": c.spending_is_good,
|
||||
})
|
||||
total_planned += g_planned
|
||||
total_actual += g_actual
|
||||
# Ueberschritten ist eine Gruppe an dem, was WIRKLICH zu viel war. Das
|
||||
# Sparen bleibt draussen: Sonst faerbt sich "Finanzen" rot, weil jemand
|
||||
# mehr zurueckgelegt hat als geplant – eine Warnung fuer etwas Gutes,
|
||||
# und daneben stuende die echte Ueberschreitung ununterscheidbar.
|
||||
# Angezeigt werden weiter die vollen Summen; nur das Urteil laesst das
|
||||
# Sparen aus.
|
||||
echt_geplant = sum(c["planned_cents"] for c in cats if not c["good"])
|
||||
echt_ist = sum(c["actual_cents"] for c in cats if not c["good"])
|
||||
groups_out.append({
|
||||
"id": g.id, "name": g.name, "icon": g.icon, "color": g.color,
|
||||
"planned_cents": g_planned, "actual_cents": g_actual, "over": g_actual > g_planned,
|
||||
"planned_cents": g_planned, "actual_cents": g_actual,
|
||||
"over": echt_ist > echt_geplant,
|
||||
"good": bool(cats) and all(c["good"] for c in cats),
|
||||
"categories": cats,
|
||||
})
|
||||
|
||||
|
||||
@@ -155,6 +155,7 @@ def create_category(
|
||||
icon=payload.icon,
|
||||
color=payload.color,
|
||||
excluded_from_stats=payload.excluded_from_stats,
|
||||
spending_is_good=payload.spending_is_good,
|
||||
position=_next_position(db, Category, owner_id=user.id, group_id=group.id),
|
||||
)
|
||||
db.add(cat)
|
||||
@@ -180,7 +181,8 @@ def update_category(
|
||||
for key, value in data.items():
|
||||
# Bei den Schaltern zählt auch False als gesetzter Wert – sonst ließe sich
|
||||
# ein einmal gesetztes Häkchen nie wieder entfernen.
|
||||
if value is not None or key in ("is_archived", "excluded_from_stats"):
|
||||
if value is not None or key in ("is_archived", "excluded_from_stats",
|
||||
"spending_is_good"):
|
||||
setattr(cat, key, value)
|
||||
db.commit()
|
||||
db.refresh(cat)
|
||||
|
||||
@@ -108,6 +108,15 @@ def _units(groups: list[CategoryGroup], by_category: bool):
|
||||
yield (g.name, g.color, [c.id for c in g.categories if not c.is_archived])
|
||||
|
||||
|
||||
def _kein_verlust(groups: list[CategoryGroup]) -> set[int]:
|
||||
"""Kategorien, deren Ausgabe kein Verlust ist (Sparen, Ruecklagen).
|
||||
|
||||
Ihre Betraege stehen neutral statt rot, und mehr als geplant ist bei ihnen
|
||||
keine Warnung.
|
||||
"""
|
||||
return {c.id for g in groups for c in g.categories if c.spending_is_good}
|
||||
|
||||
|
||||
@router.get("/budget-donut")
|
||||
def budget_donut(
|
||||
group_by: str = Query(default="group"),
|
||||
@@ -163,15 +172,22 @@ def expense_share(
|
||||
|
||||
# Als Liste: ``_units`` ist ein Generator, und er wird hier zweimal
|
||||
# gebraucht – beim zweiten Mal waere er leer und das Geplante 0.
|
||||
einheiten = list(_units(_expense_groups(db, user), by_category))
|
||||
gruppen = _expense_groups(db, user)
|
||||
gut = _kein_verlust(gruppen)
|
||||
einheiten = list(_units(gruppen, by_category))
|
||||
rows = []
|
||||
for name, color, ids in einheiten:
|
||||
val = sum(actual.get(i, 0) for i in ids)
|
||||
if val > 0:
|
||||
geplant = sum(planned.get(i, 0) for i in ids)
|
||||
# Ueberschritten ist die Zeile an dem, was WIRKLICH zu viel war –
|
||||
# das Sparen bleibt beim Urteil draussen, nicht beim Betrag.
|
||||
echt_geplant = sum(planned.get(i, 0) for i in ids if i not in gut)
|
||||
echt_ist = sum(actual.get(i, 0) for i in ids if i not in gut)
|
||||
rows.append({"name": name, "color": color, "amount": val,
|
||||
"planned": geplant,
|
||||
"over": geplant > 0 and val > geplant,
|
||||
"over": echt_geplant > 0 and echt_ist > echt_geplant,
|
||||
"good": bool(ids) and all(i in gut for i in ids),
|
||||
# Womit sich die Zeile aufschluesseln laesst.
|
||||
"ids": list(ids)})
|
||||
rows.sort(key=lambda r: r["amount"], reverse=True)
|
||||
@@ -251,12 +267,18 @@ def budget_progress(
|
||||
planned = budget.effective_planned(db, user, label)
|
||||
actual = budget.actual_expense_by_category(db, user, label, start, end, base)
|
||||
by_category = group_by == "category"
|
||||
gruppen = _expense_groups(db, user)
|
||||
gut = _kein_verlust(gruppen)
|
||||
rows = []
|
||||
for name, color, ids in _units(_expense_groups(db, user), by_category):
|
||||
for name, color, ids in _units(gruppen, by_category):
|
||||
gp = sum(planned.get(i, 0) for i in ids)
|
||||
ga = sum(actual.get(i, 0) for i in ids)
|
||||
if gp > 0 or ga > 0:
|
||||
rows.append({"name": name, "color": color, "planned": gp, "actual": ga, "over": ga > gp})
|
||||
echt_geplant = sum(planned.get(i, 0) for i in ids if i not in gut)
|
||||
echt_ist = sum(actual.get(i, 0) for i in ids if i not in gut)
|
||||
rows.append({"name": name, "color": color, "planned": gp, "actual": ga,
|
||||
"over": echt_ist > echt_geplant,
|
||||
"good": bool(ids) and all(i in gut for i in ids)})
|
||||
return {"rows": rows, "currency": base, "label": label,
|
||||
"group_by": "category" if by_category else "group"}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user