changeset 0:3d6eb6c42455

Initial commit
author Cédric Krier <ced@b2ck.com>
date Tue, 30 Jul 2013 19:50:26 +0200
parents
children 31acb0d0d691
files MANIFEST.in README mt940/MT940.txt mt940/__init__.py mt940/test.py setup.py
diffstat 6 files changed, 379 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MANIFEST.in	Tue Jul 30 19:50:26 2013 +0200
@@ -0,0 +1,2 @@
+include README
+include mt940/MT940.txt
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/README	Tue Jul 30 19:50:26 2013 +0200
@@ -0,0 +1,60 @@
+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)
+    1
+    >>> statement, = mt940.statements
+    >>> 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)
+    1
+    >>> 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.account
+    '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'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mt940/MT940.txt	Tue Jul 30 19:50:26 2013 +0200
@@ -0,0 +1,13 @@
+ABNANL2A
+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
+:62F:C120514EUR5638,62
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mt940/__init__.py	Tue Jul 30 19:50:26 2013 +0200
@@ -0,0 +1,152 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2013, Cédric Krier
+# Copyright (c) 2013, 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 <organization> 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 <COPYRIGHT HOLDER> 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.
+"""a parser for MT940 files
+"""
+__version__ = '0.1'
+__all__ = ['MT940']
+
+from collections import namedtuple, defaultdict
+from decimal import Decimal
+import datetime
+import re
+
+
+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 == 'D':
+        return -amount
+    return amount
+
+TRANSACTION_RE = re.compile(r"""
+    (?P<date>\d{6})
+    (?P<booking>\d{0,4})
+    (?P<sign>(D|C))
+    (?P<code>\D?)
+    (?P<amount>(\d|,){1,15})
+    (?P<id>\D.{3})
+    (?P<reference>.{0,16})""", re.VERBOSE)
+
+
+class MT940(object):
+
+    def __init__(self, name):
+        self.statements = []
+
+        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)
+
+        def set_statement(values, transactions):
+            self.statements.append(
+                Statement(
+                    transactions=[Transaction(*t) for t in transactions],
+                    **values))
+            values.clear()
+            del transactions[:]
+
+        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])
+
+        def get_transaction(transaction):
+            lines = transaction.splitlines()
+            if len(lines) == 1:
+                transaction, = lines
+                account = None
+            else:
+                transaction, account = 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')
+            return (date, booking, amount, id_, reference, account, '')
+
+        with open(name, 'rU') as f:
+            values = defaultdict(str)
+            transactions = []
+            for line in readline(f):
+                for name, sections in SECTIONS.iteritems():
+                    if name == 'begin':
+                        continue
+                    for section in sections:
+                        if line.startswith(section):
+                            if name in values and name == 'statement':
+                                set_statement(values, transactions)
+                            if name.endswith('_balance'):
+                                values[name] = get_balance(line[len(section):])
+                            elif name == 'transaction':
+                                transactions.append(
+                                    get_transaction(line[len(section):]))
+                            elif name == 'description':
+                                transactions[-1] = (transactions[-1][:-1]
+                                    + (line[len(section):],))
+                            else:
+                                values[name] += line[len(section):]
+            if values:
+                set_statement(values, transactions)
+
+Statement = namedtuple('Statement', ['statement', 'account', 'information',
+        'start_balance', 'transactions', 'end_balance'])
+Balance = namedtuple('Balance', ['date', 'amount', 'currency'])
+Transaction = namedtuple('Transaction', ['date', 'booking', 'amount', 'id',
+        'reference', 'account', 'description'])
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mt940/test.py	Tue Jul 30 19:50:26 2013 +0200
@@ -0,0 +1,86 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2013, Cédric Krier
+# Copyright (c) 2013, 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 <organization> 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 <COPYRIGHT HOLDER> 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.
+"""Test MT940
+"""
+import os
+import unittest
+import datetime
+from decimal import Decimal
+
+here = os.path.dirname(__file__)
+from mt940 import MT940
+
+
+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), 1)
+
+    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_transaction(self):
+        "Test transaction"
+        transaction, = self.mt940.statements[0].transactions
+        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.account, '987654321')
+        self.assertEqual(transaction.description,
+            '''/TRTP/SEPA OVERBOEKING/IBAN/FR12345678901234/BIC/GEFRADAM
+/NAME/QASD JGRED/REMI/Dit zijn de omschrijvingsregels/EREF/NOTPRO
+VIDED''')
+
+if __name__ == '__main__':
+    unittest.main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/setup.py	Tue Jul 30 19:50:26 2013 +0200
@@ -0,0 +1,66 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2013, Cédric Krier
+# Copyright (c) 2013, 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 <organization> 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 <COPYRIGHT HOLDER> 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.
+import os
+import re
+from setuptools import setup, find_packages
+
+
+def read(fname):
+    return open(os.path.join(os.path.dirname(__file__), fname)).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='Cédric Krier',
+    author_email='cedric.krier@b2ck.com',
+    url='http://code.google.com/p/mt940/',
+    description='A module to parse MT940 files',
+    long_description=read('README'),
+    packages=find_packages(),
+    package_data={
+        'mt940': ['MT940.txt'],
+        },
+    classifiers=[
+        'Intended Audience :: Developers',
+        'License :: OSI Approved :: BSD License',
+        'Operating System :: OS Independent',
+        'Programming Language :: Python :: 2',
+        'Programming Language :: Python :: 3',
+        'Topic :: Office/Business',
+        'Topic :: Software Development :: Libraries',
+        'Topic :: Utilities',
+        ],
+    license='BSD',
+    test_suite='mt940.test',
+    use_2to3=True,
+    )