statement_reconciler: Add initial Chase bank CSV statement matching

We currently don't have many examples to work with, so haven't done any
significant testing of the matching accuracy between statement and books.
This commit is contained in:
Ben Sturmfels 2024-07-19 15:53:21 +10:00
parent bd07154fbb
commit 5a8da108b9
Signed by: bsturmfels
GPG key ID: 023C05E2C9C068F0
2 changed files with 42 additions and 2 deletions

View file

@ -270,6 +270,41 @@ def read_fr_csv(f: TextIO) -> list:
) )
def validate_chase_csv(sample: str) -> None:
required_cols = {'Date', 'Description', 'Account', 'Transaction Type', 'Amount'}
reader = csv.DictReader(io.StringIO(sample))
if reader.fieldnames and not required_cols.issubset(reader.fieldnames):
sys.exit(
f"This Chase CSV doesn't seem to have the columns we're expecting, including: {', '.join(required_cols)}. Please use an unmodified statement direct from the institution."
)
def standardize_chase_record(row: Dict, line: int) -> Dict:
"""Turn an Chase CSV row into a standard dict format representing a transaction."""
return {
'date': datetime.datetime.strptime(row['Date'], '%m/%d/%y').date(),
'amount': -1 * parse_amount(row['Amount']),
# Descriptions have quite a lot of information, but the format is a little
# idiosyncratic. We'll need to see more examples before coming up with any ways
# to handle it in code. Others have used regular expressions to match the
# various transaction types:
# https://github.com/mtlynch/beancount-chase-bank/blob/master/beancount_chase/checking.py
# See also: https://awesome-beancount.com/
'payee': (row['Description'] or '').replace('ORIG CO NAME:')[:20],
'check_id': '',
'line': line,
}
def read_chase_csv(f: TextIO) -> list:
reader = csv.DictReader(f)
# The reader.line_num is the source line number, not the spreadsheet row
# number due to multi-line records.
return sort_records(
[standardize_chase_record(row, i) for i, row in enumerate(reader, 2)]
)
def standardize_beancount_record(row) -> Dict: # type: ignore[no-untyped-def] def standardize_beancount_record(row) -> Dict: # type: ignore[no-untyped-def]
"""Turn a Beancount query result row into a standard dict representing a transaction.""" """Turn a Beancount query result row into a standard dict representing a transaction."""
return { return {
@ -784,9 +819,14 @@ def main(
if 'AMEX' in args.account: if 'AMEX' in args.account:
validate_csv = validate_amex_csv validate_csv = validate_amex_csv
read_csv = read_amex_csv read_csv = read_amex_csv
else: elif 'FR' in args.account:
validate_csv = validate_fr_csv validate_csv = validate_fr_csv
read_csv = read_fr_csv read_csv = read_fr_csv
elif 'Chase' in args.account:
validate_csv = validate_chase_csv
read_csv = read_chase_csv
else:
sys.exit("This account provided doesn't match one of AMEX, FR or Chase.")
with open(args.csv_statement) as f: with open(args.csv_statement) as f:
sample = f.read(200) sample = f.read(200)

View file

@ -1,6 +1,6 @@
[metadata] [metadata]
name = conservancy_beancount name = conservancy_beancount
version = 1.19.8 version = 1.20.0
author = Software Freedom Conservancy author = Software Freedom Conservancy
author_email = info@sfconservancy.org author_email = info@sfconservancy.org
description = Plugin, library, and reports for reading Conservancys books description = Plugin, library, and reports for reading Conservancys books