From 67a32bdda900be30b4d50772d952ab8219c759c7 Mon Sep 17 00:00:00 2001 From: Guido Date: Sun, 30 Aug 2026 13:41:22 +0200 Subject: [PATCH] feat: Aufschluesselung zeigt, was der Stichtag ausschliesst MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Buchungen bis einschliesslich des Stichtags zaehlen nicht in den Kontostand – sie stecken schon im Anfangsbestand. In der Liste stehen sie aber, und in den Auswertungen zaehlen sie mit. Das ist richtig so, war in der Oberflaeche aber nirgends zu sehen: Der Unterschied zwischen dem, was man sieht, und dem, was gerechnet wird, liess sich nicht erklaeren. Ein Konto, dessen Stichtag nach dem Beginn des eingelesenen Auszugs liegt, zeigt deshalb einen Kontostand, der zu den sichtbaren Buchungen nicht passt – und niemand kaeme darauf, warum. Die Rechnung nennt jetzt Stichtag, Anzahl und Summe der ausgeschlossenen Buchungen. Co-Authored-By: Claude Opus 5 --- backend/app/services/balances.py | 31 +++++++++++++++++++++++++++++++ backend/tests/test_accounts.py | 31 +++++++++++++++++++++++++++++++ web/src/locales/de.json | 3 ++- web/src/locales/en.json | 3 ++- web/src/pages/Accounts.jsx | 14 ++++++++++++++ 5 files changed, 80 insertions(+), 2 deletions(-) diff --git a/backend/app/services/balances.py b/backend/app/services/balances.py index c2ef4ab..afc89b3 100644 --- a/backend/app/services/balances.py +++ b/backend/app/services/balances.py @@ -71,6 +71,37 @@ def aufschluesselung(db: Session, account: Account) -> dict: - teile["uebertraege_raus"]["cents"] + teile["uebertraege_rein"]["cents"]), "anzahl": 0, } + + # Was der Stichtag ausschliesst. Diese Buchungen stehen in der Liste und in + # den Auswertungen, zaehlen aber nicht in den Kontostand – sie stecken schon + # im Anfangsbestand. Ohne diese Angabe ist der Unterschied zwischen dem, was + # man sieht, und dem, was gerechnet wird, nicht zu erklaeren. + if account.opening_date is not None: + bis = Transaction.date <= account.opening_date + + def vorher(spalte, *bedingungen) -> tuple[int, int]: + summe, anzahl = (db.query(func.sum(spalte), func.count()) + .filter(*bedingungen).filter(bis).one()) + return int(summe or 0), int(anzahl or 0) + + ein, n1 = vorher(Transaction.amount_cents, + Transaction.account_id == account.id, + Transaction.type == TxnType.income) + aus, n2 = vorher(Transaction.amount_cents, + Transaction.account_id == account.id, + Transaction.type == TxnType.expense) + raus, n3 = vorher(Transaction.amount_cents, + Transaction.account_id == account.id, + Transaction.type == TxnType.transfer) + rein, n4 = vorher(func.coalesce(Transaction.to_amount_cents, + Transaction.amount_cents), + Transaction.to_account_id == account.id, + Transaction.type == TxnType.transfer) + teile["vor_stichtag"] = {"cents": ein - aus - raus + rein, + "anzahl": n1 + n2 + n3 + n4} + else: + teile["vor_stichtag"] = {"cents": 0, "anzahl": 0} + teile["stichtag"] = account.opening_date.isoformat() if account.opening_date else None return teile diff --git a/backend/tests/test_accounts.py b/backend/tests/test_accounts.py index 7a26494..9298ba9 100644 --- a/backend/tests/test_accounts.py +++ b/backend/tests/test_accounts.py @@ -269,3 +269,34 @@ def test_saldo_laesst_sich_aufschluesseln(admin): gegen = admin.get(f"/api/accounts/{b['id']}/aufschluesselung").json() assert gegen["teile"]["uebertraege_rein"] == {"cents": 30000, "anzahl": 1} assert gegen["uebertraege"][0]["raus"] is False + + +def test_aufschluesselung_zeigt_was_der_stichtag_ausschliesst(admin): + """Buchungen bis zum Stichtag stehen in der Liste, zaehlen aber nicht in + den Kontostand – sie stecken schon im Anfangsbestand. In der Oberflaeche + war das unsichtbar, und der Unterschied zwischen Gesehenem und Gerechnetem + nicht zu erklaeren.""" + import secrets + + konto = admin.post("/api/accounts", json={ + "name": f"Stichtag {secrets.token_hex(3)}", "type": "girokonto", + "currency": "CHF", "opening_balance_cents": 120000, + "opening_date": "2026-08-15"}).json() + + # Vor dem Stichtag – steckt schon im Anfangsbestand. + admin.post("/api/transactions", json={ + "type": "expense", "date": "2026-08-10", "account_id": konto["id"], + "amount_cents": 150000, "payee": "Alt"}) + # Danach – zaehlt. + admin.post("/api/transactions", json={ + "type": "expense", "date": "2026-08-20", "account_id": konto["id"], + "amount_cents": 30000, "payee": "Neu"}) + + teile = admin.get(f"/api/accounts/{konto['id']}/aufschluesselung").json()["teile"] + assert teile["stichtag"] == "2026-08-15" + assert teile["vor_stichtag"] == {"cents": -150000, "anzahl": 1} + assert teile["ausgaben"] == {"cents": 30000, "anzahl": 1}, "nur die nach dem Stichtag" + assert teile["saldo"]["cents"] == 120000 - 30000 + + # Und die Kontenliste sagt dasselbe. + assert admin.get(f"/api/accounts/{konto['id']}").json()["balance_cents"] == 90000 diff --git a/web/src/locales/de.json b/web/src/locales/de.json index 9ac8e0d..1fdbb5f 100644 --- a/web/src/locales/de.json +++ b/web/src/locales/de.json @@ -190,7 +190,8 @@ "saldo": "ergibt", "bankGebucht": "Bank (gebucht)", "bankVerfuegbar": "Bank (verfügbar)", - "legs": "Die Überträge dieses Kontos" + "legs": "Die Überträge dieses Kontos", + "beforeCutoff": "davor ({{n}} bis {{datum}}, schon im Anfangsbestand)" } }, "accountTypes": { diff --git a/web/src/locales/en.json b/web/src/locales/en.json index 65e2e1f..552c9b8 100644 --- a/web/src/locales/en.json +++ b/web/src/locales/en.json @@ -190,7 +190,8 @@ "saldo": "makes", "bankGebucht": "Bank (booked)", "bankVerfuegbar": "Bank (available)", - "legs": "This account's transfers" + "legs": "This account's transfers", + "beforeCutoff": "before ({{n}} up to {{datum}}, already in the opening balance)" } }, "accountTypes": { diff --git a/web/src/pages/Accounts.jsx b/web/src/pages/Accounts.jsx index cfaabbd..677a796 100644 --- a/web/src/pages/Accounts.jsx +++ b/web/src/pages/Accounts.jsx @@ -544,6 +544,20 @@ function Aufschluesselung({ daten, t }) { return (
{zeile("anfangsbestand", "")} + {/* Der Stichtag ist der haeufigste unsichtbare Grund dafuer, dass die + Liste etwas anderes zeigt als der Kontostand. */} + {teile.stichtag && teile.vor_stichtag.anzahl > 0 && ( +
+ + {t("accounts.part.beforeCutoff", { + datum: formatDate(teile.stichtag), n: teile.vor_stichtag.anzahl, + })} + + + {formatMoney(teile.vor_stichtag.cents, waehrung)} + +
+ )} {zeile("einnahmen", "+")} {zeile("ausgaben", "−")} {zeile("uebertraege_raus", "−")}