summaryrefslogtreecommitdiff
path: root/ofxparse/ofxparse.py
diff options
context:
space:
mode:
authorAndrew Shadura <andrewsh@debian.org>2016-10-19 17:56:53 +0200
committerAndrew Shadura <andrewsh@debian.org>2016-10-19 17:56:53 +0200
commitbdbd753fe4ab2de979939bae4caf93b16f6b5efc (patch)
tree5035feeb558cb1941364624600b21e275f29681c /ofxparse/ofxparse.py
Imported Upstream version 0.14
Diffstat (limited to 'ofxparse/ofxparse.py')
-rw-r--r--ofxparse/ofxparse.py851
1 files changed, 851 insertions, 0 deletions
diff --git a/ofxparse/ofxparse.py b/ofxparse/ofxparse.py
new file mode 100644
index 0000000..a66df88
--- /dev/null
+++ b/ofxparse/ofxparse.py
@@ -0,0 +1,851 @@
+from __future__ import absolute_import, with_statement
+
+import sys
+import decimal
+import datetime
+import codecs
+import re
+import collections
+import contextlib
+
+try:
+ from StringIO import StringIO
+except ImportError:
+ from io import StringIO
+
+import six
+
+if 'OrderedDict' in dir(collections):
+ odict = collections
+else:
+ import ordereddict as odict
+
+from . import mcc
+
+def soup_maker(fh):
+ try:
+ from bs4 import BeautifulSoup
+ return BeautifulSoup(fh)
+ except ImportError:
+ from BeautifulSoup import BeautifulStoneSoup
+ return BeautifulStoneSoup(fh)
+
+
+def try_decode(string, encoding):
+ if hasattr(string, 'decode'):
+ string = string.decode(encoding)
+ return string
+
+def is_iterable(candidate):
+ if sys.version_info < (2,6):
+ return hasattr(candidate, 'next')
+ return isinstance(candidate, collections.Iterable)
+
+@contextlib.contextmanager
+def save_pos(fh):
+ """
+ Save the position of the file handle, seek to the beginning, and
+ then restore the position.
+ """
+ orig_pos = fh.tell()
+ fh.seek(0)
+ try:
+ yield fh
+ finally:
+ fh.seek(orig_pos)
+
+class OfxFile(object):
+ def __init__(self, fh):
+ """
+ fh should be a seekable file-like byte stream object
+ """
+ self.headers = odict.OrderedDict()
+ self.fh = fh
+
+ if not is_iterable(self.fh):
+ return
+ if not hasattr(self.fh, "seek"):
+ return # fh is not a file object, we're doomed.
+
+ with save_pos(self.fh):
+ self.read_headers()
+ self.handle_encoding()
+ self.replace_NONE_headers()
+
+ def read_headers(self):
+ head_data = self.fh.read(1024 * 10)
+ head_data = head_data[:head_data.find(six.b('<'))]
+
+ for line in re.split(six.b('\r?\n?'), head_data):
+ # Newline?
+ if line.strip() == six.b(""):
+ break
+
+ header, value = line.split(six.b(":"))
+ header, value = header.strip().upper(), value.strip()
+
+ self.headers[header] = value
+
+ def handle_encoding(self):
+ """
+ Decode the headers and wrap self.fh in a decoder such that it
+ subsequently returns only text.
+ """
+ # decode the headers using ascii
+ ascii_headers = odict.OrderedDict(
+ (
+ key.decode('ascii', 'replace'),
+ value.decode('ascii', 'replace'),
+ )
+ for key, value in six.iteritems(self.headers)
+ )
+
+ enc_type = ascii_headers.get('ENCODING')
+
+ if not enc_type:
+ # no encoding specified, use the ascii-decoded headers
+ self.headers = ascii_headers
+ # decode the body as ascii as well
+ self.fh = codecs.lookup('ascii').streamreader(self.fh)
+ return
+
+ if enc_type == "USASCII":
+ cp = ascii_headers.get("CHARSET", "1252")
+ encoding = "cp%s" % (cp, )
+
+ elif enc_type in ("UNICODE", "UTF-8"):
+ encoding = "utf-8"
+
+ codec = codecs.lookup(encoding)
+
+ self.fh = codec.streamreader(self.fh)
+
+ # Decode the headers using the encoding
+ self.headers = odict.OrderedDict(
+ (key.decode(encoding), value.decode(encoding))
+ for key, value in six.iteritems(self.headers)
+ )
+
+ def replace_NONE_headers(self):
+ """
+ Any headers that indicate 'none' should be replaced with Python
+ None values
+ """
+ for header in self.headers:
+ if self.headers[header].upper() == 'NONE':
+ self.headers[header] = None
+
+
+class OfxPreprocessedFile(OfxFile):
+ def __init__(self, fh):
+ super(OfxPreprocessedFile,self).__init__(fh)
+
+ if self.fh is None:
+ return
+
+ ofx_string = self.fh.read()
+
+ # find all closing tags as hints
+ closing_tags = [ t.upper() for t in re.findall(r'(?i)</([a-z0-9_\.]+)>', ofx_string) ]
+
+ # close all tags that don't have closing tags and
+ # leave all other data intact
+ last_open_tag = None
+ tokens = re.split(r'(?i)(</?[a-z0-9_\.]+>)', ofx_string)
+ new_fh = StringIO()
+ for idx,token in enumerate(tokens):
+ is_closing_tag = token.startswith('</')
+ is_processing_tag = token.startswith('<?')
+ is_cdata = token.startswith('<!')
+ is_tag = token.startswith('<') and not is_cdata
+ is_open_tag = is_tag and not is_closing_tag and not is_processing_tag
+ if is_tag:
+ if last_open_tag is not None:
+ new_fh.write("</%s>" % last_open_tag)
+ last_open_tag = None
+ if is_open_tag:
+ tag_name = re.findall(r'(?i)<([a-z0-9_\.]+)>', token)[0]
+ if tag_name.upper() not in closing_tags:
+ last_open_tag = tag_name
+ new_fh.write(token)
+ new_fh.seek(0)
+ self.fh = new_fh
+
+
+class Ofx(object):
+ def __str__(self):
+ return ""
+# headers = "\r\n".join(":".join(el if el else "NONE" for el in item) for item in six.iteritems(self.headers))
+# headers += "\r\n\r\n"
+#
+# return headers + str(self.signon)
+
+
+class AccountType(object):
+ (Unknown, Bank, CreditCard, Investment) = range(0, 4)
+
+
+class Account(object):
+ def __init__(self):
+ self.statement = None
+ self.account_id = ''
+ self.routing_number = ''
+ self.branch_id = ''
+ self.account_type = ''
+ self.institution = None
+ self.type = AccountType.Unknown
+ # Used for error tracking
+ self.warnings = []
+
+ @property
+ def number(self):
+ # For backwards compatibility. Remove in version 1.0.
+ return self.account_id
+
+
+class InvestmentAccount(Account):
+ def __init__(self):
+ super(InvestmentAccount, self).__init__()
+ self.brokerid = ''
+
+
+class Security:
+ def __init__(self, uniqueid, name, ticker, memo):
+ self.uniqueid = uniqueid
+ self.name = name
+ self.ticker = ticker
+ self.memo = memo
+
+class Signon:
+ def __init__(self, code, severity, message):
+ self.code = code
+ self.severity = severity
+ self.message = message
+ if int(code) == 0:
+ self.success = True
+ else:
+ self.success = False
+
+ def __str__(self):
+ ret = "\t<SIGNONMSGSRSV1>\r\n" + "\t\t<SONRS>\r\n" + "\t\t\t<STATUS>\r\n"
+ ret += "\t\t\t\t<CODE>%s\r\n" % self.code
+ ret += "\t\t\t\t<SEVERITY>%s\r\n" % self.severity
+ if self.message:
+ ret += "\t\t\t\t<MESSAGE>%s\r\n" % self.message
+ ret += "\t\t\t</STATUS>\r\n" + "\t\t</SONRS>\r\n" + "\t</SIGNONMSGSRSV1>\r\n"
+ return ret
+
+class Statement(object):
+ def __init__(self):
+ self.start_date = ''
+ self.end_date = ''
+ self.currency = ''
+ self.transactions = []
+ # Error tracking:
+ self.discarded_entries = []
+ self.warnings = []
+
+
+class InvestmentStatement(object):
+ def __init__(self):
+ self.positions = []
+ self.transactions = []
+ # Error tracking:
+ self.discarded_entries = []
+ self.warnings = []
+
+
+class Transaction(object):
+ def __init__(self):
+ self.payee = ''
+ self.type = ''
+ self.date = None
+ self.amount = None
+ self.id = ''
+ self.memo = ''
+ self.sic = None
+ self.mcc = ''
+ self.checknum = ''
+
+ def __repr__(self):
+ return "<Transaction units=" + str(self.amount) + ">"
+
+
+class InvestmentTransaction(object):
+ (Unknown, BuyMF, SellMF, Reinvest, BuyStock, SellStock) = [x for x in range(-1, 5)]
+ def __init__(self, type):
+ try:
+ self.type = ['buymf', 'sellmf', 'reinvest', 'buystock', 'sellstock'].index(type.lower())
+ except ValueError:
+ self.type = InvestmentTransaction.Unknown
+ self.tradeDate = None
+ self.settleDate = None
+ self.security = ''
+ self.units = decimal.Decimal(0)
+ self.unit_price = decimal.Decimal(0)
+
+ def __repr__(self):
+ return "<InvestmentTransaction type=" + str(self.type) + ", units=" + str(self.units) + ">"
+
+
+class Position(object):
+ def __init__(self):
+ self.security = ''
+ self.units = decimal.Decimal(0)
+ self.unit_price = decimal.Decimal(0)
+
+
+class Institution(object):
+ def __init__(self):
+ self.organization = ''
+ self.fid = ''
+
+
+class OfxParserException(Exception):
+ pass
+
+
+class OfxParser(object):
+ @classmethod
+ def parse(cls_, file_handle, fail_fast=True):
+ '''
+ parse is the main entry point for an OfxParser. It takes a file
+ handle and an optional log_errors flag.
+
+ If fail_fast is True, the parser will fail on any errors.
+ If fail_fast is False, the parser will log poor statements in the
+ statement class and continue to run. Note: the library does not
+ guarantee that no exceptions will be raised to the caller, only
+ that statements will include bad transactions (which are marked).
+
+ '''
+ cls_.fail_fast = fail_fast
+
+ if isinstance(file_handle, type('')):
+ raise RuntimeError(six.u("parse() takes in a file handle, not a string"))
+
+ ofx_obj = Ofx()
+
+ # Store the headers
+ ofx_file = OfxPreprocessedFile(file_handle)
+ ofx_obj.headers = ofx_file.headers
+ ofx_obj.accounts = []
+ ofx_obj.signon = None
+
+ ofx = soup_maker(ofx_file.fh)
+ if len(ofx.contents) == 0:
+ raise OfxParserException('The ofx file is empty!')
+
+ sonrs_ofx = ofx.find('sonrs')
+ if sonrs_ofx:
+ ofx_obj.signon = cls_.parseSonrs(sonrs_ofx)
+
+ stmtrs_ofx = ofx.findAll('stmtrs')
+ if stmtrs_ofx:
+ ofx_obj.accounts += cls_.parseStmtrs(stmtrs_ofx, AccountType.Bank)
+
+ ccstmtrs_ofx = ofx.findAll('ccstmtrs')
+ if ccstmtrs_ofx:
+ ofx_obj.accounts += cls_.parseStmtrs(
+ ccstmtrs_ofx, AccountType.CreditCard)
+
+ invstmtrs_ofx = ofx.findAll('invstmtrs')
+ if invstmtrs_ofx:
+ ofx_obj.accounts += cls_.parseInvstmtrs(invstmtrs_ofx)
+ seclist_ofx = ofx.find('seclist')
+ if seclist_ofx:
+ ofx_obj.security_list = cls_.parseSeclist(seclist_ofx)
+ else:
+ ofx_obj.security_list = None
+
+ acctinfors_ofx = ofx.find('acctinfors')
+ if acctinfors_ofx:
+ ofx_obj.accounts += cls_.parseAcctinfors(acctinfors_ofx, ofx)
+
+ fi_ofx = ofx.find('fi')
+ if fi_ofx:
+ for account in ofx_obj.accounts:
+ account.institution = cls_.parseOrg(fi_ofx)
+
+ if ofx_obj.accounts:
+ ofx_obj.account = ofx_obj.accounts[0]
+
+ return ofx_obj
+
+ @classmethod
+ def parseOfxDateTime(cls_, ofxDateTime):
+ # dateAsString looks something like 20101106160000.00[-5:EST]
+ # for 6 Nov 2010 4pm UTC-5 aka EST
+
+ # Some places (e.g. Newfoundland) have non-integer offsets.
+ res = re.search("\[(?P<tz>[-+]?\d+\.?\d*)\:\w*\]$", ofxDateTime)
+ if res:
+ tz = float(res.group('tz'))
+ else:
+ tz = 0
+
+ timeZoneOffset = datetime.timedelta(hours=tz)
+
+ try:
+ local_date = datetime.datetime.strptime(
+ ofxDateTime[:14], '%Y%m%d%H%M%S'
+ )
+ return local_date - timeZoneOffset
+ except:
+ return datetime.datetime.strptime(
+ ofxDateTime[:8], '%Y%m%d') - timeZoneOffset
+
+ @classmethod
+ def parseAcctinfors(cls_, acctinfors_ofx, ofx):
+ all_accounts = []
+ for i in acctinfors_ofx.findAll('acctinfo'):
+ accounts = []
+ if i.find('invacctinfo'):
+ accounts += cls_.parseInvstmtrs([i])
+ elif i.find('ccacctinfo'):
+ accounts += cls_.parseStmtrs([i], AccountType.CreditCard)
+ elif i.find('bankacctinfo'):
+ accounts += cls_.parseStmtrs([i], AccountType.Bank)
+ else:
+ continue
+
+ fi_ofx = ofx.find('fi')
+ if fi_ofx:
+ for account in ofx_obj.accounts:
+ account.institution = cls_.parseOrg(fi_ofx)
+
+ desc = i.find('desc')
+ if hasattr(desc, 'contents'):
+ for account in accounts:
+ account.desc = desc.contents[0].strip()
+ all_accounts += accounts
+ return all_accounts
+
+ @classmethod
+ def parseInvstmtrs(cls_, invstmtrs_list):
+ ret = []
+ for invstmtrs_ofx in invstmtrs_list:
+ account = InvestmentAccount()
+ acctid_tag = invstmtrs_ofx.find('acctid')
+ if (hasattr(acctid_tag, 'contents')):
+ try:
+ account.account_id = acctid_tag.contents[0].strip()
+ except IndexError:
+ account.warnings.append(
+ six.u("Empty acctid tag for %s") % invstmtrs_ofx)
+ if cls_.fail_fast:
+ raise
+
+ brokerid_tag = invstmtrs_ofx.find('brokerid')
+ if (hasattr(brokerid_tag, 'contents')):
+ try:
+ account.brokerid = brokerid_tag.contents[0].strip()
+ except IndexError:
+ account.warnings.append(
+ six.u("Empty brokerid tag for %s") % invstmtrs_ofx)
+ if cls_.fail_fast:
+ raise
+
+ account.type = AccountType.Investment
+
+ if (invstmtrs_ofx):
+ account.statement = cls_.parseInvestmentStatement(
+ invstmtrs_ofx)
+ ret.append(account)
+ return ret
+
+ @classmethod
+ def parseSeclist(cls_, seclist_ofx):
+ securityList = []
+ for secinfo_ofx in seclist_ofx.findAll('secinfo'):
+ uniqueid_tag = secinfo_ofx.find('uniqueid')
+ name_tag = secinfo_ofx.find('secname')
+ ticker_tag = secinfo_ofx.find('ticker')
+ memo_tag = secinfo_ofx.find('memo')
+ if uniqueid_tag and name_tag and ticker_tag:
+ try:
+ memo = memo_tag.contents[0].strip()
+ except AttributeError:
+ # memo can be empty
+ memo = None
+ securityList.append(
+ Security(uniqueid_tag.contents[0].strip(),
+ name_tag.contents[0].strip(),
+ ticker_tag.contents[0].strip(),
+ memo))
+ return securityList
+
+ @classmethod
+ def parseInvestmentPosition(cls_, ofx):
+ position = Position()
+ tag = ofx.find('uniqueid')
+ if (hasattr(tag, 'contents')):
+ position.security = tag.contents[0].strip()
+ tag = ofx.find('units')
+ if (hasattr(tag, 'contents')):
+ position.units = decimal.Decimal(tag.contents[0].strip())
+ tag = ofx.find('unitprice')
+ if (hasattr(tag, 'contents')):
+ position.unit_price = decimal.Decimal(tag.contents[0].strip())
+ tag = ofx.find('dtpriceasof')
+ if (hasattr(tag, 'contents')):
+ try:
+ position.date = cls_.parseOfxDateTime(tag.contents[0].strip())
+ except ValueError:
+ raise
+ return position
+
+ @classmethod
+ def parseInvestmentTransaction(cls_, ofx):
+ transaction = InvestmentTransaction(ofx.name)
+ tag = ofx.find('fitid')
+ if (hasattr(tag, 'contents')):
+ transaction.id = tag.contents[0].strip()
+ tag = ofx.find('memo')
+ if (hasattr(tag, 'contents')):
+ transaction.memo = tag.contents[0].strip()
+ tag = ofx.find('dttrade')
+ if (hasattr(tag, 'contents')):
+ try:
+ transaction.tradeDate = cls_.parseOfxDateTime(
+ tag.contents[0].strip())
+ except ValueError:
+ raise
+ tag = ofx.find('dtsettle')
+ if (hasattr(tag, 'contents')):
+ try:
+ transaction.settleDate = cls_.parseOfxDateTime(
+ tag.contents[0].strip())
+ except ValueError:
+ raise
+ tag = ofx.find('uniqueid')
+ if (hasattr(tag, 'contents')):
+ transaction.security = tag.contents[0].strip()
+ tag = ofx.find('units')
+ if (hasattr(tag, 'contents')):
+ transaction.units = decimal.Decimal(tag.contents[0].strip())
+ tag = ofx.find('unitprice')
+ if (hasattr(tag, 'contents')):
+ transaction.unit_price = decimal.Decimal(tag.contents[0].strip())
+ return transaction
+
+ @classmethod
+ def parseInvestmentStatement(cls_, invstmtrs_ofx):
+ statement = InvestmentStatement()
+ currency_tag = invstmtrs_ofx.find('curdef')
+ if hasattr(currency_tag, "contents"):
+ statement.currency = currency_tag.contents[0].strip().lower()
+ invtranlist_ofx = invstmtrs_ofx.find('invtranlist')
+ if (invtranlist_ofx is not None):
+ tag = invtranlist_ofx.find('dtstart')
+ if (hasattr(tag, 'contents')):
+ try:
+ statement.start_date = cls_.parseOfxDateTime(
+ tag.contents[0].strip())
+ except IndexError:
+ statement.warnings.append(six.u('Empty start date.'))
+ if cls_.fail_fast:
+ raise
+ except ValueError:
+ e = sys.exc_info()[1]
+ statement.warnings.append(six.u('Invalid start date: %s') % e)
+ if cls_.fail_fast:
+ raise
+
+ tag = invtranlist_ofx.find('dtend')
+ if (hasattr(tag, 'contents')):
+ try:
+ statement.end_date = cls_.parseOfxDateTime(
+ tag.contents[0].strip())
+ except IndexError:
+ statement.warnings.append(six.u('Empty end date.'))
+ except ValueError:
+ e = sys.exc_info()[1]
+ statement.warnings.append(six.u('Invalid end date: %s') % e)
+ if cls_.fail_fast:
+ raise
+
+ for transaction_type in ['posmf', 'posstock', 'posopt']:
+ try:
+ for investment_ofx in invstmtrs_ofx.findAll(transaction_type):
+ statement.positions.append(
+ cls_.parseInvestmentPosition(investment_ofx))
+ except (ValueError, IndexError, decimal.InvalidOperation,
+ TypeError):
+ e = sys.exc_info()[1]
+ if cls_.fail_fast:
+ raise
+ statement.discarded_entries.append(
+ {six.u('error'): six.u("Error parsing positions: ") + str(e),
+ six.u('content'): investment_ofx}
+ )
+
+ for transaction_type in ['buymf', 'sellmf', 'reinvest', 'buystock',
+ 'sellstock', 'buyopt', 'sellopt']:
+ try:
+ for investment_ofx in invstmtrs_ofx.findAll(transaction_type):
+ statement.transactions.append(
+ cls_.parseInvestmentTransaction(investment_ofx))
+ except (ValueError, IndexError, decimal.InvalidOperation):
+ e = sys.exc_info()[1]
+ if cls_.fail_fast:
+ raise
+ statement.discarded_entries.append(
+ {six.u('error'): transaction_type + ": " + str(e),
+ six.u('content'): investment_ofx}
+ )
+
+ return statement
+
+ @classmethod
+ def parseOrg(cls_, fi_ofx):
+ institution = Institution()
+ org = fi_ofx.find('org')
+ if hasattr(org, 'contents'):
+ institution.organization = org.contents[0].strip()
+
+ fid = fi_ofx.find('fid')
+ if hasattr(fid, 'contents'):
+ institution.fid = fid.contents[0].strip()
+
+ return institution
+
+ @classmethod
+ def parseSonrs(cls_, sonrs):
+
+ code = int(sonrs.find('code').contents[0].strip())
+ severity = sonrs.find('severity').contents[0].strip()
+ try:
+ message = sonrs.find('message').contents[0].strip()
+ except:
+ message = ''
+
+ return Signon(code,severity,message)
+
+ @classmethod
+ def parseStmtrs(cls_, stmtrs_list, accountType):
+ ''' Parse the <STMTRS> tags and return a list of Accounts object. '''
+ ret = []
+ for stmtrs_ofx in stmtrs_list:
+ account = Account()
+ acctid_tag = stmtrs_ofx.find('acctid')
+ if hasattr(acctid_tag, 'contents'):
+ account.account_id = acctid_tag.contents[0].strip()
+ bankid_tag = stmtrs_ofx.find('bankid')
+ if hasattr(bankid_tag, 'contents'):
+ account.routing_number = bankid_tag.contents[0].strip()
+ branchid_tag = stmtrs_ofx.find('branchid')
+ if hasattr(branchid_tag, 'contents'):
+ account.branch_id = branchid_tag.contents[0].strip()
+ type_tag = stmtrs_ofx.find('accttype')
+ if hasattr(type_tag, 'contents'):
+ account.account_type = type_tag.contents[0].strip()
+ account.type = accountType
+
+ if stmtrs_ofx:
+ account.statement = cls_.parseStatement(stmtrs_ofx)
+ ret.append(account)
+ return ret
+
+ @classmethod
+ def parseStatement(cls_, stmt_ofx):
+ '''
+ Parse a statement in ofx-land and return a Statement object.
+ '''
+ statement = Statement()
+ dtstart_tag = stmt_ofx.find('dtstart')
+ if hasattr(dtstart_tag, "contents"):
+ try:
+ statement.start_date = cls_.parseOfxDateTime(
+ dtstart_tag.contents[0].strip())
+ except IndexError:
+ statement.warnings.append(
+ six.u("Statement start date was empty for %s") % stmt_ofx)
+ if cls_.fail_fast:
+ raise
+ except ValueError:
+ statement.warnings.append(
+ six.u("Statement start date was not allowed for %s") % stmt_ofx)
+ if cls_.fail_fast:
+ raise
+
+ dtend_tag = stmt_ofx.find('dtend')
+ if hasattr(dtend_tag, "contents"):
+ try:
+ statement.end_date = cls_.parseOfxDateTime(
+ dtend_tag.contents[0].strip())
+ except IndexError:
+ statement.warnings.append(
+ six.u("Statement start date was empty for %s") % stmt_ofx)
+ if cls_.fail_fast:
+ raise
+ except ValueError:
+ ve = sys.exc_info()[1]
+ msg = six.u("Statement start date was not formatted "
+ "correctly for %s")
+ statement.warnings.append(msg % stmt_ofx)
+ if cls_.fail_fast:
+ raise
+ except TypeError:
+ statement.warnings.append(
+ six.u("Statement start date was not allowed for %s") % stmt_ofx)
+ if cls_.fail_fast:
+ raise
+
+ currency_tag = stmt_ofx.find('curdef')
+ if hasattr(currency_tag, "contents"):
+ try:
+ statement.currency = currency_tag.contents[0].strip().lower()
+ except IndexError:
+ statement.warnings.append(
+ six.u("Currency definition was empty for %s") % stmt_ofx)
+ if cls_.fail_fast:
+ raise
+
+ ledger_bal_tag = stmt_ofx.find('ledgerbal')
+ if hasattr(ledger_bal_tag, "contents"):
+ balamt_tag = ledger_bal_tag.find('balamt')
+ if hasattr(balamt_tag, "contents"):
+ try:
+ statement.balance = decimal.Decimal(
+ balamt_tag.contents[0].strip())
+ except (IndexError, decimal.InvalidOperation):
+ ex = sys.exc_info()[1]
+ statement.warnings.append(
+ six.u("Ledger balance amount was empty for %s") % stmt_ofx)
+ if cls_.fail_fast:
+ raise OfxParserException("Empty ledger balance")
+
+ avail_bal_tag = stmt_ofx.find('availbal')
+ if hasattr(avail_bal_tag, "contents"):
+ balamt_tag = avail_bal_tag.find('balamt')
+ if hasattr(balamt_tag, "contents"):
+ try:
+ statement.available_balance = decimal.Decimal(
+ balamt_tag.contents[0].strip())
+ except (IndexError, decimal.InvalidOperation):
+ ex = sys.exc_info()[1]
+ msg = six.u("Available balance amount was empty for %s")
+ statement.warnings.append(msg % stmt_ofx)
+ if cls_.fail_fast:
+ raise OfxParserException("Empty available balance")
+
+ for transaction_ofx in stmt_ofx.findAll('stmttrn'):
+ try:
+ statement.transactions.append(
+ cls_.parseTransaction(transaction_ofx))
+ except OfxParserException:
+ ofxError = sys.exc_info()[1]
+ statement.discarded_entries.append(
+ {'error': str(ofxError), 'content': transaction_ofx})
+ if cls_.fail_fast:
+ raise
+
+ return statement
+
+ @classmethod
+ def parseTransaction(cls_, txn_ofx):
+ '''
+ Parse a transaction in ofx-land and return a Transaction object.
+ '''
+ transaction = Transaction()
+
+ type_tag = txn_ofx.find('trntype')
+ if hasattr(type_tag, 'contents'):
+ try:
+ transaction.type = type_tag.contents[0].lower().strip()
+ except IndexError:
+ raise OfxParserException(six.u("Empty transaction type"))
+ except TypeError:
+ raise OfxParserException(
+ six.u("No Transaction type (a required field)"))
+
+ name_tag = txn_ofx.find('name')
+ if hasattr(name_tag, "contents"):
+ try:
+ transaction.payee = name_tag.contents[0].strip()
+ except IndexError:
+ raise OfxParserException(six.u("Empty transaction name"))
+ except TypeError:
+ raise OfxParserException(
+ six.u("No Transaction name (a required field)"))
+
+ memo_tag = txn_ofx.find('memo')
+ if hasattr(memo_tag, "contents"):
+ try:
+ transaction.memo = memo_tag.contents[0].strip()
+ except IndexError:
+ # Memo can be empty.
+ pass
+ except TypeError:
+ pass
+
+ amt_tag = txn_ofx.find('trnamt')
+ if hasattr(amt_tag, "contents"):
+ try:
+ transaction.amount = decimal.Decimal(
+ amt_tag.contents[0].strip())
+ except IndexError:
+ raise OfxParserException("Invalid Transaction Date")
+ except decimal.InvalidOperation:
+ raise OfxParserException(
+ six.u("Invalid Transaction Amount: '%s'") % amt_tag.contents[0])
+ except TypeError:
+ raise OfxParserException(
+ six.u("No Transaction Amount (a required field)"))
+ else:
+ raise OfxParserException(
+ six.u("Missing Transaction Amount (a required field)"))
+
+ date_tag = txn_ofx.find('dtposted')
+ if hasattr(date_tag, "contents"):
+ try:
+ transaction.date = cls_.parseOfxDateTime(
+ date_tag.contents[0].strip())
+ except IndexError:
+ raise OfxParserException("Invalid Transaction Date")
+ except ValueError:
+ ve = sys.exc_info()[1]
+ raise OfxParserException(str(ve))
+ except TypeError:
+ raise OfxParserException(
+ six.u("No Transaction Date (a required field)"))
+ else:
+ raise OfxParserException(
+ six.u("Missing Transaction Date (a required field)"))
+
+ id_tag = txn_ofx.find('fitid')
+ if hasattr(id_tag, "contents"):
+ try:
+ transaction.id = id_tag.contents[0].strip()
+ except IndexError:
+ raise OfxParserException(six.u("Empty FIT id (a required field)"))
+ except TypeError:
+ raise OfxParserException(six.u("No FIT id (a required field)"))
+ else:
+ raise OfxParserException(six.u("Missing FIT id (a required field)"))
+
+ sic_tag = txn_ofx.find('sic')
+ if hasattr(sic_tag, 'contents'):
+ try:
+ transaction.sic = sic_tag.contents[0].strip()
+ except IndexError:
+ raise OfxParserException(six.u("Empty transaction Standard Industry Code (SIC)"))
+
+ if transaction.sic is not None and transaction.sic in mcc.codes:
+ try:
+ transaction.mcc = mcc.codes.get(transaction.sic, '').get('combined description')
+ except IndexError:
+ raise OfxParserException(six.u("Empty transaction Merchant Category Code (MCC)"))
+ except AttributeError:
+ if cls._fail_fast:
+ raise
+
+ checknum_tag = txn_ofx.find('checknum')
+ if hasattr(checknum_tag, 'contents'):
+ try:
+ transaction.checknum = checknum_tag.contents[0].strip()
+ except IndexError:
+ raise OfxParserException(six.u("Empty Check (or other reference) number"))
+
+ return transaction