From 5425d9913a279befd5d0ccaa8b9b6ce9075dc437 Mon Sep 17 00:00:00 2001 From: nmemmert <67961292+nmemmert@users.noreply.github.com> Date: Wed, 21 Oct 2026 19:04:19 -0400 Subject: [PATCH] Ally savings-only statements: separate account per savings number, own coverage row Co-Authored-By: Claude Sonnet 5.5 --- app.py | 7 ++++--- statement_loader.py | 5 ++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app.py b/app.py index 752cad6..e3fe5da 100644 --- a/app.py +++ b/app.py @@ -36,6 +36,7 @@ def coverage(sts): out = [] for acct, lst in by.items(): csv_only = all(s.kind == "csv" for s in lst) + ends = {s.end.day for s in lst}; day = cfg.get(acct) or (ends.pop() if len(ends) == 1 and max(ends) < 28 else None) # accounts not in ACCOUNTS: reuse their usual closing day cells, first = {}, min((s.start or s.end) for s in lst) for mo in months: y, m = int(mo[:4]), int(mo[5:]) @@ -44,7 +45,7 @@ def coverage(sts): cells[mo] = "have" if have else ("future" if (y, m) > (max(s.end for s in lst).year, max(s.end for s in lst).month) else ("none" if (y, m) < (first.year, first.month) else "missing")) continue have = any((s.end.year, s.end.month) == (y, m) for s in lst) - due = dt.date(y, m, cfg.get(acct) or eom(y, m)) + due = dt.date(y, m, day or eom(y, m)) started = (y, m) >= (min(s.end for s in lst).year, min(s.end for s in lst).month) cells[mo] = "have" if have else ("none" if not started else ("future" if due > today else "missing")) last = max(s.end for s in lst) @@ -52,8 +53,8 @@ def coverage(sts): nxt = f"Next download: from {last + dt.timedelta(days=1):%b %-d, %Y}" else: ny, nm = (last.year, last.month + 1) if last.month < 12 else (last.year + 1, 1) - if cfg.get(acct) is None and last.day != eom(last.year, last.month): ny, nm = last.year, last.month - nd = dt.date(ny, nm, cfg.get(acct) or eom(ny, nm)) + if day is None and last.day != eom(last.year, last.month): ny, nm = last.year, last.month + nd = dt.date(ny, nm, day or eom(ny, nm)) nxt = f"Next statement closes ~{nd:%b %-d, %Y}" + (" (ready to download)" if nd <= today else "") if acct in recent and recent[acct] > last: nxt += f" ยท recent activity loaded through {recent[acct]:%b %-d}" out.append(dict(account=acct, cells=cells, count=len(lst), through=f"{last:%b %-d, %Y}", next=nxt, kind="csv" if csv_only else "pdf")) diff --git a/statement_loader.py b/statement_loader.py index 2450c20..b5a0d00 100644 --- a/statement_loader.py +++ b/statement_loader.py @@ -135,10 +135,13 @@ def parse_ally(path, pages): bal = g(r"Ending Balance, as of \S+ \$([\d,]+\.\d\d)") if bal is None: continue apy = re.search(r"Annual Percentage Yield Earned ([\d.]+)%", sec) - st.snaps.append(dict(account="Ally " + re.sub(r" Account$", "", prod), group="Savings", date=end, balance=bal, + num = re.search(r"Account Number: x+(\d{4})", full[max(0, h.start() - 200): h.start()]) # last 4 digits tell several savings accounts apart + st.snaps.append(dict(account="Ally " + re.sub(r" Account$", "", prod) + (f" ({num.group(1)})" if num else ""), group="Savings", date=end, balance=bal, interest=g(r"Interest Paid This Period \$([\d,]+\.\d\d)"), ytd=g(r"Interest Paid Year to Date \$([\d,]+\.\d\d)"), deposits=g(r"Deposits and Other Credits \$([\d,]+\.\d\d)"), withdrawals=g(r"Withdrawals and Other Debits -?\$([\d,]+\.\d\d)"), rate=float(apy.group(1)) if apy else None)) + if not any(re.match(r"spending", h.group(1).strip(), re.I) for h in heads) and st.snaps: + st.account, st.stated_out = st.snaps[0]["account"], None # a savings-only statement: its own account in the coverage grid, not Ally Checking return st CV_TX = re.compile(r"^(\d\d/\d\d/\d{4}) (.+?) \$([\d,]+\.\d\d) \$([\d,]+\.\d\d)$")