Changeset - 8a4fb5758cb8
[Not reviewed]
main
0 2 0
Ben Sturmfels (bsturmfels) - 30 days ago 2024-08-20 13:32:51
ben@sturm.com.au
statement_reconciler: Fix TypeError in Chase reconciler

There was an incorrect call to replace().
2 files changed with 2 insertions and 2 deletions:
0 comments (0 inline, 0 general)
conservancy_beancount/reconcile/statement_reconciler.py
Show inline comments
...
 
@@ -245,97 +245,97 @@ def validate_fr_csv(sample: str) -> None:
 
    if len(row) != 6 or not date or not amount_found:
 
        sys.exit(
 
            "This First Republic CSV doesn't seem to have the 6 columns we're expecting, including a date in column 2 and an amount in columns 5 and 6. Please use an unmodified statement direct from the institution."
 
        )
 

	
 

	
 
def standardize_fr_record(line, row):
 
    record = {
 
        'date': datetime.datetime.strptime(row[1], '%m/%d/%Y').date(),
 
        'amount': parse_amount(row[4]),
 
        'payee': remove_payee_junk(row[3] or '')[:20],
 
        'check_id': row[2].replace('CHECK  ', '') if 'CHECK  ' in row[2] else '',
 
        'line': line,
 
    }
 
    return record
 

	
 

	
 
def read_fr_csv(f: TextIO) -> list:
 
    reader = 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_fr_record(i, row)
 
        for i, row in enumerate(reader, 1)
 
        if len(row) == 6 and row[2] not in {'LAST STATEMENT', 'THIS STATEMENT'}
 
    )
 

	
 

	
 
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],
 
        '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 {
 
        'date': row.date,
 
        'amount': row.number_cost_position,
 
        'payee': remove_payee_junk(
 
            f'{row.payee or ""} {row.entity or ""} {row.narration or ""}'
 
        ),
 
        'check_id': str(row.check_id or ''),
 
        'filename': row.filename,
 
        'line': row.line,
 
        'bank_statement': row.bank_statement,
 
    }
 

	
 

	
 
def format_record(record: dict) -> str:
 
    """Generate output lines for a standard 1:1 match."""
 
    if record['payee'] and record['check_id']:
 
        output = f"{record['date'].isoformat()}: {record['amount']:11,.2f} {record['payee'][:25]} #{record['check_id']}".ljust(59)
 
    elif record['payee']:
 
        output = f"{record['date'].isoformat()}: {record['amount']:11,.2f} {record['payee'][:35]}".ljust(59)
 
    else:
 
        output = f"{record['date'].isoformat()}: {record['amount']:11,.2f} #{record['check_id']}".ljust(59)
 
    return output
 

	
 

	
 
def format_multirecord(r1s: List[dict], r2s: List[dict], note: str) -> List[list]:
 
    """Generates output lines for one statement:multiple books transaction match."""
 
    assert len(r1s) == 1
 
    assert len(r2s) > 1
 
    match_output = []
 
    match_output.append(
 
        [
 
            r1s[0]['date'],
setup.cfg
Show inline comments
 
[metadata]
 
name = conservancy_beancount
 
version = 1.20.0
 
version = 1.20.1
 
author = Software Freedom Conservancy
 
author_email = info@sfconservancy.org
 
description = Plugin, library, and reports for reading Conservancy’s books
 
license = AGPLv3-or-later WITH Beancount-Plugin-Additional-Permission-1.0
 
license_files =
 
  LICENSE.txt
 
  AGPLv3.txt
 
long_description = file: README.rst
 
long_description_content_type = text/x-rst; charset=UTF-8
 
project_urls =
 
  Source = %(url)s
 
url = https://k.sfconservancy.org/NPO-Accounting/conservancy_beancount
 

	
 
[bdist_wheel]
 
universal = 1
 

	
 
[mypy]
 
disallow_any_unimported = False
 
disallow_untyped_calls = False
 
disallow_untyped_defs = True
 
show_error_codes = True
 
strict_equality = True
 
warn_redundant_casts = True
 
warn_return_any = True
 
warn_unreachable = True
 
warn_unused_configs = True
 

	
 
[options]
 
include_package_data = True
 
install_requires =
 
  babel>=2.6
 
  beancount>=2.2,<3.0.0
 
  colorama
 
  GitPython>=2.0
 
  odfpy>=1.4.0,!=1.4.1
 
  pdfminer.six>=20200101
 
  python-dateutil>=2.7
 
  PyYAML>=3.0
 
  regex
 
  rt>=2.0,<3.0
 
  thefuzz
 
packages = find:
 
python_requires = >=3.6
 

	
 
[options.extras_require]
 
test =
 
  mypy>=0.770
 
  pytest>=3.0
0 comments (0 inline, 0 general)