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 (