diff --git a/README.rst b/README.rst index 8474c6b..19db046 100644 --- a/README.rst +++ b/README.rst @@ -120,6 +120,24 @@ Simple json encoding: print(json.dumps(transactions, indent=4, cls=mt940.JSONEncoder)) +Parsing statements from the Dutch bank ASN where tag 61 does not follow the Swift specifications: + +.. code-block:: python + + def ASNB_mt940_data(): + with open('mt940_tests/ASNB/0708271685_09022020_164516.940.txt') as fh: + return fh.read() + + def test_ASNB_tags(ASNB_mt940_data): + tag_parser = mt940.tags.StatementASNB() + trs = mt940.models.Transactions(tags={ + tag_parser.id: tag_parser + }) + + trs.parse(ASNB_mt940_data) + trs_data = pprint.pformat(trs.data, sort_dicts=False) + print(trs_data) + Contributing ------------ @@ -137,7 +155,7 @@ To run the tests: pip install -r mt940_tests/requirements.txt py.test - + Or to run the tests on all available Python versions: .. code-block:: shell diff --git a/mt940/models.py b/mt940/models.py index a026e95..648ec8b 100644 --- a/mt940/models.py +++ b/mt940/models.py @@ -15,7 +15,9 @@ class Model(object): - pass + + def __repr__(self): + return '<%s>' % self.__class__.__name__ class FixedOffset(datetime.tzinfo): @@ -166,10 +168,14 @@ def __init__(self, amount, status, currency=None, **kwargs): if status == 'D': self.amount = -self.amount + def __eq__(self, other): + return self.amount == other.amount and self.currency == other.currency + + def __str__(self): + return '%s %s' % (self.amount, self.currency) + def __repr__(self): - return '<%s %s>' % ( - self.amount, - self.currency, ) + return '<%s>' % self class SumAmount(Amount): @@ -212,6 +218,9 @@ def __init__(self, status=None, amount=None, date=None, **kwargs): self.amount = amount self.date = date + def __eq__(self, other): + return self.amount == other.amount and self.status == other.status + def __repr__(self): return '<%s>' % self diff --git a/mt940/tags.py b/mt940/tags.py index a706ebb..6627dce 100644 --- a/mt940/tags.py +++ b/mt940/tags.py @@ -337,6 +337,43 @@ def __call__(self, transactions, value): return data +class StatementASNB(Statement): + '''StatementASNB + + From: https://www.sepaforcorporates.com/swift-for-corporates + + Pattern: 6!n[4!n]2a[1!a]15d1!a3!c16x[//16x] + [34x] + + But ASN bank puts the IBAN in the customer reference, which is acording to + Wikipedia at most 34 characters. + + So this is the new pattern: + + Pattern: 6!n[4!n]2a[1!a]15d1!a3!c34x[//16x] + [34x] + ''' + pattern = r'''^ + (?P\d{2}) # 6!n Value Date (YYMMDD) + (?P\d{2}) + (?P\d{2}) + (?P\d{2})? # [4!n] Entry Date (MMDD) + (?P\d{2})? + (?P[A-Z]?[DC]) # 2a Debit/Credit Mark + (?P[A-Z])? # [1!a] Funds Code (3rd character of the currency + # code, if needed) + \n? # apparently some banks (sparkassen) incorporate newlines here + (?P[\d,]{1,15}) # 15d Amount + (?P[A-Z][A-Z0-9 ]{3})? # 1!a3!c Transaction Type Identification Code + (?P.{0,34}) # 34x Customer Reference + (//(?P.{0,16}))? # [//16x] Bank Reference + (\n?(?P.{0,34}))? # [34x] Supplementary Details + $''' + + def __call__(self, transactions, value): + return super(StatementASNB, self).__call__(transactions, value) + + class ClosingBalance(BalanceBase): id = 62 diff --git a/mt940_tests/ASNB/0708271685_09022020_164516.940.txt b/mt940_tests/ASNB/0708271685_09022020_164516.940.txt new file mode 100644 index 0000000..2ad205e --- /dev/null +++ b/mt940_tests/ASNB/0708271685_09022020_164516.940.txt @@ -0,0 +1,280 @@ +{1:F01ASNBNL21XXXX0000000000}{2:O940ASNBNL21XXXXN}{3:}{4: +:20:0000000000 +:25:NL81ASNB9999999999 +:28C:1/1 +:60F:C200101EUR444,29 +:61:2001010101D65,00NOVBNL47INGB9999999999 +hr gjlm paulissen +:86:NL47INGB9999999999 hr gjlm paulissen + +Betaling sieraden + + + +:62F:C200101EUR379,29 +-}{5:} +{1:F01ASNBNL21XXXX0000000000}{2:O940ASNBNL21XXXXN}{3:}{4: +:20:0000000000 +:25:NL81ASNB9999999999 +:28C:2/1 +:60F:C200102EUR379,29 +:62F:C200102EUR379,29 +-}{5:} +{1:F01ASNBNL21XXXX0000000000}{2:O940ASNBNL21XXXXN}{3:}{4: +:20:0000000000 +:25:NL81ASNB9999999999 +:28C:3/1 +:60F:C200103EUR379,29 +:62F:C200103EUR379,29 +-}{5:} +{1:F01ASNBNL21XXXX0000000000}{2:O940ASNBNL21XXXXN}{3:}{4: +:20:0000000000 +:25:NL81ASNB9999999999 +:28C:4/1 +:60F:C200104EUR379,29 +:62F:C200104EUR379,29 +-}{5:} +{1:F01ASNBNL21XXXX0000000000}{2:O940ASNBNL21XXXXN}{3:}{4: +:20:0000000000 +:25:NL81ASNB9999999999 +:28C:5/1 +:60F:C200105EUR379,29 +:61:2001050105C1000,00NIOBNL56ASNB9999999999 +paulissen g j l m +:86:NL56ASNB9999999999 paulissen g j l m + +INTERNE OVERBOEKING VIA MOBIEL + + + +:61:2001050105D801,55NIDBNL08ABNA9999999999 +international card services +:86:NL08ABNA9999999999 international card services + +000000000000000000000000000000000 0000000000000000 Betaling aan I +CS 99999999999 ICS Referentie: 2020-01-05 19:47 000000000000000 + + +:62F:C200105EUR577,74 +-}{5:} +{1:F01ASNBNL21XXXX0000000000}{2:O940ASNBNL21XXXXN}{3:}{4: +:20:0000000000 +:25:NL81ASNB9999999999 +:28C:6/1 +:60F:C200106EUR577,74 +:62F:C200106EUR577,74 +-}{5:} +{1:F01ASNBNL21XXXX0000000000}{2:O940ASNBNL21XXXXN}{3:}{4: +:20:0000000000 +:25:NL81ASNB9999999999 +:28C:7/1 +:60F:C200107EUR577,74 +:62F:C200107EUR577,74 +-}{5:} +{1:F01ASNBNL21XXXX0000000000}{2:O940ASNBNL21XXXXN}{3:}{4: +:20:0000000000 +:25:NL81ASNB9999999999 +:28C:8/1 +:60F:C200108EUR577,74 +:62F:C200108EUR577,74 +-}{5:} +{1:F01ASNBNL21XXXX0000000000}{2:O940ASNBNL21XXXXN}{3:}{4: +:20:0000000000 +:25:NL81ASNB9999999999 +:28C:9/1 +:60F:C200109EUR577,74 +:62F:C200109EUR577,74 +-}{5:} +{1:F01ASNBNL21XXXX0000000000}{2:O940ASNBNL21XXXXN}{3:}{4: +:20:0000000000 +:25:NL81ASNB9999999999 +:28C:10/1 +:60F:C200110EUR577,74 +:62F:C200110EUR577,74 +-}{5:} +{1:F01ASNBNL21XXXX0000000000}{2:O940ASNBNL21XXXXN}{3:}{4: +:20:0000000000 +:25:NL81ASNB9999999999 +:28C:11/1 +:60F:C200111EUR577,74 +:62F:C200111EUR577,74 +-}{5:} +{1:F01ASNBNL21XXXX0000000000}{2:O940ASNBNL21XXXXN}{3:}{4: +:20:0000000000 +:25:NL81ASNB9999999999 +:28C:12/1 +:60F:C200112EUR577,74 +:62F:C200112EUR577,74 +-}{5:} +{1:F01ASNBNL21XXXX0000000000}{2:O940ASNBNL21XXXXN}{3:}{4: +:20:0000000000 +:25:NL81ASNB9999999999 +:28C:13/1 +:60F:C200113EUR577,74 +:62F:C200113EUR577,74 +-}{5:} +{1:F01ASNBNL21XXXX0000000000}{2:O940ASNBNL21XXXXN}{3:}{4: +:20:0000000000 +:25:NL81ASNB9999999999 +:28C:14/1 +:60F:C200114EUR577,74 +:62F:C200114EUR577,74 +-}{5:} +{1:F01ASNBNL21XXXX0000000000}{2:O940ASNBNL21XXXXN}{3:}{4: +:20:0000000000 +:25:NL81ASNB9999999999 +:28C:15/1 +:60F:C200115EUR577,74 +:62F:C200115EUR577,74 +-}{5:} +{1:F01ASNBNL21XXXX0000000000}{2:O940ASNBNL21XXXXN}{3:}{4: +:20:0000000000 +:25:NL81ASNB9999999999 +:28C:16/1 +:60F:C200116EUR577,74 +:62F:C200116EUR577,74 +-}{5:} +{1:F01ASNBNL21XXXX0000000000}{2:O940ASNBNL21XXXXN}{3:}{4: +:20:0000000000 +:25:NL81ASNB9999999999 +:28C:17/1 +:60F:C200117EUR577,74 +:62F:C200117EUR577,74 +-}{5:} +{1:F01ASNBNL21XXXX0000000000}{2:O940ASNBNL21XXXXN}{3:}{4: +:20:0000000000 +:25:NL81ASNB9999999999 +:28C:18/1 +:60F:C200118EUR577,74 +:62F:C200118EUR577,74 +-}{5:} +{1:F01ASNBNL21XXXX0000000000}{2:O940ASNBNL21XXXXN}{3:}{4: +:20:0000000000 +:25:NL81ASNB9999999999 +:28C:19/1 +:60F:C200119EUR577,74 +:62F:C200119EUR577,74 +-}{5:} +{1:F01ASNBNL21XXXX0000000000}{2:O940ASNBNL21XXXXN}{3:}{4: +:20:0000000000 +:25:NL81ASNB9999999999 +:28C:20/1 +:60F:C200120EUR577,74 +:62F:C200120EUR577,74 +-}{5:} +{1:F01ASNBNL21XXXX0000000000}{2:O940ASNBNL21XXXXN}{3:}{4: +:20:0000000000 +:25:NL81ASNB9999999999 +:28C:21/1 +:60F:C200121EUR577,74 +:62F:C200121EUR577,74 +-}{5:} +{1:F01ASNBNL21XXXX0000000000}{2:O940ASNBNL21XXXXN}{3:}{4: +:20:0000000000 +:25:NL81ASNB9999999999 +:28C:22/1 +:60F:C200122EUR577,74 +:62F:C200122EUR577,74 +-}{5:} +{1:F01ASNBNL21XXXX0000000000}{2:O940ASNBNL21XXXXN}{3:}{4: +:20:0000000000 +:25:NL81ASNB9999999999 +:28C:23/1 +:60F:C200123EUR577,74 +:62F:C200123EUR577,74 +-}{5:} +{1:F01ASNBNL21XXXX0000000000}{2:O940ASNBNL21XXXXN}{3:}{4: +:20:0000000000 +:25:NL81ASNB9999999999 +:28C:24/1 +:60F:C200124EUR577,74 +:62F:C200124EUR577,74 +-}{5:} +{1:F01ASNBNL21XXXX0000000000}{2:O940ASNBNL21XXXXN}{3:}{4: +:20:0000000000 +:25:NL81ASNB9999999999 +:28C:25/1 +:60F:C200125EUR577,74 +:61:2001250125D1,65NDIV +:86: + +Kosten gebruik betaalrekening inclusief 1 betaalpas + + + +:62F:C200125EUR576,09 +-}{5:} +{1:F01ASNBNL21XXXX0000000000}{2:O940ASNBNL21XXXXN}{3:}{4: +:20:0000000000 +:25:NL81ASNB9999999999 +:28C:26/1 +:60F:C200126EUR576,09 +:62F:C200126EUR576,09 +-}{5:} +{1:F01ASNBNL21XXXX0000000000}{2:O940ASNBNL21XXXXN}{3:}{4: +:20:0000000000 +:25:NL81ASNB9999999999 +:28C:27/1 +:60F:C200127EUR576,09 +:62F:C200127EUR576,09 +-}{5:} +{1:F01ASNBNL21XXXX0000000000}{2:O940ASNBNL21XXXXN}{3:}{4: +:20:0000000000 +:25:NL81ASNB9999999999 +:28C:28/1 +:60F:C200128EUR576,09 +:62F:C200128EUR576,09 +-}{5:} +{1:F01ASNBNL21XXXX0000000000}{2:O940ASNBNL21XXXXN}{3:}{4: +:20:0000000000 +:25:NL81ASNB9999999999 +:28C:29/1 +:60F:C200129EUR576,09 +:61:2001290129C828,72NOVBNL25INGB9999999999 +transfer solutions bv +:86:NL25INGB9999999999 transfer solutions bv + +2020-01-28T14:32:46-000000000000089-NL25INGB9999999999-Transfer S +olutions BV-DIVIDEND 28/01/2020 + + +:61:2001290129D1000,00NIDBNL08ABNA9999999999 +international card services +:86:NL08ABNA9999999999 international card services + +000000000000000000000000000000000 0000000000000000 Betaling aan I +CS 99999999999 ICS Referentie: 2020-01-29 18:36 000000000000000 + + +:62F:C200129EUR404,81 +-}{5:} +{1:F01ASNBNL21XXXX0000000000}{2:O940ASNBNL21XXXXN}{3:}{4: +:20:0000000000 +:25:NL81ASNB9999999999 +:28C:30/1 +:60F:C200130EUR404,81 +:62F:C200130EUR404,81 +-}{5:} +{1:F01ASNBNL21XXXX0000000000}{2:O940ASNBNL21XXXXN}{3:}{4: +:20:0000000000 +:25:NL81ASNB9999999999 +:28C:31/1 +:60F:C200131EUR404,81 +:61:2001310131C1000,18NIOBNL56ASNB9999999999 +paulissen g j l m +:86:NL56ASNB9999999999 paulissen g j l m + +INTERNE OVERBOEKING VIA MOBIEL + + + +:61:2001310131D903,76NIDBNL08ABNA9999999999 +international card services +:86:NL08ABNA9999999999 international card services + +000000000000000000000000000000000 0000000000000000 Betaling aan I +CS 99999999999 ICS Referentie: 2020-01-31 21:27 000000000000000 + + +:62F:C200131EUR501,23 +-}{5:} diff --git a/mt940_tests/conftest.py b/mt940_tests/conftest.py index 79cd26d..98b2f7b 100644 --- a/mt940_tests/conftest.py +++ b/mt940_tests/conftest.py @@ -9,6 +9,8 @@ def pytest_configure(config): + # Note: enable DEBUG logging to debug the parsing. But this becomes very + # verbose very quickly logging.basicConfig( - level=LOG_LEVELS.get(config.option.verbose, logging.DEBUG)) + level=LOG_LEVELS.get(config.option.verbose, logging.INFO)) diff --git a/mt940_tests/test_tags.py b/mt940_tests/test_tags.py index f1551fa..6cf036f 100644 --- a/mt940_tests/test_tags.py +++ b/mt940_tests/test_tags.py @@ -1,6 +1,8 @@ import pytest import mt940 -from mt940.tags import Tag +from mt940 import tags +from mt940 import models +import pprint @pytest.fixture @@ -9,7 +11,7 @@ def long_statement_number(): return fh.read() -class MyStatementNumber(Tag): +class MyStatementNumber(tags.Tag): '''Statement number / sequence number @@ -28,3 +30,92 @@ def test_specify_different_tag_classes(long_statement_number): }) transactions.parse(long_statement_number) assert transactions.data.get('statement_number') == '1810118101' + + +@pytest.fixture +def ASNB_mt940_data(): + with open('mt940_tests/ASNB/0708271685_09022020_164516.940.txt') as fh: + return fh.read() + + +def test_ASNB_tags(ASNB_mt940_data): + tag_parser = tags.StatementASNB() + trs = mt940.models.Transactions(tags={ + tag_parser.id: tag_parser + }) + + trs.parse(ASNB_mt940_data) + + assert trs.data == { + 'account_identification': 'NL81ASNB9999999999', + 'transaction_reference': '0000000000', + 'statement_number': '31', + 'sequence_number': '1', + 'final_opening_balance': models.Balance( + status='C', + amount=models.Amount('404.81', 'C', 'EUR'), + date=models.Date(2020, 1, 31), + ), + 'final_closing_balance': models.Balance( + status='C', + amount=models.Amount('501.23', 'C', 'EUR'), + date=models.Date(2020, 1, 31), + ), + } + assert len(trs) == 8 + # test first entry + td = trs.transactions[0].data.pop('transaction_details') + + pprint.pprint(trs.data) + pprint.pprint(trs.data['final_opening_balance']) + pprint.pprint(type(trs.data['final_opening_balance'])) + pprint.pprint(trs.data['final_opening_balance'].__dict__) + + assert trs.transactions[0].data == { + 'status': 'D', + 'funds_code': None, + 'amount': models.Amount('65.00', 'D', 'EUR'), + 'id': 'NOVB', + 'customer_reference': 'NL47INGB9999999999', + 'bank_reference': None, + 'extra_details': 'hr gjlm paulissen', + 'currency': 'EUR', + 'date': models.Date(2020, 1, 1), + 'entry_date': models.Date(2020, 1, 1), + 'guessed_entry_date': models.Date(2020, 1, 1), + } + + assert td == 'NL47INGB9999999999 hr gjlm paulissen\nBetaling sieraden' + assert trs.transactions[1].data['amount'] == models.Amount( + '1000.00', 'C', 'EUR') + assert trs.transactions[2].data['amount'] == models.Amount( + '801.55', 'D', 'EUR') + assert trs.transactions[3].data['amount'] == models.Amount( + '1.65', 'D', 'EUR') + assert trs.transactions[4].data['amount'] == models.Amount( + '828.72', 'C', 'EUR') + assert trs.transactions[5].data['amount'] == models.Amount( + '1000.00', 'D', 'EUR') + assert trs.transactions[6].data['amount'] == models.Amount( + '1000.18', 'C', 'EUR') + + td = trs.transactions[7].data.pop('transaction_details') + assert trs.transactions[7].data == { + 'status': 'D', + 'funds_code': None, + 'amount': models.Amount('903.76', 'D', 'EUR'), + 'id': 'NIDB', + 'customer_reference': 'NL08ABNA9999999999', + 'bank_reference': None, + 'extra_details': 'international card services', + 'currency': 'EUR', + 'date': models.Date(2020, 1, 31), + 'entry_date': models.Date(2020, 1, 31), + 'guessed_entry_date': models.Date(2020, 1, 31), + } + assert td[0:46] == 'NL08ABNA9999999999 international card services' + assert td[47:112] == \ + '000000000000000000000000000000000 0000000000000000 Betaling aan I' + assert td[113:176] == \ + 'CS 99999999999 ICS Referentie: 2020-01-31 21:27 000000000000000' + diff --git a/pytest.ini b/pytest.ini index 00851f4..283e97c 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,4 +1,7 @@ [pytest] +markers = + pep8: workaround for https://bitbucket.org/pytest-dev/pytest-pep8/issues/23/ + python_files = mt940/*.py mt940_tests/*.py diff --git a/setup.py b/setup.py index 19a61ac..c1967e1 100755 --- a/setup.py +++ b/setup.py @@ -3,11 +3,8 @@ import os import sys -import sys - from setuptools.command.test import test as TestCommand - try: from setuptools import setup, find_packages except ImportError: