Actual Importer Code Samples
So how minimal can we get our institution-specific code to be? Let’s look at best, average, and worst case examples.
Simple Importers: Most Credit Card / Banking
Almost all my credit card and banking importers look like this:
"""Chase credit card ofx importer for beancount."""
from beancount_reds_importers.libreader import ofxreader
from beancount_reds_importers.libtransactionbuilder import banking
class Importer(banking.Importer, ofxreader.Importer):
def custom_init(self):
if not self.custom_init_run:
self.max_rounding_error = 0.04
self.account_number_field = 'account_id'
self.filename_identifier_substring = 'chase'
self.custom_init_run = True
They import a file format reader (ofxreader), a transaction builder (banking),
specify the ofx field name, a filename substring, and that’s it!
A Tad Bit More Work: Fidelity
""" Fidelity Net Benefits ofx importer."""
from beancount_reds_importers.libreader import ofxreader
from beancount_reds_importers.libtransactionbuilder import investments
class Importer(investments.Importer, ofxreader.Importer):
def custom_init(self):
self.max_rounding_error = 0.14
self.account_number_field = 'account_id'
self.filename_identifier_substring = 'fidelity'
self.get_ticker_info = self.get_ticker_info_from_id
def get_target_acct_custom(self, transaction):
if transaction.memo.startswith("CONTRIBUTION"):
return self.config['transfer']
if transaction.memo.startswith("FEES"):
return self.config['fees']
return self.target_account_map.get(transaction.type, None)
This only involves slightly more effort: a few lines of code to classify an account posting target based on the memo, which in this case is not possible to get from other parts of the OFX.
More Work: Schwab CSV importer
The schwab CSV importer ends up looking like the following. The code below minimally expresses the semantics of the schwab csv format, leaving the heavy lifting to the ofxread file format reader, and investment transaction builder.
Though this involves a bit more work, it is still quite minimal since the most of the
heavy lifting is done by the csvreader and investments modules.
""" Schwab CSV importer."""
from beancount_reds_importers.libreader import csvreader
from beancount_reds_importers.libtransactionbuilder import investments
class Importer(investments.Importer, csvreader.Importer):
def custom_init(self):
self.max_rounding_error = 0.04
# Identifying files
self.account_number_field = 'number'
self.filename_identifier_substring = '_Transactions_'
self.header_identifier = '"Transactions for account ' + self.config.get('custom_header', '')
self.get_ticker_info = self.get_ticker_info_from_id
# format specific to Schwab CSV
self.date_format = '%m/%d/%Y'
self.funds_db_txt = 'funds_by_ticker'
self.skip_head_rows = 1
self.skip_tail_rows = 1
# CSV column spec
self.header_map = {
"Action": 'type',
"Date": 'date',
"tradeDate": 'tradeDate',
"Description": 'memo',
"Symbol": 'security',
"Quantity": 'units',
"Price": 'unit_price',
"Amount": 'amount',
"total": 'total',
"Fees & Comm": 'fees',
}
# Map Schwab's names for transaction types to our internal types
self.transaction_type_map = {
'Bank Interest': 'income',
'Buy': 'buystock',
'Cash Dividend': 'dividends',
'MoneyLink Transfer': 'transfer',
'Reinvest Dividend': 'dividends',
'Reinvest Shares': 'buystock',
'Sell': 'sellstock',
}
self.skip_transaction_types = ['Journal']
# Cleaning up CSV: add/remove columns, clean up weird date appearances
def prepare_raw_columns(self, rdr):
rdr = rdr.cutout('') # clean up last column
def cleanup_date(d):
"""'11/16/2018 as of 11/15/2018' --> '11/16/2018'"""
return d.split(' ', 1)[0]
rdr = rdr.convert('Date', cleanup_date)
rdr = rdr.addfield('tradeDate', lambda x: x['Date'])
rdr = rdr.addfield('total', lambda x: x['Amount'])
return rdr