diff --git a/account_banking_mt940/__init__.py b/account_banking_mt940/__init__.py new file mode 100644 index 000000000..bb289e723 --- /dev/null +++ b/account_banking_mt940/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# This module copyright (C) 2014 Therp BV (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +from . import mt940 diff --git a/account_banking_mt940/__openerp__.py b/account_banking_mt940/__openerp__.py new file mode 100644 index 000000000..3a6ee25a3 --- /dev/null +++ b/account_banking_mt940/__openerp__.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# This module copyright (C) 2014 Therp BV (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +{ + "name" : "MT940", + "version" : "1.0", + "author" : "Therp BV", + "complexity": "expert", + "description": """ +This addon provides a generic parser for MT940 files. Given that MT940 is a +non-open non-standard of pure evil in the way that every bank cooks up its own +interpretation of it, this addon alone won't help you much. It is rather +intended to be used by other addons to implement the dialect specific to a +certain bank. + +See account_banking_nl_ing_mt940 for an example on how to use it. + """, + "category" : "Dependency", + "depends" : [ + 'account_banking', + ], + "data" : [ + ], + "js": [ + ], + "css": [ + ], + "qweb": [ + ], + "auto_install": False, + "installable": True, + "application": False, + "external_dependencies" : { + 'python' : [], + }, +} diff --git a/account_banking_mt940/mt940.py b/account_banking_mt940/mt940.py new file mode 100644 index 000000000..a5c193e5a --- /dev/null +++ b/account_banking_mt940/mt940.py @@ -0,0 +1,217 @@ +#!/usr/bin/env python2 +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# This module copyright (C) 2014 Therp BV (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +""" +Parser for MT940 format files +""" +import re +import datetime +import logging +try: + from openerp.addons.account_banking.parsers.models import\ + mem_bank_statement, mem_bank_transaction + from openerp.tools.misc import DEFAULT_SERVER_DATE_FORMAT +except ImportError: + #this allows us to run this file standalone, see __main__ at the end + class mem_bank_statement: + def __init__(self): + self.transactions = [] + class mem_bank_transaction: + pass + DEFAULT_SERVER_DATE_FORMAT = "%Y-%m-%d" + +class MT940(object): + '''Inherit this class in your account_banking.parsers.models.parser, + define functions to handle the tags you need to handle and adjust static + variables as needed. + + Note that order matters: You need to do your_parser(MT940, parser), not the + other way around! + + At least, you should override handle_tag_61 and handle_tag_86. Don't forget + to call super. + handle_tag_* functions receive the remainder of the the line (that is, + without ':XX:') and are supposed to write into self.current_transaction''' + + header_lines = 3 + '''One file can contain multiple statements, each with its own poorly + documented header. For now, the best thing to do seems to skip that''' + + footer_regex = '^-}$' + footer_regex = '^-XXX$' + 'The line that denotes end of message, we need to create a new statement' + + tag_regex = '^:[0-9]{2}[A-Z]*:' + 'The beginning of a record, should be anchored to beginning of the line' + + def __init__(self, *args, **kwargs): + super(MT940, self).__init__(*args, **kwargs) + 'state variables' + self.current_statement = None + 'type account_banking.parsers.models.mem_bank_statement' + self.current_transaction = None + 'type account_banking.parsers.models.mem_bank_transaction' + self.statements = [] + 'parsed statements up to now' + + def parse(self, cr, data): + 'implements account_banking.parsers.models.parser.parse()' + iterator = data.split('\r\n').__iter__() + line = None + record_line = '' + try: + while True: + if not self.current_statement: + self.handle_header(cr, line, iterator) + line = iterator.next() + if not self.is_tag(cr, line) and not self.is_footer(cr, line): + record_line = self.append_continuation_line( + cr, record_line, line) + continue + if record_line: + self.handle_record(cr, record_line) + if self.is_footer(cr, line): + self.handle_footer(cr, line, iterator) + record_line = '' + continue + record_line = line + except StopIteration: + pass + return self.statements + + def append_continuation_line(self, cr, line, continuation_line): + '''append a continuation line for a multiline record. + Override and do data cleanups as necessary.''' + return line + continuation_line + + def create_statement(self, cr): + '''create a mem_bank_statement - override if you need a custom + implementation''' + return mem_bank_statement() + + def create_transaction(self, cr): + '''create a mem_bank_transaction - override if you need a custom + implementation''' + return mem_bank_transaction() + + def is_footer(self, cr, line): + '''determine if a line is the footer of a statement''' + return line and bool(re.match(self.footer_regex, line)) + + def is_tag(self, cr, line): + '''determine if a line has a tag''' + return line and bool(re.match(self.tag_regex, line)) + + def handle_header(self, cr, line, iterator): + '''skip header lines, create current statement''' + for i in range(self.header_lines): + iterator.next() + self.current_statement = self.create_statement(cr) + + def handle_footer(self, cr, line, iterator): + '''add current statement to list, reset state''' + self.statements.append(self.current_statement) + self.current_statement = None + + def handle_record(self, cr, line): + '''find a function to handle the record represented by line''' + tag_match = re.match(self.tag_regex, line) + tag = tag_match.group(0).strip(':') + if not hasattr(self, 'handle_tag_%s' % tag): + logging.error('Unknown tag %s', tag) + logging.error(line) + return + handler = getattr(self, 'handle_tag_%s' % tag) + handler(cr, line[tag_match.end():]) + + def handle_tag_20(self, cr, data): + '''ignore reference number''' + pass + + def handle_tag_25(self, cr, data): + '''get account owner information''' + self.current_statement.local_account = data + + def handle_tag_28C(self, cr, data): + '''get sequence number _within_this_batch_ - this alone + doesn't provide a unique id!''' + self.current_statement.id = data + + def handle_tag_60F(self, cr, data): + '''get start balance and currency''' + self.current_statement.local_currency = data[7:10] + self.current_statement.date = str2date(data[1:7]) + self.current_statement.start_balance = \ + (1 if data[0] == 'C' else -1) * str2float(data[10:]) + self.current_statement.id = '%s/%s' % ( + self.current_statement.date.strftime('%Y'), + self.current_statement.id) + + def handle_tag_62F(self, cr, data): + '''get ending balance''' + self.current_statement.end_balance = \ + (1 if data[0] == 'C' else -1) * str2float(data[10:]) + + def handle_tag_64(self, cr, data): + '''get current balance in currency''' + pass + + def handle_tag_65(self, cr, data): + '''get future balance in currency''' + pass + + def handle_tag_61(self, cr, data): + '''get transaction values''' + transaction = self.create_transaction(cr) + self.current_statement.transactions.append(transaction) + self.current_transaction = transaction + transaction.execution_date = str2date(data[:6]) + transaction.effective_date = str2date(data[:6]) + '...and the rest already is highly bank dependent' + + def handle_tag_86(self, cr, data): + '''details for previous transaction, here most differences between + banks occur''' + pass + +'utility functions' +def str2date(string, fmt='%y%m%d'): + return datetime.datetime.strptime(string, fmt) + +def str2float(string): + return float(string.replace(',', '.')) + +'testing' +def main(filename): + parser = MT940() + parser.parse(None, open(filename, 'r').read()) + for statement in parser.statements: + print '''statement found for %(local_account)s at %(date)s + with %(local_currency)s%(start_balance)s to %(end_balance)s + ''' % statement.__dict__ + for transaction in statement.transactions: + print ''' + transaction on %(execution_date)s''' % transaction.__dict__ + +if __name__ == '__main__': + import sys + main(*sys.argv[1:]) diff --git a/account_banking_mt940/static/src/img/icon.png b/account_banking_mt940/static/src/img/icon.png new file mode 100644 index 000000000..4c7ab3029 Binary files /dev/null and b/account_banking_mt940/static/src/img/icon.png differ diff --git a/account_banking_nl_ing_mt940/__init__.py b/account_banking_nl_ing_mt940/__init__.py new file mode 100644 index 000000000..3b6be506a --- /dev/null +++ b/account_banking_nl_ing_mt940/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# This module copyright (C) 2014 Therp BV (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +from . import account_banking_nl_ing_mt940 diff --git a/account_banking_nl_ing_mt940/__openerp__.py b/account_banking_nl_ing_mt940/__openerp__.py new file mode 100644 index 000000000..8eae5c53c --- /dev/null +++ b/account_banking_nl_ing_mt940/__openerp__.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# This module copyright (C) 2014 Therp BV (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +{ + "name" : "MT940 import for Dutch ING", + "version" : "1.0", + "author" : "Therp BV", + "complexity": "normal", + "description": """ +This addon imports the structured MT940 format as offered by the Dutch ING +bank. + """, + "category" : "Account Banking", + "depends" : [ + 'account_banking_mt940', + ], + "data" : [ + ], + "js": [ + ], + "css": [ + ], + "qweb": [ + ], + "auto_install": False, + "installable": True, + "application": False, + "external_dependencies" : { + 'python' : [], + }, +} diff --git a/account_banking_nl_ing_mt940/account_banking_nl_ing_mt940.py b/account_banking_nl_ing_mt940/account_banking_nl_ing_mt940.py new file mode 100644 index 000000000..a85ce2ff0 --- /dev/null +++ b/account_banking_nl_ing_mt940/account_banking_nl_ing_mt940.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# This module copyright (C) 2014 Therp BV (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +import re +from openerp.tools.translate import _ +from openerp.addons.account_banking.parsers.models import parser,\ + mem_bank_transaction +from openerp.addons.account_banking_mt940.mt940 import MT940, str2float + + +class transaction(mem_bank_transaction): + def is_valid(self): + '''allow transactions without remote account''' + return bool(self.execution_date) and bool(self.transferred_amount) + +class IngMT940Parser(MT940, parser): + name = _('ING MT940 (structured)') + country_code = 'NL' + code = 'INT_MT940_STRUC' + + tag_61_regex = re.compile( + '^(?P\d{6})(?P[CD])(?P\d+,\d{2})N(?P\d{3})' + '(?P\w{1,16})') + + def create_transaction(self, cr): + return transaction() + + def handle_tag_60F(self, cr, data): + super(IngMT940Parser, self).handle_tag_60F(cr, data) + self.current_statement.id = '%s-%s' % ( + self.get_unique_account_identifier( + cr, self.current_statement.local_account), + self.current_statement.id) + + def handle_tag_61(self, cr, data): + super(IngMT940Parser, self).handle_tag_61(cr, data) + parsed_data = self.tag_61_regex.match(data).groupdict() + self.current_transaction.transferred_amount = \ + (-1 if parsed_data['sign'] == 'D' else 1) * str2float( + parsed_data['amount']) + self.current_transaction.reference = parsed_data['reference'] + + def handle_tag_86(self, cr, data): + if not self.current_transaction: + return + super(IngMT940Parser, self).handle_tag_86(cr, data) + codewords = ['RTRN', 'BENM', 'ORDP', 'CSID', 'BUSP', 'MARF', 'EREF', + 'PREF', 'REMI', 'ID', 'PURP', 'ULTB', 'ULTD'] + subfields = {} + current_codeword = None + for word in data.split('/'): + if not word and not current_codeword: + continue + if word in codewords: + current_codeword = word + subfields[current_codeword] = [] + continue + subfields[current_codeword].append(word) + + if 'BENM' in subfields: + self.current_transaction.remote_account = subfields['BENM'][0] + self.current_transaction.remote_bank_bic = subfields['BENM'][1] + self.current_transaction.remote_owner = subfields['BENM'][2] + self.current_transaction.remote_owner_city = subfields['BENM'][3] + + if 'ORDP' in subfields: + self.current_transaction.remote_account = subfields['ORDP'][0] + self.current_transaction.remote_bank_bic = subfields['ORDP'][1] + self.current_transaction.remote_owner = subfields['ORDP'][2] + self.current_transaction.remote_owner_city = subfields['ORDP'][3] + + if 'REMI' in subfields: + self.current_transaction.message = '/'.join( + filter(lambda x: bool(x), subfields['REMI'])) + + if self.current_transaction.reference in subfields: + self.current_transaction.reference = ''.join( + subfields[self.current_transaction.reference]) + + if not subfields: + self.current_transaction.message = data + + self.current_transaction = None diff --git a/account_banking_nl_ing_mt940/static/src/img/icon.png b/account_banking_nl_ing_mt940/static/src/img/icon.png new file mode 100644 index 000000000..4c7ab3029 Binary files /dev/null and b/account_banking_nl_ing_mt940/static/src/img/icon.png differ