diff --git a/.dockerignore b/.dockerignore index c6204df..0d91737 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,3 +7,6 @@ data *.xlsx loader_rules.json *.json +!template/ +!template/*.xlsx +!loader_rules.example.json diff --git a/.gitignore b/.gitignore index 6f3dcb7..2c0e0ca 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,6 @@ Statement_Coverage.txt __pycache__/ .DS_Store loader_rules.json +!template/ +!template/Bills_Tracker.template.xlsx +!loader_rules.example.json diff --git a/Dockerfile b/Dockerfile index f6eacb8..d761826 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/README.md b/README.md index 190abbc..598d3f8 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/app.py b/app.py index 081d05b..33aded9 100644 --- a/app.py +++ b/app.py @@ -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) diff --git a/app_page.html b/app_page.html index 4aa1c1b..e223283 100644 --- a/app_page.html +++ b/app_page.html @@ -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",`First-time setup: Bills_Tracker.xlsx isn’t in the data folder (${esc(s.data_dir)}). Drop it into the box above (or copy it into that folder), then reload.`+(s.rules?"":" Also add loader_rules.json if you have one (your name, so transfers between your accounts are recognized)."));return} + $("#summary").innerHTML="";alertBox("warn",`First-time setup: Bills_Tracker.xlsx isn’t in the data folder (${esc(s.data_dir)}). Drop your existing one into the box above, or `+(s.rules?"":" Also add loader_rules.json (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",`${esc(b.file)}: the transactions I read add up to ${fmt(b.parsed)} but the statement says ${fmt(b.stated)}. It will not be imported until that matches.`)); s.skipped.forEach(b=>alertBox("warn",`${esc(b.file)}: ${esc(b.why)}`)); s.duplicates.forEach(b=>alertBox("warn",`${esc(b.file)} is a duplicate of ${esc(b.same_as)}; counted once.`)); diff --git a/loader_rules.example.json b/loader_rules.example.json new file mode 100644 index 0000000..d822ab7 --- /dev/null +++ b/loader_rules.example.json @@ -0,0 +1,5 @@ +{ + "own_names": ["JANE DOE"], + "own_bank_hints": ["MY CREDIT UNION"], + "csv_accounts": {"1234": "Capital One Card"} +} diff --git a/statement_loader.py b/statement_loader.py index 16be842..2548d7a 100644 --- a/statement_loader.py +++ b/statement_loader.py @@ -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): diff --git a/template/Bills_Tracker.template.xlsx b/template/Bills_Tracker.template.xlsx new file mode 100644 index 0000000..b1816de Binary files /dev/null and b/template/Bills_Tracker.template.xlsx differ