Align spending report groups and totals with gross outflows
This commit is contained in:
@@ -110,8 +110,8 @@ WITH lines AS (
|
||||
)
|
||||
"""
|
||||
|
||||
# Spending = outflows (net of refunds) on budget accounts, excluding transfers
|
||||
# (credit card payments, moves to savings) and income.
|
||||
# Report cash flows on budget accounts, excluding transfers and internal
|
||||
# categories. Separate outflows from inflows rather than calling their net spending.
|
||||
SPENDING_FILTER = """
|
||||
a.on_budget AND l.transfer_account_id IS NULL
|
||||
AND NOT COALESCE(c.internal, false)
|
||||
@@ -415,7 +415,7 @@ async def ynab_spending_report(
|
||||
payee: str = "",
|
||||
top: int = 15,
|
||||
):
|
||||
"""Total YNAB spending grouped by category, category_group, payee, account, or month. Use group_by='month' for trends over time.
|
||||
"""Gross YNAB outflows grouped by category, category_group, payee, account, or month, with inflows reported separately. Use group_by='month' for trends over time.
|
||||
|
||||
Args:
|
||||
group_by: category, category_group, payee, account, or month
|
||||
@@ -436,6 +436,9 @@ async def ynab_spending_report(
|
||||
except ValueError:
|
||||
return "Dates must be YYYY-MM-DD."
|
||||
|
||||
if start > end:
|
||||
return "start_date must be on or before end_date."
|
||||
|
||||
conditions, params = [SPENDING_FILTER, "l.date >= $1", "l.date <= $2"], [start, end]
|
||||
if category:
|
||||
params.append(f"%{category}%")
|
||||
@@ -447,7 +450,7 @@ async def ynab_spending_report(
|
||||
order = "key" if group_by == "month" else "spent DESC"
|
||||
rows = await _query(
|
||||
f"""{LINES_CTE}
|
||||
SELECT {key} AS key, -SUM(l.amount) AS spent, count(*) AS n
|
||||
SELECT {key} AS key, -SUM(LEAST(l.amount, 0)) AS spent, count(*) AS n
|
||||
FROM lines l
|
||||
JOIN ynab_accounts a ON a.id = l.account_id
|
||||
LEFT JOIN ynab_categories c ON c.id = l.category_id
|
||||
@@ -457,8 +460,6 @@ async def ynab_spending_report(
|
||||
*params,
|
||||
)
|
||||
rows = [r for r in rows if r["spent"] > 0]
|
||||
if not rows:
|
||||
return f"No spending found from {start} to {end}."
|
||||
|
||||
totals = await _query(
|
||||
f"""{LINES_CTE}
|
||||
@@ -471,19 +472,25 @@ async def ynab_spending_report(
|
||||
*params,
|
||||
)
|
||||
outflows, inflows = totals[0]["outflows"], totals[0]["inflows"]
|
||||
if not outflows and not inflows:
|
||||
return f"No spending found from {start} to {end}."
|
||||
top = max(1, top)
|
||||
shown = rows if group_by == "month" else rows[:top]
|
||||
lines = [
|
||||
f"Spending by {group_by} from {start} to {end}:",
|
||||
f"- Total spending (outflows): {_usd(outflows)}",
|
||||
f"- Inflows (deposits into budget accounts, not spending): {_usd(inflows)}",
|
||||
"- Group amounts are gross outflows; refunds and other inflows are reported separately.",
|
||||
]
|
||||
lines += [f"- {r['key']}: {_usd(r['spent'])} ({r['n']} txns)" for r in shown]
|
||||
if len(rows) > len(shown):
|
||||
rest = sum((r["spent"] for r in rows[len(shown) :]), Decimal(0))
|
||||
lines.append(f"- {len(rows) - len(shown)} others: {_usd(rest)}")
|
||||
if group_by == "month" and len(rows) > 1:
|
||||
lines.append(f"Monthly average: {_usd(outflows / len(rows))}")
|
||||
months = (end.year - start.year) * 12 + end.month - start.month + 1
|
||||
if group_by == "month":
|
||||
lines.append(
|
||||
f"Monthly average (outflows over {months} calendar months): {_usd(outflows / months)}"
|
||||
)
|
||||
artifact = make_artifact(
|
||||
"ynab.spending_report",
|
||||
f"Spending by {group_by.replace('_', ' ')} · "
|
||||
@@ -498,6 +505,10 @@ async def ynab_spending_report(
|
||||
for r in rows
|
||||
],
|
||||
"total": _amount(outflows),
|
||||
"total_basis": "outflows",
|
||||
"inflows": _amount(inflows),
|
||||
"net_outflows": _amount(outflows - inflows),
|
||||
"filters": {"category": category, "payee": payee},
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user