diff --git a/qifparse/parser.py b/qifparse/parser.py index 9ccebe7..751886e 100644 --- a/qifparse/parser.py +++ b/qifparse/parser.py @@ -13,10 +13,18 @@ Qif, ) + +def convertFloat(num_str): + """Convert float, possibly with embedded thousands separators""" + num_str = num_str.replace(QifParser._thousands_separator, '') + return Decimal(num_str) + + NON_INVST_ACCOUNT_TYPES = [ '!Type:Cash', '!Type:Bank', '!Type:Ccard', + '!Type:CCard', # Some Quicken exports '!Type:Oth A', '!Type:Oth L', '!Type:Invoice', # Quicken for business only @@ -29,8 +37,20 @@ class QifParserException(Exception): class QifParser(object): + _months_first = False + _thousands_separator = ',' + @classmethod - def parse(cls_, file_handle, date_format=None): + def parse(cls_, file_handle, date_format=None, + months_first=None, + thousands_separator=None, + currency_num_fractional_digits=None): + if months_first is not None: + cls_._months_first = months_first + if thousands_separator is not None: + cls_._thousands_separator = thousands_separator + if currency_num_fractional_digits is not None: + Qif.set_currency_num_fractional_digits(int(currency_num_fractional_digits)) if isinstance(file_handle, type('')): raise RuntimeError( six.u("parse() takes in a file handle, not a string")) @@ -54,6 +74,7 @@ def parse(cls_, file_handle, date_format=None): if not chunk: continue first_line = chunk.split('\n')[0] + first_line = first_line.strip() if first_line == '!Type:Cat': last_type = 'category' elif first_line == '!Account': @@ -209,7 +230,7 @@ def parseMemorizedTransaction(cls_, chunk, date_format=None): split.address.append(line[1:]) elif line[0] == '$': split = curItem.splits[-1] - split.amount = Decimal(line[1:]) + split.amount = convertFloat(line[1:]) else: # don't recognise this line; ignore it print ("Skipping unknown line:\n" + str(line)) @@ -232,7 +253,9 @@ def parseTransaction(cls_, chunk, date_format=None): elif line[0] == 'N': curItem.num = line[1:] elif line[0] == 'T': - curItem.amount = Decimal(line[1:]) + curItem.amount = convertFloat(line[1:]) + elif line[0] == 'U': + curItem.amount_U = convertFloat(line[1:]) elif line[0] == 'C': curItem.cleared = line[1:] elif line[0] == 'P': @@ -281,7 +304,7 @@ def parseTransaction(cls_, chunk, date_format=None): split.address.append(line[1:]) elif line[0] == '$': split = curItem.splits[-1] - split.amount = Decimal(line[1:]) + split.amount = convertFloat(line[1:-1]) else: # don't recognise this line; ignore it print ("Skipping unknown line:\n" + str(line)) @@ -302,7 +325,7 @@ def parseInvestment(cls_, chunk, date_format=None): elif line[0] == 'D': curItem.date = cls_.parseQifDateTime(line[1:]) elif line[0] == 'T': - curItem.amount = Decimal(line[1:]) + curItem.amount = convertFloat(line[1:]) elif line[0] == 'N': curItem.action = line[1:] elif line[0] == 'Y': @@ -341,12 +364,36 @@ def parseQifDateTime(cls_, qdate): for i in range(len(qdate)): if qdate[i] == " ": qdate = qdate[:i] + "0" + qdate[i+1:] + if len(qdate) == 10: # new form with YYYY date - iso_date = qdate[6:10] + "-" + qdate[3:5] + "-" + qdate[0:2] - return datetime.strptime(iso_date, '%Y-%m-%d') + year = qdate[6:10] + if cls_._months_first: + month = qdate[0:2] + day = qdate[3:5] + else: + month = qdate[3:5] + day = qdate[0:2] + iso_date = year + "-" + month + "-" + day + try: + dtime = datetime.strptime(iso_date, '%Y-%m-%d') + except: + raise ValueError("ERROR in time format QIF(%s) ISO(%s)" % (qdate, iso_date)) + return dtime + if qdate[5] == "'": C = "20" else: C = "19" - iso_date = C + qdate[6:8] + "-" + qdate[3:5] + "-" + qdate[0:2] - return datetime.strptime(iso_date, '%Y-%m-%d') + year = C + qdate[6:8] + if cls_._months_first: + month = qdate[0:2] + day = qdate[3:5] + else: + month = qdate[3:5] + day = qdate[0:2] + iso_date = year + "-" + month + "-" + day + try: + dtime = datetime.strptime(iso_date, '%Y-%m-%d') + except: + raise ValueError("ERROR in time format QIF(%s) ISO(%s)" % (qdate, iso_date)) + return dtime diff --git a/qifparse/qif.py b/qifparse/qif.py index 2317f82..4e82816 100644 --- a/qifparse/qif.py +++ b/qifparse/qif.py @@ -23,6 +23,9 @@ class Qif(object): + + _currency_format = '%.2f' + def __init__(self): self._accounts = [] self._categories = [] @@ -103,6 +106,17 @@ def get_transactions(self, recursive=False): for acc in self._accounts: tr.extend(acc.transactions) + @classmethod + def set_currency_num_fractional_digits(cls, num): + """Set the number of digits in the fractional part of currency values + (for __str__ output)""" + cls._currency_format = "%%.%df" % int(num) + print(" CUR FORM: ", Qif._currency_format) + + @classmethod + def currency_format(cls): + return cls._currency_format + def __str__(self): res = [] if self._categories: @@ -185,7 +199,10 @@ class Transaction(BaseEntry): _fields = [ Field('date', 'datetime', 'D', required=True, default=datetime.now()), Field('num', 'string', 'N'), - Field('amount', 'decimal', 'T', required=True), + Field('amount', 'decimal', 'T', required=True, + custom_print_format='%s' + Qif.currency_format()), + Field('amount_U', 'decimal', 'U', + custom_print_format='%s' + Qif.currency_format()), Field('cleared', 'string', 'C'), Field('payee', 'string', 'P'), Field('memo', 'string', 'M'), @@ -240,7 +257,8 @@ class AmountSplit(BaseEntry): _fields = [ Field('category', 'string', 'S'), Field('to_account', 'reference', 'S'), - Field('amount', 'decimal', '$'), + Field('amount', 'decimal', '$', + custom_print_format='%s' + Qif.currency_format()), Field('percent', 'string', '%'), Field('address', 'multilinestring', 'A'), Field('memo', 'string', 'M'), @@ -253,7 +271,7 @@ class Investment(BaseEntry): Field('date', 'datetime', 'D', required=True, default=datetime.now()), Field('action', 'string', 'N'), Field('security', 'string', 'Y'), - Field('price', 'decimal', 'I', custom_print_format='%s%.3f'), + Field('price', 'decimal', 'I', custom_print_format='%s%.3f'), # Why 3? Field('quantity', 'decimal', 'Q', custom_print_format='%s%.3f'), Field('cleared', 'string', 'C'), Field('amount', 'decimal', 'T'),