Envelope budget: Budget page, per-month assignments stored on a Budget tab, rollover and ready-to-assign
build-container / build (push) Successful in 50s
build-container / build (push) Successful in 50s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
837f89e563
commit
4b93d6229d
8 files changed
+235
-5
No files matched your search
@@ -0,0 +1,51 @@
|
||||
"""Envelope budget maths. Pure functions on the dashboard data (no workbook access here).
|
||||
|
||||
Model: each category on the Lists tab is an envelope. Each month you assign money to envelopes ("Budgeted").
|
||||
Spending comes from the Bill Log. Available = everything ever budgeted (since the first budget month) minus
|
||||
everything spent in that envelope since then, so leftovers roll over and overspending carries forward as a negative.
|
||||
Ready to assign = income received since the first budget month minus everything budgeted so far.
|
||||
"""
|
||||
import datetime as dt
|
||||
from collections import defaultdict
|
||||
|
||||
def ym_add(ym, n):
|
||||
y, m = int(ym[:4]), int(ym[5:7]); k = y * 12 + (m - 1) + n
|
||||
return f"{k // 12}-{k % 12 + 1:02d}"
|
||||
|
||||
def this_month(): return f"{dt.date.today():%Y-%m}"
|
||||
|
||||
def compute(D, month):
|
||||
cats = list(D.get("categories") or [])
|
||||
spent = defaultdict(float) # (cat, ym)
|
||||
for t in D["tx"]:
|
||||
spent[(t[2], t[0][:7])] += t[5]
|
||||
if t[2] not in cats: cats.append(t[2])
|
||||
income = defaultdict(float)
|
||||
for i in D.get("income", []): income[i[0][:7]] += i[3]
|
||||
bud = defaultdict(float) # (cat, ym)
|
||||
for m, env, amt in D.get("budget", []):
|
||||
bud[(env, m)] += amt
|
||||
if env not in cats: cats.append(env)
|
||||
budget_months = sorted({m for (_, m) in bud})
|
||||
start = min(budget_months[0], month) if budget_months else month
|
||||
first_tx = min((t[0][:7] for t in D["tx"]), default=month)
|
||||
|
||||
def upto(table, c): # sum of table[(c, m)] for start <= m <= month
|
||||
return sum(v for (cc, m), v in table.items() if cc == c and start <= m <= month)
|
||||
rows = []
|
||||
for c in cats:
|
||||
b = bud.get((c, month), 0.0); s = spent.get((c, month), 0.0)
|
||||
avail = upto(bud, c) - upto(spent, c)
|
||||
recent = [spent.get((c, ym_add(month, -k)), 0.0) for k in (1, 2, 3) if ym_add(month, -k) >= first_tx]
|
||||
suggest = round(max(0.0, sum(recent) / len(recent)) / 5) * 5 if recent else 0
|
||||
rows.append(dict(envelope=c, budgeted=round(b, 2), spent=round(s, 2), prior=round(avail - b + s, 2),
|
||||
available=round(avail, 2), suggest=suggest))
|
||||
inc_month = income.get(month, 0.0)
|
||||
inc_cum = sum(v for m, v in income.items() if start <= m <= month)
|
||||
bud_cum = sum(v for (_, m), v in bud.items() if m <= month)
|
||||
months, m = [], min(first_tx, start)
|
||||
while m <= ym_add(this_month(), 1):
|
||||
months.append(m); m = ym_add(m, 1)
|
||||
return dict(month=month, start=start, envelopes=rows, income_month=round(inc_month, 2),
|
||||
ready=round(inc_cum - bud_cum, 2), budgeted_month=round(sum(r["budgeted"] for r in rows), 2),
|
||||
has_budget=bool(budget_months), months=months, prev_has=any(v for (c, m), v in bud.items() if m == ym_add(month, -1)))
|
||||
Reference in new issue
Block a user