diff options
Diffstat (limited to 'ofxparse/ofxparse.py')
-rw-r--r-- | ofxparse/ofxparse.py | 349 |
1 files changed, 187 insertions, 162 deletions
diff --git a/ofxparse/ofxparse.py b/ofxparse/ofxparse.py index 18b9667..2621286 100644 --- a/ofxparse/ofxparse.py +++ b/ofxparse/ofxparse.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import, with_statement +from __future__ import absolute_import import sys import decimal @@ -14,45 +14,18 @@ except ImportError: from io import StringIO import six - -if 'OrderedDict' in dir(collections): - odict = collections -else: - import ordereddict as odict - from . import mcc +odict = collections -def skip_headers(fh): - ''' - Prepare `fh` for parsing by BeautifulSoup by skipping its OFX - headers. - ''' - if fh is None or isinstance(fh, six.string_types): - return - fh.seek(0) - header_re = re.compile(r"^\s*\w+:\s*\w+\s*$") - while True: - pos = fh.tell() - line = fh.readline() - if not line: - break - if header_re.search(line) is None: - fh.seek(pos) - return - +try: + from bs4 import BeautifulSoup -def soup_maker(fh): - skip_headers(fh) - try: - from bs4 import BeautifulSoup - soup = BeautifulSoup(fh, "html.parser") - for tag in soup.findAll(): - tag.name = tag.name.lower() - except ImportError: - from BeautifulSoup import BeautifulStoneSoup - soup = BeautifulStoneSoup(fh) - return soup + def soup_maker(fh): + return BeautifulSoup(fh, 'html.parser') +except ImportError: + from BeautifulSoup import BeautifulStoneSoup + soup_maker = BeautifulStoneSoup def try_decode(string, encoding): @@ -94,6 +67,12 @@ class OfxFile(object): if not hasattr(self.fh, "seek"): return # fh is not a file object, we're doomed. + # If the file handler is text stream, convert to bytes one: + first = self.fh.read(1) + self.fh.seek(0) + if not isinstance(first, bytes): + self.fh = six.BytesIO(six.b(self.fh.read())) + with save_pos(self.fh): self.read_headers() self.handle_encoding() @@ -174,14 +153,14 @@ class OfxPreprocessedFile(OfxFile): # find all closing tags as hints closing_tags = [t.upper() for t in re.findall(r'(?i)</([a-z0-9_\.]+)>', - ofx_string)] + 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): + for token in tokens: is_closing_tag = token.startswith('</') is_processing_tag = token.startswith('<?') is_cdata = token.startswith('<!') @@ -239,11 +218,13 @@ class InvestmentAccount(Account): super(InvestmentAccount, self).__init__() self.brokerid = '' + class BrokerageBalance: def __init__(self): self.name = None self.description = None - self.value = None # decimal + self.value = None # decimal + class Security: def __init__(self, uniqueid, name, ticker, memo): @@ -293,7 +274,7 @@ class Signon: ret += "\t\t\t</FI>\r\n" if self.intu_bid is not None: ret += "\t\t\t<INTU.BID>" + self.intu_bid + "\r\n" - ret += "\t\t</SONRS>\r\n" + ret += "\t\t</SONRS>\r\n" ret += "\t</SIGNONMSGSRSV1>\r\n" return ret @@ -354,6 +335,7 @@ class InvestmentTransaction(object): self.commission = decimal.Decimal(0) self.fees = decimal.Decimal(0) self.total = decimal.Decimal(0) + self.tferaction = None def __repr__(self): return "<InvestmentTransaction type=" + str(self.type) + ", \ @@ -365,6 +347,7 @@ class Position(object): self.security = '' self.units = decimal.Decimal(0) self.unit_price = decimal.Decimal(0) + self.market_value = decimal.Decimal(0) class Institution(object): @@ -379,7 +362,7 @@ class OfxParserException(Exception): class OfxParser(object): @classmethod - def parse(cls_, file_handle, fail_fast=True): + def parse(cls, file_handle, fail_fast=True, custom_date_format=None): ''' parse is the main entry point for an OfxParser. It takes a file handle and an optional log_errors flag. @@ -391,7 +374,8 @@ class OfxParser(object): that statements will include bad transactions (which are marked). ''' - cls_.fail_fast = fail_fast + cls.fail_fast = fail_fast + cls.custom_date_format = custom_date_format if not hasattr(file_handle, 'seek'): raise TypeError(six.u('parse() accepts a seek-able file handle\ @@ -405,14 +389,13 @@ class OfxParser(object): ofx_obj.accounts = [] ofx_obj.signon = None - skip_headers(ofx_file.fh) ofx = soup_maker(ofx_file.fh) if ofx.find('ofx') is None: raise OfxParserException('The ofx file is empty!') sonrs_ofx = ofx.find('sonrs') if sonrs_ofx: - ofx_obj.signon = cls_.parseSonrs(sonrs_ofx) + ofx_obj.signon = cls.parseSonrs(sonrs_ofx) stmttrnrs = ofx.find('stmttrnrs') if stmttrnrs: @@ -429,32 +412,47 @@ class OfxParser(object): ofx_obj.status['severity'] = \ stmttrnrs_status.find('severity').contents[0].strip() + ccstmttrnrs = ofx.find('ccstmttrnrs') + if ccstmttrnrs: + ccstmttrnrs_trnuid = ccstmttrnrs.find('trnuid') + if ccstmttrnrs_trnuid: + ofx_obj.trnuid = ccstmttrnrs_trnuid.contents[0].strip() + + ccstmttrnrs_status = ccstmttrnrs.find('status') + if ccstmttrnrs_status: + ofx_obj.status = {} + ofx_obj.status['code'] = int( + ccstmttrnrs_status.find('code').contents[0].strip() + ) + ofx_obj.status['severity'] = \ + ccstmttrnrs_status.find('severity').contents[0].strip() + stmtrs_ofx = ofx.findAll('stmtrs') if stmtrs_ofx: - ofx_obj.accounts += cls_.parseStmtrs(stmtrs_ofx, AccountType.Bank) + ofx_obj.accounts += cls.parseStmtrs(stmtrs_ofx, AccountType.Bank) ccstmtrs_ofx = ofx.findAll('ccstmtrs') if ccstmtrs_ofx: - ofx_obj.accounts += cls_.parseStmtrs( + ofx_obj.accounts += cls.parseStmtrs( ccstmtrs_ofx, AccountType.CreditCard) invstmtrs_ofx = ofx.findAll('invstmtrs') if invstmtrs_ofx: - ofx_obj.accounts += cls_.parseInvstmtrs(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) + 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) + 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) + account.institution = cls.parseOrg(fi_ofx) if ofx_obj.accounts: ofx_obj.account = ofx_obj.accounts[0] @@ -462,12 +460,12 @@ class OfxParser(object): return ofx_obj @classmethod - def parseOfxDateTime(cls_, ofxDateTime): + 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) + res = re.search(r"\[(?P<tz>[-+]?\d+\.?\d*)\:\w*\]$", ofxDateTime) if res: tz = float(res.group('tz')) else: @@ -475,42 +473,44 @@ class OfxParser(object): timeZoneOffset = datetime.timedelta(hours=tz) - res = re.search("^[0-9]*\.([0-9]{0,5})", ofxDateTime) + res = re.search(r"^[0-9]*\.([0-9]{0,5})", ofxDateTime) if res: msec = datetime.timedelta(seconds=float("0." + res.group(1))) else: msec = datetime.timedelta(seconds=0) try: - local_date = datetime.datetime.strptime( - ofxDateTime[:14], '%Y%m%d%H%M%S' - ) + local_date = datetime.datetime.strptime(ofxDateTime[:14], '%Y%m%d%H%M%S') return local_date - timeZoneOffset + msec - except: + except ValueError: if ofxDateTime[:8] == "00000000": return None - return datetime.datetime.strptime( - ofxDateTime[:8], '%Y%m%d') - timeZoneOffset + msec + if not cls.custom_date_format: + return datetime.datetime.strptime( + ofxDateTime[:8], '%Y%m%d') - timeZoneOffset + msec + else: + return datetime.datetime.strptime( + ofxDateTime[:8], cls.custom_date_format) - timeZoneOffset + msec @classmethod - def parseAcctinfors(cls_, acctinfors_ofx, ofx): + def parseAcctinfors(cls, acctinfors_ofx, ofx): all_accounts = [] for i in acctinfors_ofx.findAll('acctinfo'): accounts = [] if i.find('invacctinfo'): - accounts += cls_.parseInvstmtrs([i]) + accounts += cls.parseInvstmtrs([i]) elif i.find('ccacctinfo'): - accounts += cls_.parseStmtrs([i], AccountType.CreditCard) + accounts += cls.parseStmtrs([i], AccountType.CreditCard) elif i.find('bankacctinfo'): - accounts += cls_.parseStmtrs([i], AccountType.Bank) + 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) + fi_ofx = ofx.find('fi') + if fi_ofx: + for account in all_accounts: + account.institution = cls.parseOrg(fi_ofx) desc = i.find('desc') if hasattr(desc, 'contents'): @@ -520,40 +520,40 @@ class OfxParser(object): return all_accounts @classmethod - def parseInvstmtrs(cls_, invstmtrs_list): + 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')): + 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: + if cls.fail_fast: raise brokerid_tag = invstmtrs_ofx.find('brokerid') - if (hasattr(brokerid_tag, 'contents')): + 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: + if cls.fail_fast: raise account.type = AccountType.Investment - if (invstmtrs_ofx): - account.statement = cls_.parseInvestmentStatement( + if invstmtrs_ofx: + account.statement = cls.parseInvestmentStatement( invstmtrs_ofx) ret.append(account) return ret @classmethod - def parseSeclist(cls_, seclist_ofx): + def parseSeclist(cls, seclist_ofx): securityList = [] for secinfo_ofx in seclist_ofx.findAll('secinfo'): uniqueid_tag = secinfo_ofx.find('uniqueid') @@ -579,102 +579,108 @@ class OfxParser(object): return securityList @classmethod - def parseInvestmentPosition(cls_, ofx): + def parseInvestmentPosition(cls, ofx): position = Position() tag = ofx.find('uniqueid') - if (hasattr(tag, 'contents')): + if hasattr(tag, 'contents'): position.security = tag.contents[0].strip() tag = ofx.find('units') - if (hasattr(tag, 'contents')): - position.units = cls_.toDecimal(tag) + if hasattr(tag, 'contents'): + position.units = cls.toDecimal(tag) tag = ofx.find('unitprice') - if (hasattr(tag, 'contents')): - position.unit_price = cls_.toDecimal(tag) + if hasattr(tag, 'contents'): + position.unit_price = cls.toDecimal(tag) + tag = ofx.find('mktval') + if hasattr(tag, 'contents'): + position.market_value = cls.toDecimal(tag) tag = ofx.find('dtpriceasof') - if (hasattr(tag, 'contents')): + if hasattr(tag, 'contents'): try: - position.date = cls_.parseOfxDateTime(tag.contents[0].strip()) + position.date = cls.parseOfxDateTime(tag.contents[0].strip()) except ValueError: raise return position @classmethod - def parseInvestmentTransaction(cls_, ofx): + def parseInvestmentTransaction(cls, ofx): transaction = InvestmentTransaction(ofx.name) tag = ofx.find('fitid') - if (hasattr(tag, 'contents')): + if hasattr(tag, 'contents'): transaction.id = tag.contents[0].strip() tag = ofx.find('memo') - if (hasattr(tag, 'contents')): + if hasattr(tag, 'contents'): transaction.memo = tag.contents[0].strip() tag = ofx.find('dttrade') - if (hasattr(tag, 'contents')): + if hasattr(tag, 'contents'): try: - transaction.tradeDate = cls_.parseOfxDateTime( + transaction.tradeDate = cls.parseOfxDateTime( tag.contents[0].strip()) except ValueError: raise tag = ofx.find('dtsettle') - if (hasattr(tag, 'contents')): + if hasattr(tag, 'contents'): try: - transaction.settleDate = cls_.parseOfxDateTime( + transaction.settleDate = cls.parseOfxDateTime( tag.contents[0].strip()) except ValueError: raise tag = ofx.find('uniqueid') - if (hasattr(tag, 'contents')): + if hasattr(tag, 'contents'): transaction.security = tag.contents[0].strip() tag = ofx.find('incometype') - if (hasattr(tag, 'contents')): + if hasattr(tag, 'contents'): transaction.income_type = tag.contents[0].strip() tag = ofx.find('units') - if (hasattr(tag, 'contents')): - transaction.units = cls_.toDecimal(tag) + if hasattr(tag, 'contents'): + transaction.units = cls.toDecimal(tag) tag = ofx.find('unitprice') - if (hasattr(tag, 'contents')): - transaction.unit_price = cls_.toDecimal(tag) + if hasattr(tag, 'contents'): + transaction.unit_price = cls.toDecimal(tag) tag = ofx.find('commission') - if (hasattr(tag, 'contents')): - transaction.commission = cls_.toDecimal(tag) + if hasattr(tag, 'contents'): + transaction.commission = cls.toDecimal(tag) tag = ofx.find('fees') - if (hasattr(tag, 'contents')): - transaction.fees = cls_.toDecimal(tag) + if hasattr(tag, 'contents'): + transaction.fees = cls.toDecimal(tag) tag = ofx.find('total') - if (hasattr(tag, 'contents')): - transaction.total = cls_.toDecimal(tag) + if hasattr(tag, 'contents'): + transaction.total = cls.toDecimal(tag) tag = ofx.find('inv401ksource') - if (hasattr(tag, 'contents')): + if hasattr(tag, 'contents'): transaction.inv401ksource = tag.contents[0].strip() + tag = ofx.find('tferaction') + if hasattr(tag, 'contents'): + transaction.tferaction = tag.contents[0].strip() return transaction @classmethod - def parseInvestmentStatement(cls_, invstmtrs_ofx): + 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): + if invtranlist_ofx is not None: tag = invtranlist_ofx.find('dtstart') - if (hasattr(tag, 'contents')): + if hasattr(tag, 'contents'): try: - statement.start_date = cls_.parseOfxDateTime( + statement.start_date = cls.parseOfxDateTime( tag.contents[0].strip()) except IndexError: statement.warnings.append(six.u('Empty start date.')) - if cls_.fail_fast: + 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: + if cls.fail_fast: raise tag = invtranlist_ofx.find('dtend') - if (hasattr(tag, 'contents')): + if hasattr(tag, 'contents'): try: - statement.end_date = cls_.parseOfxDateTime( + statement.end_date = cls.parseOfxDateTime( tag.contents[0].strip()) except IndexError: statement.warnings.append(six.u('Empty end date.')) @@ -682,18 +688,19 @@ class OfxParser(object): e = sys.exc_info()[1] statement.warnings.append(six.u('Invalid end date: \ %s') % e) - if cls_.fail_fast: + if cls.fail_fast: raise - for transaction_type in ['posmf', 'posstock', 'posopt']: + for transaction_type in ['posmf', 'posstock', 'posopt', 'posother', + 'posdebt']: try: for investment_ofx in invstmtrs_ofx.findAll(transaction_type): statement.positions.append( - cls_.parseInvestmentPosition(investment_ofx)) + cls.parseInvestmentPosition(investment_ofx)) except (ValueError, IndexError, decimal.InvalidOperation, TypeError): e = sys.exc_info()[1] - if cls_.fail_fast: + if cls.fail_fast: raise statement.discarded_entries.append( {six.u('error'): six.u("Error parsing positions: \ @@ -704,31 +711,43 @@ class OfxParser(object): try: for investment_ofx in invstmtrs_ofx.findAll(transaction_type): statement.transactions.append( - cls_.parseInvestmentTransaction(investment_ofx)) + cls.parseInvestmentTransaction(investment_ofx)) except (ValueError, IndexError, decimal.InvalidOperation): e = sys.exc_info()[1] - if cls_.fail_fast: + if cls.fail_fast: raise statement.discarded_entries.append( {six.u('error'): transaction_type + ": " + str(e), six.u('content'): investment_ofx} ) + for transaction_ofx in invstmtrs_ofx.findAll('invbanktran'): + for stmt_ofx in transaction_ofx.findAll('stmttrn'): + try: + statement.transactions.append( + cls.parseTransaction(stmt_ofx)) + except OfxParserException: + ofxError = sys.exc_info()[1] + statement.discarded_entries.append( + {'error': str(ofxError), 'content': transaction_ofx}) + if cls.fail_fast: + raise + invbal_ofx = invstmtrs_ofx.find('invbal') if invbal_ofx is not None: - #<AVAILCASH>18073.98<MARGINBALANCE>+00000000000.00<SHORTBALANCE>+00000000000.00<BUYPOWER>+00000000000.00 + # <AVAILCASH>18073.98<MARGINBALANCE>+00000000000.00<SHORTBALANCE>+00000000000.00<BUYPOWER>+00000000000.00 availcash_ofx = invbal_ofx.find('availcash') if availcash_ofx is not None: - statement.available_cash = cls_.toDecimal(availcash_ofx) + statement.available_cash = cls.toDecimal(availcash_ofx) margin_balance_ofx = invbal_ofx.find('marginbalance') if margin_balance_ofx is not None: - statement.margin_balance = cls_.toDecimal(margin_balance_ofx) + statement.margin_balance = cls.toDecimal(margin_balance_ofx) short_balance_ofx = invbal_ofx.find('shortbalance') if short_balance_ofx is not None: - statement.short_balance = cls_.toDecimal(short_balance_ofx) + statement.short_balance = cls.toDecimal(short_balance_ofx) buy_power_ofx = invbal_ofx.find('buypower') if buy_power_ofx is not None: - statement.buy_power = cls_.toDecimal(buy_power_ofx) + statement.buy_power = cls.toDecimal(buy_power_ofx) ballist_ofx = invbal_ofx.find('ballist') if ballist_ofx is not None: @@ -740,16 +759,17 @@ class OfxParser(object): brokerage_balance.name = name_ofx.contents[0].strip() description_ofx = balance_ofx.find('desc') if description_ofx is not None: - brokerage_balance.description = description_ofx.contents[0].strip() + brokerage_balance.description = \ + description_ofx.contents[0].strip() value_ofx = balance_ofx.find('value') if value_ofx is not None: - brokerage_balance.value = cls_.toDecimal(value_ofx) + brokerage_balance.value = cls.toDecimal(value_ofx) statement.balance_list.append(brokerage_balance) return statement @classmethod - def parseOrg(cls_, fi_ofx): + def parseOrg(cls, fi_ofx): institution = Institution() org = fi_ofx.find('org') if hasattr(org, 'contents'): @@ -762,7 +782,7 @@ class OfxParser(object): return institution @classmethod - def parseSonrs(cls_, sonrs): + def parseSonrs(cls, sonrs): items = [ 'code', @@ -779,7 +799,7 @@ class OfxParser(object): for i in items: try: idict[i] = sonrs.find(i).contents[0].strip() - except: + except Exception: idict[i] = None idict['code'] = int(idict['code']) if idict['message'] is None: @@ -788,35 +808,35 @@ class OfxParser(object): return Signon(idict) @classmethod - def parseStmtrs(cls_, stmtrs_list, accountType): + 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() act_curdef = stmtrs_ofx.find('curdef') - if act_curdef: + if act_curdef and act_curdef.contents: account.curdef = act_curdef.contents[0].strip() acctid_tag = stmtrs_ofx.find('acctid') - if hasattr(acctid_tag, 'contents'): + if acctid_tag and acctid_tag.contents: account.account_id = acctid_tag.contents[0].strip() bankid_tag = stmtrs_ofx.find('bankid') - if hasattr(bankid_tag, 'contents'): + if bankid_tag and bankid_tag.contents: account.routing_number = bankid_tag.contents[0].strip() branchid_tag = stmtrs_ofx.find('branchid') - if hasattr(branchid_tag, 'contents'): + if branchid_tag and branchid_tag.contents: account.branch_id = branchid_tag.contents[0].strip() type_tag = stmtrs_ofx.find('accttype') - if hasattr(type_tag, 'contents'): + if type_tag and type_tag.contents: account.account_type = type_tag.contents[0].strip() account.type = accountType if stmtrs_ofx: - account.statement = cls_.parseStatement(stmtrs_ofx) + account.statement = cls.parseStatement(stmtrs_ofx) ret.append(account) return ret @classmethod - def parseBalance(cls_, statement, stmt_ofx, bal_tag_name, bal_attr, + def parseBalance(cls, statement, stmt_ofx, bal_tag_name, bal_attr, bal_date_attr, bal_type_string): bal_tag = stmt_ofx.find(bal_tag_name) if hasattr(bal_tag, "contents"): @@ -824,34 +844,33 @@ class OfxParser(object): dtasof_tag = bal_tag.find('dtasof') if hasattr(balamt_tag, "contents"): try: - setattr(statement, bal_attr, cls_.toDecimal(balamt_tag)) + setattr(statement, bal_attr, cls.toDecimal(balamt_tag)) except (IndexError, decimal.InvalidOperation): - ex = sys.exc_info()[1] statement.warnings.append( six.u("%s balance amount was empty for \ %s") % (bal_type_string, stmt_ofx)) - if cls_.fail_fast: + if cls.fail_fast: raise OfxParserException("Empty %s balance\ " % bal_type_string) if hasattr(dtasof_tag, "contents"): try: - setattr(statement, bal_date_attr, cls_.parseOfxDateTime( + setattr(statement, bal_date_attr, cls.parseOfxDateTime( dtasof_tag.contents[0].strip())) except IndexError: statement.warnings.append( six.u("%s balance date was empty for %s\ ") % (bal_type_string, stmt_ofx)) - if cls_.fail_fast: + if cls.fail_fast: raise except ValueError: statement.warnings.append( six.u("%s balance date was not allowed for \ %s") % (bal_type_string, stmt_ofx)) - if cls_.fail_fast: + if cls.fail_fast: raise @classmethod - def parseStatement(cls_, stmt_ofx): + def parseStatement(cls, stmt_ofx): ''' Parse a statement in ofx-land and return a Statement object. ''' @@ -859,42 +878,41 @@ class OfxParser(object): dtstart_tag = stmt_ofx.find('dtstart') if hasattr(dtstart_tag, "contents"): try: - statement.start_date = cls_.parseOfxDateTime( + 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: + 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: + if cls.fail_fast: raise dtend_tag = stmt_ofx.find('dtend') if hasattr(dtend_tag, "contents"): try: - statement.end_date = cls_.parseOfxDateTime( + 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: + 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: + 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: + if cls.fail_fast: raise currency_tag = stmt_ofx.find('curdef') @@ -904,30 +922,30 @@ class OfxParser(object): except IndexError: statement.warnings.append( six.u("Currency definition was empty for %s") % stmt_ofx) - if cls_.fail_fast: + if cls.fail_fast: raise - cls_.parseBalance(statement, stmt_ofx, 'ledgerbal', - 'balance', 'balance_date', 'ledger') + cls.parseBalance(statement, stmt_ofx, 'ledgerbal', + 'balance', 'balance_date', 'ledger') - cls_.parseBalance(statement, stmt_ofx, 'availbal', 'available_balance', - 'available_balance_date', 'ledger') + cls.parseBalance(statement, stmt_ofx, 'availbal', 'available_balance', + 'available_balance_date', 'ledger') for transaction_ofx in stmt_ofx.findAll('stmttrn'): try: statement.transactions.append( - cls_.parseTransaction(transaction_ofx)) + 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: + if cls.fail_fast: raise return statement @classmethod - def parseTransaction(cls_, txn_ofx): + def parseTransaction(cls, txn_ofx): ''' Parse a transaction in ofx-land and return a Transaction object. ''' @@ -966,7 +984,7 @@ class OfxParser(object): amt_tag = txn_ofx.find('trnamt') if hasattr(amt_tag, "contents"): try: - transaction.amount = cls_.toDecimal(amt_tag) + transaction.amount = cls.toDecimal(amt_tag) except IndexError: raise OfxParserException("Invalid Transaction Date") except decimal.InvalidOperation: @@ -987,7 +1005,7 @@ class OfxParser(object): date_tag = txn_ofx.find('dtposted') if hasattr(date_tag, "contents"): try: - transaction.date = cls_.parseOfxDateTime( + transaction.date = cls.parseOfxDateTime( date_tag.contents[0].strip()) except IndexError: raise OfxParserException("Invalid Transaction Date") @@ -1030,7 +1048,7 @@ class OfxParser(object): raise OfxParserException(six.u("Empty transaction Merchant Category \ Code (MCC)")) except AttributeError: - if cls._fail_fast: + if cls.fail_fast: raise checknum_tag = txn_ofx.find('checknum') @@ -1044,8 +1062,15 @@ class OfxParser(object): return transaction @classmethod - def toDecimal(cls_, tag): + def toDecimal(cls, tag): d = tag.contents[0].strip() + # Handle 10,000.50 formatted numbers + if re.search(r'.*\..*,', d): + d = d.replace('.', '') + # Handle 10.000,50 formatted numbers + if re.search(r'.*,.*\.', d): + d = d.replace(',', '') + # Handle 10000,50 formatted numbers if '.' not in d and ',' in d: d = d.replace(',', '.') return decimal.Decimal(d) |