feat: Der Soll-Betrag kann sich aus den Vertraegen rechnen

Die Vertraege standen bisher nur auf ihrer eigenen Seite. Im Budget sah man
nichts davon – man musste selbst nachrechnen, was fest gebunden ist, und beim
naechsten Preisaufschlag noch einmal.

Jetzt hat jede Kategorie zwei Betriebsarten, umschaltbar ueber ein Sigma neben
dem Eingabefeld:

  fest        eine Zahl, die man hinschreibt. Wie bisher.
  berechnet   Summe der Vertraege dieser Kategorie plus einem Zuschlag.

Der Zuschlag ist der Punkt. Wer fuer Streaming 62 plant, weil 42 gebunden sind
und 20 Luft sein sollen, muss beim naechsten Preisaufschlag nicht nachrechnen
– und verliert seine 20 dabei auch nicht. Unter der Zeile steht die Rechnung
offen, sonst stuende im Feld eine Zahl, die nicht der Soll-Betrag ist.

Das gilt fuer JEDE Kategorie, nicht nur fuer eine Abo-Kategorie: Ein
Handyvertrag liegt in "Telefon & Internet", und die rechnet sich genauso.

Auch auf "fest" steht der Hinweis da ("davon 16.84 fest gebunden") – sonst
sieht man nie, was einen schon bindet.

Zwei Dinge, die sonst still danebengingen, melden sich jetzt oben auf der
Seite: Vertraege OHNE Kategorie (ihr Geld fehlt im Budget, ohne dass irgendwo
steht, dass es fehlt) und Waehrungen ohne Kurs.

Gerechnet wird in effective_planned – der einen Stelle, aus der Budget-Seite
UND Dashboard ihr Soll beziehen. Damit koennen die beiden gar nicht erst
auseinanderlaufen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-07 20:49:21 +02:00
co-authored by Claude Opus 5
parent 26718b1301
commit ab5d18e0d0
11 changed files with 613 additions and 31 deletions
+59 -12
View File
@@ -122,14 +122,41 @@ def _period(db: Session, user: User, label: str) -> BudgetPeriod | None:
)
def effective_planned(db: Session, user: User, label: str) -> dict[int, int]:
"""Wirksame Soll-Beträge je Kategorie: Monats-Override falls vorhanden, sonst Vorlage."""
def hauptwaehrung(db: Session, user: User) -> str:
"""Waehrung, in der Soll-Betraege gefuehrt werden."""
from ..routers.settings import BASE_CURRENCY_KEY, get_user_setting
return get_user_setting(db, user, BASE_CURRENCY_KEY) or "EUR"
def soll_einer_zeile(zeile, aus_vertraegen: dict[int, int]) -> int:
"""Was eine Budget-Zeile als Soll bedeutet.
Bei "berechnet" ist die hingeschriebene Zahl nicht der Soll-Betrag, sondern
nur der Zuschlag – dazu kommt, was die Vertraege dieser Kategorie kosten.
"""
if zeile.modus == "berechnet":
return aus_vertraegen.get(zeile.category_id, 0) + zeile.zuschlag_cents
return zeile.planned_cents
def _zeilen(db: Session, user: User, label: str):
"""Die wirksamen Budget-Zeilen: Monats-Override falls vorhanden, sonst Vorlage."""
period = _period(db, user, label)
if period is not None:
return {ln.category_id: ln.planned_cents for ln in period.lines}
return period.lines
return db.query(BudgetTemplateLine).filter(
BudgetTemplateLine.owner_id == user.id).all()
def effective_planned(db: Session, user: User, label: str) -> dict[int, int]:
"""Wirksame Soll-Beträge je Kategorie: Monats-Override falls vorhanden, sonst Vorlage."""
from . import abos
aus_vertraegen = abos.monatlich_je_kategorie(db, user, hauptwaehrung(db, user))
return {
tl.category_id: tl.planned_cents
for tl in db.query(BudgetTemplateLine).filter(BudgetTemplateLine.owner_id == user.id)
ln.category_id: soll_einer_zeile(ln, aus_vertraegen)
for ln in _zeilen(db, user, label)
}
@@ -145,23 +172,43 @@ def ensure_customized(db: Session, user: User, label: str) -> BudgetPeriod:
db.add(period)
db.flush()
for tl in db.query(BudgetTemplateLine).filter(BudgetTemplateLine.owner_id == user.id):
period.lines.append(
BudgetPeriodLine(category_id=tl.category_id, planned_cents=tl.planned_cents)
)
period.lines.append(BudgetPeriodLine(
category_id=tl.category_id, planned_cents=tl.planned_cents,
# Ohne diese beiden fiele ein angepasster Monat auf die Zahl
# zurueck, die zufaellig zuletzt in ``planned_cents`` stand.
modus=tl.modus, zuschlag_cents=tl.zuschlag_cents,
))
db.flush()
return period
def set_month_category(db: Session, user: User, label: str, category_id: int, planned_cents: int) -> None:
def set_month_category(db: Session, user: User, label: str, category_id: int,
planned_cents: int | None = None, modus: str | None = None,
zuschlag_cents: int | None = None) -> None:
period = ensure_customized(db, user, label)
line = next((ln for ln in period.lines if ln.category_id == category_id), None)
if line is None:
period.lines.append(BudgetPeriodLine(category_id=category_id, planned_cents=planned_cents))
else:
line.planned_cents = planned_cents
line = BudgetPeriodLine(category_id=category_id, planned_cents=0)
period.lines.append(line)
uebernimm(line, planned_cents, modus, zuschlag_cents)
db.commit()
def uebernimm(zeile, planned_cents: int | None, modus: str | None,
zuschlag_cents: int | None) -> None:
"""Setzt an einer Budget-Zeile, was mitgegeben wurde – und nur das.
Getrennt gehalten, weil Vorlage und Monat dieselbe Regel brauchen und zwei
Kopien davon genau die Stelle waeren, an der sie auseinanderlaufen.
"""
if planned_cents is not None:
zeile.planned_cents = max(0, planned_cents)
if zuschlag_cents is not None:
zeile.zuschlag_cents = max(0, zuschlag_cents)
if modus is not None:
zeile.modus = modus
def reset_month(db: Session, user: User, label: str) -> None:
period = _period(db, user, label)
if period is not None: