Auto-categorize with built-in merchant rules, payee category editor, income/net on dashboard
build-container / build (push) Successful in 35s
build-container / build (push) Successful in 35s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6073279a1b
commit
837f89e563
7 files changed
+191
-17
No files matched your search
@@ -74,7 +74,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=[], categories=[], excluded=0, updated="", setup=True, data_dir=str(DATA))
|
||||
if not L.WORKBOOK.exists(): return dict(tx=[], income=[], 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,9 +87,16 @@ 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 ""])
|
||||
ex = sum(1 for r in wb["Excluded Transfers"].iter_rows(min_row=5, max_col=1, values_only=True) if isinstance(r[0], dt.datetime))
|
||||
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
|
||||
ex += 1
|
||||
if r[5] == "Money in" and r[6] in ("Income (payroll deposit)", "Interest earned"):
|
||||
m = re.search(r"EFT (.+?)\s*[;,]", r[2] or "")
|
||||
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, 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, 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):
|
||||
@@ -124,6 +131,10 @@ 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 == "/api/payees":
|
||||
with LOCK:
|
||||
try: return self._send(200, L.uncategorized())
|
||||
except Exception as e: return self._send(500, {"error": str(e)})
|
||||
if u.path == "/api/state":
|
||||
with LOCK:
|
||||
try: return self._send(200, state())
|
||||
@@ -140,7 +151,13 @@ class H(BaseHTTPRequestHandler):
|
||||
if p.name == "loader_rules.json": json.loads(data) # must be valid JSON
|
||||
DATA.mkdir(parents=True, exist_ok=True); p.write_bytes(data)
|
||||
return self._send(200, {"saved": p.name})
|
||||
self.rfile.read(n)
|
||||
body = self.rfile.read(n)
|
||||
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']}."})
|
||||
if u.path == "/api/autocategorize":
|
||||
with LOCK: out = L.autocategorize()
|
||||
return self._send(200, {"message": f"Auto-categorized {out['updated']} payee(s); {out['remaining']} still need you. Backup: {out['backup']}."})
|
||||
if u.path == "/api/import":
|
||||
with LOCK:
|
||||
res = L.run_preview(DATA)
|
||||
|
||||
Reference in new issue
Block a user