././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1631967577.828968 mt940-0.6.0/0000755000175000017500000000000000000000000010443 5ustar00cedced././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1584483104.0 mt940-0.6.0/.flake80000644000175000017500000000005100000000000011612 0ustar00cedced[flake8] ignore=E123,E124,E126,E128,W503 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1631967571.0 mt940-0.6.0/.hgtags0000644000175000017500000000042200000000000011717 0ustar00cedced5130a7997e093fd6b670d18dcdd3fd00590a93e9 0.1 282e8574e478185b1a076bed6032c95c1dd351d7 0.2 8894defcb0e5f6a8baacb64365a5d7c933c01081 0.3 1f237393bb0588d3308d0e427114c15183f2c7a0 0.4 d4026ca043d06d8501764b954d51e35a8c5db35e 0.5.0 8dcc839f4d9fd5bf8124b1f5c597b13760de7f89 0.6.0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1631967529.0 mt940-0.6.0/CHANGELOG0000644000175000017500000000116200000000000011655 0ustar00cedcedVersion 0.6.0 - 2021-09-18 * Remove support for Python older than 3.5 * Add support for Python 3.9 Version 0.5.0 - 2020-03-29 * Add support for Python 3.5, 3.6, 3.7, 3.8 * Do not fail on optional balances Version 0.4 - 2017-07-05 * Allow to use file-like object * Allow to define file encoding Version 0.3 - 2016-01-27 * Add support for RegioBank description format * Set a default description after setting a statement Version 0.2 - 2015-04-29 * Add Statement description * Renamed properties of Transaction to follow the norm * Add support for ING specific description format Version 0.1 - 2014-06-25 * Initial release ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1631967492.0 mt940-0.6.0/COPYRIGHT0000644000175000017500000000305000000000000011734 0ustar00cedcedCopyright (c) 2013-2021, Cédric Krier Copyright (c) 2014-2017, Nicolas Évrard Copyright (c) 2013-2021, B2CK All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1631967462.0 mt940-0.6.0/MANIFEST.in0000644000175000017500000000011300000000000012174 0ustar00cedcedinclude COPYRIGHT include README include CHANGELOG include mt940/MT940.txt ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1631967577.828968 mt940-0.6.0/PKG-INFO0000644000175000017500000000532400000000000011544 0ustar00cedcedMetadata-Version: 1.2 Name: mt940 Version: 0.6.0 Summary: A module to parse MT940 files Home-page: http://mt940.b2ck.com/ Author: B2CK Author-email: info@b2ck.com License: BSD Description: mt940 ===== mt940 is a parser for MT940 files. Nutshell -------- Import:: >>> import os >>> from mt940 import MT940 Instanciate:: >>> mt940 = MT940('mt940/MT940.txt') The statements:: >>> len(mt940.statements) 2 >>> statement = mt940.statements[0] >>> statement.account '123456789' >>> statement.information '13501/1' >>> start_balance = statement.start_balance >>> start_balance.date datetime.date(2012, 5, 11) >>> start_balance.amount Decimal('5138.61') >>> start_balance.currency 'EUR' >>> end_balance = statement.end_balance >>> end_balance.date datetime.date(2012, 5, 14) >>> end_balance.amount Decimal('5638.62') >>> end_balance.currency 'EUR' The transactions:: >>> len(statement.transactions) 3 >>> transaction, _, _ = statement.transactions >>> transaction.date datetime.date(2012, 5, 12) >>> transaction.booking datetime.date(2012, 5, 14) >>> transaction.amount Decimal('500.01') >>> transaction.id 'N654' >>> transaction.reference 'NONREF' >>> transaction.additional_data '987654321' >>> transaction.description # doctest: +NORMALIZE_WHITESPACE '/TRTP/SEPA OVERBOEKING/IBAN/FR12345678901234/BIC/GEFRADAM\n/NAME/QASD JGRED/REMI/Dit zijn de omschrijvingsregels/EREF/NOTPRO\nVIDED' To report issues please visit the `mt940 bugtracker`_. .. _mt940 bugtracker: http://mt940.b2ck.com/ Platform: UNKNOWN Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Topic :: Office/Business Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Utilities Requires-Python: >=3.5 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1631399641.0 mt940-0.6.0/README0000644000175000017500000000267200000000000011332 0ustar00cedcedmt940 ===== mt940 is a parser for MT940 files. Nutshell -------- Import:: >>> import os >>> from mt940 import MT940 Instanciate:: >>> mt940 = MT940('mt940/MT940.txt') The statements:: >>> len(mt940.statements) 2 >>> statement = mt940.statements[0] >>> statement.account '123456789' >>> statement.information '13501/1' >>> start_balance = statement.start_balance >>> start_balance.date datetime.date(2012, 5, 11) >>> start_balance.amount Decimal('5138.61') >>> start_balance.currency 'EUR' >>> end_balance = statement.end_balance >>> end_balance.date datetime.date(2012, 5, 14) >>> end_balance.amount Decimal('5638.62') >>> end_balance.currency 'EUR' The transactions:: >>> len(statement.transactions) 3 >>> transaction, _, _ = statement.transactions >>> transaction.date datetime.date(2012, 5, 12) >>> transaction.booking datetime.date(2012, 5, 14) >>> transaction.amount Decimal('500.01') >>> transaction.id 'N654' >>> transaction.reference 'NONREF' >>> transaction.additional_data '987654321' >>> transaction.description # doctest: +NORMALIZE_WHITESPACE '/TRTP/SEPA OVERBOEKING/IBAN/FR12345678901234/BIC/GEFRADAM\n/NAME/QASD JGRED/REMI/Dit zijn de omschrijvingsregels/EREF/NOTPRO\nVIDED' To report issues please visit the `mt940 bugtracker`_. .. _mt940 bugtracker: http://mt940.b2ck.com/ ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1631967577.828968 mt940-0.6.0/mt940/0000755000175000017500000000000000000000000011320 5ustar00cedced././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1538254915.0 mt940-0.6.0/mt940/MT940-optional.txt0000644000175000017500000000031600000000000014461 0ustar00cedcedABNANL2A 940 ABNANL2A :20:ABN AMRO BANK NV :25:123456789 :28:13501/1 :61:1205120514C500,01N654NONREF 987654321 :61:1412051205RC15,67IDXXREF DATA :61:171214C15,67TIDXTEST//REFERENCE :86:/SUM/ - ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1538253096.0 mt940-0.6.0/mt940/MT940.txt0000644000175000017500000000134700000000000012643 0ustar00cedcedABNANL2A 940 ABNANL2A :20:ABN AMRO BANK NV :25:123456789 :28:13501/1 :60F:C120511EUR5138,61 :61:1205120514C500,01N654NONREF 987654321 :86:/TRTP/SEPA OVERBOEKING/IBAN/FR12345678901234/BIC/GEFRADAM /NAME/QASD JGRED/REMI/Dit zijn de omschrijvingsregels/EREF/NOTPRO VIDED :61:1412051205RC15,67IDXXREF DATA :61:171214C15,67TIDXTEST//REFERENCE :62F:C120514EUR5638,62 :86:/SUM/ - :20:ABN AMRO BANK NV :25:123456789 :28:13501/1 :60F:C120511EUR5138,61 :61:1205120514C500,01N654NONREF 987654321 :86:/TRTP/SEPA OVERBOEKING/IBAN/FR12345678901234/BIC/GEFRADAM /NAME/QASD JGRED/REMI/Dit zijn de omschrijvingsregels/EREF/NOTPRO VIDED :61:1412051205RC15,67IDXXREF DATA :61:171214C15,67TIDXTEST//REFERENCE :62F:C120514EUR5638,62 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1631967547.0 mt940-0.6.0/mt940/__init__.py0000644000175000017500000002272500000000000013441 0ustar00cedced# This file is part of mt940. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. """a parser for MT940 files """ __version__ = '0.6.0' __all__ = ['MT940', 'rabo_description', 'abn_amro_description', 'ing_description', 'regiobank_description'] from collections import namedtuple, defaultdict from decimal import Decimal import datetime import re import io SECTIONS = { 'begin': [':940:'], 'statement': [':20:'], 'account': [':25:'], 'information': [':28:', ':28C:'], 'start_balance': [':60F:'], 'transaction': [':61:'], 'description': [':86:'], 'end_balance': [':62F:'], } def _parse_date(date): return datetime.datetime.strptime(date, '%y%m%d').date() def _parse_amount(amount, sign='C'): amount = Decimal(amount.replace(',', '.')) if sign in ('D', 'RC'): return -amount return amount TRANSACTION_RE = re.compile(r""" (?P\d{6}) (?P\d{4})? (?PD|C|RC|RD) (?P\w)?? # ING skips this mandatory field (?P(\d|,){1,15}) (?P\w{4}) (?P.{0,34})""", re.VERBOSE) class MT940(object): def __init__(self, name, encoding=None): self.statements = [] if isinstance(name, (bytes, str)): with io.open(name, encoding=encoding, mode='r') as f: self._parse(f) else: self._parse(name) def _parse(self, f): values = defaultdict(str) transactions = [] for line in self._readline(f): for name, sections in SECTIONS.items(): if name == 'begin': continue for section in sections: if line.startswith(section): if name in values and name == 'statement': self._set_statement(values, transactions) if name.endswith('_balance'): values[name] = self._get_balance( line[len(section):]) elif name == 'transaction': transactions.append( self._get_transaction(line[len(section):])) elif name == 'description': description = line[len(section):] if 'end_balance' in values: values['description'] += description else: transactions[-1] = (transactions[-1][:-1] + (description,)) else: values[name] += line[len(section):] if values: self._set_statement(values, transactions) @staticmethod def _readline(f): buf = [] for line in f: line = line.strip('\n') if buf: if (line.startswith(':') or line.startswith('-')): yield '\n'.join(buf) del buf[:] buf.append(line) if buf: yield '\n'.join(buf) @staticmethod def _get_balance(balance): date = _parse_date(balance[1:7]) amount = _parse_amount(balance[10:], balance[0]) return Balance(date=date, amount=amount, currency=balance[7:10]) @staticmethod def _get_transaction(transaction): lines = transaction.splitlines() if len(lines) == 1: transaction, = lines additional_data = None else: transaction, additional_data = lines transaction = TRANSACTION_RE.match(transaction) date = _parse_date(transaction.group('date')) if transaction.group('booking'): booking = _parse_date( transaction.group('date')[:2] + transaction.group('booking')) else: booking = None amount = _parse_amount(transaction.group('amount'), transaction.group('sign')) id_ = transaction.group('id') reference = transaction.group('reference') reference, _, institution_reference = reference.partition('//') return (date, booking, amount, id_, reference, institution_reference, additional_data, '') def _set_statement(self, values, transactions): # Set optional values values.setdefault('start_balance') values.setdefault('end_balance') values.setdefault('description') self.statements.append( Statement( transactions=[Transaction(*t) for t in transactions], **values)) values.clear() del transactions[:] Statement = namedtuple('Statement', ['statement', 'account', 'information', 'start_balance', 'transactions', 'end_balance', 'description']) Balance = namedtuple('Balance', ['date', 'amount', 'currency']) Transaction = namedtuple('Transaction', ['date', 'booking', 'amount', 'id', 'reference', 'institution_reference', 'additional_data', 'description']) def _find_swift_tags(tags, description): values = {} for tag, name in tags: if description.startswith(tag): description = description[len(tag):] cursor = len(description) for next_tag, _ in tags: if next_tag in values or next_tag == tag: continue index = description.find(next_tag) if index == -1: continue cursor = min(cursor, index) next_tag_index = cursor values[name] = description[:next_tag_index] description = description[next_tag_index:] if not description: break return values RABO_TAGS = [ ('/MARF/', 'marf'), ('/EREF/', 'eref'), ('/PREF/', 'pref'), ('/TRCD/', 'trcd'), ('/BENM/', 'benm'), ('/ORDP/', 'ordp'), ('/NAME/', 'name'), ('/ID/', 'id'), ('/ADDR/', 'addr'), ('/REMI/', 'remi'), ('/CDTRREFTP//CD/SCOR/ISSR/CUR/CDTRREF/', 'cdtrref'), ('/CSID/', 'csid'), ('/ISDT/', 'isdt'), ('/RTRN/', 'rtrn'), ] def rabo_description(description): "Return dictionary with Rabo informations" description = ''.join(description.splitlines()) return _find_swift_tags(RABO_TAGS, description) ABN_AMRO_ACCOUNT = re.compile(r""" ^([0-9]{1,3}\.[0-9]{1,2}\.[0-9]{1,2}\.[0-9]{1,3})""", re.VERBOSE) ABN_AMRO_GIRO = re.compile(r""" ^GIRO\ +([0-9]+)""", re.VERBOSE) ABN_AMRO_TAGS = [ ('/TRTP/', 'trtp'), ('/IBAN/', 'iban'), ('/BIC/', 'bic'), ('/CSID', 'csid'), ('/NAME/', 'name'), ('/REMI/', 'remi'), ('/EREF/', 'eref'), ('/ORDP//ID/', 'ordp'), ('/BENM//ID/', 'benm'), ] def abn_amro_description(description): "Return dictionary with ABN AMRO informations" description = ''.join(description.splitlines()) values = {} m = ABN_AMRO_ACCOUNT.match(description) if m: values['account'] = m.group(1).replace('.', '') m = ABN_AMRO_GIRO.match(description) if m: values['account'] = m.group(1) values.update(_find_swift_tags(ABN_AMRO_TAGS, description)) return values ING_TAGS = re.compile(r'/(RTRN|EREF|PREF|MARF|CSID|CNTP|REMI|PURP|ULT[CD])/') ING_TAGS_DEFINITION = { 'RTRN': ('rtrn', []), 'EREF': ('eref', []), 'PREF': ('pref', []), 'MARF': ('marf', []), 'CSID': ('csid', []), 'CNTP': ('cntp', ['account_number', 'bic', 'name', 'city']), 'REMI': ('remi', ['code', 'issuer', 'remittance_info']), 'PURP': ('purp', []), 'ULTC': ('ultc', ['name', 'id']), 'ULTD': ('ultd', ['name', 'id']), } def ing_description(description): "Return dictionary with ING informations" description = ''.join(description.splitlines()) values = {} ing_tags = iter(ING_TAGS.split(description)[1:]) for tag, tag_value in zip(ing_tags, ing_tags): tag_value = tag_value[:-1] name, subfields = ING_TAGS_DEFINITION[tag] if not subfields: values[name] = tag_value continue values[name] = {} if 'name' in subfields or 'remittance_info' in subfields: special_tag = 'name' if 'name' in subfields else 'remittance_info' tag_idx = subfields.index(special_tag) subtags = tag_value.split('/', tag_idx) for sf_name, sf_value in zip(subfields[:tag_idx], subtags[:-1]): values[name][sf_name] = sf_value subtags = subtags[-1].rsplit('/', len(subfields) - tag_idx - 1) for sf_name, sf_value in zip(subfields[tag_idx:], subtags): values[name][sf_name] = sf_value else: subtags = tag_value.split('/') for sf_name, sf_value in zip(subfields, subtags): values[name][sf_name] = sf_value return values def regiobank_description(description): "Return dictionary with RegioBank informations" lines = description.splitlines() values = {} try: first, second, third = lines[0], lines[1], ''.join(lines[2:]) except (ValueError, IndexError): return {} try: values['account_number'], values['name'] = first.split(' ', 1) except ValueError: return {} values['address'] = second # XXX Not clear how to split it if third.startswith('aan %s' % values['name']): _, values['iban'], values['remittance_info'], values['description'] = \ third.split(',') else: values['reference'] = third return values ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1631967431.0 mt940-0.6.0/mt940/test.py0000644000175000017500000002167100000000000012660 0ustar00cedced#!/usr/bin/env python # This file is part of mt940. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. """Test MT940 """ import datetime import doctest import io import os import sys import unittest from decimal import Decimal from mt940 import (MT940, rabo_description, abn_amro_description, ing_description, regiobank_description) here = os.path.dirname(__file__) readme = os.path.normpath(os.path.join(here, '..', 'README')) class TestMT940(unittest.TestCase): def setUp(self): self.mt940 = MT940(os.path.join(here, 'MT940.txt')) def test_number_statements(self): "Test number of statements" self.assertEqual(len(self.mt940.statements), 2) def test_statement_account(self): "Test statement account" self.assertEqual(self.mt940.statements[0].account, '123456789') def test_statement_information(self): "Test statement information" self.assertEqual(self.mt940.statements[0].information, '13501/1') def test_statement_start_balance(self): "Test statement start balance" start_balance = self.mt940.statements[0].start_balance self.assertEqual(start_balance.date, datetime.date(2012, 5, 11)) self.assertEqual(start_balance.amount, Decimal('5138.61')) self.assertEqual(start_balance.currency, 'EUR') def test_statement_end_balance(self): "Test statement end balance" end_balance = self.mt940.statements[0].end_balance self.assertEqual(end_balance.date, datetime.date(2012, 5, 14)) self.assertEqual(end_balance.amount, Decimal('5638.62')) self.assertEqual(end_balance.currency, 'EUR') def test_statement_description(self): self.assertEqual(self.mt940.statements[0].description, '/SUM/') def test_transaction(self): "Test transaction" transaction = self.mt940.statements[0].transactions[0] self.assertEqual(transaction.date, datetime.date(2012, 5, 12)) self.assertEqual(transaction.booking, datetime.date(2012, 5, 14)) self.assertEqual(transaction.amount, Decimal('500.01')) self.assertEqual(transaction.id, 'N654') self.assertEqual(transaction.reference, 'NONREF') self.assertEqual(transaction.additional_data, '987654321') self.assertEqual(transaction.description, '''/TRTP/SEPA OVERBOEKING/IBAN/FR12345678901234/BIC/GEFRADAM /NAME/QASD JGRED/REMI/Dit zijn de omschrijvingsregels/EREF/NOTPRO VIDED''') transaction = self.mt940.statements[0].transactions[1] self.assertEqual(transaction.date, datetime.date(2014, 12, 5)) self.assertEqual(transaction.booking, datetime.date(2014, 12, 5)) self.assertEqual(transaction.amount, Decimal('-15.67')) self.assertEqual(transaction.id, 'IDXX') self.assertEqual(transaction.reference, 'REF'), self.assertEqual(transaction.institution_reference, '') self.assertEqual(transaction.additional_data, 'DATA') self.assertEqual(transaction.description, '') transaction = self.mt940.statements[0].transactions[2] self.assertEqual(transaction.date, datetime.date(2017, 12, 14)) self.assertEqual(transaction.booking, None) self.assertEqual(transaction.amount, Decimal('15.67')) self.assertEqual(transaction.id, 'TIDX') self.assertEqual(transaction.reference, 'TEST'), self.assertEqual(transaction.institution_reference, 'REFERENCE') self.assertEqual(transaction.additional_data, None) self.assertEqual(transaction.description, '') class TestMT940Stream(TestMT940): def setUp(self): with io.open(os.path.join(here, 'MT940.txt')) as fp: self.mt940 = MT940(fp, encoding='ascii') class TestMT940Optional(unittest.TestCase): def setUp(self): self.mt940 = MT940(os.path.join(here, 'MT940-optional.txt')) def test_statement_start_balance(self): "Test statement has not start balance" start_balance = self.mt940.statements[0].start_balance self.assertEqual(start_balance, None) def test_statement_end_balance(self): "Test statement has no end balance" end_balance = self.mt940.statements[0].end_balance self.assertEqual(end_balance, None) def test_statement_description(self): "Test statement has no description" description = self.mt940.statements[0].description self.assertEqual(description, None) class TestRaboDescription(unittest.TestCase): def test_one_tag(self): self.assertEqual(rabo_description('/EREF/foo'), {'eref': 'foo'}) def test_empty_tags(self): self.assertEqual(rabo_description('/BENM//NAME/Doe'), {'benm': '', 'name': 'Doe'}) def test_long_tags(self): self.assertEqual(rabo_description( '/ORDP//NAME/Doe/REMI//CDTRREFTP//CD/SCOR/ISSR/CUR/CDTRREF/' '12345' )['cdtrref'], '12345') def test_non_rabo(self): self.assertEqual(rabo_description('foo'), {}) self.assertEqual(rabo_description('/FOO/BAR/NAME/'), {}) def test_mixed_tags(self): self.assertEqual( rabo_description( '/EREF/0007301960/ORDP//NAME/Acist Europe B.V./ADDR/' 'Heerlen 6422 PH Heerlen NL/REMI//INV/16000291 29.7.2016'), {'eref': '0007301960', 'ordp': '', 'name': 'Acist Europe B.V.', 'addr': 'Heerlen 6422 PH Heerlen NL', 'remi': '/INV/16000291 29.7.2016'}) class TestABNAMRODescription(unittest.TestCase): def test_account(self): self.assertEqual(abn_amro_description('12.34.56.789 John Doe'), {'account': '123456789'}) def test_giro(self): self.assertEqual(abn_amro_description('GIRO 4090309'), {'account': '4090309'}) def test_tag(self): self.assertEqual(abn_amro_description( '''/TRTP/SEPA OVERBOEKING/IBAN/FR001234567890/BIC/GEF RADAM/NAME/ENERGIE BEDRIJF/EREF/NOTPROVIDED'''), { 'trtp': 'SEPA OVERBOEKING', 'iban': 'FR001234567890', 'bic': 'GEFRADAM', 'name': 'ENERGIE BEDRIJF', 'eref': 'NOTPROVIDED', }) def test_non_abn_amro(self): self.assertEqual(abn_amro_description('foo'), {}) self.assertEqual(rabo_description('/FOO/BAR/NAME/'), {}) class TestINGDescription(unittest.TestCase): def test_tag(self): description = """/EREF/170330P40411570.4342.2964442//CNTP/ NL94RABO0123456789/RABONL2U/ENERGIE BEDRIJF///REMI/USTD// 170330/REM INFO/""" self.assertEqual(ing_description(description), { 'eref': '170330P40411570.4342.2964442', 'cntp': { 'account_number': 'NL94RABO0123456789', 'bic': 'RABONL2U', 'name': 'ENERGIE BEDRIJF', 'city': '', }, 'remi': { 'code': 'USTD', 'issuer': '', 'remittance_info': '170330/REM INFO', }, }) def test_non_ing(self): self.assertEqual(ing_description('foo'), {}) self.assertEqual(ing_description('/FOO/BAR/NAME/'), {}) class TestRegioBankDescription(unittest.TestCase): def test_reference(self): description = """0102792984 jyhhenewr f j k rgt-test-004""" self.assertEqual(regiobank_description(description), { 'account_number': '0102792984', 'name': 'jyhhenewr f j k', 'address': '', 'reference': 'rgt-test-004', }) def test_sepa(self): description = """0707464188 dsfg w van aan dsfg w van,nl04asnb070746418 8,sct2013021540684000000000004, t est 1""" self.assertEqual(regiobank_description(description), { 'account_number': '0707464188', 'name': 'dsfg w van', 'address': '', 'iban': 'nl04asnb070746418 8', 'remittance_info': 'sct2013021540684000000000004', 'description': 't est 1', }) def test_non_regiobank(self): self.assertEqual(regiobank_description('foo'), {}) description = """foo bar test""" self.assertEqual(regiobank_description(description), {}) def test_suite(): suite = additional_tests() loader = unittest.TestLoader() suite.addTests(loader.loadTestsFromTestCase(TestMT940)) return suite def additional_tests(): suite = unittest.TestSuite() if os.path.isfile(readme): suite.addTest(doctest.DocFileSuite(readme, module_relative=False)) return suite def main(): suite = test_suite() runner = unittest.TextTestRunner() return runner.run(suite) if __name__ == '__main__': sys.path.insert(0, os.path.dirname(os.path.dirname( os.path.dirname(os.path.abspath(__file__))))) sys.exit(not main().wasSuccessful()) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1631967577.828968 mt940-0.6.0/mt940.egg-info/0000755000175000017500000000000000000000000013012 5ustar00cedced././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1631967577.0 mt940-0.6.0/mt940.egg-info/PKG-INFO0000644000175000017500000000532400000000000014113 0ustar00cedcedMetadata-Version: 1.2 Name: mt940 Version: 0.6.0 Summary: A module to parse MT940 files Home-page: http://mt940.b2ck.com/ Author: B2CK Author-email: info@b2ck.com License: BSD Description: mt940 ===== mt940 is a parser for MT940 files. Nutshell -------- Import:: >>> import os >>> from mt940 import MT940 Instanciate:: >>> mt940 = MT940('mt940/MT940.txt') The statements:: >>> len(mt940.statements) 2 >>> statement = mt940.statements[0] >>> statement.account '123456789' >>> statement.information '13501/1' >>> start_balance = statement.start_balance >>> start_balance.date datetime.date(2012, 5, 11) >>> start_balance.amount Decimal('5138.61') >>> start_balance.currency 'EUR' >>> end_balance = statement.end_balance >>> end_balance.date datetime.date(2012, 5, 14) >>> end_balance.amount Decimal('5638.62') >>> end_balance.currency 'EUR' The transactions:: >>> len(statement.transactions) 3 >>> transaction, _, _ = statement.transactions >>> transaction.date datetime.date(2012, 5, 12) >>> transaction.booking datetime.date(2012, 5, 14) >>> transaction.amount Decimal('500.01') >>> transaction.id 'N654' >>> transaction.reference 'NONREF' >>> transaction.additional_data '987654321' >>> transaction.description # doctest: +NORMALIZE_WHITESPACE '/TRTP/SEPA OVERBOEKING/IBAN/FR12345678901234/BIC/GEFRADAM\n/NAME/QASD JGRED/REMI/Dit zijn de omschrijvingsregels/EREF/NOTPRO\nVIDED' To report issues please visit the `mt940 bugtracker`_. .. _mt940 bugtracker: http://mt940.b2ck.com/ Platform: UNKNOWN Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Topic :: Office/Business Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Utilities Requires-Python: >=3.5 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1631967577.0 mt940-0.6.0/mt940.egg-info/SOURCES.txt0000644000175000017500000000040400000000000014674 0ustar00cedced.flake8 .hgtags CHANGELOG COPYRIGHT MANIFEST.in README setup.py tox.ini mt940/MT940-optional.txt mt940/MT940.txt mt940/__init__.py mt940/test.py mt940.egg-info/PKG-INFO mt940.egg-info/SOURCES.txt mt940.egg-info/dependency_links.txt mt940.egg-info/top_level.txt././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1631967577.0 mt940-0.6.0/mt940.egg-info/dependency_links.txt0000644000175000017500000000000100000000000017060 0ustar00cedced ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1631967577.0 mt940-0.6.0/mt940.egg-info/top_level.txt0000644000175000017500000000000600000000000015540 0ustar00cedcedmt940 ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1631967577.828968 mt940-0.6.0/setup.cfg0000644000175000017500000000004600000000000012264 0ustar00cedced[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1631967449.0 mt940-0.6.0/setup.py0000644000175000017500000000273600000000000012165 0ustar00cedced#!/usr/bin/env python # This file is part of mt940. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. import os import re import codecs from setuptools import setup, find_packages def read(fname): return codecs.open( os.path.join(os.path.dirname(__file__), fname), 'r', 'utf-8').read() def get_version(): init = read(os.path.join('mt940', '__init__.py')) return re.search("__version__ = '([0-9.]*)'", init).group(1) setup(name='mt940', version=get_version(), author='B2CK', author_email='info@b2ck.com', url='http://mt940.b2ck.com/', description='A module to parse MT940 files', long_description=read('README'), packages=find_packages(), package_data={ 'mt940': ['MT940.txt', 'MT940-optional.txt'], }, python_requires='>=3.5', classifiers=[ 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Topic :: Office/Business', 'Topic :: Software Development :: Libraries', 'Topic :: Utilities', ], license='BSD', test_suite='mt940.test', ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1631399563.0 mt940-0.6.0/tox.ini0000644000175000017500000000053700000000000011763 0ustar00cedced# Tox (http://tox.testrun.org/) is a tool for running tests # in multiple virtualenvs. This configuration file will run the # test suite on all supported python versions. To use it, "pip install tox" # and then run "tox" from this directory. [tox] envlist = py35, py36, py37, py38, py39, pypy3 [testenv] commands = {envpython} setup.py test deps =