Ally savings-only statements: separate account per savings number, own coverage row
build-container / build (push) Successful in 39s
build-container / build (push) Successful in 39s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1f556bccfa
commit
5425d9913a
2 files changed
+8
-4
No files matched your search
@@ -36,6 +36,7 @@ def coverage(sts):
|
|||||||
out = []
|
out = []
|
||||||
for acct, lst in by.items():
|
for acct, lst in by.items():
|
||||||
csv_only = all(s.kind == "csv" for s in lst)
|
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)
|
cells, first = {}, min((s.start or s.end) for s in lst)
|
||||||
for mo in months:
|
for mo in months:
|
||||||
y, m = int(mo[:4]), int(mo[5:])
|
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"))
|
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
|
continue
|
||||||
have = any((s.end.year, s.end.month) == (y, m) for s in lst)
|
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)
|
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"))
|
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)
|
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}"
|
nxt = f"Next download: from {last + dt.timedelta(days=1):%b %-d, %Y}"
|
||||||
else:
|
else:
|
||||||
ny, nm = (last.year, last.month + 1) if last.month < 12 else (last.year + 1, 1)
|
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
|
if day 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))
|
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 "")
|
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}"
|
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"))
|
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"))
|
||||||
|
|||||||
+4
-1
@@ -135,10 +135,13 @@ def parse_ally(path, pages):
|
|||||||
bal = g(r"Ending Balance, as of \S+ \$([\d,]+\.\d\d)")
|
bal = g(r"Ending Balance, as of \S+ \$([\d,]+\.\d\d)")
|
||||||
if bal is None: continue
|
if bal is None: continue
|
||||||
apy = re.search(r"Annual Percentage Yield Earned ([\d.]+)%", sec)
|
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)"),
|
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)"),
|
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))
|
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
|
return st
|
||||||
|
|
||||||
CV_TX = re.compile(r"^(\d\d/\d\d/\d{4}) (.+?) \$([\d,]+\.\d\d) \$([\d,]+\.\d\d)$")
|
CV_TX = re.compile(r"^(\d\d/\d\d/\d{4}) (.+?) \$([\d,]+\.\d\d) \$([\d,]+\.\d\d)$")
|
||||||
|
|||||||
Reference in new issue
Block a user