Changeset - 5a8da108b983
[Not reviewed]
0 2 0
Ben Sturmfels (bsturmfels) - 1 month ago 2024-07-19 05:57:07
ben@sturm.com.au
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.
2 files changed with 42 insertions and 2 deletions:
0 comments (0 inline, 0 general)
conservancy_beancount/reconcile/statement_reconciler.py
Show inline comments
...
 
@@ -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]
 
    """Turn a Beancount query result row into a standard dict representing a transaction."""
 
    return {
...
 
@@ -784,9 +819,14 @@ def main(
 
    if 'AMEX' in args.account:
 
        validate_csv = validate_amex_csv
 
        read_csv = read_amex_csv
 
    else:
 
    elif 'FR' in args.account:
 
        validate_csv = validate_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:
 
        sample = f.read(200)
setup.cfg
Show inline comments
 
[metadata]
 
name = conservancy_beancount
 
version = 1.19.8
 
version = 1.20.0
 
author = Software Freedom Conservancy
 
author_email = info@sfconservancy.org
 
description = Plugin, library, and reports for reading Conservancy’s books
0 comments (0 inline, 0 general)