Add blank workbook template, example rules file, and Create blank workbook button
build-container / build (push) Successful in 33s
build-container / build (push) Successful in 33s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6f4ecd42f7
commit
6073279a1b
9 files changed
+27
-2
No files matched your search
@@ -7,3 +7,6 @@ data
|
||||
*.xlsx
|
||||
loader_rules.json
|
||||
*.json
|
||||
!template/
|
||||
!template/*.xlsx
|
||||
!loader_rules.example.json
|
||||
@@ -10,3 +10,6 @@ Statement_Coverage.txt
|
||||
__pycache__/
|
||||
.DS_Store
|
||||
loader_rules.json
|
||||
!template/
|
||||
!template/Bills_Tracker.template.xlsx
|
||||
!loader_rules.example.json
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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.`));
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"own_names": ["JANE DOE"],
|
||||
"own_bank_hints": ["MY CREDIT UNION"],
|
||||
"csv_accounts": {"1234": "Capital One Card"}
|
||||
}
|
||||
+1
-1
@@ -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.
Reference in new issue
Block a user