Browse Source

Merge pull request #19 from acsone/8.0-refactor-new-api

8.0 refactor new api
pull/23/head
Pedro M. Baeza 10 years ago
parent
commit
e1698a7818
  1. 55
      account_bank_statement_import/README.rst
  2. 4
      account_bank_statement_import/__init__.py
  3. 11
      account_bank_statement_import/__openerp__.py
  4. 322
      account_bank_statement_import/account_bank_statement_import.py
  5. 8
      account_bank_statement_import/account_bank_statement_import_view.xml
  6. 45
      account_bank_statement_import/i18n/account_bank_statement_import.pot
  7. 71
      account_bank_statement_import/res_partner_bank.py
  8. 3
      account_bank_statement_import/tests/__init__.py
  9. 74
      account_bank_statement_import/tests/test_import_bank_statement.py
  10. 70
      account_bank_statement_import/tests/test_res_partner_bank.py
  11. 63
      account_bank_statement_import_ofx/README.rst
  12. 3
      account_bank_statement_import_ofx/__init__.py
  13. 34
      account_bank_statement_import_ofx/__openerp__.py
  14. 53
      account_bank_statement_import_ofx/account_bank_statement_import_ofx.py
  15. 27
      account_bank_statement_import_ofx/demo/demo_data.xml
  16. 30
      account_bank_statement_import_ofx/i18n/account_bank_statement_import_ofx.pot
  17. 43
      account_bank_statement_import_ofx/tests/test_import_bank_statement.py
  18. 58
      account_bank_statement_import_qif/README.rst
  19. 2
      account_bank_statement_import_qif/__init__.py
  20. 24
      account_bank_statement_import_qif/__openerp__.py
  21. 85
      account_bank_statement_import_qif/account_bank_statement_import_qif.py
  22. 24
      account_bank_statement_import_qif/account_bank_statement_import_qif_view.xml
  23. 49
      account_bank_statement_import_qif/i18n/account_bank_statement_import_qif.pot
  24. 2
      account_bank_statement_import_qif/tests/__init__.py
  25. 36
      account_bank_statement_import_qif/tests/test_import_bank_statement.py

55
account_bank_statement_import/README.rst

@ -0,0 +1,55 @@
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
:alt: License: AGPL-3
Account Bank Statement Import
=============================
This module add a generic wizard to import Bank Statements. It also extend
the bank account module to sanitize the account number and extend the search
method to use this field when searching on account_number.
The module has been initiated by a backport of the new framework developed
by Odoo for V9 at its early stage. It's no more kept in sync with the V9 since
it has reach a stage where maintaining a pure backport of 9.0 in 8.0 is not
feasible anymore
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
------------
* Odoo SA
* Pedro M. Baeza <pedro.baeza@gmail.com>
* Alexis de Lattre <alexis@via.ecp.fr>
* Laurent Mignon <laurent.mignon@acsone.eu>
* 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.

4
account_bank_statement_import/__init__.py

@ -1,4 +1,4 @@
# -*- encoding: utf-8 -*- # -*- encoding: utf-8 -*-
from . import account_bank_statement_import
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
from . import res_partner_bank
from . import account_bank_statement_import

11
account_bank_statement_import/__openerp__.py

@ -1,17 +1,12 @@
# -*- encoding: utf-8 -*- # -*- encoding: utf-8 -*-
# noqa: This is a backport from Odoo. OCA has no control over style here.
# flake8: noqa
{ {
'name': 'Account Bank Statement Import', 'name': 'Account Bank Statement Import',
'category': 'Accounting & Finance', 'category': 'Accounting & Finance',
'version': '1.0', 'version': '1.0',
'author': 'OpenERP SA',
'author': 'OpenERP SA,'
'Odoo Community Association (OCA)',
'website': 'https://github.com/OCA/bank-statement-import',
'depends': ['account'], 'depends': ['account'],
'demo': [],
'description' : """Generic Wizard to Import Bank Statements.
Backport from Odoo 9.0
""",
'data': [ 'data': [
'account_bank_statement_import_view.xml', 'account_bank_statement_import_view.xml',
], ],

322
account_bank_statement_import/account_bank_statement_import.py

@ -1,11 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# noqa: This is a backport from Odoo. OCA has no control over style here.
# flake8: noqa
import base64 import base64
from openerp import SUPERUSER_ID
from openerp.osv import fields, osv
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
@ -13,54 +9,54 @@ import logging
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
class account_bank_statement_line(osv.osv):
class AccountBankStatementLine(models.Model):
_inherit = "account.bank.statement.line" _inherit = "account.bank.statement.line"
_columns = {
# Ensure transactions can be imported only once (if the import format provides unique transaction ids)
'unique_import_id': fields.char('Import ID', readonly=True, copy=False),
}
# Ensure transactions can be imported only once (if the import format
# provides unique transaction ids)
unique_import_id = fields.Char('Import ID', readonly=True, copy=False)
_sql_constraints = [ _sql_constraints = [
('unique_import_id', 'unique (unique_import_id)', 'A bank account transactions can be imported only once !')
('unique_import_id',
'unique (unique_import_id)',
'A bank account transactions can be imported only once !')
] ]
class account_bank_statement_import(osv.TransientModel):
class AccountBankStatementImport(models.TransientModel):
_name = 'account.bank.statement.import' _name = 'account.bank.statement.import'
_description = 'Import Bank Statement' _description = 'Import Bank Statement'
_columns = {
'data_file': fields.binary('Bank Statement File', required=True, help='Get you bank statements in electronic format from your bank and select them here.'),
}
def import_file(self, cr, uid, ids, context=None):
""" Process the file chosen in the wizard, create bank statement(s) and go to reconciliation. """
context = dict(context or {})
#set the active_id in the context, so that any extension module could
#reuse the fields chosen in the wizard if needed (see .QIF for example)
context.update({'active_id': ids[0]})
data_file = self.browse(cr, uid, ids[0], context=context).data_file
# The appropriate implementation module returns the required data
currency_code, account_number, stmts_vals = self._parse_file(cr, uid, base64.b64decode(data_file), context=context)
# Check raw data
self._check_parsed_data(cr, uid, stmts_vals, context=context)
# Try to find the bank account and currency in odoo
currency_id, bank_account_id = self._find_additional_data(cr, uid, currency_code, account_number, context=context)
# Find or create the bank journal
journal_id = self._get_journal(cr, uid, currency_id, bank_account_id, account_number, context=context)
# Create the bank account if not already existing
if not bank_account_id and account_number:
self._create_bank_account(cr, uid, account_number, journal_id=journal_id, partner_id=uid, context=context)
# Prepare statement data to be used for bank statements creation
stmts_vals = self._complete_stmts_vals(cr, uid, stmts_vals, journal_id, account_number, context=context)
# Create the bank statements
statement_ids, notifications = self._create_bank_statements(cr, uid, stmts_vals, context=context)
# Finally dispatch to reconciliation interface
model, action_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account', 'action_bank_reconcile_bank_statements')
action = self.pool[model].browse(cr, uid, action_id, context=context)
@api.model
def _get_hide_journal_field(self):
""" Return False if the journal_id can't be provided by the parsed
file and must be provided by the wizard.
See account_bank_statement_import_qif """
return True
journal_id = fields.Many2one(
'account.journal', string='Journal',
help='Accounting journal related to the bank statement you\'re '
'importing. It has be be manually chosen for statement formats which '
'doesn\'t allow automatic journal detection (QIF for example).')
hide_journal_field = fields.Boolean(
'Hide the journal field in the view', default=_get_hide_journal_field)
data_file = fields.Binary(
'Bank Statement File', required=True,
help='Get you bank statements in electronic format from your bank '
'and select them here.')
@api.multi
def import_file(self):
""" Process the file chosen in the wizard, create bank statement(s) and
go to reconciliation. """
self.ensure_one()
data_file = base64.b64decode(self.data_file)
statement_ids, notifications = self.with_context(
active_id=self.id)._import_file(data_file)
# dispatch to reconciliation interface
action = self.env.ref(
'account.action_bank_reconcile_bank_statements')
return { return {
'name': action.name, 'name': action.name,
'tag': action.tag, 'tag': action.tag,
@ -71,33 +67,73 @@ class account_bank_statement_import(osv.TransientModel):
'type': 'ir.actions.client', 'type': 'ir.actions.client',
} }
def _parse_file(self, cr, uid, data_file, context=None):
""" Each module adding a file support must extends this method. It processes the file if it can, returns super otherwise, resulting in a chain of responsability.
This method parses the given file and returns the data required by the bank statement import process, as specified below.
@api.model
def _import_file(self, data_file):
""" Create bank statement(s) from file
"""
# 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)
# Try to find the bank account and currency in odoo
currency_id, bank_account_id = self._find_additional_data(
currency_code, account_number)
# Create the bank account if not already existing
if not bank_account_id and account_number:
journal_id = self.env.context.get('journal_id')
company_id = self.env.user.company_id.id
if journal_id:
journal = self.env['account.journal'].browse(journal_id)
company_id = journal.company_id.id
bank_account_id = self._create_bank_account(
account_number, company_id=company_id,
currency_id=currency_id).id
# Find or create the bank journal
journal_id = self._get_journal(
currency_id, bank_account_id, account_number)
# 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)
@api.model
def _parse_file(self, data_file):
""" Each module adding a file support must extends this method. It
rocesses the file if it can, returns super otherwise, resulting in a
chain of responsability.
This method parses the given file and returns the data required by
the bank statement import process, as specified below.
rtype: triplet (if a value can't be retrieved, use None) rtype: triplet (if a value can't be retrieved, use None)
- currency code: string (e.g: 'EUR') - currency code: string (e.g: 'EUR')
The ISO 4217 currency code, case insensitive The ISO 4217 currency code, case insensitive
- account number: string (e.g: 'BE1234567890') - 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 items marked by o) :
The number of the bank account which the statement belongs
to
- bank statements data: list of dict containing (optional
items marked by o) :
- '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)
-o 'balance_end_real': float (e.g: 8888.88) -o 'balance_end_real': float (e.g: 8888.88)
- 'transactions': list of dict containing : - 'transactions': list of dict containing :
- 'name': string (e.g: 'KBC-INVESTERINGSKREDIET 787-5562831-01')
- 'name': string
(e.g: 'KBC-INVESTERINGSKREDIET 787-5562831-01')
- 'date': date - 'date': date
- 'amount': float - 'amount': float
- 'unique_import_id': string - 'unique_import_id': string
-o 'account_number': string -o 'account_number': string
Will be used to find/create the res.partner.bank in odoo
Will be used to find/create the res.partner.bank
in odoo
-o 'note': string -o 'note': string
-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.\nDid you '
'install the module to support this type of file ?'))
def _check_parsed_data(self, cr, uid, stmts_vals, context=None):
@api.model
def _check_parsed_data(self, stmts_vals):
""" Basic and structural verifications """ """ Basic and structural verifications """
if len(stmts_vals) == 0: if len(stmts_vals) == 0:
raise Warning(_('This file doesn\'t contain any statement.')) raise Warning(_('This file doesn\'t contain any statement.'))
@ -110,132 +146,138 @@ class account_bank_statement_import(osv.TransientModel):
if no_st_line: if no_st_line:
raise Warning(_('This file doesn\'t contain any transaction.')) raise Warning(_('This file doesn\'t contain any transaction.'))
def _find_additional_data(self, cr, uid, currency_code, account_number, context=None):
@api.model
def _find_additional_data(self, currency_code, account_number):
""" Get the res.currency ID and the res.partner.bank ID """ """ Get the res.currency ID and the res.partner.bank ID """
currency_id = False # So if no currency_code is provided, we'll use the company currency
# if no currency_code is provided, we'll use the company currency
currency_id = False
if currency_code: if currency_code:
currency_ids = self.pool.get('res.currency').search(cr, uid, [('name', '=ilike', currency_code)], context=context)
company_currency_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id
currency_ids = self.env['res.currency'].search(
[('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: if currency_ids[0] != company_currency_id:
currency_id = currency_ids[0]
currency_id = currency_ids[0].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:
account_number = account_number.replace(' ', '').replace('-', '')
cr.execute("select id from res_partner_bank where replace(replace(acc_number,' ',''),'-','') like %s and journal_id is not null", ('%' + account_number + '%',))
bank_account_ids = [id[0] for id in cr.fetchall()]
bank_account_ids = self.env['res.partner.bank'].search(
[('acc_number', '=', account_number)], limit=1)
if bank_account_ids: if bank_account_ids:
bank_account_id = bank_account_ids[0]
bank_account_id = bank_account_ids[0].id
return currency_id, bank_account_id return currency_id, bank_account_id
def _get_journal(self, cr, uid, currency_id, bank_account_id, account_number, context=None):
@api.model
def _get_journal(self, currency_id, bank_account_id, account_number):
""" Find or create the journal """ """ Find or create the journal """
if context is None:
context = {}
bank_pool = self.pool.get('res.partner.bank')
bank_model = self.env['res.partner.bank']
# Find the journal from context or bank account # Find the journal from context or bank account
journal_id = context.get('journal_id')
journal_id = self.env.context.get('journal_id')
if bank_account_id: if bank_account_id:
bank_account = bank_pool.browse(cr, uid, bank_account_id, context=context)
bank_account = bank_model.browse(bank_account_id)
if journal_id: if journal_id:
if bank_account.journal_id.id and bank_account.journal_id.id != journal_id:
raise Warning(_('The account of this statement is linked to another journal.'))
if (bank_account.journal_id.id and
bank_account.journal_id.id != journal_id):
raise Warning(
_('The account of this statement is linked to '
'another journal.'))
if not bank_account.journal_id.id: if not bank_account.journal_id.id:
bank_pool.write(cr, uid, [bank_account_id], {'journal_id': journal_id}, context=context)
bank_model.write({'journal_id': journal_id})
else: else:
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 as the bank statement
# If importing into an existing journal, its currency must be the same
# as the bank statement
if journal_id: if journal_id:
journal_currency_id = self.pool.get('account.journal').browse(cr, uid, journal_id, context=context).currency.id
journal_currency_id = self.env['account.journal'].browse(
journal_id).currency.id
if currency_id and currency_id != journal_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 there is no journal, create one (and its account)
# I think it's too dangerous, so I disable that code by default -- Alexis de Lattre
# -- Totally disabled, Ronald Portier
# if context.get('allow_auto_create_journal') and not journal_id and account_number:
# journal_id = self._create_journal(cr, uid, currency_id, account_number, context=context)
# if bank_account_id:
# bank_pool.write(cr, uid, [bank_account_id], {'journal_id': journal_id}, context=context)
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 we couldn't find/create a journal, everything is lost
if not journal_id: if not journal_id:
raise Warning(_('Cannot find in which journal import this statement. Please manually select a journal.'))
raise Warning(_('Cannot find in which journal import this '
'statement. Please manually select a journal.'))
return journal_id return journal_id
def _create_journal(self, cr, uid, currency_id, account_number, context=None):
""" Create a journal and its account """
wmca_pool = self.pool.get('wizard.multi.charts.accounts')
company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id
vals_account = {'currency_id': currency_id, 'acc_name': account_number, 'account_type': 'bank'}
vals_account = wmca_pool._prepare_bank_account(cr, uid, company, vals_account, context=context)
account_id = self.pool.get('account.account').create(cr, uid, vals_account, context=context)
vals_journal = {'currency_id': currency_id, 'acc_name': _('Bank') + ' ' + account_number, 'account_type': 'bank'}
vals_journal = wmca_pool._prepare_bank_journal(cr, uid, company, vals_journal, account_id, context=context)
return self.pool.get('account.journal').create(cr, uid, vals_journal, context=context)
def _create_bank_account(self, cr, uid, account_number, journal_id=False, partner_id=False, context=None):
@api.model
@api.returns('res.partner.bank')
def _create_bank_account(self, account_number, company_id=False,
currency_id=False):
try: try:
type_model, type_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'base', 'bank_normal')
type_id = self.pool.get('res.partner.bank.type').browse(cr, uid, type_id, context=context)
bank_code = type_id.code
bank_type = self.env.ref('base.bank_normal')
bank_code = bank_type.code
except ValueError: except ValueError:
bank_code = 'bank' bank_code = 'bank'
account_number = account_number.replace(' ', '').replace('-', '')
vals_acc = { vals_acc = {
'acc_number': account_number, 'acc_number': account_number,
'state': bank_code, 'state': bank_code,
} }
# Odoo users bank accounts (which we import statement from) have company_id and journal_id set
# while 'counterpart' bank accounts (from which statement transactions originate) don't.
# Warning : if company_id is set, the method post_write of class bank will create a journal
if journal_id:
vals_acc['partner_id'] = uid
vals_acc['journal_id'] = journal_id
vals_acc['company_id'] = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
return self.pool.get('res.partner.bank').create(cr, uid, vals_acc, context=context)
def _complete_stmts_vals(self, cr, uid, stmts_vals, journal_id, account_number, context=None):
# Odoo users bank accounts (which we import statement from) have
# company_id and journal_id set while 'counterpart' bank accounts
# (from which statement transactions originate) don't.
# Warning : if company_id is set, the method post_write of class
# bank will create a journal
if company_id:
vals = self.env['res.partner.bank'].onchange_company_id(company_id)
vals_acc.update(vals.get('value', {}))
vals_acc['company_id'] = company_id
# When the journal is created at same time of the bank account, we need
# to specify the currency to use for the account.account and
# account.journal
return self.env['res.partner.bank'].with_context(
default_currency_id=currency_id,
default_currency=currency_id).create(vals_acc)
@api.model
def _complete_stmts_vals(self, stmts_vals, journal_id, account_number):
for st_vals in stmts_vals: for st_vals in stmts_vals:
st_vals['journal_id'] = journal_id st_vals['journal_id'] = journal_id
for line_vals in st_vals['transactions']: for line_vals in st_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'] = (account_number and account_number + '-' or '') + unique_import_id
if not line_vals.get('partner_id') and not line_vals.get('bank_account_id'):
# Find the partner and his bank account or create the bank account. The partner selected during the
# reconciliation process will be linked to the bank when the statement is closed.
line_vals['unique_import_id'] = (
account_number and account_number + '-' or '') + \
unique_import_id
if not line_vals.get('bank_account_id'):
# Find the partner and his bank account or create the bank
# account. The partner selected during the reconciliation
# process will be linked to the bank when the statement is
# closed.
partner_id = False partner_id = False
bank_account_id = False bank_account_id = False
identifying_string = line_vals.get('account_number', False)
identifying_string = line_vals.get('account_number')
if identifying_string: if identifying_string:
identifying_string = identifying_string.replace(' ', '').replace('-', '')
ids = self.pool.get('res.partner.bank').search(cr, uid, [('acc_number', '=', identifying_string)], context=context)
if ids:
bank_account_id = ids[0]
partner_id = self.pool.get('res.partner.bank').browse(cr, uid, bank_account_id, context=context).partner_id.id
bank_model = self.env['res.partner.bank']
banks = bank_model.search(
[('acc_number', '=', identifying_string)], limit=1)
if banks:
bank_account_id = banks[0].id
partner_id = banks[0].partner_id.id
else: else:
bank_account_id = self._create_bank_account(cr, uid, identifying_string, context=context)
bank_account_id = self._create_bank_account(
identifying_string).id
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 stmts_vals
def _create_bank_statements(self, cr, uid, stmts_vals, context=None):
""" Create new bank statements from imported values, filtering out already imported transactions, and returns data used by the reconciliation widget """
bs_obj = self.pool.get('account.bank.statement')
bsl_obj = self.pool.get('account.bank.statement.line')
@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
reconciliation widget
"""
bs_model = self.env['account.bank.statement']
bsl_model = self.env['account.bank.statement.line']
# Filter out already imported transactions and create statements # Filter out already imported transactions and create statements
statement_ids = [] statement_ids = []
@ -243,20 +285,25 @@ class account_bank_statement_import(osv.TransientModel):
for st_vals in stmts_vals: for st_vals in stmts_vals:
filtered_st_lines = [] filtered_st_lines = []
for line_vals in st_vals['transactions']: for line_vals in st_vals['transactions']:
if not 'unique_import_id' in line_vals \
if 'unique_import_id' not in line_vals \
or not line_vals['unique_import_id'] \ or not line_vals['unique_import_id'] \
or not bool(bsl_obj.search(cr, SUPERUSER_ID, [('unique_import_id', '=', line_vals['unique_import_id'])], limit=1, context=context)):
or not bool(bsl_model.sudo().search(
[('unique_import_id', '=',
line_vals['unique_import_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_statement_lines_import_ids.append(
line_vals['unique_import_id'])
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) st_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 # Create the satement
st_vals['line_ids'] = [[0, False, line] for line in filtered_st_lines]
statement_ids.append(bs_obj.create(cr, uid, st_vals, context=context))
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: if len(statement_ids) == 0:
raise Warning(_('You have already imported that file.')) raise Warning(_('You have already imported that file.'))
@ -266,13 +313,18 @@ class account_bank_statement_import(osv.TransientModel):
if num_ignored > 0: if num_ignored > 0:
notifications += [{ notifications += [{
'type': 'warning', 'type': 'warning',
'message': _("%d transactions had already been imported and were ignored.") % num_ignored if num_ignored > 1 else _("1 transaction had already been imported and was ignored."),
'message': _("%d transactions had already been imported and "
"were ignored.") % num_ignored
if num_ignored > 1
else _("1 transaction had already been imported and "
"was ignored."),
'details': { 'details': {
'name': _('Already imported items'), 'name': _('Already imported items'),
'model': 'account.bank.statement.line', 'model': 'account.bank.statement.line',
'ids': bsl_obj.search(cr, uid, [('unique_import_id', 'in', ignored_statement_lines_import_ids)], context=context)
'ids': bsl_model.search(
[('unique_import_id', 'in',
ignored_statement_lines_import_ids)]).ids
} }
}] }]
return statement_ids, notifications return statement_ids, notifications

8
account_bank_statement_import/account_bank_statement_import_view.xml

@ -9,6 +9,12 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="Import Bank Statements"> <form string="Import Bank Statements">
<field name="data_file"/> <field name="data_file"/>
<field name="hide_journal_field" invisible="1"/>
<label for="journal_id"/>
<field name="journal_id"
domain="[('type', '=', 'bank')]"
attrs="{'invisible': [('hide_journal_field', '=', True)]}"
context="{'default_type':'bank'}"/>
<br/><br/><b> How to import your bank statement :</b> <br/><br/><b> How to import your bank statement :</b>
<br/><label string= "1. Download your bank statements from your bank website."/> <br/><label string= "1. Download your bank statements from your bank website."/>
<br/><label string= "2. Make sure you have installed the right module to support the file format."/> <br/><label string= "2. Make sure you have installed the right module to support the file format."/>
@ -23,7 +29,7 @@
</record> </record>
<record id="action_account_bank_statement_import" model="ir.actions.act_window"> <record id="action_account_bank_statement_import" model="ir.actions.act_window">
<field name="name">Import Bank Statement</field>
<field name="name">Import</field>
<field name="type">ir.actions.act_window</field> <field name="type">ir.actions.act_window</field>
<field name="res_model">account.bank.statement.import</field> <field name="res_model">account.bank.statement.import</field>
<field name="view_type">form</field> <field name="view_type">form</field>

45
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-02-20 00:25+0000\n"
"PO-Revision-Date: 2015-02-20 00:25+0000\n"
"POT-Creation-Date: 2015-06-08 12:02+0000\n"
"PO-Revision-Date: 2015-06-08 12:02+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:269
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:313
#, 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:269
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:316
#, 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 ""
@ -48,15 +48,19 @@ msgid "A bank account transactions can be imported only once !"
msgstr "" msgstr ""
#. module: account_bank_statement_import #. module: account_bank_statement_import
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:271
#: sql_constraint:res.partner.bank:0
msgid "Account Number must be unique"
msgstr ""
#. module: account_bank_statement_import
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:319
#, python-format #, python-format
msgid "Already imported items" msgid "Already imported items"
msgstr "" msgstr ""
#. module: account_bank_statement_import #. module: account_bank_statement_import
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:182
#, python-format
msgid "Bank"
#: model:ir.model,name:account_bank_statement_import.model_res_partner_bank
msgid "Bank Accounts"
msgstr "" msgstr ""
#. module: account_bank_statement_import #. module: account_bank_statement_import
@ -75,13 +79,13 @@ 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:169
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:200
#, python-format #, python-format
msgid "Cannot find in which journal import this statement. Please manually select a journal." msgid "Cannot find in which journal import this statement. Please manually select a journal."
msgstr "" msgstr ""
#. module: account_bank_statement_import #. module: account_bank_statement_import
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:99
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:129
#, 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 ?"
@ -114,8 +118,12 @@ msgstr ""
#. module: account_bank_statement_import #. module: account_bank_statement_import
#: model:ir.actions.act_window,name:account_bank_statement_import.action_account_bank_statement_import #: model:ir.actions.act_window,name:account_bank_statement_import.action_account_bank_statement_import
#: model:ir.model,name:account_bank_statement_import.model_account_bank_statement_import
#: model:ir.ui.menu,name:account_bank_statement_import.menu_account_bank_statement_import #: model:ir.ui.menu,name:account_bank_statement_import.menu_account_bank_statement_import
msgid "Import"
msgstr ""
#. module: account_bank_statement_import
#: model:ir.model,name:account_bank_statement_import.model_account_bank_statement_import
msgid "Import Bank Statement" msgid "Import Bank Statement"
msgstr "" msgstr ""
@ -140,31 +148,36 @@ msgid "Last Updated on"
msgstr "" msgstr ""
#. module: account_bank_statement_import #. module: account_bank_statement_import
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:146
#: field:res.partner.bank,sanitized_acc_number:0
msgid "Sanitized Account Number"
msgstr ""
#. module: account_bank_statement_import
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:181
#, 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:157
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:195
#, 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:104
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:136
#, 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:112
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:144
#, 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:261
#: code:addons/account_bank_statement_import/account_bank_statement_import.py:305
#, python-format #, python-format
msgid "You have already imported that file." msgid "You have already imported that file."
msgstr "" msgstr ""

71
account_bank_statement_import/res_partner_bank.py

@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# This file is part of account_bank_statement_import,
# an Odoo module.
#
# Copyright (c) 2015 ACSONE SA/NV (<http://acsone.eu>)
#
# account_bank_statement_importis 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.
#
# account_bank_statement_import 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 account_bank_statement_import_coda.
# If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import re
from openerp import api, models, fields
class ResPartnerBank(models.Model):
_inherit = 'res.partner.bank'
sanitized_acc_number = fields.Char(
'Sanitized Account Number', size=64, readonly=True,
compute='_get_sanitized_account_number', store=True)
def _sanitize_account_number(self, acc_number):
if acc_number:
return re.sub(r'\W+', '', acc_number).upper()
return False
@api.one
@api.depends('acc_number')
def _get_sanitized_account_number(self):
self.sanitized_acc_number = self._sanitize_account_number(
self.acc_number)
@api.returns('self')
def search(self, cr, user, args, offset=0, limit=None, order=None,
context=None, count=False):
pos = 0
while pos < len(args):
if args[pos][0] == 'acc_number':
op = args[pos][1]
value = args[pos][2]
if hasattr(value, '__iter__'):
value = [self._sanitize_account_number(i) for i in value]
else:
value = self._sanitize_account_number(value)
if 'like' in op:
value = '%' + value + '%'
args[pos] = ('sanitized_acc_number', op, value)
pos += 1
return super(ResPartnerBank, self).search(
cr, user, args, offset=0, limit=None, order=None, context=None,
count=False)
_sql_constraints = [
('unique_number', 'unique(sanitized_acc_number)',
'Account Number must be unique'),
]

3
account_bank_statement_import/tests/__init__.py

@ -0,0 +1,3 @@
# -*- encoding: utf-8 -*-
from . import test_res_partner_bank
from . import test_import_bank_statement

74
account_bank_statement_import/tests/test_import_bank_statement.py

@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# This file is part of account_bank_statement_import,
# an Odoo module.
#
# Copyright (c) 2015 ACSONE SA/NV (<http://acsone.eu>)
#
# account_bank_statement_import 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.
#
# account_bank_statement_import 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 account_bank_statement_import_coda.
# If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.tests.common import TransactionCase
class TestAccountBankStatemetImport(TransactionCase):
"""Tests for import bank statement file import
(account.bank.statement.import)
"""
def setUp(self):
super(TestAccountBankStatemetImport, self).setUp()
self.statement_import_model = self.env[
'account.bank.statement.import']
self.account_journal_model = self.env['account.journal']
self.res_users_model = self.env['res.users']
self.journal_id = self.ref('account.bank_journal')
self.base_user_root_id = self.ref('base.user_root')
self.base_user_root = self.res_users_model.browse(
self.base_user_root_id)
# create a new user that belongs to the same company as
# user_root
self.other_partner_id = self.env['res.partner'].create(
{"name": "My other partner",
"is_company": False,
"email": "test@tes.ttest",
})
self.company_id = self.base_user_root.company_id.id
self.other_user_id_a = self.res_users_model.create(
{"partner_id": self.other_partner_id.id,
"company_id": self.company_id,
"company_ids": [(4, self.company_id)],
"login": "my_login a",
"name": "my user",
"groups_id": [(4, self.ref('account.group_account_manager'))]
})
def test_create_bank_account(self):
"""Checks that the bank_account created by the import belongs to the
partner linked to the company of the provided journal
"""
journal = self.account_journal_model.browse(self.journal_id)
expected_id = journal.company_id.partner_id.id
st_import = self.statement_import_model.sudo(self.other_user_id_a.id)
bank = st_import._create_bank_account(
'001251882303', company_id=self.company_id)
self.assertEqual(bank.partner_id.id,
expected_id)

70
account_bank_statement_import/tests/test_res_partner_bank.py

@ -0,0 +1,70 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# This file is part of account_bank_statement_import,
# an Odoo module.
#
# Copyright (c) 2015 ACSONE SA/NV (<http://acsone.eu>)
#
# account_bank_statement_import 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.
#
# account_bank_statement_import 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 account_bank_statement_import_coda.
# If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.tests.common import TransactionCase
class TestResPartnerBank(TransactionCase):
"""Tests acc_number
"""
def test_sanitized_acc_number(self):
partner_bank_model = self.env['res.partner.bank']
acc_number = " BE-001 2518823 03 "
vals = partner_bank_model.search([('acc_number', '=', acc_number)])
self.assertEquals(0, len(vals))
partner_bank = partner_bank_model.create({
'acc_number': acc_number,
'partner_id': self.ref('base.res_partner_2'),
'state': 'bank',
})
vals = partner_bank_model.search([('acc_number', '=', acc_number)])
self.assertEquals(1, len(vals))
self.assertEquals(partner_bank, vals[0])
vals = partner_bank_model.search([('acc_number', 'in', [acc_number])])
self.assertEquals(1, len(vals))
self.assertEquals(partner_bank, vals[0])
self.assertEqual(partner_bank.acc_number, acc_number)
# sanitaze the acc_number
sanitized_acc_number = 'BE001251882303'
vals = partner_bank_model.search(
[('acc_number', '=', sanitized_acc_number)])
self.assertEquals(1, len(vals))
self.assertEquals(partner_bank, vals[0])
vals = partner_bank_model.search(
[('acc_number', 'in', [sanitized_acc_number])])
self.assertEquals(1, len(vals))
self.assertEquals(partner_bank, vals[0])
self.assertEqual(partner_bank.sanitized_acc_number,
sanitized_acc_number)
# search is case insensitive
vals = partner_bank_model.search(
[('acc_number', '=', sanitized_acc_number.lower())])
self.assertEquals(1, len(vals))
vals = partner_bank_model.search(
[('acc_number', '=', acc_number.lower())])
self.assertEquals(1, len(vals))

63
account_bank_statement_import_ofx/README.rst

@ -0,0 +1,63 @@
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
:alt: License: AGPL-3
Import OFX Bank Statement
=========================
This module allows you to import the machine readable OFX Files in Odoo: they are parsed and stored in human readable format in
Accounting \ Bank and Cash \ Bank Statements.
Bank Statements may be generated containing a subset of the OFX information (only those transaction lines that are required for the
creation of the Financial Accounting records).
The module has been initiated by a backport of the new framework developed
by Odoo for V9 at its early stage. It's no more kept in sync with the V9 since
it has reach a stage where maintaining a pure backport of 9.0 in 8.0 is not
feasible anymore
Installation
============
The module requires one additional python lib:
* `ofxparse <http://pypi.python.org/pypi/ofxparse>`_
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_ofx%0Aversion:%208.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Credits
=======
Contributors
------------
* Odoo SA
* Alexis de Lattre <alexis@via.ecp.fr>
* Laurent Mignon <laurent.mignon@acsone.eu>
* 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.

3
account_bank_statement_import_ofx/__init__.py

@ -1,4 +1,3 @@
# -*- encoding: utf-8 -*- # -*- encoding: utf-8 -*-
from . import account_bank_statement_import_ofx
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
from . import account_bank_statement_import_ofx

34
account_bank_statement_import_ofx/__openerp__.py

@ -1,33 +1,17 @@
# -*- encoding: utf-8 -*- # -*- encoding: utf-8 -*-
# noqa: This is a backport from Odoo. OCA has no control over style here.
# flake8: noqa
{ {
'name': 'Import OFX Bank Statement', 'name': 'Import OFX Bank Statement',
'category': 'Accounting & Finance', 'category': 'Accounting & Finance',
'version': '1.0', 'version': '1.0',
'author': 'OpenERP SA',
'depends': ['account_bank_statement_import'],
'demo': [],
'description' : """
Module to import OFX bank statements.
======================================
This module allows you to import the machine readable OFX Files in Odoo: they are parsed and stored in human readable format in
Accounting \ Bank and Cash \ Bank Statements.
Bank Statements may be generated containing a subset of the OFX information (only those transaction lines that are required for the
creation of the Financial Accounting records).
Backported from Odoo 9.0
When testing with the provided test file, make sure the demo data from the
base account_bank_statement_import module has been imported, or manually
create periods for the year 2013.
""",
'data' : [],
'demo': [
'demo/demo_data.xml',
'author': 'OpenERP SA,'
'Odoo Community Association (OCA)',
'website': 'https://github.com/OCA/bank-statement-import',
'depends': [
'account_bank_statement_import'
], ],
'auto_install': False,
'external_dependencies': {
'python': ['ofxparse'],
},
'auto_install': True,
'installable': True, 'installable': True,
} }

53
account_bank_statement_import_ofx/account_bank_statement_import_ofx.py

@ -1,11 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# noqa: This is a backport from Odoo. OCA has no control over style here.
# flake8: noqa
import logging import logging
import StringIO import StringIO
from openerp.osv import osv
from openerp import api, models
from openerp.tools.translate import _ from openerp.tools.translate import _
from openerp.exceptions import Warning from openerp.exceptions import Warning
@ -14,41 +12,49 @@ _logger = logging.getLogger(__name__)
try: try:
from ofxparse import OfxParser as ofxparser from ofxparse import OfxParser as ofxparser
except ImportError: except ImportError:
_logger.error("OFX parser unavailable because the `ofxparse` Python library cannot be found."
"It can be downloaded and installed from `https://pypi.python.org/pypi/ofxparse`.")
_logger.warn("ofxparse not found, OFX parsing disabled.")
ofxparser = None ofxparser = None
class account_bank_statement_import(osv.TransientModel):
class AccountBankStatementImport(models.TransientModel):
_inherit = 'account.bank.statement.import' _inherit = 'account.bank.statement.import'
def _check_ofx(self, cr, uid, file, context=None):
@api.model
def _check_ofx(self, data_file):
if ofxparser is None: if ofxparser is None:
return False return False
try: try:
ofx = ofxparser.parse(file)
ofx = ofxparser.parse(StringIO.StringIO(data_file))
except: except:
return False return False
return ofx return ofx
def _parse_file(self, cr, uid, data_file, context=None):
ofx = self._check_ofx(cr, uid, StringIO.StringIO(data_file), context=context)
@api.model
def _parse_file(self, data_file):
ofx = self._check_ofx(data_file)
if not ofx: if not ofx:
return super(account_bank_statement_import, self)._parse_file(cr, uid, data_file, context=context)
return super(AccountBankStatementImport, self)._parse_file(
data_file)
transactions = [] transactions = []
total_amt = 0.00 total_amt = 0.00
try: try:
for transaction in ofx.account.statement.transactions: for transaction in ofx.account.statement.transactions:
# Since ofxparse doesn't provide account numbers, we'll have to find res.partner and res.partner.bank here
# (normal behavious is to provide 'account_number', which the generic module uses to find partner/bank)
# Since ofxparse doesn't provide account numbers, we'll have
# to find res.partner and res.partner.bank here
# (normal behavious is to provide 'account_number', which the
# generic module uses to find partner/bank)
bank_account_id = partner_id = False bank_account_id = partner_id = False
ids = self.pool.get('res.partner.bank').search(cr, uid, [('owner_name', '=', transaction.payee)], context=context)
if ids:
bank_account_id = bank_account_id = ids[0]
partner_id = self.pool.get('res.partner.bank').browse(cr, uid, bank_account_id, context=context).partner_id.id
banks = self.env['res.partner.bank'].search(
[('owner_name', '=', transaction.payee)], limit=1)
if banks:
bank_account = banks[0]
bank_account_id = bank_account.id
partner_id = bank_account.partner_id.id
vals_line = { vals_line = {
'date': transaction.date, 'date': transaction.date,
'name': transaction.payee + (transaction.memo and ': ' + transaction.memo or ''),
'name': transaction.payee + (
transaction.memo and ': ' + transaction.memo or ''),
'ref': transaction.id, 'ref': transaction.id,
'amount': transaction.amount, 'amount': transaction.amount,
'unique_import_id': transaction.id, 'unique_import_id': transaction.id,
@ -58,12 +64,15 @@ class account_bank_statement_import(osv.TransientModel):
total_amt += float(transaction.amount) total_amt += float(transaction.amount)
transactions.append(vals_line) transactions.append(vals_line)
except Exception, e: except Exception, e:
raise Warning(_("The following problem occurred during import. The file might not be valid.\n\n %s" % e.message))
raise Warning(_("The following problem occurred during import. "
"The file might not be valid.\n\n %s" % e.message))
vals_bank_statement = { vals_bank_statement = {
'name': ofx.account.routing_number, 'name': ofx.account.routing_number,
'transactions': transactions, 'transactions': transactions,
'balance_start': float(ofx.account.statement.balance) - total_amt,
'balance_end_real': float(ofx.account.statement.balance),
'balance_start': ofx.account.statement.balance,
'balance_end_real':
float(ofx.account.statement.balance) + total_amt,
} }
return ofx.account.statement.currency, ofx.account.number, [vals_bank_statement]
return ofx.account.statement.currency, ofx.account.number, [
vals_bank_statement]

27
account_bank_statement_import_ofx/demo/demo_data.xml

@ -1,27 +0,0 @@
<?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>

30
account_bank_statement_import_ofx/i18n/account_bank_statement_import_ofx.pot

@ -0,0 +1,30 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * account_bank_statement_import_ofx
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 8.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-06-08 09:11+0000\n"
"PO-Revision-Date: 2015-06-08 09:11+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: account_bank_statement_import_ofx
#: model:ir.model,name:account_bank_statement_import_ofx.model_account_bank_statement_import
msgid "Import Bank Statement"
msgstr ""
#. module: account_bank_statement_import_ofx
#: code:addons/account_bank_statement_import_ofx/account_bank_statement_import_ofx.py:67
#, python-format
msgid "The following problem occurred during import. The file might not be valid.\n"
"\n"
" %s"
msgstr ""

43
account_bank_statement_import_ofx/tests/test_import_bank_statement.py

@ -1,32 +1,35 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# noqa: This is a backport from Odoo. OCA has no control over style here.
# flake8: noqa
from openerp.tests.common import TransactionCase from openerp.tests.common import TransactionCase
from openerp.modules.module import get_module_resource from openerp.modules.module import get_module_resource
class TestOfxFile(TransactionCase): class TestOfxFile(TransactionCase):
"""Tests for import bank statement ofx file format (account.bank.statement.import)
"""Tests for import bank statement ofx file format
(account.bank.statement.import)
""" """
def setUp(self): def setUp(self):
super(TestOfxFile, self).setUp() super(TestOfxFile, self).setUp()
self.statement_import_model = self.registry('account.bank.statement.import')
self.bank_statement_model = self.registry('account.bank.statement')
self.statement_import_model = self.env['account.bank.statement.import']
self.bank_statement_model = self.env['account.bank.statement']
def test_ofx_file_import(self): def test_ofx_file_import(self):
try:
from ofxparse import OfxParser as ofxparser
except ImportError:
#the Python library isn't installed on the server, the OFX import is unavailable and the test cannot be run
return True
cr, uid = self.cr, self.uid
ofx_file_path = get_module_resource('account_bank_statement_import_ofx', 'test_ofx_file', 'test_ofx.ofx')
ofx_file_path = get_module_resource(
'account_bank_statement_import_ofx',
'test_ofx_file', 'test_ofx.ofx')
ofx_file = open(ofx_file_path, 'rb').read().encode('base64') ofx_file = open(ofx_file_path, 'rb').read().encode('base64')
bank_statement_id = self.statement_import_model.create(cr, uid, dict(
data_file=ofx_file,
))
self.statement_import_model.import_file(cr, uid, [bank_statement_id], {'allow_auto_create_journal': True})
statement_id = self.bank_statement_model.search(cr, uid, [('name', '=', '000000123')])[0]
bank_st_record = self.bank_statement_model.browse(cr, uid, statement_id)
self.assertEquals(bank_st_record.balance_start, 2516.56)
self.assertEquals(bank_st_record.balance_end_real, 2156.56)
bank_statement = self.statement_import_model.create(
dict(data_file=ofx_file))
bank_statement.import_file()
bank_st_record = self.bank_statement_model.search(
[('name', '=', '000000123')])[0]
self.assertEquals(bank_st_record.balance_start, 2156.56)
self.assertEquals(bank_st_record.balance_end_real, 1796.56)
line = bank_st_record.line_ids[0]
self.assertEquals(line.name, 'Agrolait')
self.assertEquals(line.ref, '219378')
self.assertEquals(line.partner_id.id, self.ref('base.res_partner_2'))
self.assertEquals(
line.bank_account_id.id,
self.ref('account_bank_statement_import.ofx_partner_bank_1'))

58
account_bank_statement_import_qif/README.rst

@ -0,0 +1,58 @@
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
:alt: License: AGPL-3
Module to import QIF bank statements.
=====================================
This module allows you to import the machine readable QIF Files in Odoo: they are parsed and stored in human readable format in
Accounting \ Bank and Cash \ Bank Statements.
Important Note
---------------
Because of the QIF format limitation, we cannot ensure the same transactions aren't imported several times or handle multicurrency.
Whenever possible, you should use a more appropriate file format like OFX.
The module has been initiated by a backport of the new framework developed
by Odoo for V9 at its early stage. It's no more kept in sync with the V9 since
it has reach a stage where maintaining a pure backport of 9.0 in 8.0 is not
feasible anymore
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_ofx%0Aversion:%208.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Credits
=======
Contributors
------------
* Odoo SA
* Alexis de Lattre <alexis@via.ecp.fr>
* Laurent Mignon <laurent.mignon@acsone.eu>
* 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.

2
account_bank_statement_import_qif/__init__.py

@ -1,4 +1,2 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from . import account_bank_statement_import_qif from . import account_bank_statement_import_qif
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

24
account_bank_statement_import_qif/__openerp__.py

@ -1,28 +1,16 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# noqa: This is a backport from Odoo. OCA has no control over style here.
# flake8: noqa
{ {
'name': 'Import QIF Bank Statement', 'name': 'Import QIF Bank Statement',
'category': 'Accounting & Finance', 'category': 'Accounting & Finance',
'version': '1.0', 'version': '1.0',
'author': 'OpenERP SA',
'description': '''
Module to import QIF bank statements.
======================================
This module allows you to import the machine readable QIF Files in Odoo: they are parsed and stored in human readable format in
Accounting \ Bank and Cash \ Bank Statements.
Important Note
---------------------------------------------
Because of the QIF format limitation, we cannot ensure the same transactions aren't imported several times or handle multicurrency.
Whenever possible, you should use a more appropriate file format like OFX.
''',
'author': 'OpenERP SA,'
'Odoo Community Association (OCA)',
'website': 'https://github.com/OCA/bank-statement-import',
'images': [], 'images': [],
'depends': ['account_bank_statement_import'],
'demo': [],
'data': ['account_bank_statement_import_qif_view.xml'],
'depends': [
'account_bank_statement_import'
],
'auto_install': False, 'auto_install': False,
'installable': True, 'installable': True,
} }

85
account_bank_statement_import_qif/account_bank_statement_import_qif.py

@ -1,46 +1,44 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# noqa: This is a backport from Odoo. OCA has no control over style here.
# flake8: noqa
import dateutil.parser import dateutil.parser
import StringIO import StringIO
from openerp.tools.translate import _ from openerp.tools.translate import _
from openerp.osv import osv, fields
from openerp import api, models
from openerp.exceptions import Warning from openerp.exceptions import Warning
class account_bank_statement_import(osv.TransientModel):
_inherit = "account.bank.statement.import"
_columns = {
'journal_id': fields.many2one('account.journal', string='Journal', help='Accounting journal related to the bank statement you\'re importing. It has be be manually chosen for statement formats which doesn\'t allow automatic journal detection (QIF for example).'),
'hide_journal_field': fields.boolean('Hide the journal field in the view'),
}
def _get_hide_journal_field(self, cr, uid, context=None):
return context and 'journal_id' in context or False
class AccountBankStatementImport(models.TransientModel):
_inherit = "account.bank.statement.import"
_defaults = {
'hide_journal_field': _get_hide_journal_field,
}
@api.model
def _get_hide_journal_field(self):
return self.env.context.get('journal_id') and True
def _get_journal(self, cr, uid, currency_id, bank_account_id, account_number, context=None):
""" As .QIF format does not allow us to detect the journal, we need to let the user choose it.
We set it in context before to call super so it's the same as calling the widget from a journal """
if context is None:
context = {}
if context.get('active_id'):
record = self.browse(cr, uid, context.get('active_id'), context=context)
@api.model
def _get_journal(self, currency_id, bank_account_id, account_number):
""" As .QIF format does not allow us to detect the journal, we need to
let the user choose it.
We set it in context before to call super so it's the same as
calling the widget from a journal """
record = self
active_id = self.env.context.get('active_id')
if active_id:
record = self.browse(active_id)
if record.journal_id: if record.journal_id:
context['journal_id'] = record.journal_id.id
return super(account_bank_statement_import, self)._get_journal(cr, uid, currency_id, bank_account_id, account_number, context=context)
record = record.with_context(journal_id=record.journal_id.id)
return super(AccountBankStatementImport, record)._get_journal(
currency_id, bank_account_id, account_number)
def _check_qif(self, cr, uid, data_file, context=None):
@api.model
def _check_qif(self, data_file):
return data_file.strip().startswith('!Type:') return data_file.strip().startswith('!Type:')
def _parse_file(self, cr, uid, data_file, context=None):
if not self._check_qif(cr, uid, data_file, context=context):
return super(account_bank_statement_import, self)._parse_file(cr, uid, data_file, context=context)
@api.model
def _parse_file(self, data_file):
if not self._check_qif(data_file):
return super(AccountBankStatementImport, self)._parse_file(
data_file)
try: try:
file_data = "" file_data = ""
@ -64,22 +62,31 @@ class account_bank_statement_import(osv.TransientModel):
if not line: if not line:
continue continue
if line[0] == 'D': # date of transaction if line[0] == 'D': # date of transaction
vals_line['date'] = dateutil.parser.parse(line[1:], fuzzy=True).date()
vals_line['date'] = dateutil.parser.parse(
line[1:], fuzzy=True).date()
elif line[0] == 'T': # Total amount elif line[0] == 'T': # Total amount
total += float(line[1:].replace(',', '')) total += float(line[1:].replace(',', ''))
vals_line['amount'] = float(line[1:].replace(',', '')) vals_line['amount'] = float(line[1:].replace(',', ''))
elif line[0] == 'N': # Check number elif line[0] == 'N': # Check number
vals_line['ref'] = line[1:] vals_line['ref'] = line[1:]
elif line[0] == 'P': # Payee elif line[0] == 'P': # Payee
vals_line['name'] = 'name' in vals_line and line[1:] + ': ' + vals_line['name'] or line[1:]
# Since QIF doesn't provide account numbers, we'll have to find res.partner and res.partner.bank here
# (normal behavious is to provide 'account_number', which the generic module uses to find partner/bank)
ids = self.pool.get('res.partner.bank').search(cr, uid, [('owner_name', '=', line[1:])], context=context)
if ids:
vals_line['bank_account_id'] = bank_account_id = ids[0]
vals_line['partner_id'] = self.pool.get('res.partner.bank').browse(cr, uid, bank_account_id, context=context).partner_id.id
vals_line['name'] = ('name' in vals_line and
line[1:] + ': ' + vals_line['name'] or
line[1:])
# Since QIF doesn't provide account numbers, we'll have to
# find res.partner and res.partner.bank here
# (normal behavious is to provide 'account_number', which
# the generic module uses to find partner/bank)
banks = self.env['res.partner.bank'].search(
[('owner_name', '=', line[1:])], limit=1)
if banks:
bank_account = banks[0]
vals_line['bank_account_id'] = bank_account.id
vals_line['partner_id'] = bank_account.partner_id.id
elif line[0] == 'M': # Memo elif line[0] == 'M': # Memo
vals_line['name'] = 'name' in vals_line and vals_line['name'] + ': ' + line[1:] or line[1:]
vals_line['name'] = ('name' in vals_line and
vals_line['name'] + ': ' + line[1:] or
line[1:])
elif line[0] == '^': # end of item elif line[0] == '^': # end of item
transactions.append(vals_line) transactions.append(vals_line)
vals_line = {} vals_line = {}
@ -88,11 +95,11 @@ class account_bank_statement_import(osv.TransientModel):
else: else:
pass pass
else: else:
raise Warning(_('This file is either not a bank statement or is not correctly formed.'))
raise Warning(_('This file is either not a bank statement or is '
'not correctly formed.'))
vals_bank_statement.update({ vals_bank_statement.update({
'balance_end_real': total, 'balance_end_real': total,
'transactions': transactions 'transactions': transactions
}) })
return None, None, [vals_bank_statement] return None, None, [vals_bank_statement]

24
account_bank_statement_import_qif/account_bank_statement_import_qif_view.xml

@ -1,24 +0,0 @@
<?xml version="1.0" ?>
<openerp>
<data>
<record id="account_bank_statement_import_view_inherited" model="ir.ui.view">
<field name="name">Import Bank Statements Inherited</field>
<field name="model">account.bank.statement.import</field>
<field name="priority" eval="20"/>
<field name="inherit_id" ref="account_bank_statement_import.account_bank_statement_import_view" />
<field name="arch" type="xml">
<xpath expr="//field[@name='data_file']" position="after">
<field name="hide_journal_field" invisible="1"/>
<label for="journal_id"/>
<field name="journal_id"
domain="[('type', '=', 'bank')]"
attrs="{'invisible': [('hide_journal_field', '=', True)]}"
context="{'default_type':'bank'}"/>
</xpath>
</field>
</record>
</data>
</openerp>

49
account_bank_statement_import_qif/i18n/account_bank_statement_import_qif.pot

@ -0,0 +1,49 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * account_bank_statement_import_qif
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 8.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-06-08 12:01+0000\n"
"PO-Revision-Date: 2015-06-08 12:01+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: account_bank_statement_import_qif
#: help:account.bank.statement.import,journal_id:0
msgid "Accounting journal related to the bank statement you're importing. It has be be manually chosen for statement formats which doesn't allow automatic journal detection (QIF for example)."
msgstr ""
#. module: account_bank_statement_import_qif
#: code:addons/account_bank_statement_import_qif/account_bank_statement_import_qif.py:54
#, python-format
msgid "Could not decipher the QIF file."
msgstr ""
#. module: account_bank_statement_import_qif
#: field:account.bank.statement.import,hide_journal_field:0
msgid "Hide the journal field in the view"
msgstr ""
#. module: account_bank_statement_import_qif
#: model:ir.model,name:account_bank_statement_import_qif.model_account_bank_statement_import
msgid "Import Bank Statement"
msgstr ""
#. module: account_bank_statement_import_qif
#: field:account.bank.statement.import,journal_id:0
msgid "Journal"
msgstr ""
#. module: account_bank_statement_import_qif
#: code:addons/account_bank_statement_import_qif/account_bank_statement_import_qif.py:98
#, python-format
msgid "This file is either not a bank statement or is not correctly formed."
msgstr ""

2
account_bank_statement_import_qif/tests/__init__.py

@ -1,4 +1,2 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# noqa: This is a backport from Odoo. OCA has no control over style here.
# flake8: noqa
from . import test_import_bank_statement from . import test_import_bank_statement

36
account_bank_statement_import_qif/tests/test_import_bank_statement.py

@ -1,33 +1,29 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# noqa: This is a backport from Odoo. OCA has no control over style here.
# flake8: noqa
from openerp.tests.common import TransactionCase from openerp.tests.common import TransactionCase
from openerp.modules.module import get_module_resource from openerp.modules.module import get_module_resource
class TestQifFile(TransactionCase): class TestQifFile(TransactionCase):
"""Tests for import bank statement qif file format (account.bank.statement.import)
"""Tests for import bank statement qif file format
(account.bank.statement.import)
""" """
def setUp(self): def setUp(self):
super(TestQifFile, self).setUp() super(TestQifFile, self).setUp()
self.statement_import_model = self.registry('account.bank.statement.import')
self.bank_statement_model = self.registry('account.bank.statement')
self.bank_statement_line_model = self.registry('account.bank.statement.line')
self.statement_import_model = self.env['account.bank.statement.import']
self.statement_line_model = self.env['account.bank.statement.line']
def test_qif_file_import(self): def test_qif_file_import(self):
from openerp.tools import float_compare from openerp.tools import float_compare
cr, uid = self.cr, self.uid
qif_file_path = get_module_resource('account_bank_statement_import_qif', 'test_qif_file', 'test_qif.qif')
qif_file_path = get_module_resource(
'account_bank_statement_import_qif',
'test_qif_file', 'test_qif.qif')
qif_file = open(qif_file_path, 'rb').read().encode('base64') qif_file = open(qif_file_path, 'rb').read().encode('base64')
bank_statement_id = self.statement_import_model.create(cr, uid, dict(
data_file=qif_file,
))
context = {
'journal_id': self.registry('ir.model.data').get_object_reference(cr, uid, 'account', 'bank_journal')[1],
'allow_auto_create_journal': True,
}
self.statement_import_model.import_file(cr, uid, [bank_statement_id], context=context)
line_id = self.bank_statement_line_model.search(cr, uid, [('name', '=', 'YOUR LOCAL SUPERMARKET')])[0]
statement_id = self.bank_statement_line_model.browse(cr, uid, line_id).statement_id.id
bank_st_record = self.bank_statement_model.browse(cr, uid, statement_id)
assert float_compare(bank_st_record.balance_end_real, -1896.09, 2) == 0
bank_statement_improt = self.statement_import_model.with_context(
journal_id=self.ref('account.bank_journal')).create(
dict(data_file=qif_file))
bank_statement_improt.import_file()
bank_statement = self.statement_line_model.search(
[('name', '=', 'YOUR LOCAL SUPERMARKET')], limit=1)[0].statement_id
assert float_compare(bank_statement.balance_end_real, -1896.09, 2) == 0
Loading…
Cancel
Save