Browse Source

Merge pull request #15 from NL66278/8.0_import_parsers_new_api_therp

8.0 import parsers new api therp
pull/35/head
Stéphane Bidoul (ACSONE) 9 years ago
parent
commit
72a7095389
  1. 2
      account_bank_statement_import/__openerp__.py
  2. 250
      account_bank_statement_import/account_bank_statement_import.py
  3. 56
      account_bank_statement_import/i18n/account_bank_statement_import.pot
  4. 236
      account_bank_statement_import/i18n/nl.po
  5. 236
      account_bank_statement_import/parserlib.py
  6. 2
      account_bank_statement_import/tests/__init__.py
  7. 9
      account_bank_statement_import/tests/test_import_bank_statement.py
  8. 131
      account_bank_statement_import/tests/test_import_file.py
  9. 49
      account_bank_statement_import_camt/README.rst
  10. 1
      account_bank_statement_import_camt/__init__.py
  11. 34
      account_bank_statement_import_camt/__openerp__.py
  12. 44
      account_bank_statement_import_camt/account_bank_statement_import.py
  13. 239
      account_bank_statement_import_camt/camt.py
  14. 26
      account_bank_statement_import_camt/demo/demo_data.xml
  15. 241
      account_bank_statement_import_camt/test_files/test-camt053.xml
  16. 23
      account_bank_statement_import_camt/tests/__init__.py
  17. 45
      account_bank_statement_import_camt/tests/test_import_bank_statement.py
  18. 53
      account_bank_statement_import_mt940_base/README.rst
  19. 1
      account_bank_statement_import_mt940_base/__init__.py
  20. 31
      account_bank_statement_import_mt940_base/__openerp__.py
  21. 262
      account_bank_statement_import_mt940_base/mt940.py
  22. 51
      account_bank_statement_import_mt940_nl_ing/README.rst
  23. 1
      account_bank_statement_import_mt940_nl_ing/__init__.py
  24. 34
      account_bank_statement_import_mt940_nl_ing/__openerp__.py
  25. 44
      account_bank_statement_import_mt940_nl_ing/account_bank_statement_import.py
  26. 26
      account_bank_statement_import_mt940_nl_ing/demo/demo_data.xml
  27. 66
      account_bank_statement_import_mt940_nl_ing/mt940.py
  28. 61
      account_bank_statement_import_mt940_nl_ing/test_files/test-ing-old.940
  29. 62
      account_bank_statement_import_mt940_nl_ing/test_files/test-ing.940
  30. 25
      account_bank_statement_import_mt940_nl_ing/tests/__init__.py
  31. 53
      account_bank_statement_import_mt940_nl_ing/tests/test_import_bank_statement.py
  32. 47
      account_bank_statement_import_mt940_nl_rabo/README.rst
  33. 20
      account_bank_statement_import_mt940_nl_rabo/__init__.py
  34. 32
      account_bank_statement_import_mt940_nl_rabo/__openerp__.py
  35. 46
      account_bank_statement_import_mt940_nl_rabo/account_bank_statement_import.py
  36. 86
      account_bank_statement_import_mt940_nl_rabo/mt940.py
  37. 29
      account_bank_statement_import_mt940_nl_rabo/test_files/test-rabo.swi
  38. 23
      account_bank_statement_import_mt940_nl_rabo/tests/__init__.py
  39. 46
      account_bank_statement_import_mt940_nl_rabo/tests/test_import_bank_statement.py
  40. 5
      account_bank_statement_import_ofx/__openerp__.py
  41. 27
      account_bank_statement_import_ofx/demo/demo_data.xml
  42. 2
      account_bank_statement_import_qif/__openerp__.py

2
account_bank_statement_import/__openerp__.py

