@@ -118,6 +118,7 @@ async function loadPayees(){
$("#cattbl").innerHTML=j.payees.length?"
Payee
Payments
Total
Category
Type
"+j.payees.map((p,i)=>`
${esc(p.payee)}
${p.n}
${fmt(p.total)}
`).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});
}
+$("#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))}};
$("#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();
diff --git a/statement_loader.py b/statement_loader.py
index 55ce06b..32cef4d 100644
--- a/statement_loader.py
+++ b/statement_loader.py
@@ -487,6 +487,25 @@ def set_categories(changes, workbook=WORKBOOK):
_save_workbook(wb, workbook)
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):
"""Apply the built-in merchant rules to payees still set to Other / Unsorted."""
from openpyxl import load_workbook