diff --git a/.travis.yml b/.travis.yml
index 282ae7d..8cad73c 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,9 +1,10 @@
language: python
python:
- '2.7'
-- '3.4'
- '3.5'
- '3.6'
+- '3.7'
+- '3.8'
install:
- if [[ $TRAVIS_PYTHON_VERSION == 2* ]]; then pip install BeautifulSoup six nose coverage
python-coveralls; fi
@@ -15,6 +16,7 @@ after_success:
- coveralls
deploy:
provider: pypi
+ edge: true
user: jseutter
password:
secure: buE5iS5WhggpFcqR7iIEfcnDNHGeZ4zcYlgy3p9mJKEP8s7NMVeYJc+0FnnNs2fOEVR1QUX/URFtAZegtW9Bi/hVSc2bECZxM75uH342vqtea2rNJ7wQLSugUO+w9Q7HvC2KqeVl3s5Qa4Y3+mwv3Ej4tPI/WfASaNZG3XkwX4c=
@@ -22,3 +24,4 @@ deploy:
tags: true
distributions: sdist bdist_wheel
repo: jseutter/ofxparse
+ skip_existing: true
diff --git a/README.rst b/README.rst
index de44e9a..cf42fdb 100644
--- a/README.rst
+++ b/README.rst
@@ -32,7 +32,7 @@ Here's a sample program
# Account
- account = ofx.occount
+ account = ofx.account
account.account_id # The account number
account.number # The account number (deprecated -- returns account_id)
account.routing_number # The bank routing number
@@ -73,6 +73,7 @@ Here's a sample program
transaction.payee
transaction.type
transaction.date
+ transaction.user_date
transaction.amount
transaction.id
transaction.memo
diff --git a/ofxparse/__init__.py b/ofxparse/__init__.py
index b12d53b..a2983cf 100644
--- a/ofxparse/__init__.py
+++ b/ofxparse/__init__.py
@@ -4,7 +4,7 @@
Statement, Transaction)
from .ofxprinter import OfxPrinter
-__version__ = '0.18'
+__version__ = '0.21'
__all__ = [
'OfxParser',
'OfxParserException',
diff --git a/ofxparse/ofxparse.py b/ofxparse/ofxparse.py
index 2621286..fa3a791 100644
--- a/ofxparse/ofxparse.py
+++ b/ofxparse/ofxparse.py
@@ -13,6 +13,11 @@
except ImportError:
from io import StringIO
+try:
+ from collections.abc import Iterable
+except ImportError:
+ from collections import Iterable
+
import six
from . import mcc
@@ -37,7 +42,7 @@ def try_decode(string, encoding):
def is_iterable(candidate):
if sys.version_info < (2, 6):
return hasattr(candidate, 'next')
- return isinstance(candidate, collections.Iterable)
+ return isinstance(candidate, Iterable)
@contextlib.contextmanager
@@ -117,7 +122,10 @@ def handle_encoding(self):
if enc_type == "USASCII":
cp = ascii_headers.get("CHARSET", "1252")
- encoding = "cp%s" % (cp, )
+ if cp == "8859-1":
+ encoding = "iso-8859-1"
+ else:
+ encoding = "cp%s" % (cp, )
elif enc_type in ("UNICODE", "UTF-8"):
encoding = "utf-8"
@@ -304,6 +312,7 @@ def __init__(self):
self.payee = ''
self.type = ''
self.date = None
+ self.user_date = None
self.amount = None
self.id = ''
self.memo = ''
@@ -411,6 +420,9 @@ def parse(cls, file_handle, fail_fast=True, custom_date_format=None):
)
ofx_obj.status['severity'] = \
stmttrnrs_status.find('severity').contents[0].strip()
+ message = stmttrnrs_status.find('message')
+ ofx_obj.status['message'] = \
+ message.contents[0].strip() if message else None
ccstmttrnrs = ofx.find('ccstmttrnrs')
if ccstmttrnrs:
@@ -426,6 +438,9 @@ def parse(cls, file_handle, fail_fast=True, custom_date_format=None):
)
ofx_obj.status['severity'] = \
ccstmttrnrs_status.find('severity').contents[0].strip()
+ message = ccstmttrnrs_status.find('message')
+ ofx_obj.status['message'] = \
+ message.contents[0].strip() if message else None
stmtrs_ofx = ofx.findAll('stmtrs')
if stmtrs_ofx:
@@ -1019,6 +1034,19 @@ def parseTransaction(cls, txn_ofx):
raise OfxParserException(
six.u("Missing Transaction Date (a required field)"))
+ user_date_tag = txn_ofx.find('dtuser')
+ if hasattr(user_date_tag, "contents"):
+ try:
+ transaction.user_date = cls.parseOfxDateTime(
+ user_date_tag.contents[0].strip())
+ except IndexError:
+ raise OfxParserException("Invalid Transaction User Date")
+ except ValueError:
+ ve = sys.exc_info()[1]
+ raise OfxParserException(str(ve))
+ except TypeError:
+ pass
+
id_tag = txn_ofx.find('fitid')
if hasattr(id_tag, "contents"):
try:
@@ -1051,13 +1079,14 @@ def parseTransaction(cls, txn_ofx):
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"))
+ for check_field in ('checknum', 'chknum'):
+ checknum_tag = txn_ofx.find(check_field)
+ 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
@@ -1073,4 +1102,8 @@ def toDecimal(cls, tag):
# Handle 10000,50 formatted numbers
if '.' not in d and ',' in d:
d = d.replace(',', '.')
+ # Handle 1 025,53 formatted numbers
+ d = d.replace(' ', '')
+ # Handle +1058,53 formatted numbers
+ d = d.replace('+', '')
return decimal.Decimal(d)
diff --git a/requirements.txt b/requirements.txt
index ae10411..e076f7c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1 +1,2 @@
python-coveralls
+beautifulsoup4
diff --git a/tests/fixtures/error_message.ofx b/tests/fixtures/error_message.ofx
new file mode 100644
index 0000000..6996ab1
--- /dev/null
+++ b/tests/fixtures/error_message.ofx
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+ 0
+ INFO
+ SUCCESS
+
+ 20180521052952.749[-7:PDT]
+ ENG
+
+ svb.com
+ 944
+
+
+
+
+
+ ae91f50f-f16d-4bc1-b88f-2a7fa04b6de1
+
+ 2000
+ ERROR
+ General Server Error
+
+
+
+
diff --git a/tests/test_parse.py b/tests/test_parse.py
index 46ad680..cb5753c 100644
--- a/tests/test_parse.py
+++ b/tests/test_parse.py
@@ -415,6 +415,7 @@ def testThatParseTransactionReturnsATransaction(self):
input = '''
POS
+ 20090131
20090401122017.000[-5:EST]
-6.60
0000123456782009040100001
@@ -427,6 +428,7 @@ def testThatParseTransactionReturnsATransaction(self):
self.assertEqual('pos', transaction.type)
self.assertEqual(datetime(
2009, 4, 1, 12, 20, 17) - timedelta(hours=-5), transaction.date)
+ self.assertEqual(datetime(2009, 1, 31, 0, 0), transaction.user_date)
self.assertEqual(Decimal('-6.60'), transaction.amount)
self.assertEqual('0000123456782009040100001', transaction.id)
self.assertEqual("MCDONALD'S #112", transaction.payee)
@@ -447,6 +449,20 @@ def testThatParseTransactionWithFieldCheckNum(self):
transaction = OfxParser.parseTransaction(txn.find('stmttrn'))
self.assertEqual('700', transaction.checknum)
+ def testThatParseTransactionWithFieldChkNum(self):
+ input = '''
+
+ CHECK
+ 20231121
+ -113.71
+ 0000489
+ 1932
+
+'''
+ txn = soup_maker(input)
+ transaction = OfxParser.parseTransaction(txn.find('stmttrn'))
+ self.assertEqual('1932', transaction.checknum)
+
def testThatParseTransactionWithCommaAsDecimalPoint(self):
input = '''
@@ -493,6 +509,38 @@ def testThatParseTransactionWithDotAsDecimalPointAndCommaAsSeparator(self):
transaction = OfxParser.parseTransaction(txn.find('stmttrn'))
self.assertEqual(Decimal('-1006.60'), transaction.amount)
+ def testThatParseTransactionWithLeadingPlusSign(self):
+ " Parse numbers with a leading '+' sign. "
+ input = '''
+
+ POS
+ 20090401122017.000[-5:EST]
+ +1,006.60
+ 0000123456782009040100001
+ MCDONALD'S #112
+ POS MERCHANDISE;MCDONALD'S #112
+
+'''
+ txn = soup_maker(input)
+ transaction = OfxParser.parseTransaction(txn.find('stmttrn'))
+ self.assertEqual(Decimal('1006.60'), transaction.amount)
+
+ def testThatParseTransactionWithSpaces(self):
+ " Parse numbers with a space separating the thousands. "
+ input = '''
+
+ POS
+ 20090401122017.000[-5:EST]
+ +1 006,60
+ 0000123456782009040100001
+ MCDONALD'S #112
+ POS MERCHANDISE;MCDONALD'S #112
+
+'''
+ txn = soup_maker(input)
+ transaction = OfxParser.parseTransaction(txn.find('stmttrn'))
+ self.assertEqual(Decimal('1006.60'), transaction.amount)
+
def testThatParseTransactionWithNullAmountIgnored(self):
"""A null transaction value is converted to 0.
@@ -852,7 +900,7 @@ def testPositions(self):
account = ofx.accounts[0]
statement = account.statement
positions = statement.positions
- self.assertEquals(len(positions), 2)
+ self.assertEqual(len(positions), 2)
expected_positions = [
{
@@ -981,6 +1029,14 @@ def testEmptyBalance(self):
with open_file('fail_nice/empty_balance.ofx') as f:
self.assertRaises(OfxParserException, OfxParser.parse, f)
+ def testErrorInTransactionList(self):
+ """There is an error in the transaction list."""
+ with open_file('error_message.ofx') as f:
+ ofx = OfxParser.parse(f, False)
+ self.assertEqual(ofx.status['code'], 2000)
+ self.assertEqual(ofx.status['severity'], 'ERROR')
+ self.assertEqual(ofx.status['message'], 'General Server Error')
+
class TestParseSonrs(TestCase):