fix: lueckenlose taegliche Saldo-Reihen fuer Grafana

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 08:32:36 +02:00
parent f5fa3e45e8
commit 09b34ae13b
2 changed files with 80 additions and 15 deletions

View File

@@ -26,22 +26,83 @@ _ACCOUNT_OFFSET = """
WHERE a.anchor_date IS NOT NULL AND a.anchor_balance IS NOT NULL
"""
# Per-account earliest day the daily series needs to start at: the earlier of
# the account's first confirmed booking and its anchor_date (an anchor can
# predate the first booking, e.g. an anchor set before any import). Accounts
# with neither an anchor nor any confirmed booking have no meaningful start
# and are excluded (nothing to plot).
_ACCOUNT_START = """
SELECT a.id AS account_id, a.name AS account,
LEAST(
COALESCE(a.anchor_date, ft.first_date),
COALESCE(ft.first_date, a.anchor_date)
) AS start_date
FROM accounts a
LEFT JOIN (
SELECT account_id, MIN(booking_date) AS first_date
FROM transactions WHERE status = 'confirmed'
GROUP BY account_id
) ft ON ft.account_id = a.id
WHERE a.anchor_date IS NOT NULL OR ft.first_date IS NOT NULL
"""
VIEWS: dict[str, str] = {
# Daily (gap-free) balance per account: one row per calendar day from the
# account's start date (see _ACCOUNT_START) through CURRENT_DATE. The
# cumulative SUM ... OVER (... ROWS UNBOUNDED PRECEDING) carries the
# previous day's balance forward on days without any confirmed booking,
# matching services.balances.account_balance's per-Tagesende semantics
# (base + cumulated amounts up to and including that day).
"v_balance_history": f"""
SELECT t.booking_date AS day, a.name AS account,
COALESCE(b.base, 0)
+ SUM(SUM(t.amount)) OVER (PARTITION BY a.id
ORDER BY t.booking_date) AS balance
FROM transactions t JOIN accounts a ON a.id = t.account_id
LEFT JOIN ({_ACCOUNT_OFFSET}) b ON b.account_id = a.id
WHERE t.status = 'confirmed'
GROUP BY a.id, a.name, t.booking_date, b.base""",
WITH offsets AS ({_ACCOUNT_OFFSET}),
bounds AS ({_ACCOUNT_START}),
days AS (
SELECT b.account_id, b.account, gs.day::date AS day
FROM bounds b
CROSS JOIN LATERAL generate_series(
b.start_date, CURRENT_DATE, interval '1 day') AS gs(day)
),
daily_tx AS (
SELECT account_id, booking_date AS day, SUM(amount) AS day_amount
FROM transactions WHERE status = 'confirmed'
GROUP BY account_id, booking_date
)
SELECT d.day, d.account,
COALESCE(o.base, 0)
+ SUM(COALESCE(dt.day_amount, 0)) OVER (
PARTITION BY d.account_id ORDER BY d.day
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS balance
FROM days d
LEFT JOIN daily_tx dt ON dt.account_id = d.account_id AND dt.day = d.day
LEFT JOIN offsets o ON o.account_id = d.account_id""",
# Daily (gap-free) total balance across all accounts: same carry-forward
# logic as v_balance_history, but summed globally (equivalent to summing
# each account's own base + cumulated amounts, since a cumulative sum of
# a union of per-day amounts equals the sum of per-account cumulative
# sums). The day series spans from the earliest account start date
# through CURRENT_DATE.
"v_balance_total": f"""
SELECT booking_date AS day,
(SELECT COALESCE(SUM(base), 0)
FROM ({_ACCOUNT_OFFSET}) s)
+ SUM(SUM(amount)) OVER (ORDER BY booking_date) AS balance
FROM transactions WHERE status = 'confirmed' GROUP BY booking_date""",
WITH offsets AS ({_ACCOUNT_OFFSET}),
bounds AS ({_ACCOUNT_START}),
days AS (
SELECT gs.day::date AS day
FROM (SELECT MIN(start_date) AS start_date FROM bounds) b
CROSS JOIN LATERAL generate_series(
b.start_date, CURRENT_DATE, interval '1 day') AS gs(day)
),
daily_tx AS (
SELECT booking_date AS day, SUM(amount) AS day_amount
FROM transactions WHERE status = 'confirmed'
GROUP BY booking_date
)
SELECT d.day,
(SELECT COALESCE(SUM(base), 0) FROM offsets)
+ SUM(COALESCE(dt.day_amount, 0)) OVER (
ORDER BY d.day
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS balance
FROM days d
LEFT JOIN daily_tx dt ON dt.day = d.day""",
"v_monthly_by_category": """
SELECT date_trunc('month', t.booking_date) AS month,
COALESCE(c.name, 'unkategorisiert') AS category,

View File

@@ -26,7 +26,9 @@
"custom": {
"drawStyle": "line",
"lineWidth": 1,
"fillOpacity": 0
"fillOpacity": 0,
"lineInterpolation": "stepAfter",
"spanNulls": true
},
"unit": "currencyEUR"
},
@@ -63,7 +65,9 @@
"custom": {
"drawStyle": "line",
"lineWidth": 2,
"fillOpacity": 10
"fillOpacity": 10,
"lineInterpolation": "stepAfter",
"spanNulls": true
},
"unit": "currencyEUR"
},