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
@@ -7,6 +7,7 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
import statement_loader as L
|
||||
import budget as B
|
||||
|
||||
PORT = int(os.environ.get("PORT", "8765"))
|
||||
BIND = os.environ.get("BIND", "127.0.0.1") # in Docker: 0.0.0.0 (publish the port to 127.0.0.1 only)
|
||||
@@ -74,7 +75,7 @@ _DATA = {}
|
||||
def workbook_data():
|
||||
"""Read-only snapshot of the Bill Log for the web dashboard (computed from raw cells, not cached formulas)."""
|
||||
from openpyxl import load_workbook
|
||||
if not L.WORKBOOK.exists(): return dict(tx=[], income=[], categories=[], excluded=0, updated="", setup=True, data_dir=str(DATA))
|
||||
if not L.WORKBOOK.exists(): return dict(tx=[], income=[], budget=[], categories=[], excluded=0, updated="", setup=True, data_dir=str(DATA))
|
||||
key = L.WORKBOOK.stat().st_mtime_ns
|
||||
if _DATA.get("key") == key: return _DATA["val"]
|
||||
wb = load_workbook(L.WORKBOOK, read_only=True)
|
||||
@@ -87,6 +88,11 @@ def workbook_data():
|
||||
if isinstance(r[0], dt.datetime) and r[1] and r[5] is not None:
|
||||
c, t = pay.get(r[1], ("Other / Unsorted", "Spending"))
|
||||
tx.append([f"{r[0]:%Y-%m-%d}", r[1], c, t, r[4] or "", round(float(r[5]), 2), r[6] or "", r[8] or ""])
|
||||
budget = []
|
||||
if "Budget" in wb.sheetnames:
|
||||
for r in wb["Budget"].iter_rows(min_row=2, max_col=3, values_only=True):
|
||||
if r[0] and r[1] and isinstance(r[2], (int, float)):
|
||||
budget.append([r[0] if isinstance(r[0], str) else f"{r[0]:%Y-%m}", r[1], float(r[2])])
|
||||
ex, income = 0, []
|
||||
for r in wb["Excluded Transfers"].iter_rows(min_row=5, max_col=7, values_only=True):
|
||||
if not isinstance(r[0], dt.datetime): continue
|
||||
@@ -96,7 +102,7 @@ def workbook_data():
|
||||
src = (m.group(1).strip().title() if m else (r[2] or "Other").title()) if r[6].startswith("Income") else "Interest"
|
||||
income.append([f"{r[0]:%Y-%m-%d}", src, "Interest" if r[6].startswith("Interest") else "Paycheck", round(float(r[4]), 2), r[1] or ""])
|
||||
wb.close()
|
||||
_DATA["key"], _DATA["val"] = key, dict(tx=tx, income=income, categories=cats, excluded=ex, updated=f"{dt.datetime.fromtimestamp(L.WORKBOOK.stat().st_mtime):%b %-d, %Y %-I:%M %p}")
|
||||
_DATA["key"], _DATA["val"] = key, dict(tx=tx, income=income, budget=budget, categories=cats, excluded=ex, updated=f"{dt.datetime.fromtimestamp(L.WORKBOOK.stat().st_mtime):%b %-d, %Y %-I:%M %p}")
|
||||
return _DATA["val"]
|
||||
|
||||
def safe_name(name):
|
||||
@@ -131,6 +137,16 @@ class H(BaseHTTPRequestHandler):
|
||||
with LOCK:
|
||||
try: return self._send(200, workbook_data())
|
||||
except Exception as e: return self._send(500, {"error": str(e)})
|
||||
if u.path == "/budget":
|
||||
f = HERE / "app_budget.html"
|
||||
return self._send(200, f.read_bytes().replace(b"__TOKEN__", TOKEN.encode()) if f.exists() else b"app_budget.html missing", "text/html; charset=utf-8")
|
||||
if u.path == "/api/budget":
|
||||
month = parse_qs(u.query).get("month", [B.this_month()])[0]
|
||||
with LOCK:
|
||||
try:
|
||||
if not L.WORKBOOK.exists(): return self._send(200, {"setup": True})
|
||||
return self._send(200, B.compute(workbook_data(), month))
|
||||
except Exception as e: return self._send(500, {"error": str(e)})
|
||||
if u.path == "/api/payees":
|
||||
with LOCK:
|
||||
try: return self._send(200, L.uncategorized())
|
||||
@@ -152,6 +168,10 @@ class H(BaseHTTPRequestHandler):
|
||||
DATA.mkdir(parents=True, exist_ok=True); p.write_bytes(data)
|
||||
return self._send(200, {"saved": p.name})
|
||||
body = self.rfile.read(n)
|
||||
if u.path == "/api/budget":
|
||||
req = json.loads(body or b"{}")
|
||||
with LOCK: out = L.set_budget(req["month"], req["budgets"])
|
||||
return self._send(200, {"message": f"Saved the {req['month']} budget ({out['rows']} envelopes). Backup: {out['backup']}."})
|
||||
if u.path == "/api/set_categories":
|
||||
with LOCK: out = L.set_categories(json.loads(body or b"[]"))
|
||||
return self._send(200, {"message": f"Updated {out['updated']} payee(s). Backup: {out['backup']}."})
|
||||
|
||||
Reference in new issue
Block a user