@ -1,7 +1,7 @@
# -*- encoding: utf-8 -*- # -*- encoding: utf-8 -*-
{ {
'name': 'Account Bank Statement Import', 'name': 'Account Bank Statement Import',
'category': 'Accounting & Finance',
'category': 'Banking addons',
'version': '1.0', 'version': '1.0',
'author': 'OpenERP SA,' 'author': 'OpenERP SA,'
'Odoo Community Association (OCA)', 'Odoo Community Association (OCA)',

250
account_bank_statement_import/account_bank_statement_import.py

@ -1,15 +1,17 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Framework for importing bank statement files."""
import logging
import base64 import base64
from openerp import api, models, fields from openerp import api, models, fields
from openerp.tools.translate import _ from openerp.tools.translate import _
from openerp.exceptions import Warning from openerp.exceptions import Warning
import logging
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
class AccountBankStatementLine(models.Model): class AccountBankStatementLine(models.Model):
"""Extend model account.bank.statement.line."""
_inherit = "account.bank.statement.line" _inherit = "account.bank.statement.line"
# Ensure transactions can be imported only once (if the import format # Ensure transactions can be imported only once (if the import format
@ -24,6 +26,7 @@ class AccountBankStatementLine(models.Model):
class AccountBankStatementImport(models.TransientModel): class AccountBankStatementImport(models.TransientModel):
"""Extend model account.bank.statement."""
_name = 'account.bank.statement.import' _name = 'account.bank.statement.import'
_description = 'Import Bank Statement' _description = 'Import Bank Statement'
@ -50,7 +53,7 @@ class AccountBankStatementImport(models.TransientModel):
@api.multi @api.multi
def import_file(self): def import_file(self):
""" Process the file chosen in the wizard, create bank statement(s) and """ Process the file chosen in the wizard, create bank statement(s) and
go to reconciliation. """
go to reconciliation."""
self.ensure_one() self.ensure_one()
data_file = base64.b64decode(self.data_file) data_file = base64.b64decode(self.data_file)
statement_ids, notifications = self.with_context( statement_ids, notifications = self.with_context(
@ -70,15 +73,43 @@ class AccountBankStatementImport(models.TransientModel):
@api.model @api.model
def _import_file(self, data_file): def _import_file(self, data_file):
""" Create bank statement(s) from file
"""
""" Create bank statement(s) from file."""
# The appropriate implementation module returns the required data # The appropriate implementation module returns the required data
currency_code, account_number, stmts_vals = self._parse_file(data_file)
# Check raw data
self._check_parsed_data(stmts_vals)
statement_ids = []
notifications = []
parse_result = self._parse_file(data_file)
# Check for old version result, with separate currency and account
if isinstance(parse_result, tuple) and len(parse_result) == 3:
(currency_code, account_number, statements) = parse_result
for stmt_vals in statements:
stmt_vals['currency_code'] = currency_code
stmt_vals['account_number'] = account_number
else:
statements = parse_result
# Check raw data:
self._check_parsed_data(statements)
# Import all statements:
for stmt_vals in statements:
(statement_id, new_notifications) = (
self._import_statement(stmt_vals))
if statement_id:
statement_ids.append(statement_id)
notifications.append(new_notifications)
if len(statement_ids) == 0:
raise Warning(_('You have already imported that file.'))
return statement_ids, notifications
@api.model
def _import_statement(self, stmt_vals):
"""Import a single bank-statement.
Return ids of created statements and notifications.
"""
currency_code = stmt_vals.pop('currency_code')
account_number = stmt_vals.pop('account_number')
# Try to find the bank account and currency in odoo # Try to find the bank account and currency in odoo
currency_id, bank_account_id = self._find_additional_data(
currency_code, account_number)
currency_id = self._find_currency_id(currency_code)
bank_account_id = self._find_bank_account_id(account_number)
# Create the bank account if not already existing # Create the bank account if not already existing
if not bank_account_id and account_number: if not bank_account_id and account_number:
journal_id = self.env.context.get('journal_id') journal_id = self.env.context.get('journal_id')
@ -90,29 +121,30 @@ class AccountBankStatementImport(models.TransientModel):
account_number, company_id=company_id, account_number, company_id=company_id,
currency_id=currency_id).id currency_id=currency_id).id
# Find or create the bank journal # Find or create the bank journal
journal_id = self._get_journal(
currency_id, bank_account_id, account_number)
journal_id = self._get_journal(currency_id, bank_account_id)
# By now journal and account_number must be known
if not journal_id:
raise Warning(_('Can not determine journal for import.'))
# Prepare statement data to be used for bank statements creation # Prepare statement data to be used for bank statements creation
stmts_vals = self._complete_stmts_vals(
stmts_vals, journal_id, account_number)
# Create the bank statements
return self._create_bank_statements(stmts_vals)
stmt_vals = self._complete_statement(
stmt_vals, journal_id, account_number)
# Create the bank stmt_vals
return self._create_bank_statement(stmt_vals)
@api.model @api.model
def _parse_file(self, data_file): def _parse_file(self, data_file):
""" Each module adding a file support must extends this method. It """ Each module adding a file support must extends this method. It
rocesses the file if it can, returns super otherwise, resulting in a
processes the file if it can, returns super otherwise, resulting in a
chain of responsability. chain of responsability.
This method parses the given file and returns the data required by This method parses the given file and returns the data required by
the bank statement import process, as specified below. the bank statement import process, as specified below.
rtype: triplet (if a value can't be retrieved, use None)
- currency code: string (e.g: 'EUR')
The ISO 4217 currency code, case insensitive
- account number: string (e.g: 'BE1234567890')
The number of the bank account which the statement belongs
to
- bank statements data: list of dict containing (optional - bank statements data: list of dict containing (optional
items marked by o) : items marked by o) :
-o currency code: string (e.g: 'EUR')
The ISO 4217 currency code, case insensitive
-o account number: string (e.g: 'BE1234567890')
The number of the bank account which the statement
belongs to
- 'name': string (e.g: '000000123') - 'name': string (e.g: '000000123')
- 'date': date (e.g: 2013-06-26) - 'date': date (e.g: 2013-06-26)
-o 'balance_start': float (e.g: 8368.56) -o 'balance_start': float (e.g: 8368.56)
@ -130,47 +162,49 @@ class AccountBankStatementImport(models.TransientModel):
-o 'partner_name': string -o 'partner_name': string
-o 'ref': string -o 'ref': string
""" """
raise Warning(_('Could not make sense of the given file.\nDid you '
'install the module to support this type of file ?'))
raise Warning(_(
'Could not make sense of the given file.\n'
'Did you install the module to support this type of file?'
))
@api.model @api.model
def _check_parsed_data(self, stmts_vals):
def _check_parsed_data(self, statements):
""" Basic and structural verifications """ """ Basic and structural verifications """
if len(stmts_vals) == 0:
if len(statements) == 0:
raise Warning(_('This file doesn\'t contain any statement.')) raise Warning(_('This file doesn\'t contain any statement.'))
no_st_line = True
for vals in stmts_vals:
if vals['transactions'] and len(vals['transactions']) > 0:
no_st_line = False
break
if no_st_line:
for stmt_vals in statements:
if 'transactions' in stmt_vals and stmt_vals['transactions']:
return
# If we get here, no transaction was found:
raise Warning(_('This file doesn\'t contain any transaction.')) raise Warning(_('This file doesn\'t contain any transaction.'))
@api.model @api.model
def _find_additional_data(self, currency_code, account_number):
""" Get the res.currency ID and the res.partner.bank ID """
# if no currency_code is provided, we'll use the company currency
currency_id = False
def _find_currency_id(self, currency_code):
""" Get res.currency ID."""
if currency_code: if currency_code:
currency_ids = self.env['res.currency'].search( currency_ids = self.env['res.currency'].search(
[('name', '=ilike', currency_code)]) [('name', '=ilike', currency_code)])
company_currency_id = self.env.user.company_id.currency_id
if currency_ids: if currency_ids:
if currency_ids[0] != company_currency_id:
currency_id = currency_ids[0].id
return currency_ids[0].id
else:
raise Warning(_(
'Statement has invalid currency code %s') % currency_code)
# if no currency_code is provided, we'll use the company currency
return self.env.user.company_id.currency_id.id
@api.model
def _find_bank_account_id(self, account_number):
""" Get res.partner.bank ID """
bank_account_id = None bank_account_id = None
if account_number and len(account_number) > 4: if account_number and len(account_number) > 4:
bank_account_ids = self.env['res.partner.bank'].search( bank_account_ids = self.env['res.partner.bank'].search(
[('acc_number', '=', account_number)], limit=1) [('acc_number', '=', account_number)], limit=1)
if bank_account_ids: if bank_account_ids:
bank_account_id = bank_account_ids[0].id bank_account_id = bank_account_ids[0].id
return currency_id, bank_account_id
return bank_account_id
@api.model @api.model
def _get_journal(self, currency_id, bank_account_id, account_number):
def _get_journal(self, currency_id, bank_account_id):
""" Find the journal """ """ Find the journal """
bank_model = self.env['res.partner.bank'] bank_model = self.env['res.partner.bank']
# Find the journal from context, wizard or bank account # Find the journal from context, wizard or bank account
@ -189,23 +223,45 @@ class AccountBankStatementImport(models.TransientModel):
if bank_account.journal_id.id: if bank_account.journal_id.id:
journal_id = bank_account.journal_id.id journal_id = bank_account.journal_id.id
# If importing into an existing journal, its currency must be the same # If importing into an existing journal, its currency must be the same
# as the bank statement
if journal_id:
journal_currency_id = self.env['account.journal'].browse(
journal_id).currency.id
if currency_id and currency_id != journal_currency_id:
raise Warning(_('The currency of the bank statement is not '
'the same as the currency of the journal !'))
# If we couldn't find/create a journal, everything is lost
if not journal_id:
raise Warning(_('Cannot find in which journal import this '
'statement. Please manually select a journal.'))
# as the bank statement. When journal has no currency, currency must
# be equal to company currency.
if journal_id and currency_id:
journal_obj = self.env['account.journal'].browse(journal_id)
if journal_obj.currency:
journal_currency_id = journal_obj.currency.id
if currency_id != journal_currency_id:
# ALso log message with id's for technical analysis:
_logger.warn(
_('Statement currency id is %d,'
' but journal currency id = %d.'),
currency_id,
journal_currency_id
)
raise Warning(_(
'The currency of the bank statement is not '
'the same as the currency of the journal !'
))
else:
company_currency_id = self.env.user.company_id.currency_id.id
if currency_id != company_currency_id:
# ALso log message with id's for technical analysis:
_logger.warn(
_('Statement currency id is %d,'
' but company currency id = %d.'),
currency_id,
company_currency_id
)
raise Warning(_(
'The currency of the bank statement is not '
'the same as the company currency !'
))
return journal_id return journal_id
@api.model @api.model
@api.returns('res.partner.bank') @api.returns('res.partner.bank')
def _create_bank_account(self, account_number, company_id=False,
currency_id=False):
def _create_bank_account(
self, account_number, company_id=False, currency_id=False):
"""Automagically create bank account, when not yet existing."""
try: try:
bank_type = self.env.ref('base.bank_normal') bank_type = self.env.ref('base.bank_normal')
bank_code = bank_type.code bank_code = bank_type.code
@ -233,17 +289,16 @@ class AccountBankStatementImport(models.TransientModel):
default_currency=currency_id).create(vals_acc) default_currency=currency_id).create(vals_acc)
@api.model @api.model
def _complete_stmts_vals(self, stmts_vals, journal_id, account_number):
for st_vals in stmts_vals:
st_vals['journal_id'] = journal_id
for line_vals in st_vals['transactions']:
def _complete_statement(self, stmt_vals, journal_id, account_number):
"""Complete statement from information passed."""
stmt_vals['journal_id'] = journal_id
for line_vals in stmt_vals['transactions']:
unique_import_id = line_vals.get('unique_import_id', False) unique_import_id = line_vals.get('unique_import_id', False)
if unique_import_id: if unique_import_id:
line_vals['unique_import_id'] = ( line_vals['unique_import_id'] = (
account_number and account_number + '-' or '') + \
(account_number and account_number + '-' or '') +
unique_import_id unique_import_id
)
if not line_vals.get('bank_account_id'): if not line_vals.get('bank_account_id'):
# Find the partner and his bank account or create the bank # Find the partner and his bank account or create the bank
# account. The partner selected during the reconciliation # account. The partner selected during the reconciliation
@ -251,66 +306,60 @@ class AccountBankStatementImport(models.TransientModel):
# closed. # closed.
partner_id = False partner_id = False
bank_account_id = False bank_account_id = False
identifying_string = line_vals.get('account_number')
if identifying_string:
account_number = line_vals.get('account_number')
if account_number:
bank_model = self.env['res.partner.bank'] bank_model = self.env['res.partner.bank']
banks = bank_model.search( banks = bank_model.search(
[('acc_number', '=', identifying_string)], limit=1)
[('acc_number', '=', account_number)], limit=1)
if banks: if banks:
bank_account_id = banks[0].id bank_account_id = banks[0].id
partner_id = banks[0].partner_id.id partner_id = banks[0].partner_id.id
else: else:
bank_account_id = self._create_bank_account(
identifying_string).id
bank_obj = self._create_bank_account(account_number)
bank_account_id = bank_obj and bank_obj.id or False
line_vals['partner_id'] = partner_id line_vals['partner_id'] = partner_id
line_vals['bank_account_id'] = bank_account_id line_vals['bank_account_id'] = bank_account_id
return stmts_vals
return stmt_vals
@api.model @api.model
def _create_bank_statements(self, stmts_vals):
""" Create new bank statements from imported values, filtering out
already imported transactions, and returns data used by the
def _create_bank_statement(self, stmt_vals):
""" Create bank statement from imported values, filtering out
already imported transactions, and return data used by the
reconciliation widget reconciliation widget
""" """
bs_model = self.env['account.bank.statement'] bs_model = self.env['account.bank.statement']
bsl_model = self.env['account.bank.statement.line'] bsl_model = self.env['account.bank.statement.line']
# Filter out already imported transactions and create statements
statement_ids = []
ignored_statement_lines_import_ids = []
for st_vals in stmts_vals:
# Filter out already imported transactions and create statement
ignored_line_ids = []
filtered_st_lines = [] filtered_st_lines = []
for line_vals in st_vals['transactions']:
if 'unique_import_id' not in line_vals \
or not line_vals['unique_import_id'] \
or not bool(bsl_model.sudo().search(
[('unique_import_id', '=',
line_vals['unique_import_id'])],
limit=1)):
for line_vals in stmt_vals['transactions']:
unique_id = (
'unique_import_id' in line_vals and
line_vals['unique_import_id']
)
if not unique_id or not bool(bsl_model.sudo().search(
[('unique_import_id', '=', unique_id)], limit=1)):
filtered_st_lines.append(line_vals) filtered_st_lines.append(line_vals)
else: else:
ignored_statement_lines_import_ids.append(
line_vals['unique_import_id'])
ignored_line_ids.append(unique_id)
statement_id = False
if len(filtered_st_lines) > 0: if len(filtered_st_lines) > 0:
# Remove values that won't be used to create records # Remove values that won't be used to create records
st_vals.pop('transactions', None)
stmt_vals.pop('transactions', None)
for line_vals in filtered_st_lines: for line_vals in filtered_st_lines:
line_vals.pop('account_number', None) line_vals.pop('account_number', None)
# Create the satement
st_vals['line_ids'] = [[0, False, line] for line in
filtered_st_lines]
statement_ids.append(bs_model.create(st_vals).id)
if len(statement_ids) == 0:
raise Warning(_('You have already imported that file.'))
# Create the statement
stmt_vals['line_ids'] = [
[0, False, line] for line in filtered_st_lines]
statement_id = bs_model.create(stmt_vals).id
# Prepare import feedback # Prepare import feedback
notifications = [] notifications = []
num_ignored = len(ignored_statement_lines_import_ids)
num_ignored = len(ignored_line_ids)
if num_ignored > 0: if num_ignored > 0:
notifications += [{ notifications += [{
'type': 'warning', 'type': 'warning',
'message': _("%d transactions had already been imported and "
'message':
_("%d transactions had already been imported and "
"were ignored.") % num_ignored "were ignored.") % num_ignored
if num_ignored > 1 if num_ignored > 1
else _("1 transaction had already been imported and " else _("1 transaction had already been imported and "
@ -319,9 +368,6 @@ class AccountBankStatementImport(models.TransientModel):
'name': _('Already imported items'), 'name': _('Already imported items'),
'model': 'account.bank.statement.line', 'model': 'account.bank.statement.line',
'ids': bsl_model.search( 'ids': bsl_model.search(
[('unique_import_id', 'in',
ignored_statement_lines_import_ids)]).ids
}
[('unique_import_id', 'in', ignored_line_ids)]).ids}
}] }]
return statement_ids, notifications
return statement_id, notifications

56
account_bank_statement_import/i18n/account_bank_statement_import.pot

@ -6,8 +6,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Odoo Server 8.0\n" "Project-Id-Version: Odoo Server 8.0\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-06-08 12:02+0000\n"
"PO-Revision-Date: 2015-06-08 12:02+0000\n"
"POT-Creation-Date: 2015-06-11 12:15+0000\n"
"PO-Revision-Date: 2015-06-11 12:15+0000\n"
"Last-Translator: <>\n" "Last-Translator: <>\n"
"Language-Team: \n" "Language-Team: \n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@ -16,13 +16,13 @@ msgstr ""
"Plural-Forms: \n" "Plural-Forms: \n"
#. module: account_bank_statement_import #. module: account_bank_statement_import
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:313
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:347
#, python-format #, python-format
msgid "%d transactions had already been imported and were ignored." msgid "%d transactions had already been imported and were ignored."
msgstr "" msgstr ""
#. module: account_bank_statement_import #. module: account_bank_statement_import
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:316
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:350
#, python-format #, python-format
msgid "1 transaction had already been imported and was ignored." msgid "1 transaction had already been imported and was ignored."
msgstr "" msgstr ""
@ -53,7 +53,7 @@ msgid "Account Number must be unique"
msgstr "" msgstr ""
#. module: account_bank_statement_import #. module: account_bank_statement_import
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:319
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:353
#, python-format #, python-format
msgid "Already imported items" msgid "Already imported items"
msgstr "" msgstr ""
@ -74,18 +74,18 @@ msgid "Bank Statement Line"
msgstr "" msgstr ""
#. module: account_bank_statement_import #. module: account_bank_statement_import
#: view:account.bank.statement.import:account_bank_statement_import.account_bank_statement_import_view
msgid "Cancel"
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:116
#, python-format
msgid "Can not determine journal for import."
msgstr "" msgstr ""
#. module: account_bank_statement_import #. module: account_bank_statement_import
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:200
#, python-format
msgid "Cannot find in which journal import this statement. Please manually select a journal."
#: view:account.bank.statement.import:account_bank_statement_import.account_bank_statement_import_view
msgid "Cancel"
msgstr "" msgstr ""
#. module: account_bank_statement_import #. module: account_bank_statement_import
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:129
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:154
#, python-format #, python-format
msgid "Could not make sense of the given file.\n" msgid "Could not make sense of the given file.\n"
"Did you install the module to support this type of file ?" "Did you install the module to support this type of file ?"
@ -153,31 +153,55 @@ msgid "Sanitized Account Number"
msgstr "" msgstr ""
#. module: account_bank_statement_import #. module: account_bank_statement_import
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:181
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:237
#, python-format
msgid "Statement currency id is %d, but company currency id = %d."
msgstr ""
#. module: account_bank_statement_import
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:224
#, python-format
msgid "Statement currency id is %d, but journal currency id = %d."
msgstr ""
#. module: account_bank_statement_import
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:179
#, python-format
msgid "Statement has invalid currency code %s"
msgstr ""
#. module: account_bank_statement_import
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:207
#, python-format #, python-format
msgid "The account of this statement is linked to another journal." msgid "The account of this statement is linked to another journal."
msgstr "" msgstr ""
#. module: account_bank_statement_import #. module: account_bank_statement_import
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:195
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:242
#, python-format
msgid "The currency of the bank statement is not the same as the company currency !"
msgstr ""
#. module: account_bank_statement_import
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:229
#, python-format #, python-format
msgid "The currency of the bank statement is not the same as the currency of the journal !" msgid "The currency of the bank statement is not the same as the currency of the journal !"
msgstr "" msgstr ""
#. module: account_bank_statement_import #. module: account_bank_statement_import
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:136
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:163
#, python-format #, python-format
msgid "This file doesn't contain any statement." msgid "This file doesn't contain any statement."
msgstr "" msgstr ""
#. module: account_bank_statement_import #. module: account_bank_statement_import
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:144
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:168
#, python-format #, python-format
msgid "This file doesn't contain any transaction." msgid "This file doesn't contain any transaction."
msgstr "" msgstr ""
#. module: account_bank_statement_import #. module: account_bank_statement_import
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:305
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:88
#, python-format #, python-format
msgid "You have already imported that file." msgid "You have already imported that file."
msgstr "" msgstr ""

236
account_bank_statement_import/i18n/nl.po

@ -0,0 +1,236 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * account_bank_statement_import
# Therp BV <therp.nl>, 2015.
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 8.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-06-11 12:15+0000\n"
"PO-Revision-Date: 2015-06-11 14:37+0200\n"
"Last-Translator: Therp BV <therp.nl>\n"
"Language: nl\n"
"Language-Team: nl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
"X-Generator: Gtranslator 2.91.6\n"
#. module: account_bank_statement_import
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:347
#, python-format
msgid "%d transactions had already been imported and were ignored."
msgstr "%d Transacties zijn overgeslagen omdat ze al reeds waren geïmporteerd."
#. module: account_bank_statement_import
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:350
#, python-format
msgid "1 transaction had already been imported and was ignored."
msgstr "1. Transactie was al geïmporteerd en is overgeslagen."
#. module: account_bank_statement_import
#: view:account.bank.statement.import:account_bank_statement_import.account_bank_statement_import_view
msgid "1. Download your bank statements from your bank website."
msgstr "1. Download het bankafschrift bestand van de website van uw bank."
#. module: account_bank_statement_import
#: view:account.bank.statement.import:account_bank_statement_import.account_bank_statement_import_view
msgid ""
"2. Make sure you have installed the right module to support the file format."
msgstr ""
"2. Zorg ervoor dat de modules die het formaat van uw bestand ondersteunen "
"zijn geïnstalleerd."
#. module: account_bank_statement_import
#: view:account.bank.statement.import:account_bank_statement_import.account_bank_statement_import_view
msgid "3. Select the file and click 'Import'."
msgstr "3. Selecteer het bestand en klik op \"Importeren\"."
#. module: account_bank_statement_import
#: sql_constraint:account.bank.statement.line:0
msgid "A bank account transactions can be imported only once !"
msgstr "De transacties kunnen slechts eenmalig worden geïmporteerd."
#. module: account_bank_statement_import
#: sql_constraint:res.partner.bank:0
msgid "Account Number must be unique"
msgstr "Rekeningnummer moet uniek zijn"
#. module: account_bank_statement_import
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:353
#, python-format
msgid "Already imported items"
msgstr "Al eerder geïmporteerde regels"
#. module: account_bank_statement_import
#: model:ir.model,name:account_bank_statement_import.model_res_partner_bank
msgid "Bank Accounts"
msgstr "Bankrekeningen"
#. module: account_bank_statement_import
#: field:account.bank.statement.import,data_file:0
msgid "Bank Statement File"
msgstr "Bankafschriften bestand"
#. module: account_bank_statement_import
#: model:ir.model,name:account_bank_statement_import.model_account_bank_statement_line
msgid "Bank Statement Line"
msgstr "Bankafschrift regel"
#. module: account_bank_statement_import
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:116
#, python-format
msgid "Can not determine journal for import."
msgstr "Kan niet bepalen welk dagboek gebruikt moet worden."
#. module: account_bank_statement_import
#: view:account.bank.statement.import:account_bank_statement_import.account_bank_statement_import_view
msgid "Cancel"
msgstr "Annuleren"
#. module: account_bank_statement_import
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:154
#, python-format
msgid ""
"Could not make sense of the given file.\n"
"Did you install the module to support this type of file ?"
msgstr ""
"Kon het bestand niet interpreteren.\n"
"Heeft u de juiste modules voor dit type bestand geïnstalleerd?"
#. module: account_bank_statement_import
#: field:account.bank.statement.import,create_uid:0
msgid "Created by"
msgstr "Aangemaakt door"
#. module: account_bank_statement_import
#: field:account.bank.statement.import,create_date:0
msgid "Created on"
msgstr "Aangemaakt op"
#. module: account_bank_statement_import
#: help:account.bank.statement.import,data_file:0
msgid ""
"Get you bank statements in electronic format from your bank and select them "
"here."
msgstr ""
"Verkrijg de bankafschriften van uw bank in elektronische vorm en selecteer "
"ze hier."
#. module: account_bank_statement_import
#: view:account.bank.statement.import:account_bank_statement_import.account_bank_statement_import_view
msgid "How to import your bank statement :"
msgstr "Hoe uw bankafschrift te importeren:"
#. module: account_bank_statement_import
#: field:account.bank.statement.import,id:0
msgid "ID"
msgstr "ID"
#. module: account_bank_statement_import
#: model:ir.actions.act_window,name:account_bank_statement_import.action_account_bank_statement_import
#: model:ir.ui.menu,name:account_bank_statement_import.menu_account_bank_statement_import
msgid "Import"
msgstr "Import"
#. module: account_bank_statement_import
#: model:ir.model,name:account_bank_statement_import.model_account_bank_statement_import
msgid "Import Bank Statement"
msgstr "Geïmporteerd bankafschrift"
#. module: account_bank_statement_import
#: view:account.bank.statement.import:account_bank_statement_import.account_bank_statement_import_view
msgid "Import Bank Statements"
msgstr "Importeer bankafschriften"
#. module: account_bank_statement_import
#: field:account.bank.statement.line,unique_import_id:0
msgid "Import ID"
msgstr "Import ID"
#. module: account_bank_statement_import
#: field:account.bank.statement.import,write_uid:0
msgid "Last Updated by"
msgstr "Laatst bijgewerkt door"
#. module: account_bank_statement_import
#: field:account.bank.statement.import,write_date:0
msgid "Last Updated on"
msgstr "Laatst bijgewerkt op"
#. module: account_bank_statement_import
#: field:res.partner.bank,sanitized_acc_number:0
msgid "Sanitized Account Number"
msgstr "Gestandaardiseerd rekeningnummer"
#. module: account_bank_statement_import
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:237
#, python-format
msgid "Statement currency id is %d, but company currency id = %d."
msgstr "Valuta id van afschrift = %d, maar valuta id van bedrijf = %d."
#. module: account_bank_statement_import
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:224
#, python-format
msgid "Statement currency id is %d, but journal currency id = %d."
msgstr "Valuta id van afschrift = %d, maar valuta id van dagboek = %d."
#. module: account_bank_statement_import
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:179
#, python-format
msgid "Statement has invalid currency code %s"
msgstr "Bankafschrift heeft ongeldige valutacode %s"
#. module: account_bank_statement_import
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:207
#, python-format
msgid "The account of this statement is linked to another journal."
msgstr ""
"Het rekeningnummer van dit afschrift is gekoppeld aan een ander dagboek."
#. module: account_bank_statement_import
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:242
#, python-format
msgid ""
"The currency of the bank statement is not the same as the company currency !"
msgstr ""
"De valuta van het afschrift is niet gelijk aan de valuta van het bedrijf!"
#. module: account_bank_statement_import
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:229
#, python-format
msgid ""
"The currency of the bank statement is not the same as the currency of the "
"journal !"
msgstr ""
"De valuta van het afschrift is niet hetzelfde als de valuta van het dagboek!"
#. module: account_bank_statement_import
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:163
#, python-format
msgid "This file doesn't contain any statement."
msgstr "Dit bestand bevat geen enkel afschrift."
#. module: account_bank_statement_import
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:168
#, python-format
msgid "This file doesn't contain any transaction."
msgstr "Dit bestand bevat geen enkele transactie."
#. module: account_bank_statement_import
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:88
#, python-format
msgid "You have already imported that file."
msgstr "U heeft dit bestand al geïmporteerd."
#. module: account_bank_statement_import
#: view:account.bank.statement.import:account_bank_statement_import.account_bank_statement_import_view
msgid "_Import"
msgstr ""
#. module: account_bank_statement_import
#: view:account.bank.statement.import:account_bank_statement_import.account_bank_statement_import_view
msgid "or"
msgstr ""

236
account_bank_statement_import/parserlib.py

@ -0,0 +1,236 @@
# -*- encoding: utf-8 -*-
"""Classes and definitions used in parsing bank statements."""
##############################################################################
#
# Copyright (C) 2015 Therp BV <http://therp.nl>.
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
class BankTransaction(dict):
"""Single transaction that is part of a bank statement."""
@property
def value_date(self):
"""property getter"""
return self['date']
@value_date.setter
def value_date(self, value_date):
"""property setter"""
self['date'] = value_date
@property
def name(self):
"""property getter"""
return self['name']
@name.setter
def name(self, name):
"""property setter"""
self['name'] = name
@property
def transferred_amount(self):
"""property getter"""
return self['amount']
@transferred_amount.setter
def transferred_amount(self, transferred_amount):
"""property setter"""
self['amount'] = transferred_amount
@property
def eref(self):
"""property getter"""
return self['ref']
@eref.setter
def eref(self, eref):
"""property setter"""
self['ref'] = eref
if not self.message:
self.name = eref
@property
def message(self):
"""property getter"""
return self._message
@message.setter
def message(self, message):
"""property setter"""
self._message = message
self.name = message
@property
def remote_owner(self):
"""property getter"""
return self['partner_name']
@remote_owner.setter
def remote_owner(self, remote_owner):
"""property setter"""
self['partner_name'] = remote_owner
if not (self.message or self.eref):
self.name = remote_owner
@property
def remote_account(self):
"""property getter"""
return self['account_number']
@remote_account.setter
def remote_account(self, remote_account):
"""property setter"""
self['account_number'] = remote_account
@property
def note(self):
return self['note']
@note.setter
def note(self, note):
self['note'] = note
def __init__(self):
"""Define and initialize attributes.
Not all attributes are already used in the actual import.
"""
super(BankTransaction, self).__init__()
self.transfer_type = False # Action type that initiated this message
self.execution_date = False # The posted date of the action
self.value_date = False # The value date of the action
self.remote_account = False # The account of the other party
self.remote_currency = False # The currency used by the other party
self.exchange_rate = 0.0
# The exchange rate used for conversion of local_currency and
# remote_currency
self.transferred_amount = 0.0 # actual amount transferred
self.name = ''
self._message = False # message from the remote party
self.eref = False # end to end reference for transactions
self.remote_owner = False # name of the other party
self.remote_owner_address = [] # other parties address lines
self.remote_owner_city = False # other parties city name
self.remote_owner_postalcode = False # other parties zip code
self.remote_owner_country_code = False # other parties country code
self.remote_bank_bic = False # bic of other party's bank
self.provision_costs = False # costs charged by bank for transaction
self.provision_costs_currency = False
self.provision_costs_description = False
self.error_message = False # error message for interaction with user
self.storno_retry = False
# If True, make cancelled debit eligible for a next direct debit run
self.data = '' # Raw data from which the transaction has been parsed
class BankStatement(dict):
"""A bank statement groups data about several bank transactions."""
@property
def statement_id(self):
"""property getter"""
return self['name']
def _set_transaction_ids(self):
"""Set transaction ids to statement_id with sequence-number."""
subno = 0
for transaction in self['transactions']:
subno += 1
transaction['unique_import_id'] = (
self.statement_id + str(subno).zfill(4))
@statement_id.setter
def statement_id(self, statement_id):
"""property setter"""
self['name'] = statement_id
self._set_transaction_ids()
@property
def local_account(self):
"""property getter"""
return self['account_number']
@local_account.setter
def local_account(self, local_account):
"""property setter"""
self['account_number'] = local_account
@property
def local_currency(self):
"""property getter"""
return self['currency_code']
@local_currency.setter
def local_currency(self, local_currency):
"""property setter"""
self['currency_code'] = local_currency
@property
def start_balance(self):
"""property getter"""
return self['balance_start']
@start_balance.setter
def start_balance(self, start_balance):
"""property setter"""
self['balance_start'] = start_balance
@property
def end_balance(self):
"""property getter"""
return self['balance_end']
@end_balance.setter
def end_balance(self, end_balance):
"""property setter"""
self['balance_end'] = end_balance
self['balance_end_real'] = end_balance
@property
def date(self):
"""property getter"""
return self['date']
@date.setter
def date(self, date):
"""property setter"""
self['date'] = date
def create_transaction(self):
"""Create and append transaction.
This should only be called after statement_id has been set, because
statement_id will become part of the unique transaction_id.
"""
transaction = BankTransaction()
self['transactions'].append(transaction)
# Fill default id, but might be overruled
transaction['unique_import_id'] = (
self.statement_id + str(len(self['transactions'])).zfill(4))
return transaction
def __init__(self):
super(BankStatement, self).__init__()
self['transactions'] = []
self.statement_id = ''
self.local_account = ''
self.local_currency = ''
self.date = ''
self.start_balance = 0.0
self.end_balance = 0.0

2
account_bank_statement_import/tests/__init__.py

@ -1,3 +1,5 @@
# -*- encoding: utf-8 -*- # -*- encoding: utf-8 -*-
"""Define tests to be run."""
from . import test_res_partner_bank from . import test_res_partner_bank
from . import test_import_bank_statement from . import test_import_bank_statement
from .test_import_file import TestStatementFile

9
account_bank_statement_import/tests/test_import_bank_statement.py

@ -25,13 +25,13 @@
from openerp.tests.common import TransactionCase from openerp.tests.common import TransactionCase
class TestAccountBankStatemetImport(TransactionCase):
class TestAccountBankStatementImport(TransactionCase):
"""Tests for import bank statement file import """Tests for import bank statement file import
(account.bank.statement.import) (account.bank.statement.import)
""" """
def setUp(self): def setUp(self):
super(TestAccountBankStatemetImport, self).setUp()
super(TestAccountBankStatementImport, self).setUp()
self.statement_import_model = self.env[ self.statement_import_model = self.env[
'account.bank.statement.import'] 'account.bank.statement.import']
self.account_journal_model = self.env['account.journal'] self.account_journal_model = self.env['account.journal']
@ -65,10 +65,7 @@ class TestAccountBankStatemetImport(TransactionCase):
""" """
journal = self.account_journal_model.browse(self.journal_id) journal = self.account_journal_model.browse(self.journal_id)
expected_id = journal.company_id.partner_id.id expected_id = journal.company_id.partner_id.id
st_import = self.statement_import_model.sudo(self.other_user_id_a.id) st_import = self.statement_import_model.sudo(self.other_user_id_a.id)
bank = st_import._create_bank_account( bank = st_import._create_bank_account(
'001251882303', company_id=self.company_id) '001251882303', company_id=self.company_id)
self.assertEqual(bank.partner_id.id,
expected_id)
self.assertEqual(bank.partner_id.id, expected_id)

131
account_bank_statement_import/tests/test_import_file.py

@ -0,0 +1,131 @@
# -*- coding: utf-8 -*-
"""Provide common base for bank statement import tests."""
##############################################################################
#
# Copyright (C) 2015 Therp BV <http://therp.nl>.
#
# All other contributions are (C) by their respective contributors
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
import logging
from openerp.tests.common import TransactionCase
from openerp.modules.module import get_module_resource
_logger = logging.getLogger(__name__)
class TestStatementFile(TransactionCase):
"""Check wether statements with transactions correctly imported.
No actual tests are done in this class, implementations are in
subclasses in actual import modules.
"""
def _test_transaction(
self, statement_obj, remote_account=False,
transferred_amount=False, value_date=False, ref=False):
"""Check wether transaction with attributes passed was created.
Actually this method also tests wether automatic creation of
partner bank accounts is working.
"""
transaction_model = self.env['account.bank.statement.line']
partner_bank_model = self.env['res.partner.bank']
domain = [('statement_id', '=', statement_obj.id)]
if remote_account:
bids = partner_bank_model.search(
[('acc_number', '=', remote_account)])
self.assertTrue(
bids,
'Bank-account %s not found after parse.' % remote_account
)
domain.append(('bank_account_id', '=', bids[0].id))
if transferred_amount:
domain.append(('amount', '=', transferred_amount))
if value_date:
domain.append(('date', '=', value_date))
if ref:
domain.append(('ref', '=', ref))
ids = transaction_model.search(domain)
if not ids:
# We will get assertion error, but to solve we need to see
# what transactions have been added:
self.cr.execute(
"select name, date, amount, ref, bank_account_id"
" from account_bank_statement_line"
" where statement_id=%d" % statement_obj.id)
_logger.error(
"Transaction not found in %s" %
str(self.cr.fetchall())
)
self.assertTrue(
ids,
'Transaction %s not found after parse.' % str(domain)
)
def _test_statement_import(
self, module_name, file_name, statement_name, local_account=False,
start_balance=False, end_balance=False, transactions=None):
"""Test correct creation of single statement."""
import_model = self.env['account.bank.statement.import']
partner_bank_model = self.env['res.partner.bank']
statement_model = self.env['account.bank.statement']
statement_path = get_module_resource(
module_name,
'test_files',
file_name
)
statement_file = open(
statement_path, 'rb').read().encode('base64')
bank_statement_id = import_model.create(
dict(
data_file=statement_file,
)
)
bank_statement_id.import_file()
# Check wether bank account has been created:
if local_account:
bids = partner_bank_model.search(
[('acc_number', '=', local_account)])
self.assertTrue(
bids,
'Bank account %s not created from statement' % local_account
)
# statement name is account number + '-' + date of last 62F line:
ids = statement_model.search([('name', '=', statement_name)])
self.assertTrue(
ids,
'Statement %s not found after parse.' % statement_name
)
statement_obj = ids[0]
if start_balance:
self.assertTrue(
abs(statement_obj.balance_start - start_balance) < 0.00001,
'Start balance %f not equal to expected %f' %
(statement_obj.balance_start, start_balance)
)
if end_balance:
self.assertTrue(
abs(statement_obj.balance_end_real - end_balance) < 0.00001,
'End balance %f not equal to expected %f' %
(statement_obj.balance_end_real, end_balance)
)
# Maybe we need to test transactions?
if transactions:
for transaction in transactions:
self._test_transaction(statement_obj, **transaction)

49
account_bank_statement_import_camt/README.rst

@ -0,0 +1,49 @@
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
:alt: License: AGPL-3
Bank Statement Parse Camt
=========================
Module to import SEPA CAMT.053 Format bank statement files.
Based on the Banking addons framework.
Known issues / Roadmap
======================
* None
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/bank-statement-import/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed feedback
`here <https://github.com/OCA/bank-statement-import/issues/new?body=module:%20account_bank_statement_import%0Aversion:%208.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Credits
=======
Contributors
------------
* Stefan Rijnhart <srijnhart@therp.nl>
* Ronald Portier <rportier@therp.nl>
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 http://odoo-community.org.
This module should make it easy to migrate bank statement import
modules written for earlies versions of Odoo/OpenERP.

1
account_bank_statement_import_camt/__init__.py

@ -0,0 +1 @@
from . import account_bank_statement_import

34
account_bank_statement_import_camt/__openerp__.py

@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (C) 2013-2015 Therp BV <http://therp.nl>
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
{
'name': 'CAMT Format Bank Statements Import',
'version': '0.3',
'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',
],
'demo': [
'demo/demo_data.xml',
],
'installable': True,
}

44
account_bank_statement_import_camt/account_bank_statement_import.py

@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
"""Add process_camt method to account.bank.statement.import."""
##############################################################################
#
# Copyright (C) 2013-2015 Therp BV <http://therp.nl>
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
import logging
from openerp import models
from .camt import CamtParser as Parser
_logger = logging.getLogger(__name__)
class AccountBankStatementImport(models.TransientModel):
"""Add process_camt method to account.bank.statement.import."""
_inherit = 'account.bank.statement.import'
def _parse_file(self, cr, uid, data_file, context=None):
"""Parse a CAMT053 XML file."""
parser = Parser()
try:
_logger.debug("Try parsing with camt.")
return parser.parse(data_file)
except ValueError:
# Not a camt file, returning super will call next candidate:
_logger.debug("Statement file was not a camt file.",
exc_info=True)
return super(AccountBankStatementImport, self)._parse_file(
cr, uid, data_file, context=context)

239
account_bank_statement_import_camt/camt.py

@ -0,0 +1,239 @@
# -*- coding: utf-8 -*-
"""Class to parse camt files."""
##############################################################################
#
# Copyright (C) 2013-2015 Therp BV <http://therp.nl>
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
import re
from datetime import datetime
from lxml import etree
from openerp.addons.account_bank_statement_import.parserlib import (
BankStatement)
class CamtParser(object):
"""Parser for camt bank statement import files."""
def parse_amount(self, ns, node):
"""Parse element that contains Amount and CreditDebitIndicator."""
if node is None:
return 0.0
sign = 1
amount = 0.0
sign_node = node.xpath('ns:CdtDbtInd', namespaces={'ns': ns})
if sign_node and sign_node[0].text == 'DBIT':
sign = -1
amount_node = node.xpath('ns:Amt', namespaces={'ns': ns})
if amount_node:
amount = sign * float(amount_node[0].text)
return amount
def add_value_from_node(
self, ns, node, xpath_str, obj, attr_name, join_str=None):
"""Add value to object from first or all nodes found with xpath.
If xpath_str is a list (or iterable), it will be seen as a series
of search path's in order of preference. The first item that results
in a found node will be used to set a value."""
if not isinstance(xpath_str, (list, tuple)):
xpath_str = [xpath_str]
for search_str in xpath_str:
found_node = node.xpath(search_str, namespaces={'ns': ns})
if found_node:
if join_str is None:
attr_value = found_node[0].text
else:
attr_value = join_str.join([x.text for x in found_node])
setattr(obj, attr_name, attr_value)
break
def parse_transaction_details(self, ns, node, transaction):
"""Parse transaction details (message, party, account...)."""
# message
self.add_value_from_node(
ns, node, [
'./ns:RmtInf/ns:Ustrd',
'./ns:AddtlTxInf',
'./ns:AddtlNtryInf',
], transaction, 'message')
# eref
self.add_value_from_node(
ns, node, [
'./ns:RmtInf/ns:Strd/ns:CdtrRefInf/ns:Ref',
'./ns:Refs/ns:EndToEndId',
],
transaction, 'eref'
)
# remote party values
party_type = 'Dbtr'
party_type_node = node.xpath(
'../../ns:CdtDbtInd', namespaces={'ns': ns})
if party_type_node and party_type_node[0].text != 'CRDT':
party_type = 'Cdtr'
party_node = node.xpath(
'./ns:RltdPties/ns:%s' % party_type, namespaces={'ns': ns})
if party_node:
self.add_value_from_node(
ns, party_node[0], './ns:Nm', transaction, 'remote_owner')
self.add_value_from_node(
ns, party_node[0], './ns:PstlAdr/ns:Ctry', transaction,
'remote_owner_country'
)
address_node = party_node[0].xpath(
'./ns:PstlAdr/ns:AdrLine', namespaces={'ns': ns})
if address_node:
transaction.remote_owner_address = [address_node[0].text]
# Get remote_account from iban or from domestic account:
account_node = node.xpath(
'./ns:RltdPties/ns:%sAcct/ns:Id' % party_type,
namespaces={'ns': ns}
)
if account_node:
iban_node = account_node[0].xpath(
'./ns:IBAN', namespaces={'ns': ns})
if iban_node:
transaction.remote_account = iban_node[0].text
bic_node = node.xpath(
'./ns:RltdAgts/ns:%sAgt/ns:FinInstnId/ns:BIC' % party_type,
namespaces={'ns': ns}
)
if bic_node:
transaction.remote_bank_bic = bic_node[0].text
else:
self.add_value_from_node(
ns, account_node[0], './ns:Othr/ns:Id', transaction,
'remote_account'
)
def parse_transaction(self, ns, node, transaction):
"""Parse transaction (entry) node."""
self.add_value_from_node(
ns, node, './ns:BkTxCd/ns:Prtry/ns:Cd', transaction,
'transfer_type'
)
self.add_value_from_node(
ns, node, './ns:BookgDt/ns:Dt', transaction, 'execution_date')
self.add_value_from_node(
ns, node, './ns:ValDt/ns:Dt', transaction, 'value_date')
transaction.transferred_amount = self.parse_amount(ns, node)
details_node = node.xpath(
'./ns:NtryDtls/ns:TxDtls', namespaces={'ns': ns})
if details_node:
self.parse_transaction_details(ns, details_node[0], transaction)
transaction.data = etree.tostring(node)
return transaction
def get_balance_amounts(self, ns, node):
"""Return opening and closing balance.
Depending on kind of balance and statement, the balance might be in a
different kind of node:
OPBD = OpeningBalance
PRCD = PreviousClosingBalance
ITBD = InterimBalance (first ITBD is start-, second is end-balance)
CLBD = ClosingBalance
"""
start_balance_node = None
end_balance_node = None
for node_name in ['OPBD', 'PRCD', 'CLBD', 'ITBD']:
code_expr = (
'./ns:Bal/ns:Tp/ns:CdOrPrtry/ns:Cd[text()="%s"]/../../..' %
node_name
)
balance_node = node.xpath(code_expr, namespaces={'ns': ns})
if balance_node:
if node_name in ['OPBD', 'PRCD']:
start_balance_node = balance_node[0]
elif node_name == 'CLBD':
end_balance_node = balance_node[0]
else:
if not start_balance_node:
start_balance_node = balance_node[0]
if not end_balance_node:
end_balance_node = balance_node[-1]
return (
self.parse_amount(ns, start_balance_node),
self.parse_amount(ns, end_balance_node)
)
def parse_statement(self, ns, node):
"""Parse a single Stmt node."""
statement = BankStatement()
self.add_value_from_node(
ns, node, [
'./ns:Acct/ns:Id/ns:IBAN',
'./ns:Acct/ns:Id/ns:Othr/ns:Id',
], statement, 'local_account'
)
self.add_value_from_node(
ns, node, './ns:Id', statement, 'statement_id')
self.add_value_from_node(
ns, node, './ns:Acct/ns:Ccy', statement, 'local_currency')
(statement.start_balance, statement.end_balance) = (
self.get_balance_amounts(ns, node))
transaction_nodes = node.xpath('./ns:Ntry', namespaces={'ns': ns})
for entry_node in transaction_nodes:
transaction = statement.create_transaction()
self.parse_transaction(ns, entry_node, transaction)
if statement['transactions']:
statement.date = datetime.strptime(
statement['transactions'][0].execution_date, "%Y-%m-%d")
return statement
def check_version(self, ns, root):
"""Validate validity of camt file."""
# Check wether it is camt at all:
re_camt = re.compile(
r'(^urn:iso:std:iso:20022:tech:xsd:camt.'
r'|^ISO:camt.)'
)
if not re_camt.search(ns):
raise ValueError('no camt: ' + ns)
# Check wether version 052 or 053:
re_camt_version = re.compile(
r'(^urn:iso:std:iso:20022:tech:xsd:camt.053.'
r'|^urn:iso:std:iso:20022:tech:xsd:camt.052.'
r'|^ISO:camt.053.'
r'|^ISO:camt.052.)'
)
if not re_camt_version.search(ns):
raise ValueError('no camt 052 or 053: ' + ns)
# Check GrpHdr element:
root_0_0 = root[0][0].tag[len(ns) + 2:] # strip namespace
if root_0_0 != 'GrpHdr':
raise ValueError('expected GrpHdr, got: ' + root_0_0)
def parse(self, data):
"""Parse a camt.052 or camt.053 file."""
try:
root = etree.fromstring(
data, parser=etree.XMLParser(recover=True))
except etree.XMLSyntaxError:
# ABNAmro is known to mix up encodings
root = etree.fromstring(
data.decode('iso-8859-15').encode('utf-8'))
if root is None:
raise ValueError(
'Not a valid xml file, or not an xml file at all.')
ns = root.tag[1:root.tag.index("}")]
self.check_version(ns, root)
statements = []
for node in root[0][1:]:
statement = self.parse_statement(ns, node)
if len(statement['transactions']):
statements.append(statement)
return statements

26
account_bank_statement_import_camt/demo/demo_data.xml

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="camt_bank_journal" model="account.journal">
<field name="name">Bank Journal - (test camt)</field>
<field name="code">TBNKCAMT</field>
<field name="type">bank</field>
<field name="sequence_id" ref="account.sequence_bank_journal"/>
<field name="default_debit_account_id" ref="account.bnk"/>
<field name="default_credit_account_id" ref="account.bnk"/>
<field name="user_id" ref="base.user_root"/>
</record>
<record id="camt_company_bank" model="res.partner.bank">
<field name="owner_name">Your Company</field>
<field name="acc_number">NL77ABNA0574908765</field>
<field name="partner_id" ref="base.partner_root"></field>
<field name="company_id" ref="base.main_company"></field>
<field name="journal_id" ref="camt_bank_journal"></field>
<field name="state">bank</field>
<field name="bank" ref="base.res_bank_1"/>
</record>
</data>
</openerp>

241
account_bank_statement_import_camt/test_files/test-camt053.xml

@ -0,0 +1,241 @@
<Document xmlns="urn:iso:std:iso:20022:tech:xsd:camt.053.001.02">
<BkToCstmrStmt>
<GrpHdr>
<MsgId>TESTBANK/NL/1420561226673</MsgId>
<CreDtTm>2013-01-06T16:20:26.673Z</CreDtTm>
</GrpHdr>
<Stmt>
<Id>1234Test/1</Id>
<LglSeqNb>2</LglSeqNb>
<CreDtTm>2013-01-06T16:20:26.673Z</CreDtTm>
<FrToDt>
<FrDtTm>2013-01-05T00:00:00.000Z</FrDtTm>
<ToDtTm>2013-01-05T23:59:59.999Z</ToDtTm>
</FrToDt>
<Acct>
<Id>
<IBAN>NL77ABNA0574908765</IBAN>
</Id>
<Nm>Example company</Nm>
<Svcr>
<FinInstnId>
<BIC>ABNANL2A</BIC>
</FinInstnId>
</Svcr>
</Acct>
<Bal>
<Tp>
<CdOrPrtry>
<Cd>OPBD</Cd>
</CdOrPrtry>
</Tp>
<Amt Ccy="EUR">15568.27</Amt>
<CdtDbtInd>CRDT</CdtDbtInd>
<Dt>
<Dt>2013-01-05</Dt>
</Dt>
</Bal>
<Bal>
<Tp>
<CdOrPrtry>
<Cd>CLBD</Cd>
</CdOrPrtry>
</Tp>
<Amt Ccy="EUR">15121.12</Amt>
<CdtDbtInd>CRDT</CdtDbtInd>
<Dt>
<Dt>2013-01-05</Dt>
</Dt>
</Bal>
<Ntry>
<Amt Ccy="EUR">754.25</Amt>
<CdtDbtInd>DBIT</CdtDbtInd>
<Sts>BOOK</Sts>
<BookgDt>
<Dt>2013-01-05</Dt>
</BookgDt>
<ValDt>
<Dt>2013-01-05</Dt>
</ValDt>
<BkTxCd>
<Domn>
<Cd>PMNT</Cd>
<Fmly>
<Cd>RDDT</Cd>
<SubFmlyCd>ESDD</SubFmlyCd>
</Fmly>
</Domn>
<Prtry>
<Cd>EI</Cd>
</Prtry>
</BkTxCd>
<NtryDtls>
<TxDtls>
<Refs>
<InstrId>INNDNL2U20141231000142300002844</InstrId>
<EndToEndId>435005714488-ABNO33052620</EndToEndId>
<MndtId>1880000341866</MndtId>
</Refs>
<AmtDtls>
<TxAmt>
<Amt Ccy="EUR">754.25</Amt>
</TxAmt>
</AmtDtls>
<RltdPties>
<Cdtr>
<Nm>INSURANCE COMPANY TESTX</Nm>
<PstlAdr>
<StrtNm>TEST STREET 20</StrtNm>
<TwnNm>1234 AB TESTCITY</TwnNm>
<Ctry>NL</Ctry>
</PstlAdr>
</Cdtr>
<CdtrAcct>
<Id>
<IBAN>NL46ABNA0499998748</IBAN>
</Id>
</CdtrAcct>
</RltdPties>
<RltdAgts>
<CdtrAgt>
<FinInstnId>
<BIC>ABNANL2A</BIC>
</FinInstnId>
</CdtrAgt>
</RltdAgts>
<RmtInf>
<Ustrd>Insurance policy 857239PERIOD 01.01.2013 - 31.12.2013</Ustrd>
</RmtInf>
<AddtlTxInf>MKB Insurance 859239PERIOD 01.01.2013 - 31.12.2013</AddtlTxInf>
</TxDtls>
</NtryDtls>
</Ntry>
<Ntry>
<Amt Ccy="EUR">594.05</Amt>
<CdtDbtInd>DBIT</CdtDbtInd>
<RvslInd>true</RvslInd>
<Sts>BOOK</Sts>
<BookgDt>
<Dt>2013-01-05</Dt>
</BookgDt>
<ValDt>
<Dt>2013-01-05</Dt>
</ValDt>
<BkTxCd>
<Domn>
<Cd>PMNT</Cd>
<Fmly>
<Cd>IDDT</Cd>
<SubFmlyCd>UPDD</SubFmlyCd>
</Fmly>
</Domn>
<Prtry>
<Cd>EIST</Cd>
</Prtry>
</BkTxCd>
<NtryDtls>
<TxDtls>
<Refs>
<InstrId>TESTBANK/NL/20141229/01206408</InstrId>
<EndToEndId>TESTBANK/NL/20141229/01206408</EndToEndId>
<MndtId>NL22ZZZ524885430000-C0125.1</MndtId>
</Refs>
<AmtDtls>
<TxAmt>
<Amt Ccy="EUR">564.05</Amt>
</TxAmt>
</AmtDtls>
<RltdPties>
<Cdtr>
<Nm>Test Customer</Nm>
<PstlAdr>
<Ctry>NL</Ctry>
</PstlAdr>
</Cdtr>
<CdtrAcct>
<Id>
<IBAN>NL46ABNA0499998748</IBAN>
</Id>
</CdtrAcct>
</RltdPties>
<RltdAgts>
<CdtrAgt>
<FinInstnId>
<BIC>ABNANL2A</BIC>
</FinInstnId>
</CdtrAgt>
</RltdAgts>
<RmtInf>
<Ustrd>Direct Debit S14 0410</Ustrd>
</RmtInf>
<RtrInf>
<Rsn>
<Cd>AC06</Cd>
</Rsn>
</RtrInf>
<AddtlTxInf>Direct debit S14 0410 AC07 Rek.nummer blokkade TESTBANK/NL/20141229/01206408</AddtlTxInf>
</TxDtls>
</NtryDtls>
</Ntry>
<Ntry>
<Amt Ccy="EUR">1405.31</Amt>
<CdtDbtInd>CRDT</CdtDbtInd>
<Sts>BOOK</Sts>
<BookgDt>
<Dt>2013-01-05</Dt>
</BookgDt>
<ValDt>
<Dt>2013-01-05</Dt>
</ValDt>
<BkTxCd>
<Domn>
<Cd>PMNT</Cd>
<Fmly>
<Cd>RCDT</Cd>
<SubFmlyCd>ESCT</SubFmlyCd>
</Fmly>
</Domn>
<Prtry>
<Cd>ET</Cd>
</Prtry>
</BkTxCd>
<NtryDtls>
<TxDtls>
<Refs>
<InstrId>INNDNL2U20130105000217200000708</InstrId>
<EndToEndId>115</EndToEndId>
</Refs>
<AmtDtls>
<TxAmt>
<Amt Ccy="EUR">1405.31</Amt>
</TxAmt>
</AmtDtls>
<RltdPties>
<Dbtr>
<Nm>3rd party Media</Nm>
<PstlAdr>
<StrtNm>SOMESTREET 570-A</StrtNm>
<TwnNm>1276 ML HOUSCITY</TwnNm>
<Ctry>NL</Ctry>
</PstlAdr>
</Dbtr>
<DbtrAcct>
<Id>
<IBAN>NL69ABNA0522123643</IBAN>
</Id>
</DbtrAcct>
</RltdPties>
<RltdAgts>
<DbtrAgt>
<FinInstnId>
<BIC>ABNANL2A</BIC>
</FinInstnId>
</DbtrAgt>
</RltdAgts>
<AddtlTxInf>#RD PARTY MEDIA CUSNO 90782 4210773</AddtlTxInf>
</TxDtls>
</NtryDtls>
</Ntry>
</Stmt>
</BkToCstmrStmt>
</Document>

23
account_bank_statement_import_camt/tests/__init__.py

@ -0,0 +1,23 @@
# -*- encoding: utf-8 -*-
"""Test import of bank statement for camt.053."""
##############################################################################
#
# Copyright (C) 2015 Therp BV <http://therp.nl>.
#
# All other contributions are (C) by their respective contributors
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
from . import test_import_bank_statement

45
account_bank_statement_import_camt/tests/test_import_bank_statement.py

@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
"""Run test to import camt.053 import."""
##############################################################################
#
# Copyright (C) 2015 Therp BV <http://therp.nl>.
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.addons.account_bank_statement_import.tests import (
TestStatementFile)
class TestImport(TestStatementFile):
"""Run test to import camt import."""
def test_statement_import(self):
"""Test correct creation of single statement."""
transactions = [
{
'remote_account': 'NL46ABNA0499998748',
'transferred_amount': -754.25,
'value_date': '2013-01-05',
'ref': '435005714488-ABNO33052620',
},
]
# statement name is account number + '-' + date of last 62F line:
self._test_statement_import(
'account_bank_statement_import_camt', 'test-camt053.xml',
'1234Test/1',
local_account='NL77ABNA0574908765',
start_balance=15568.27, end_balance=15121.12,
transactions=transactions
)

53
account_bank_statement_import_mt940_base/README.rst

@ -0,0 +1,53 @@
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
: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 bank_statement_parse_nl_ing_mt940 for an example on how to use it.
Known issues / Roadmap
======================
* None
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/bank-statement-import/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed feedback
`here <https://github.com/OCA/bank-statement-import/issues/new?body=module:%20account_bank_statement_import%0Aversion:%208.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Credits
=======
Contributors
------------
* Stefan Rijnhart <srijnhart@therp.nl>
* Ronald Portier <rportier@therp.nl>
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 http://odoo-community.org.
This module should make it easy to migrate bank statement import
modules written for earlies versions of Odoo/OpenERP.

1
account_bank_statement_import_mt940_base/__init__.py

@ -0,0 +1 @@
from . import mt940

31
account_bank_statement_import_mt940_base/__openerp__.py

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (C) 2013-2015 Therp BV <http://therp.nl>
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
{
'name': 'MT940 Bank Statements Import',
'version': '1.1',
'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
}

262
account_bank_statement_import_mt940_base/mt940.py

@ -0,0 +1,262 @@
# -*- coding: utf-8 -*-
"""Generic parser for MT940 files, base for customized versions per bank."""
##############################################################################
#
# Copyright (C) 2014-2015 Therp BV <http://therp.nl>.
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
import re
import logging
from datetime import datetime
from openerp.addons.account_bank_statement_import.parserlib import (
BankStatement)
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.remote_account = subfield[0]
if len(subfield) >= 2 and subfield[1]:
transaction.remote_bank_bic = subfield[1]
if len(subfield) >= 3 and subfield[2]:
transaction.remote_owner = subfield[2]
if len(subfield) >= 4 and subfield[3]:
transaction.remote_owner_city = subfield[3]
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])
# REMI: Remitter information (text entered by other party on trans.):
if 'REMI' in subfields:
transaction.message = (
'/'.join(x for x in subfields['REMI'] if x))
# Get transaction reference subfield (might vary):
if transaction.eref in subfields:
transaction.eref = ''.join(
subfields[transaction.eref])
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'
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 = []
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 parse(self, data):
"""Parse mt940 bank statement file contents."""
self.is_mt940(data)
iterator = data.replace('\r\n', '\n').split('\n').__iter__()
line = None
record_line = ''
try:
while True:
if not self.current_statement:
self.handle_header(line, iterator)
line = iterator.next()
if not self.is_tag(line) and not self.is_footer(line):
record_line += 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.statements
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):
"""skip header lines, create current statement"""
for dummy_i in range(self.header_lines):
iterator.next()
self.current_statement = BankStatement()
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):
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.current_statement.local_account = 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.
stmt = self.current_statement
if not stmt.local_currency:
stmt.local_currency = data[7:10]
stmt.start_balance = str2amount(data[0], data[10:])
def handle_tag_61(self, data):
"""get transaction values"""
transaction = self.current_statement.create_transaction()
self.current_transaction = transaction
transaction.execution_date = datetime.strptime(data[:6], '%y%m%d')
transaction.value_date = datetime.strptime(data[:6], '%y%m%d')
# ...and the rest already is highly bank dependent
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.
"""
stmt = self.current_statement
stmt.end_balance = str2amount(data[0], data[10:])
stmt.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.
test_empty_id = re.sub(r'[\s0]', '', stmt.statement_id)
if ((not test_empty_id) or
(stmt.statement_id.startswith(stmt.local_account))):
stmt.statement_id = '%s-%s' % (
stmt.local_account,
stmt.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

51
account_bank_statement_import_mt940_nl_ing/README.rst

@ -0,0 +1,51 @@
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
:alt: License: AGPL-3
Import MT940 IBAN ING Bank Statements
=====================================
This module allows you to import the MT940 IBAN files from the Dutch ING bank
in Odoo as bank statements.
The specifications are published at:
https://www.ing.nl/media/ING_ming_mt940s_24_juli_tcm162-46356.pdf
and were last updated august 2014.
Known issues / Roadmap
======================
* None
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/bank-statement-import/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed feedback
`here <https://github.com/OCA/bank-statement-import/issues/new?body=module:%20account_bank_statement_import%0Aversion:%208.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Credits
=======
Contributors
------------
* Stefan Rijnhart <srijnhart@therp.nl>
* Ronald Portier <rportier@therp.nl>
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 http://odoo-community.org.
This module should make it easy to migrate bank statement import
modules written for earlies versions of Odoo/OpenERP.

1
account_bank_statement_import_mt940_nl_ing/__init__.py

@ -0,0 +1 @@
from . import account_bank_statement_import

34
account_bank_statement_import_mt940_nl_ing/__openerp__.py

@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (C) 2013-2015 Therp BV <http://therp.nl>
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
{
'name': 'MT940 IBAN ING Format Bank Statements Import',
'version': '0.3',
'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_mt940_base'
],
'demo': [
'demo/demo_data.xml',
],
'installable': True
}

44
account_bank_statement_import_mt940_nl_ing/account_bank_statement_import.py

@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
"""Parse a MT940 ING file."""
##############################################################################
#
# Copyright (C) 2013-2015 Therp BV <http://therp.nl>
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
import logging
from openerp import models
from .mt940 import MT940Parser as Parser
_logger = logging.getLogger(__name__)
class AccountBankStatementImport(models.TransientModel):
"""Add parsing of mt940 files to bank statement import."""
_inherit = 'account.bank.statement.import'
def _parse_file(self, cr, uid, data_file, context=None):
"""Parse a MT940 IBAN ING file."""
parser = Parser()
try:
_logger.debug("Try parsing with MT940 IBAN ING.")
return parser.parse(data_file)
except ValueError:
# Returning super will call next candidate:
_logger.debug("Statement file was not a MT940 IBAN ING file.",
exc_info=True)
return super(AccountBankStatementImport, self)._parse_file(
cr, uid, data_file, context=context)

26
account_bank_statement_import_mt940_nl_ing/demo/demo_data.xml

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="mt940_ing_bank_journal" model="account.journal">
<field name="name">Bank Journal - (test mt940 ING)</field>
<field name="code">TBNKMT940ING</field>
<field name="type">bank</field>
<field name="sequence_id" ref="account.sequence_bank_journal"/>
<field name="default_debit_account_id" ref="account.bnk"/>
<field name="default_credit_account_id" ref="account.bnk"/>
<field name="user_id" ref="base.user_root"/>
</record>
<record id="mt940_ing_company_bank" model="res.partner.bank">
<field name="owner_name">Your Company</field>
<field name="acc_number">NL77INGB0574908765</field>
<field name="partner_id" ref="base.partner_root"></field>
<field name="company_id" ref="base.main_company"></field>
<field name="journal_id" ref="mt940_ing_bank_journal"></field>
<field name="state">bank</field>
<field name="bank" ref="base.res_bank_1"/>
</record>
</data>
</openerp>

66
account_bank_statement_import_mt940_nl_ing/mt940.py

@ -0,0 +1,66 @@
# -*- coding: utf-8 -*-
"""Implement BankStatementParser for MT940 IBAN ING files."""
##############################################################################
#
# Copyright (C) 2014-2015 Therp BV <http://therp.nl>.
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
import re
from openerp.addons.account_bank_statement_import_mt940_base.mt940 import (
MT940, str2amount, get_subfields, handle_common_subfields)
class MT940Parser(MT940):
"""Parser for ing MT940 bank statement import files."""
tag_61_regex = re.compile(
r'^(?P<date>\d{6})(?P<line_date>\d{0,4})'
r'(?P<sign>[CD])(?P<amount>\d+,\d{2})N(?P<type>.{3})'
r'(?P<reference>\w{1,50})'
)
def __init__(self):
"""Initialize parser - override at least header_regex."""
super(MT940Parser, self).__init__()
self.mt940_type = 'ING'
def handle_tag_61(self, data):
"""get transaction values"""
super(MT940Parser, self).handle_tag_61(data)
re_61 = self.tag_61_regex.match(data)
if not re_61:
raise ValueError("Cannot parse %s" % data)
parsed_data = re_61.groupdict()
self.current_transaction.transferred_amount = (
str2amount(parsed_data['sign'], parsed_data['amount']))
self.current_transaction.eref = parsed_data['reference']
def handle_tag_86(self, data):
"""Parse 86 tag containing reference data."""
if not self.current_transaction:
return
codewords = ['RTRN', 'BENM', 'ORDP', 'CSID', 'BUSP', 'MARF', 'EREF',
'PREF', 'REMI', 'ID', 'PURP', 'ULTB', 'ULTD',
'CREF', 'IREF', 'CNTP', 'ULTC', 'EXCH', 'CHGS']
subfields = get_subfields(data, codewords)
transaction = self.current_transaction
# If we have no subfields, set message to whole of data passed:
if not subfields:
transaction.message = data
else:
handle_common_subfields(transaction, subfields)
# Prevent handling tag 86 later for non transaction details:
self.current_transaction = None

61
account_bank_statement_import_mt940_nl_ing/test_files/test-ing-old.940

@ -0,0 +1,61 @@
0000 01INGBNL2AXXXX00001
0000 01INGBNL2AXXXX00001
940 00
:20:P140120000000001
:25:NL77INGB0574908765EUR
:28C:0000
0
:60F:C140119EUR662,23
:61:1401200220C1,56NTRFEREF//00000000001
005
/TRCD/00100/
:86:/EREF/EV12341REP1231456T1234//CNTP/NL32INGB0000012345/INGBNL2
A/ING BANK NV INZAKE WEB///REMI/USTD//EV10001REP1000000T1000/
:61:1401200220D1,57NTRFPREF//00000000001006
/TRCD/00200/
:86:/PREF/M000000003333333//REMI/USTD//TOTAAL 1 VZ/
:61:1401200220C1,57NRTIEREF//00000000001007
/TRCD/00190/
:86:/RTRN/MS03//EREF/20120123456789//CNTP/NL32INGB0000012345/INGB
NL2A/J.Janssen///REMI/USTD//Factuurnr 123456 Klantnr 00123/
:61:1401200220D1,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:1401200220C1,45NDDTPREF//00000000001008
/TRCD/01000/
:86:/PREF/M000000001111111/
/CSID/
NL32ZZZ999999991234
/
/REMI/USTD//
TOTAAL 1 POSTEN/
:61:1401200220D12,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:1401200220C32,00NTRF9001123412341234//00000000001011
/TRCD/00108/
:86:/EREF/15814016000676480//CNTP/NL32INGB0000012345/INGBNL2A/J.J
anssen///REMI/STRD/CUR/9001123412341234/
:61:1401200220D119,00NTRF1070123412341234//00000000001012
/
TRCD/00108/
:86:/EREF/15614016000384600//CNTP/NL32INGB0000012345/INGBNL2A/ING
BANK NV///REMI/STRD/CUR/1070123412341234/
:62F:C140120EUR564,35
:64:C140120EUR564,35
:65:C140121EUR564,35
:65:C140124EUR564,35
:86:/SUM/4/4/134,46/36,58/
-XXX

62
account_bank_statement_import_mt940_nl_ing/test_files/test-ing.940

@ -0,0 +1,62 @@
{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/
-
}

25
account_bank_statement_import_mt940_nl_ing/tests/__init__.py

@ -0,0 +1,25 @@
# -*- encoding: utf-8 -*-
"""Test import of bank statement for MT940 ING."""
##############################################################################
#
# Copyright (C) 2015 Therp BV <http://therp.nl>.
#
# All other contributions are (C) by their respective contributors
#
# All Rights Reserved
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
from . import test_import_bank_statement

53
account_bank_statement_import_mt940_nl_ing/tests/test_import_bank_statement.py

@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
"""Run test to import MT940 IBAN ING import."""
##############################################################################
#
# Copyright (C) 2015 Therp BV <http://therp.nl>.
#
# All other contributions are (C) by their respective contributors
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.addons.account_bank_statement_import.tests import (
TestStatementFile)
class TestImport(TestStatementFile):
"""Run test to import MT940 ING import."""
def test_old_statement_import(self):
"""Test correct creation of single statement from old format."""
self._test_statement_import(
'account_bank_statement_import_mt940_nl_ing', 'test-ing-old.940',
'NL77INGB0574908765-2014-01-20',
start_balance=662.23, end_balance=564.35
)
def test_statement_import(self):
"""Test correct creation of single statement."""
transactions = [
{
'remote_account': 'NL32INGB0000012345',
'transferred_amount': 1.56,
'value_date': '2014-02-20',
'ref': 'EV12341REP1231456T1234',
},
]
self._test_statement_import(
'account_bank_statement_import_mt940_nl_ing', 'test-ing.940',
'NL77INGB0574908765-2014-02-20',
start_balance=662.23, end_balance=564.35,
transactions=transactions
)

47
account_bank_statement_import_mt940_nl_rabo/README.rst

@ -0,0 +1,47 @@
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
:alt: License: AGPL-3
Bank Statement NL Rabobank MT940
================================
This addon imports the structured MT940 format as offered by
the dutch Rabobank.
Known issues / Roadmap
======================
* None
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/bank-statement-import/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed feedback
`here <https://github.com/OCA/bank-statement-import/issues/new?body=module:%20account_bank_statement_import%0Aversion:%208.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Credits
=======
Contributors
------------
* Ronald Portier <rportier@therp.nl>
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 http://odoo-community.org.
This module should make it easy to migrate bank statement import
modules written for earlies versions of Odoo/OpenERP.

20
account_bank_statement_import_mt940_nl_rabo/__init__.py

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (C) 2014-2015 Therp BV (<http://therp.nl>).
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
from . import account_bank_statement_import

32
account_bank_statement_import_mt940_nl_rabo/__openerp__.py

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (C) 2014-2015 Therp BV <http://therp.nl>.
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
{
'name': 'MT940 import for dutch Rabobank',
'version': '1.1',
'author': 'Odoo Community Association (OCA), Therp BV',
'website': 'https://github.com/OCA/bank-statement-import',
'category': 'Banking addons',
'depends': [
'account_bank_statement_import_mt940_base'
],
'auto_install': False,
'installable': True,
'application': False,
}

46
account_bank_statement_import_mt940_nl_rabo/account_bank_statement_import.py

@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
"""Parse a MT940 RABO file."""
##############################################################################
#
# Copyright (C) 2013-2015 Therp BV <http://therp.nl>
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
import logging
from openerp import models
from .mt940 import MT940Parser as Parser
_logger = logging.getLogger(__name__)
class AccountBankStatementImport(models.TransientModel):
"""Add parsing of RABO mt940 files to bank statement import."""
_inherit = 'account.bank.statement.import'
def _parse_file(self, cr, uid, data_file, context=None):
"""Parse a MT940 RABO file."""
parser = Parser()
try:
_logger.debug("Try parsing with MT940 RABO.")
statements = parser.parse(data_file)
return statements
except ValueError:
# Returning super will call next candidate:
_logger.debug("Statement file was not a MT940 RABO file.",
exc_info=True)
return super(AccountBankStatementImport, self)._parse_file(
cr, uid, data_file, context=context)

86
account_bank_statement_import_mt940_nl_rabo/mt940.py

@ -0,0 +1,86 @@
# -*- coding: utf-8 -*-
"""Implement parser for MT940 files - Rabobank dialect."""
##############################################################################
#
# Copyright (C) 2015 Therp BV <http://therp.nl>.
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
import re
from string import printable
from openerp.addons.account_bank_statement_import_mt940_base.mt940 import (
MT940, str2amount, get_subfields, handle_common_subfields)
class MT940Parser(MT940):
"""Implement parser for MT940 files - Rabobank dialect."""
tag_61_regex = re.compile(
r'^(?P<date>\d{6})(?P<sign>[CD])(?P<amount>\d+,\d{2})N(?P<type>.{3})'
r'(?P<reference>MARF|EREF|PREF|NONREF)\s*'
r'\n?(?P<remote_account>\w{1,34})?'
)
def __init__(self):
"""Initialize parser - override at least header_regex."""
super(MT940Parser, self).__init__()
self.mt940_type = 'RABO'
self.header_lines = 1 # Number of lines to skip
# Do not user $ for end of string below: line contains much
# more data than just the first line.
self.header_regex = '^:940:' # Start of relevant data
def parse(self, data):
"""Filter Unprintable characters from file data.
The file contents of the Rabobank tend to contain unprintable
characters that prevent proper parsing. These will be removed.
"""
data = ''.join([x for x in data if x in printable])
return super(MT940Parser, self).parse(data)
def handle_tag_61(self, data):
"""Handle tag 61: transaction data."""
super(MT940Parser, self).handle_tag_61(data)
parsed_data = self.tag_61_regex.match(data).groupdict()
self.current_transaction.transferred_amount = (
str2amount(parsed_data['sign'], parsed_data['amount']))
self.current_transaction.eref = parsed_data['reference']
if parsed_data['remote_account']:
self.current_transaction.remote_account = (
parsed_data['remote_account'])
def handle_tag_86(self, data):
"""Handle tag 86: transaction details"""
if not self.current_transaction:
return
codewords = ['RTRN', 'BENM', 'ORDP', 'CSID', 'BUSP', 'MARF', 'EREF',
'PREF', 'REMI', 'ID', 'PURP', 'ULTB', 'ULTD',
'CREF', 'IREF', 'NAME', 'ADDR', 'ULTC', 'EXCH', 'CHGS']
subfields = get_subfields(data, codewords)
transaction = self.current_transaction
# If we have no subfields, set message to whole of data passed:
if not subfields:
transaction.message = data
else:
handle_common_subfields(transaction, subfields)
# Use subfields for transaction details:
if 'NAME' in subfields:
transaction.remote_owner = ' '.join(subfields['NAME'])
if 'ADDR' in subfields:
# Do NOT join address fields, array is expected on other code!
transaction.remote_owner_address = subfields['ADDR']
# Prevent handling tag 86 later for non transaction details:
self.current_transaction = None

29
account_bank_statement_import_mt940_nl_rabo/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

23
account_bank_statement_import_mt940_nl_rabo/tests/__init__.py

@ -0,0 +1,23 @@
# -*- encoding: utf-8 -*-
"""Test import of bank statement for MT940 ING."""
##############################################################################
#
# Copyright (C) 2015 Therp BV <http://therp.nl>.
#
# All other contributions are (C) by their respective contributors
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
from . import test_import_bank_statement

46
account_bank_statement_import_mt940_nl_rabo/tests/test_import_bank_statement.py

@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
"""Run test to import MT940 IBAN RABO import."""
##############################################################################
#
# Copyright (C) 2015 Therp BV <http://therp.nl>.
# All other contributions are (C) by their respective contributors
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.addons.account_bank_statement_import.tests import (
TestStatementFile)
class TestImport(TestStatementFile):
"""Run test to import MT940 RABO import."""
def test_statement_import(self):
"""Test correct creation of single statement."""
transactions = [
{
'remote_account': 'NL66RABO0160878799',
'transferred_amount': 400.00,
'value_date': '2014-01-02',
'ref': 'NONREF',
},
]
# statement name is account number + '-' + date of last 62F line:
self._test_statement_import(
'account_bank_statement_import_mt940_nl_rabo', 'test-rabo.swi',
'NL34RABO0142623393-2014-01-07',
local_account='NL34RABO0142623393',
start_balance=4433.52, end_balance=4798.91,
transactions=transactions
)

5
account_bank_statement_import_ofx/__openerp__.py

@ -1,7 +1,7 @@
# -*- encoding: utf-8 -*- # -*- encoding: utf-8 -*-
{ {
'name': 'Import OFX Bank Statement', 'name': 'Import OFX Bank Statement',
'category': 'Accounting & Finance',
'category': 'Banking addons',
'version': '1.0', 'version': '1.0',
'author': 'OpenERP SA,' 'author': 'OpenERP SA,'
'Odoo Community Association (OCA)', 'Odoo Community Association (OCA)',
@ -9,6 +9,9 @@
'depends': [ 'depends': [
'account_bank_statement_import' 'account_bank_statement_import'
], ],
'demo': [
'demo/demo_data.xml',
],
'external_dependencies': { 'external_dependencies': {
'python': ['ofxparse'], 'python': ['ofxparse'],
}, },

27
account_bank_statement_import_ofx/demo/demo_data.xml

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="ofx_bank_journal" model="account.journal">
<field name="name">Bank Journal - (test ofx)</field>
<field name="code">TBNKOFX</field>
<field name="type">bank</field>
<field name="sequence_id" ref="account.sequence_bank_journal"/>
<field name="default_debit_account_id" ref="account.usd_bnk"/>
<field name="default_credit_account_id" ref="account.usd_bnk"/>
<field name="user_id" ref="base.user_root"/>
<field name="currency" ref="base.USD"/>
</record>
<record id="ofx_company_bank" model="res.partner.bank">
<field name="owner_name">Your Company</field>
<field name="acc_number">123456</field>
<field name="partner_id" ref="base.partner_root"></field>
<field name="company_id" ref="base.main_company"></field>
<field name="journal_id" ref="ofx_bank_journal"></field>
<field name="state">bank</field>
<field name="bank" ref="base.res_bank_1"/>
</record>
</data>
</openerp>

2
account_bank_statement_import_qif/__openerp__.py

@ -2,7 +2,7 @@
{ {
'name': 'Import QIF Bank Statement', 'name': 'Import QIF Bank Statement',
'category': 'Accounting & Finance',
'category': 'Banking addons',
'version': '1.0', 'version': '1.0',
'author': 'OpenERP SA,' 'author': 'OpenERP SA,'
'Odoo Community Association (OCA)', 'Odoo Community Association (OCA)',

Loading…
Cancel
Save