Add new categories/envelopes from the Budget page and the loader's categorize card
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
690316e541
commit
5068021b09
4 files changed
+33
-1
No files matched your search
@@ -172,6 +172,12 @@ class H(BaseHTTPRequestHandler):
|
|||||||
req = json.loads(body or b"{}")
|
req = json.loads(body or b"{}")
|
||||||
with LOCK: out = L.set_budget(req["month"], req["budgets"])
|
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']}."})
|
return self._send(200, {"message": f"Saved the {req['month']} budget ({out['rows']} envelopes). Backup: {out['backup']}."})
|
||||||
|
if u.path == "/api/add_category":
|
||||||
|
try:
|
||||||
|
with LOCK: out = L.add_category(json.loads(body or b"{}").get("name"))
|
||||||
|
except ValueError as e: return self._send(400, {"error": str(e)})
|
||||||
|
note = " It will show on the web pages; the Excel summary tabs only have 20 category slots." if out["beyond_excel"] else ""
|
||||||
|
return self._send(200, {"message": f"Added category '{out['name']}'.{note} Backup: {out['backup']}.", "name": out["name"]})
|
||||||
if u.path == "/api/set_categories":
|
if u.path == "/api/set_categories":
|
||||||
with LOCK: out = L.set_categories(json.loads(body or b"[]"))
|
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']}."})
|
return self._send(200, {"message": f"Updated {out['updated']} payee(s). Backup: {out['backup']}."})
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ input.amt{width:110px;text-align:right}
|
|||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
<button id="copy" title="Use last month's amounts">Copy last month</button>
|
<button id="copy" title="Use last month's amounts">Copy last month</button>
|
||||||
<button id="suggest" title="Average of the last 3 months of spending">Suggest from spending</button>
|
<button id="suggest" title="Average of the last 3 months of spending">Suggest from spending</button>
|
||||||
|
<button id="addenv" title="Add a new category / envelope">Add envelope</button>
|
||||||
<button id="movebtn">Move money</button>
|
<button id="movebtn">Move money</button>
|
||||||
<button class="primary" id="save" disabled>Save budget</button>
|
<button class="primary" id="save" disabled>Save budget</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -105,6 +106,11 @@ $("#showall").onchange=render;
|
|||||||
$("#copy").onclick=async()=>{const p=await (await fetch("/api/budget?month="+add(month,-1))).json();p.envelopes.forEach(e=>{const r=rows.find(x=>x.envelope==e.envelope);if(r)r.edit=e.budgeted});dirty=true;render()};
|
$("#copy").onclick=async()=>{const p=await (await fetch("/api/budget?month="+add(month,-1))).json();p.envelopes.forEach(e=>{const r=rows.find(x=>x.envelope==e.envelope);if(r)r.edit=e.budgeted});dirty=true;render()};
|
||||||
$("#suggest").onclick=()=>{rows.forEach(r=>{if(r.suggest)r.edit=r.suggest});dirty=true;render()};
|
$("#suggest").onclick=()=>{rows.forEach(r=>{if(r.suggest)r.edit=r.suggest});dirty=true;render()};
|
||||||
$("#movebtn").onclick=()=>{const o=rows.map(r=>`<option>${esc(r.envelope)}</option>`).join("");$("#mfrom").innerHTML=o;$("#mto").innerHTML=o;$("#movepanel").style.display="block"};
|
$("#movebtn").onclick=()=>{const o=rows.map(r=>`<option>${esc(r.envelope)}</option>`).join("");$("#mfrom").innerHTML=o;$("#mto").innerHTML=o;$("#movepanel").style.display="block"};
|
||||||
|
$("#addenv").onclick=async()=>{
|
||||||
|
const name=(prompt("Name for the new envelope (category):")||"").trim();if(!name)return;
|
||||||
|
try{const r=await fetch("/api/add_category",{method:"POST",headers:{"X-Token":TOKEN},body:JSON.stringify({name})});const j=await r.json();if(!r.ok)throw new Error(j.error);
|
||||||
|
rows.push({envelope:j.name,budgeted:0,spent:0,prior:0,available:0,suggest:0,edit:0});$("#showall").checked=true;render();$("#msg").innerHTML=`<div class="alert">${esc(j.message)}</div>`}
|
||||||
|
catch(e){$("#msg").innerHTML=`<div class="alert bad">${esc(e.message)}</div>`}};
|
||||||
$("#mcancel").onclick=()=>$("#movepanel").style.display="none";
|
$("#mcancel").onclick=()=>$("#movepanel").style.display="none";
|
||||||
$("#mgo").onclick=()=>{const f=rows.find(r=>r.envelope==$("#mfrom").value),t=rows.find(r=>r.envelope==$("#mto").value),a=+$("#mamt").value;
|
$("#mgo").onclick=()=>{const f=rows.find(r=>r.envelope==$("#mfrom").value),t=rows.find(r=>r.envelope==$("#mto").value),a=+$("#mamt").value;
|
||||||
if(!f||!t||f===t||!(a>0))return;f.edit=Math.max(0,(+f.edit||0)-a);t.edit=(+t.edit||0)+a;dirty=true;$("#movepanel").style.display="none";$("#mamt").value="";$("#showall").checked||(0);render()};
|
if(!f||!t||f===t||!(a>0))return;f.edit=Math.max(0,(+f.edit||0)-a);t.edit=(+t.edit||0)+a;dirty=true;$("#movepanel").style.display="none";$("#mamt").value="";$("#showall").checked||(0);render()};
|
||||||
|
|||||||
+2
-1
@@ -55,7 +55,7 @@ th{font-size:12px;color:var(--mute);font-weight:600}td.num{text-align:right;font
|
|||||||
</div>
|
</div>
|
||||||
<div class="card" id="catcard">
|
<div class="card" id="catcard">
|
||||||
<div class="row"><h2 style="margin:0">4 · Categorize payees</h2><div class="spacer"></div>
|
<div class="row"><h2 style="margin:0">4 · Categorize payees</h2><div class="spacer"></div>
|
||||||
<button id="auto">Auto-categorize</button><button class="primary" id="savecats" disabled>Save changes</button></div>
|
<button id="addcat">Add category</button><button id="auto">Auto-categorize</button><button class="primary" id="savecats" disabled>Save changes</button></div>
|
||||||
<div id="catsum" class="mute" style="margin:10px 0"></div>
|
<div id="catsum" class="mute" style="margin:10px 0"></div>
|
||||||
<div class="grid" style="max-height:420px;overflow:auto"><table id="cattbl"></table></div>
|
<div class="grid" style="max-height:420px;overflow:auto"><table id="cattbl"></table></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -118,6 +118,7 @@ async function loadPayees(){
|
|||||||
$("#cattbl").innerHTML=j.payees.length?"<tr><th>Payee</th><th class='num'>Payments</th><th class='num'>Total</th><th>Category</th><th>Type</th></tr>"+j.payees.map((p,i)=>`<tr><td>${esc(p.payee)}</td><td class="num">${p.n}</td><td class="num">${fmt(p.total)}</td><td><select data-i="${i}" data-k="category">${opt(j.categories,p.category)}</select></td><td><select data-i="${i}" data-k="type">${opt(j.types,p.type)}</select></td></tr>`).join(""):"";
|
$("#cattbl").innerHTML=j.payees.length?"<tr><th>Payee</th><th class='num'>Payments</th><th class='num'>Total</th><th>Category</th><th>Type</th></tr>"+j.payees.map((p,i)=>`<tr><td>${esc(p.payee)}</td><td class="num">${p.n}</td><td class="num">${fmt(p.total)}</td><td><select data-i="${i}" data-k="category">${opt(j.categories,p.category)}</select></td><td><select data-i="${i}" data-k="type">${opt(j.types,p.type)}</select></td></tr>`).join(""):"";
|
||||||
document.querySelectorAll("#cattbl select").forEach(sel=>sel.onchange=()=>{const p=j.payees[+sel.dataset.i];(EDITS[p.payee]=EDITS[p.payee]||{payee:p.payee,category:p.category,type:p.type})[sel.dataset.k]=sel.value;$("#savecats").disabled=false});
|
document.querySelectorAll("#cattbl select").forEach(sel=>sel.onchange=()=>{const p=j.payees[+sel.dataset.i];(EDITS[p.payee]=EDITS[p.payee]||{payee:p.payee,category:p.category,type:p.type})[sel.dataset.k]=sel.value;$("#savecats").disabled=false});
|
||||||
}
|
}
|
||||||
|
$("#addcat").onclick=async()=>{const name=(prompt("Name for the new category:")||"").trim();if(!name)return;try{const j=await post("/api/add_category",JSON.stringify({name}));await loadPayees();alertBox("good",esc(j.message))}catch(e){alertBox("bad",esc(e.message))}};
|
||||||
$("#auto").onclick=async()=>{try{const j=await post("/api/autocategorize","");await loadPayees();alertBox("good",esc(j.message))}catch(e){alertBox("bad",esc(e.message))}};
|
$("#auto").onclick=async()=>{try{const j=await post("/api/autocategorize","");await loadPayees();alertBox("good",esc(j.message))}catch(e){alertBox("bad",esc(e.message))}};
|
||||||
$("#savecats").onclick=async()=>{try{const j=await post("/api/set_categories",JSON.stringify(Object.values(EDITS)));await loadPayees();alertBox("good",esc(j.message))}catch(e){alertBox("bad",esc(e.message))}};
|
$("#savecats").onclick=async()=>{try{const j=await post("/api/set_categories",JSON.stringify(Object.values(EDITS)));await loadPayees();alertBox("good",esc(j.message))}catch(e){alertBox("bad",esc(e.message))}};
|
||||||
refresh();loadPayees();
|
refresh();loadPayees();
|
||||||
|
|||||||
@@ -487,6 +487,25 @@ def set_categories(changes, workbook=WORKBOOK):
|
|||||||
_save_workbook(wb, workbook)
|
_save_workbook(wb, workbook)
|
||||||
return dict(updated=n, backup=b.name)
|
return dict(updated=n, backup=b.name)
|
||||||
|
|
||||||
|
def add_category(name, workbook=WORKBOOK):
|
||||||
|
"""Add a category (envelope) to the Lists tab: first free slot in E4:E23, else E25:E40 (the web pages show them all;
|
||||||
|
the Excel summary tabs only have 20 slots)."""
|
||||||
|
from openpyxl import load_workbook
|
||||||
|
from copy import copy
|
||||||
|
name = re.sub(r"\s+", " ", (name or "")).strip()
|
||||||
|
if not name or len(name) > 40: raise ValueError("Give the category a name (40 characters or fewer).")
|
||||||
|
b = _backup(workbook); wb = load_workbook(workbook); ls = wb["Lists"]
|
||||||
|
if name.lower() in {str(c).lower() for c in _categories(wb)}: raise ValueError(f"'{name}' already exists.")
|
||||||
|
row = next((r for r in list(range(4, 24)) + list(range(25, 41)) if ls.cell(r, 5).value is None), None)
|
||||||
|
if row is None: raise ValueError("The Lists tab has no free category slots left.")
|
||||||
|
cell = ls.cell(row, 5); cell.value = name
|
||||||
|
src = ls.cell(4, 5); cell.font, cell.border, cell.alignment, cell.fill = copy(src.font), copy(src.border), copy(src.alignment), copy(src.fill)
|
||||||
|
if row > 23:
|
||||||
|
for dv in ls.data_validations.dataValidation:
|
||||||
|
if dv.formula1 == "$E$4:$E$23": dv.formula1 = "$E$4:$E$40"
|
||||||
|
_save_workbook(wb, workbook)
|
||||||
|
return dict(name=name, backup=b.name, beyond_excel=row > 23)
|
||||||
|
|
||||||
def autocategorize(workbook=WORKBOOK):
|
def autocategorize(workbook=WORKBOOK):
|
||||||
"""Apply the built-in merchant rules to payees still set to Other / Unsorted."""
|
"""Apply the built-in merchant rules to payees still set to Other / Unsorted."""
|
||||||
from openpyxl import load_workbook
|
from openpyxl import load_workbook
|
||||||
|
|||||||
Reference in new issue
Block a user