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

Notes mentioning this note