diff --git a/account_bank_statement_import_mt940_base/README.rst b/account_bank_statement_import_mt940_base/README.rst new file mode 100644 index 0000000..86c524f --- /dev/null +++ b/account_bank_statement_import_mt940_base/README.rst @@ -0,0 +1,55 @@ +.. image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: https://www.gnu.org/licenses/agpl + :alt: License: AGPL-3 + +==================== +Bank Statement MT940 +==================== + +This module 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_bank_statement_import_mt940_nl_ing for an example on how to use it. + +Known issues / Roadmap +====================== + +* None + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In case of trouble, please +check there if your issue has already been reported. If you spotted it first, +help us smash it by providing detailed and welcomed feedback. + +Credits +======= + +Contributors +------------ + +* Stefan Rijnhart +* Ronald Portier +* Andrea Stirpe +* Fekete Mihai + +Maintainer +---------- + + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +To contribute to this module, please visit https://odoo-community.org. diff --git a/account_bank_statement_import_mt940_base/__init__.py b/account_bank_statement_import_mt940_base/__init__.py new file mode 100644 index 0000000..d31f98c --- /dev/null +++ b/account_bank_statement_import_mt940_base/__init__.py @@ -0,0 +1 @@ +from . import mt940 diff --git a/account_bank_statement_import_mt940_base/__manifest__.py b/account_bank_statement_import_mt940_base/__manifest__.py new file mode 100644 index 0000000..a88409e --- /dev/null +++ b/account_bank_statement_import_mt940_base/__manifest__.py @@ -0,0 +1,14 @@ +# Copyright (C) 2013-2015 Therp BV + +{ + 'name': 'MT940 Bank Statements Import', + 'version': '11.0.1.0.0', + 'license': 'AGPL-3', + 'author': 'Odoo Community Association (OCA), Therp BV', + 'website': 'https://github.com/OCA/bank-statement-import', + 'category': 'Banking addons', + 'depends': [ + 'account_bank_statement_import', + ], + 'installable': True +} diff --git a/account_bank_statement_import_mt940_base/mt940.py b/account_bank_statement_import_mt940_base/mt940.py new file mode 100644 index 0000000..160e8a8 --- /dev/null +++ b/account_bank_statement_import_mt940_base/mt940.py @@ -0,0 +1,310 @@ +# Copyright (C) 2014-2015 Therp BV . +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +"""Generic parser for MT940 files, base for customized versions per bank.""" + +import re +import logging +from datetime import datetime + + +def str2amount(sign, amount_str): + """Convert sign (C or D) and amount in string to signed amount (float).""" + factor = (1 if sign == 'C' else -1) + return factor * float(amount_str.replace(',', '.')) + + +def get_subfields(data, codewords): + """Return dictionary with value array for each codeword in data. + + For instance: + data = + /BENM//NAME/Kosten/REMI/Periode 01-10-2013 t/m 31-12-2013/ISDT/20 + codewords = ['BENM', 'ADDR', 'NAME', 'CNTP', ISDT', 'REMI'] + Then return subfields = { + 'BENM': [], + 'NAME': ['Kosten'], + 'REMI': ['Periode 01-10-2013 t', 'm 31-12-2013'], + 'ISDT': ['20'], + } + """ + 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 + if current_codeword in subfields: + subfields[current_codeword].append(word) + return subfields + + +def get_counterpart(transaction, subfield): + """Get counterpart from transaction. + + Counterpart is often stored in subfield of tag 86. The subfield + can be BENM, ORDP, CNTP""" + if not subfield: + return # subfield is empty + if len(subfield) >= 1 and subfield[0]: + transaction.update({'account_number': subfield[0]}) + if len(subfield) >= 2 and subfield[1]: + transaction.update({'account_bic': subfield[1]}) + if len(subfield) >= 3 and subfield[2]: + transaction.update({'partner_name': subfield[2]}) + + +def handle_common_subfields(transaction, subfields): + """Deal with common functionality for tag 86 subfields.""" + # Get counterpart from CNTP, BENM or ORDP subfields: + for counterpart_field in ['CNTP', 'BENM', 'ORDP']: + if counterpart_field in subfields: + get_counterpart(transaction, subfields[counterpart_field]) + if not transaction.get('name'): + transaction['name'] = '' + # REMI: Remitter information (text entered by other party on trans.): + if 'REMI' in subfields: + transaction['name'] += ( + subfields['REMI'][2] + # this might look like + # /REMI/USTD/// + # or + # /REMI/STRD/CUR// + if len(subfields['REMI']) >= 3 and subfields['REMI'][0] in [ + 'STRD', 'USTD' + ] + else + '/'.join(x for x in subfields['REMI'] if x) + ) + # EREF: End-to-end reference + if 'EREF' in subfields: + transaction['name'] += '/'.join(filter(bool, subfields['EREF'])) + # Get transaction reference subfield (might vary): + if transaction.get('ref') in subfields: + transaction['ref'] = ''.join(subfields[transaction['ref']]) + + +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. + + 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 + """ + + def __init__(self): + """Initialize parser - override at least header_regex. + + This in fact uses the ING syntax, override in others.""" + self.mt940_type = 'General' + self.header_lines = 3 # Number of lines to skip + self.header_regex = '^0000 01INGBNL2AXXXX|^{1' # Start of header + self.footer_regex = '^-}$|^-XXX$' # Stop processing on seeing this + self.tag_regex = '^:[0-9]{2}[A-Z]*:' # Start of new tag + self.current_statement = None + self.current_transaction = None + self.statements = [] + self.currency_code = None + self.account_number = None + + def is_mt940(self, line): + """determine if a line is the header of a statement""" + if not bool(re.match(self.header_regex, line)): + raise ValueError( + 'File starting with %s does not seem to be a' + ' valid %s MT940 format bank statement.' % + (line[:12], self.mt940_type) + ) + + def is_mt940_statement(self, line): + """determine if line is the start of a statement""" + if not bool(line.startswith('{4:')): + raise ValueError( + 'The pre processed match %s does not seem to be a' + ' valid %s MT940 format bank statement. Every statement' + ' should start be a dict starting with {4:..' % line + ) + + def pre_process_data(self, data): + matches = [] + self.is_mt940(line=data) + data = data.replace( + '-}', '}').replace('}{', '}\r\n{').replace('\r\n', '\n') + if data.startswith(':940:'): + for statement in data.replace(':940:', '').split(':20:'): + match = '{4:\n:20:' + statement + '}' + matches.append(match) + else: + tag_re = re.compile( + r'(\{4:[^{}]+\})', + re.MULTILINE) + matches = tag_re.findall(data) + return matches + + def parse(self, data, header_lines=None): + """Parse mt940 bank statement file contents.""" + data = data.decode() + matches = self.pre_process_data(data) + for match in matches: + self.is_mt940_statement(line=match) + iterator = '\n'.join( + match.split('\n')[1:-1]).split('\n').__iter__() + line = None + record_line = '' + try: + while True: + if not self.current_statement: + self.handle_header(line, iterator, + header_lines=header_lines) + line = next(iterator) + if not self.is_tag(line) and not self.is_footer(line): + record_line = self.add_record_line(line, record_line) + continue + if record_line: + self.handle_record(record_line) + if self.is_footer(line): + self.handle_footer(line, iterator) + record_line = '' + continue + record_line = line + except StopIteration: + pass + if self.current_statement: + if record_line: + self.handle_record(record_line) + record_line = '' + self.statements.append(self.current_statement) + self.current_statement = None + return self.currency_code, self.account_number, self.statements + + def add_record_line(self, line, record_line): + record_line += line + return record_line + + def is_footer(self, 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, line): + """determine if a line has a tag""" + return line and bool(re.match(self.tag_regex, line)) + + def handle_header(self, dummy_line, iterator, header_lines=None): + """skip header lines, create current statement""" + if not header_lines: + header_lines = self.header_lines + for dummy_i in range(header_lines): + next(iterator) + self.current_statement = { + 'name': None, + 'date': None, + 'balance_start': 0.0, + 'balance_end_real': 0.0, + 'transactions': [] + } + + def handle_footer(self, dummy_line, dummy_iterator): + """add current statement to list, reset state""" + self.statements.append(self.current_statement) + self.current_statement = None + + def handle_record(self, 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): # pragma: no cover + logging.error('Unknown tag %s', tag) + logging.error(line) + return + handler = getattr(self, 'handle_tag_%s' % tag) + handler(line[tag_match.end():]) + + def handle_tag_20(self, data): + """Contains unique ? message ID""" + pass + + def handle_tag_25(self, data): + """Handle tag 25: local bank account information.""" + data = data.replace('EUR', '').replace('.', '').strip() + self.account_number = data + + def handle_tag_28C(self, data): + """Sequence number within batch - normally only zeroes.""" + pass + + def handle_tag_60F(self, data): + """get start balance and currency""" + # For the moment only first 60F record + # The alternative would be to split the file and start a new + # statement for each 20: tag encountered. + if not self.currency_code: + self.currency_code = data[7:10] + self.current_statement['balance_start'] = str2amount( + data[0], + data[10:] + ) + if not self.current_statement['date']: + self.current_statement['date'] = datetime.strptime(data[1:7], + '%y%m%d') + + def handle_tag_61(self, data): + """get transaction values""" + self.current_statement['transactions'].append({}) + self.current_transaction = self.current_statement['transactions'][-1] + self.current_transaction['date'] = datetime.strptime( + data[:6], + '%y%m%d' + ) + + def handle_tag_62F(self, data): + """Get ending balance, statement date and id. + + We use the date on the last 62F tag as statement date, as the date + on the 60F record (previous end balance) might contain a date in + a previous period. + + We generate the statement.id from the local_account and the end-date, + this should normally be unique, provided there is a maximum of + one statement per day. + + Depending on the bank, there might be multiple 62F tags in the import + file. The last one counts. + """ + + self.current_statement['balance_end_real'] = str2amount( + data[0], + data[10:] + ) + self.current_statement['date'] = datetime.strptime(data[1:7], '%y%m%d') + + # Only replace logically empty (only whitespace or zeroes) id's: + # But do replace statement_id's added before (therefore starting + # with local_account), because we need the date on the last 62F + # record. + statement_name = self.current_statement['name'] or '' + test_empty_id = re.sub(r'[\s0]', '', statement_name) + is_account_number = statement_name.startswith(self.account_number) + if not test_empty_id or is_account_number: + self.current_statement['name'] = '%s-%s' % ( + self.account_number, + self.current_statement['date'].strftime('%Y-%m-%d'), + ) + + def handle_tag_64(self, data): + """get current balance in currency""" + pass + + def handle_tag_65(self, data): + """get future balance in currency""" + pass + + def handle_tag_86(self, data): + """details for previous transaction, here most differences between + banks occur""" + pass diff --git a/account_bank_statement_import_mt940_base/test_files/test-ing.940 b/account_bank_statement_import_mt940_base/test_files/test-ing.940 new file mode 100644 index 0000000..5d98ef7 --- /dev/null +++ b/account_bank_statement_import_mt940_base/test_files/test-ing.940 @@ -0,0 +1,61 @@ +{1:F01INGBNL2ABXXX0000000000} +{2:I940INGBNL2AXXXN} +{4: +:20:P140220000000001 +:25:NL77INGB0574908765EUR +:28C:0000 +0 +:60F:C140219EUR662,23 +:61:1402200220C1,56NTRFEREF//00000000001 +005 +/TRCD/00100/ +:86:/EREF/EV12341REP1231456T1234//CNTP/NL32INGB0000012345/INGBNL2 +A/ING BANK NV INZAKE WEB///REMI/USTD//EV10001REP1000000T1000/ +:61:1402200220D1,57NTRFPREF//00000000001006 +/TRCD/00200/ +:86:/PREF/M000000003333333//REMI/USTD//TOTAAL 1 VZ/ +:61:1402200220C1,57NRTIEREF//00000000001007 +/TRCD/00190/ +:86:/RTRN/MS03//EREF/20120123456789//CNTP/NL32INGB0000012345/INGB +NL2A/J.Janssen///REMI/USTD//Factuurnr 123456 Klantnr 00123/ +:61:1402200220D1,14NDDTEREF//00000000001009 +/TRCD/010 +16 +/ +:86:/EREF/EV123R +EP123412T1234//MARF/MND +- +EV01//CSID/NL32ZZZ9999999 +91234//CNTP/NL32INGB0000012345/INGBNL2A/ING Bank N.V. inzake WeB/ +//REMI/USTD//EV123REP123412T1234/ +:61:1402200220C1,45NDDTPREF//00000000001008 +/TRCD/01000/ +:86:/PREF/M000000001111111/ +/CSID/ +NL32ZZZ999999991234 +/ +/REMI/USTD// +TOTAAL 1 POSTEN/ +:61:1402200220D12,75NRTIEREF//00000000001010 +/TRCD/01315/ +:86:/RTRN/MS03//EREF/20120501P0123478//MARF/MND +- +120123//CSID/NL32 +ZZZ999999991234//CNTP/NL32INGB0000012345/INGBNL2A/J.Janssen///REM +I/USTD//CO +NTRIBUTIE FEB 2014/ +:61:1402200220C32,00NTRF9001123412341234//00000000001011 +/TRCD/00108/ +:86:/EREF/15814016000676480//CNTP/NL32INGB0000012345/INGBNL2A/J.J +anssen///REMI/STRD/CUR/9001123412341234/ +:61:1402200220D119,00NTRF1070123412341234//00000000001012 +/ +TRCD/00108/ +:86:/EREF/15614016000384600//CNTP/NL32INGB0000012345/INGBNL2A/ING +BANK NV///REMI/STRD/CUR/1070123412341234/ +:62F:C140220EUR564,35 +:64:C140220EUR564,35 +:65:C140221EUR564,35 +:65:C140224EUR564,35 +:86:/SUM/4/4/134,46/36,58/ +-} diff --git a/account_bank_statement_import_mt940_base/test_files/test-rabo.swi b/account_bank_statement_import_mt940_base/test_files/test-rabo.swi new file mode 100644 index 0000000..6e347e2 --- /dev/null +++ b/account_bank_statement_import_mt940_base/test_files/test-rabo.swi @@ -0,0 +1,29 @@ +:940: +:20:940S140102 +:25:NL34RABO0142623393 EUR +:28C:0 +:60F:C131231EUR000000004433,52 +:61:140102C000000000400,00N541NONREF +NL66RABO0160878799 +:86:/ORDP//NAME/R. SMITH/ADDR/Green market 74 3311BE Sheepcity Nederl +and NL/REMI/Test money paid by other partner: +/ISDT/2014-01-02 +:62F:C140102EUR000000004833,52 +:20:940S140103 +:25:NL34RABO0142623393 EUR +:28C:0 +:60F:C140102EUR000000004833,52 +:62F:C140103EUR000000004833,52 +:20:940S140106 +:25:NL34RABO0142623393 EUR +:28C:0 +:60F:C140103EUR000000004833,52 +:61:140101D000000000034,61N093NONREF +:86:/BENM//NAME/Kosten/REMI/Periode 01-10-2013 t/m 31-12-2013/ISDT/20 +14-01-01 +:62F:C140106EUR000000004798,91 +:20:940S140107 +:25:NL34RABO0142623393 EUR +:28C:0 +:60F:C140106EUR000000004798,91 +:62F:C140107EUR000000004798,91 diff --git a/account_bank_statement_import_mt940_base/test_files/test-sns.940 b/account_bank_statement_import_mt940_base/test_files/test-sns.940 new file mode 100755 index 0000000..a1b032c --- /dev/null +++ b/account_bank_statement_import_mt940_base/test_files/test-sns.940 @@ -0,0 +1,78 @@ +{1:F01SNSBNL2AXXXX0000000000}{2:O940SNSBNL2AXXXXN}{3:}{4: +:20:0000000000 +:25:NL05SNSB0908244436 +:28C:361/1 +:60F:C171227EUR3026,96 +:61:1712271227D713,13NOVBNL49RABO0166416932 +gerrits glas en schilderwerk +:86:NL49RABO0166416932 gerrits glas en schilderwerk + +Factuur 17227/248/20 + + + +:61:1712271227D10,18NBEA//228ohu/972795 +:86: + +Jumbo Wijchen B.V. >WIJCHEN 27.12.2017 13U38 KV005 4XZJ4Z M +CC:5411 Contactloze betaling + + +:61:1712271227D13,52NINCNL94INGB0000869000 +vitens nv +:86:NL94INGB0000869000 vitens nv + +Europese incasso door:VITENS NV NL-Factuurnr 072304597540 VNKlant +nr 00000000000 BTW 0,77PC 6605 DW 6223, DEC-Incassant ID: NL84ZZZ0 +50695810000-Kenmerk Machtiging: bla + +:61:1712271227D25,61NBEA//229hro/195867 +:86: + +Albert Heijn 1370 >WIJCHEN 27.12.2017 19U13 KV006 70X708 M +CC:5411 + + +:62F:C171227EUR2264,52 +-}{5:} +{1:F01SNSBNL2AXXXX0000000000}{2:O940SNSBNL2AXXXXN}{3:}{4: +:20:0000000000 +:25:NL05SNSB0908244436 +:28C:362/1 +:60F:C171228EUR2264,52 +:61:1712281228D10,95NINCNL40RABO0127859497 +antagonist b.v. +:86:NL40RABO0127859497 antagonist b.v. + +Europese incasso door:ANTAGONIST B.V. NL-DDWPN954156 AI529942Cweb +share.nl-Incassant ID: NL65ZZZ091364410000-Kenmerk Machtiging: bla + +:61:1712281228C0,00NDIV +:86: + +Mobiel betalen is vanaf nu niet meer mogelijk voor B +ERG M A VAN DEN met Lenovo P2 + + +:62F:C171228EUR2253,57 +-}{5:} +{1:F01SNSBNL2AXXXX0000000000}{2:O940SNSBNL2AXXXXN}{3:}{4: +:20:0000000000 +:25:NL05SNSB0908244436 +:28C:363/1 +:60F:C171229EUR2253,57 +:61:1712291229D907,29NINCNL19ABNA0427093546 +florius +:86:NL19ABNA0427093546 florius + +Europese incasso door:FLORIUS NL-Verschuldigde bedragen PERIODE 1 +2-2017-Incassant ID: NL42ZZZ080242850000-Kenmerk Machtiging: bla + +:61:1712291229D35,00NINCNL28DEUT0265186439 +stichting derdengelden bucka +:86:NL28DEUT0265186439 stichting derdengelden bucka + +Europese incasso door:STICHTING DERDENGELDEN BUCKAROO-T-Mobile Th +uis B.V.: Rekening dec 2017. Bekijk je rekening op thuismy.t-mobi +le.nl-Incassant ID: NL39ZZZ302317620000-Kenmerk Machtiging: bla +-}{5:} diff --git a/account_bank_statement_import_mt940_base/test_files/test-wrong-file.940 b/account_bank_statement_import_mt940_base/test_files/test-wrong-file.940 new file mode 100644 index 0000000..ae75738 --- /dev/null +++ b/account_bank_statement_import_mt940_base/test_files/test-wrong-file.940 @@ -0,0 +1,11 @@ +:9401: +:20:940S140102 +:25:NL34RABO0142623393 EUR +:28C:0 +:60F:C131231EUR000000004433,52 +:61:140102C000000000400,00N541NONREF +NL66RABO0160878799 +:86:/ORDP//NAME/R. SMITH/ADDR/Green market 74 3311BE Sheepcity Nederl +and NL/REMI/Test money paid by other partner: +/ISDT/2014-01-02 +:62F:C140102EUR000000004833,52 diff --git a/account_bank_statement_import_mt940_base/tests/__init__.py b/account_bank_statement_import_mt940_base/tests/__init__.py new file mode 100644 index 0000000..bb3456a --- /dev/null +++ b/account_bank_statement_import_mt940_base/tests/__init__.py @@ -0,0 +1 @@ +from . import test_import_bank_statement diff --git a/account_bank_statement_import_mt940_base/tests/test_import_bank_statement.py b/account_bank_statement_import_mt940_base/tests/test_import_bank_statement.py new file mode 100644 index 0000000..9ff19e0 --- /dev/null +++ b/account_bank_statement_import_mt940_base/tests/test_import_bank_statement.py @@ -0,0 +1,239 @@ +# Copyright 2017 Onestein () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import base64 +from mock import patch +from odoo.tests.common import TransactionCase +from odoo.modules.module import get_module_resource +from ..mt940 import MT940, get_subfields, handle_common_subfields + + +class TestImport(TransactionCase): + """Run test to import mt940 import.""" + transactions = [ + { + 'account_number': 'NL46ABNA0499998748', + 'amount': -754.25, + 'ref': '435005714488-ABNO33052620', + 'name': 'test line', + }, + ] + + def setUp(self): + super(TestImport, self).setUp() + bank1 = self.env['res.partner.bank'].create({ + 'acc_number': 'NL77INGB0574908765', + 'partner_id': self.env.ref('base.main_partner').id, + 'company_id': self.env.ref('base.main_company').id, + 'bank_id': self.env.ref('base.res_bank_1').id, + }) + self.env['account.journal'].create({ + 'name': 'Bank Journal - (test1 mt940)', + 'code': 'TBNK1MT940', + 'type': 'bank', + 'bank_account_id': bank1.id, + 'currency_id': self.env.ref('base.EUR').id, + }) + bank2 = self.env['res.partner.bank'].create({ + 'acc_number': 'NL34RABO0142623393', + 'partner_id': self.env.ref('base.main_partner').id, + 'company_id': self.env.ref('base.main_company').id, + 'bank_id': self.env.ref('base.res_bank_1').id, + }) + self.env['account.journal'].create({ + 'name': 'Bank Journal - (test2 mt940)', + 'code': 'TBNK2MT940', + 'type': 'bank', + 'bank_account_id': bank2.id, + 'currency_id': self.env.ref('base.EUR').id, + }) + bank3 = self.env['res.partner.bank'].create({ + 'acc_number': 'NL05SNSB0908244436', + 'partner_id': self.env.ref('base.main_partner').id, + 'company_id': self.env.ref('base.main_company').id, + 'bank_id': self.env.ref('base.res_bank_1').id, + }) + self.env['account.journal'].create({ + 'name': 'Bank Journal - (test3 mt940)', + 'code': 'TBNK3MT940', + 'type': 'bank', + 'bank_account_id': bank3.id, + 'currency_id': self.env.ref('base.EUR').id, + }) + + self.data =\ + "/BENM//NAME/Cost/REMI/Period 01-10-2013 t/m 31-12-2013/ISDT/20" + self.codewords = ['BENM', 'ADDR', 'NAME', 'CNTP', 'ISDT', 'REMI'] + + def test_wrong_file_import(self): + """Test wrong file import.""" + testfile = get_module_resource( + 'account_bank_statement_import_mt940_base', + 'test_files', + 'test-wrong-file.940', + ) + parser = MT940() + datafile = open(testfile, 'rb').read() + with self.assertRaises(ValueError): + parser.parse(datafile, header_lines=1) + + def test_statement_import(self): + """Test correct creation of single statement ING.""" + + def _prepare_statement_lines(statements): + transact = self.transactions[0] + for st_vals in statements[2]: + for line_vals in st_vals['transactions']: + line_vals['amount'] = transact['amount'] + line_vals['name'] = transact['name'] + line_vals['account_number'] = transact['account_number'] + line_vals['ref'] = transact['ref'] + + testfile = get_module_resource( + 'account_bank_statement_import_mt940_base', + 'test_files', + 'test-ing.940', + ) + parser = MT940() + datafile = open(testfile, 'rb').read() + statements = parser.parse(datafile, header_lines=1) + + _prepare_statement_lines(statements) + + path_addon = 'odoo.addons.account_bank_statement_import.' + path_file = 'account_bank_statement_import.' + path_class = 'AccountBankStatementImport.' + method = path_addon + path_file + path_class + '_parse_file' + with patch(method) as my_mock: + my_mock.return_value = statements + + action = self.env['account.bank.statement.import'].create({ + 'data_file': base64.b64encode(datafile), + }).import_file() + + transact = self.transactions[0] + for statement in self.env['account.bank.statement'].browse( + action['context']['statement_ids']): + for line in statement.line_ids: + self.assertTrue( + line.bank_account_id.acc_number == + transact['account_number']) + self.assertTrue(line.amount == transact['amount']) + self.assertTrue(line.date == '2014-02-20') + self.assertTrue(line.name == transact['name']) + self.assertTrue(line.ref == transact['ref']) + + def test_get_subfields(self): + """Unit Test function get_subfields().""" + + res = get_subfields(self.data, self.codewords) + espected_res = { + 'BENM': [''], + 'NAME': ['Cost'], + 'REMI': ['Period 01-10-2013 t', 'm 31-12-2013'], + 'ISDT': ['20'], + } + self.assertTrue(res == espected_res) + + def test_handle_common_subfields(self): + """Unit Test function handle_common_subfields().""" + + subfields = get_subfields(self.data, self.codewords) + transaction = self.transactions[0] + + handle_common_subfields(transaction, subfields) + + def test_statement_import2(self): + """Test correct creation of single statement RABO.""" + + def _prepare_statement_lines(statements): + transact = self.transactions[0] + for st_vals in statements[2]: + for line_vals in st_vals['transactions']: + line_vals['amount'] = transact['amount'] + line_vals['name'] = transact['name'] + line_vals['account_number'] = transact['account_number'] + line_vals['ref'] = transact['ref'] + + testfile = get_module_resource( + 'account_bank_statement_import_mt940_base', + 'test_files', + 'test-rabo.swi', + ) + parser = MT940() + parser.header_regex = '^:940:' # Start of header + parser.header_lines = 1 # Number of lines to skip + datafile = open(testfile, 'rb').read() + statements = parser.parse(datafile, header_lines=1) + + _prepare_statement_lines(statements) + + path_addon = 'odoo.addons.account_bank_statement_import.' + path_file = 'account_bank_statement_import.' + path_class = 'AccountBankStatementImport.' + method = path_addon + path_file + path_class + '_parse_file' + with patch(method) as my_mock: + my_mock.return_value = statements + + action = self.env['account.bank.statement.import'].create({ + 'data_file': base64.b64encode(datafile), + }).import_file() + # The file contains 4 statements, but only 2 with transactions + self.assertTrue(len(action['context']['statement_ids']) == 2) + transact = self.transactions[0] + for statement in self.env['account.bank.statement'].browse( + action['context']['statement_ids']): + for line in statement.line_ids: + self.assertTrue( + line.bank_account_id.acc_number == + transact['account_number']) + self.assertTrue(line.amount == transact['amount']) + self.assertTrue(line.date) + self.assertTrue(line.name == transact['name']) + self.assertTrue(line.ref == transact['ref']) + + def test_statement_import3(self): + """Test correct creation of multiple statements SNS.""" + + def _prepare_statement_lines(statements): + transact = self.transactions[0] + for st_vals in statements[2]: + for line_vals in st_vals['transactions']: + line_vals['amount'] = transact['amount'] + line_vals['name'] = transact['name'] + line_vals['account_number'] = transact['account_number'] + line_vals['ref'] = transact['ref'] + + testfile = get_module_resource( + 'account_bank_statement_import_mt940_base', + 'test_files', + 'test-sns.940', + ) + parser = MT940() + datafile = open(testfile, 'rb').read() + statements = parser.parse(datafile, header_lines=1) + + _prepare_statement_lines(statements) + + path_addon = 'odoo.addons.account_bank_statement_import.' + path_file = 'account_bank_statement_import.' + path_class = 'AccountBankStatementImport.' + method = path_addon + path_file + path_class + '_parse_file' + with patch(method) as my_mock: + my_mock.return_value = statements + + action = self.env['account.bank.statement.import'].create({ + 'data_file': base64.b64encode(datafile), + }).import_file() + self.assertTrue(len(action['context']['statement_ids']) == 3) + transact = self.transactions[-1] + for statement in self.env['account.bank.statement'].browse( + action['context']['statement_ids'][-1]): + for line in statement.line_ids: + self.assertTrue( + line.bank_account_id.acc_number == + transact['account_number']) + self.assertTrue(line.amount == transact['amount']) + self.assertTrue(line.date == statement.date) + self.assertTrue(line.name == transact['name']) + self.assertTrue(line.ref == transact['ref'])