Files
ScarriffleandClaude Opus 5 ab5d18e0d0 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>
2026-09-07 20:49:21 +02:00

76 lines
2.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Was die Verträge je Kategorie im Monat kosten.
Die eine Stelle, an der aus Verträgen eine Budget-Zahl wird. Budget-Seite und
Dashboard fragen beide hier – sonst könnten sie unterschiedliche Antworten
geben, und man müsste raten, welche stimmt.
Gerechnet wird in der Hauptwährung: Ein Vertrag in Euro und einer in Franken
lassen sich sonst nicht addieren. Was sich mangels Kurs nicht umrechnen lässt,
wird GEMELDET und nicht stillschweigend als Null verbucht – eine zu kleine
Summe im Budget ist schlimmer als gar keine.
"""
from __future__ import annotations
from collections import defaultdict
from datetime import date
from sqlalchemy.orm import Session
from ..models import Subscription, User
from . import currency
def _monatlich_in(db: Session, s: Subscription, base: str, heute: date) -> int | None:
"""Monatswert eines Vertrags in der Hauptwährung, oder None ohne Kurs."""
monatlich = s.monatlich_cents
if s.currency == base:
return monatlich
return currency.convert(db, monatlich, s.currency, base, heute)
def uebersicht(db: Session, user: User, base: str) -> dict:
"""Alles, was die Budget-Seite über die Verträge wissen muss.
``je_kategorie`` Monatswert je Kategorie, in Hauptwährung
``anzahl`` wie viele Verträge je Kategorie – „aus 2 Verträgen"
``ohne_kategorie`` Summe und Anzahl der Verträge, die keiner Kategorie
zugeordnet sind. Ohne diese Angabe fehlt ihr Geld im
Budget, ohne dass irgendwo steht, dass es fehlt.
``ohne_kurs`` Währungen, die sich nicht umrechnen liessen.
"""
heute = date.today()
je_kategorie: dict[int, int] = defaultdict(int)
anzahl: dict[int, int] = defaultdict(int)
ohne_kategorie_cents = 0
ohne_kategorie_anzahl = 0
ohne_kurs: set[str] = set()
for s in db.query(Subscription).filter(
Subscription.owner_id == user.id,
Subscription.is_active.is_(True),
):
cents = _monatlich_in(db, s, base, heute)
if cents is None:
ohne_kurs.add(s.currency)
continue
if s.category_id is None:
ohne_kategorie_cents += cents
ohne_kategorie_anzahl += 1
continue
je_kategorie[s.category_id] += cents
anzahl[s.category_id] += 1
return {
"je_kategorie": dict(je_kategorie),
"anzahl": dict(anzahl),
"ohne_kategorie_cents": ohne_kategorie_cents,
"ohne_kategorie_anzahl": ohne_kategorie_anzahl,
"ohne_kurs": sorted(ohne_kurs),
}
def monatlich_je_kategorie(db: Session, user: User, base: str) -> dict[int, int]:
"""Nur die Beträge – für die Soll-Berechnung."""
return uebersicht(db, user, base)["je_kategorie"]