Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 56 additions & 9 deletions qifparse/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"))
Expand All @@ -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':
Expand Down Expand Up @@ -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))
Expand All @@ -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':
Expand Down Expand Up @@ -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))
Expand All @@ -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':
Expand Down Expand Up @@ -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
24 changes: 21 additions & 3 deletions qifparse/qif.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@


class Qif(object):

_currency_format = '%.2f'

def __init__(self):
self._accounts = []
self._categories = []
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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'),
Expand Down Expand Up @@ -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'),
Expand All @@ -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'),
Expand Down