statement_reconciler: Add initial support for Citizens Bank
This commit is contained in:
parent
e2f90d86bb
commit
e21142f0ba
3 changed files with 52 additions and 1 deletions
|
@ -305,6 +305,39 @@ def read_chase_csv(f: TextIO) -> list:
|
|||
)
|
||||
|
||||
|
||||
def validate_citizens_csv(sample: str) -> None:
|
||||
required_cols = {'Effective Date', 'Amount', 'Account Number', 'Transaction Description'}
|
||||
reader = csv.DictReader(io.StringIO(sample))
|
||||
if reader.fieldnames and not required_cols.issubset(reader.fieldnames):
|
||||
sys.exit(
|
||||
f"This Citizens 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_citizens_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['Effective Date'], '%m/%d/%Y').date(),
|
||||
'amount': parse_amount(row['Amount']),
|
||||
'payee': ('{originator} {beneficiary}'.format(
|
||||
originator=row['Originator Name'].strip(),
|
||||
beneficiary=row['Beneficiary Name'].strip(),
|
||||
) or '')[:50],
|
||||
'check_id': '',
|
||||
'line': line,
|
||||
}
|
||||
|
||||
def read_citizens_csv(f: TextIO) -> list:
|
||||
reader = csv.reader(f)
|
||||
headers = next(reader)
|
||||
# CSV duplicates the "Effective Date" field, so we can't use DictReader.
|
||||
headers[21] = 'Effective Date 2'
|
||||
reader = (dict(zip(headers, row)) for row in csv.reader(f))
|
||||
# The reader.line_num is the source line number, not the spreadsheet row
|
||||
# number due to multi-line records.
|
||||
return sort_records(
|
||||
[standardize_citizens_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 {
|
||||
|
@ -826,6 +859,9 @@ def main(
|
|||
elif 'Chase' in args.account:
|
||||
validate_csv = validate_chase_csv
|
||||
read_csv = read_chase_csv
|
||||
elif 'Citizens' in args.account:
|
||||
validate_csv = validate_citizens_csv
|
||||
read_csv = read_citizens_csv
|
||||
else:
|
||||
sys.exit("This account provided doesn't match one of AMEX, FR or Chase.")
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[metadata]
|
||||
name = conservancy_beancount
|
||||
version = 1.21.0
|
||||
version = 1.22.0
|
||||
author = Software Freedom Conservancy
|
||||
author_email = info@sfconservancy.org
|
||||
description = Plugin, library, and reports for reading Conservancy’s books
|
||||
|
|
|
@ -17,6 +17,7 @@ from conservancy_beancount.reconcile.statement_reconciler import (
|
|||
payee_match,
|
||||
read_amex_csv,
|
||||
read_fr_csv,
|
||||
read_citizens_csv,
|
||||
remove_duplicate_words,
|
||||
remove_payee_junk,
|
||||
round_to_month,
|
||||
|
@ -421,6 +422,20 @@ def test_handles_fr_csv():
|
|||
assert read_fr_csv(io.StringIO(CSV)) == expected
|
||||
|
||||
|
||||
def test_handles_citizens_csv():
|
||||
CSV = """Effective Date,Bank ID,Account Number,Account Name,Transaction Description,Status,Debit/Credit,Amount,Bank Reference,Customer Reference,Transaction Detail,BAI Code,Type,Additional Information,Currency,Image,Advice Reference,Transaction Number,Originator Id,Originator Name,Company Entry Description,Effective Date,Related Reference,Value Date of Payment,Transaction Reference,SEC Type,Settlement Reference,Payer Account Name,Payer Account,Payer Address 1,Payer Address 2,Payer Address 3,Payer Address 4,Payer Bank Code,Payer Bank Name,Payer Bank Address 1,Payer Bank Address 2,Payer Bank Address 3,Sender Bank Id,Sender Bank,Sender Address 1,Sender Address 2,Sender Address 3,Sender Address 4,Beneficiary Name,Beneficiary Account,Beneficiary Address 1,Beneficiary Address 2,Beneficiary Address 3,Beneficiary Address 4,Beneficiary Bank Code,Beneficiary Bank Name,Beneficiary Bank Address 1,Beneficiary Bank Address 2,Beneficiary Bank Address 3,Reference For Beneficiary,Foreign Amount,Foreign Currency,Exchange Rate,Payment Details 1,Payment Details 2,Payment Details 3,Payment Details 4,Payment Details 5,Payment Details 6,Bank Reference,Bank Reference 2,Bank Reference 3,Bank Reference 4,Bank Reference 5,Bank Reference 6,Receiving Bank Id,Receiving Bank,Receiving Bank Address 1,Receiving Bank Address 2,Receiving Bank Address 3,Receiving Bank Address 4,Intermediary Bank Id,Intermediary Bank Name,Intermediary Bank Address 1,Intermediary Bank Address 2,Intermediary Bank Address 3,Intermediary Bank Address 4,Ordering Bank,Ordering Bank Address 1,Ordering Bank Address 2,Ordering Bank Address 3,Ordering Bank Address 4,Instructing Bank,Terminal Location,Terminal City,Terminal State,TIME\n05/30/2025,021313103,4030961273,SOFTWARE FREEDOM CONSERVANCY INC,PREAUTHORIZED ACH DEBIT,Cleared,Debit,-275.64,,,SEC : CCD ORIG NAME : PAYCHEX EIB CO. ENTRY DESC: INVOICE RECIP NAME: SOFTWARE FREEDOM CONSE INDIVIDUAL ID: X12201000015079 EFFECTIVE DATE: 250530 CO DESCRIPTION DATE: 250530 PAR#: 025149005728064 ADVICEREF: 025149005728064 ,455,ACH,View Details,USD,, 025149005728064, 025149005728064,, PAYCHEX EIB , INVOICE , 250530 ,,,, CCD ,,,,,,,,,,,,,,,,,,, SOFTWARE FREEDOM CONSE ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,\n"""
|
||||
expected = [
|
||||
{
|
||||
'date': datetime.date(2025, 5, 30),
|
||||
'amount': decimal.Decimal('-275.64'),
|
||||
'payee': 'PAYCHEX EIB SOFTWARE FREEDOM CONSE',
|
||||
'check_id': '',
|
||||
'line': 2,
|
||||
},
|
||||
]
|
||||
assert read_citizens_csv(io.StringIO(CSV)) == expected
|
||||
|
||||
|
||||
def test_format_output():
|
||||
statement = [S1]
|
||||
books = [B1]
|
||||
|
|
Loading…
Add table
Reference in a new issue