Add blank workbook template, example rules file, and Create blank workbook button
build-container / build (push) Successful in 33s

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-21 13:07:03 -04:00
1 parent 6f4ecd42f7
commit 6073279a1b
9 files changed
+27 -2

No files matched your search

+3
View File
@@ -7,3 +7,6 @@ data
*.xlsx
loader_rules.json
*.json
!template/
!template/*.xlsx
!loader_rules.example.json
+3
View File
@@ -10,3 +10,6 @@ Statement_Coverage.txt
__pycache__/
.DS_Store
loader_rules.json
!template/
!template/Bills_Tracker.template.xlsx
!loader_rules.example.json
+1
View File
@@ -7,6 +7,7 @@ COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY statement_loader.py app.py app_page.html app_dashboard.html ./
COPY template/ ./template/
# statements, Bills_Tracker.xlsx and backups live in this volume, never in the image
RUN useradd -m -u 1000 app && mkdir /data && chown app /data
+5
View File
@@ -16,6 +16,11 @@ mkdir -p data && cp /path/to/Bills_Tracker.xlsx data/
AppID=money PUID=1000 PGID=1000 TZ=America/New_York docker compose up -d # then edit the bind path in compose.yaml or symlink
```
## Starting fresh
`template/Bills_Tracker.template.xlsx` is a blank workbook (all tabs, formulas and charts; no data). The app's first-run screen has a
"Create a blank workbook" button that copies it into the data folder. Then drop statements in; new payees land on the Lists tab as
"Other / Unsorted" for you to categorize. `loader_rules.example.json` shows the private rules file format.
## Personal rules (not in git)
Your name, card last-4 and similar live in `loader_rules.json` next to the workbook (git-ignored, not in the image):
```json
+7
View File
@@ -155,6 +155,13 @@ class H(BaseHTTPRequestHandler):
keep = L.BACKUPS / f"before_undo_{dt.datetime.now():%Y%m%d_%H%M%S}.xlsx"
shutil.copy2(L.WORKBOOK, keep); shutil.copy2(b[-1], L.WORKBOOK)
return self._send(200, {"message": f"Restored the workbook from {b[-1].name}."})
if u.path == "/api/create_blank":
tpl = HERE / "template" / "Bills_Tracker.template.xlsx"
with LOCK:
if L.WORKBOOK.exists(): return self._send(400, {"error": "Bills_Tracker.xlsx already exists; it won't be overwritten."})
if not tpl.exists(): return self._send(400, {"error": "Template not found in the app."})
DATA.mkdir(parents=True, exist_ok=True); shutil.copy2(tpl, L.WORKBOOK)
return self._send(200, {"message": "Created a blank Bills_Tracker.xlsx. Now drop your statements in."})
if u.path == "/api/open":
if sys.platform != "darwin": return self._send(200, {"message": "Open Bills_Tracker.xlsx from the data folder on the host."})
subprocess.run(["open", str(L.WORKBOOK)], check=False)
+2 -1
View File
@@ -68,7 +68,8 @@ async function refresh(msg){
const r=await fetch("/api/state");const s=await r.json();
if(s.error){alertBox("bad",esc(s.error));return}
if(s.setup){$("#coverage").innerHTML="";$("#preview").innerHTML="";$("#import").disabled=true;
$("#summary").innerHTML="";alertBox("warn",`<b>First-time setup:</b> <code>Bills_Tracker.xlsx</code> isn’t in the data folder (<code>${esc(s.data_dir)}</code>). Drop it into the box above (or copy it into that folder), then reload.`+(s.rules?"":" Also add <code>loader_rules.json</code> if you have one (your name, so transfers between your accounts are recognized)."));return}
$("#summary").innerHTML="";alertBox("warn",`<b>First-time setup:</b> <code>Bills_Tracker.xlsx</code> isn’t in the data folder (<code>${esc(s.data_dir)}</code>). Drop your existing one into the box above, or <button id="blank" style="margin-left:6px">Create a blank workbook</button>`+(s.rules?"":" Also add <code>loader_rules.json</code> (see loader_rules.example.json: your name, so transfers between your own accounts aren’t counted as spending)."));
const b=$("#blank");if(b)b.onclick=async()=>{try{const j=await post("/api/create_blank","");await refresh(esc(j.message))}catch(e){alertBox("bad",esc(e.message))}};return}
s.bad.forEach(b=>alertBox("bad",`<b>${esc(b.file)}</b>: the transactions I read add up to ${fmt(b.parsed)} but the statement says ${fmt(b.stated)}. It will <b>not</b> be imported until that matches.`));
s.skipped.forEach(b=>alertBox("warn",`<b>${esc(b.file)}</b>: ${esc(b.why)}`));
s.duplicates.forEach(b=>alertBox("warn",`<b>${esc(b.file)}</b> is a duplicate of ${esc(b.same_as)}; counted once.`));
+5
View File
@@ -0,0 +1,5 @@
{
"own_names": ["JANE DOE"],
"own_bank_hints": ["MY CREDIT UNION"],
"csv_accounts": {"1234": "Capital One Card"}
}
+1 -1
View File
@@ -418,7 +418,7 @@ def apply(new_rows, workbook=WORKBOOK):
lo[a] = min(lo.get(a, d), d); hi[a] = max(hi.get(a, d), d)
for rr in range(1, rc.max_row + 1):
a = rc.cell(rr, 1).value
if a in lo and rc.cell(rr, 2).value and "–" in str(rc.cell(rr, 2).value):
if a in lo and rc.cell(rr, 2).value and ("–" in str(rc.cell(rr, 2).value) or str(rc.cell(rr, 2).value) == "no data yet"):
rc.cell(rr, 2).value = f"{lo[a]:%-m/%-d/%Y} – {hi[a]:%-m/%-d/%Y}"
# Review Items
if added_payees or any(r.needs_review for r in log_rows):
Binary file not shown.