[FIX] account: bank statement import

- use a wizard to ask about creating a new bank account + journal
- fix 'Import Statement' action from the dashboard
- make l10n_be_coda new-api-compliant
This commit is contained in:
Arthur Maniet
2015-05-31 16:20:15 +02:00
committed by Arthur Maniet
parent cc5a2e44a8
commit 2d9569cc18
9 changed files with 100 additions and 104 deletions
+3 -1
View File
@@ -343,5 +343,7 @@ class account_journal(models.Model):
action_name = 'action_account_bank_statement_import'
ir_model_obj = self.pool['ir.model.data']
model, action_id = ir_model_obj.get_object_reference(self._cr, self._uid, 'account_bank_statement_import', action_name)
action = self.pool[model].read(self._cr, self._uid, action_id, context=self._context)
action = self.pool[model].read(self._cr, self._uid, action_id, context=self.env.context)
# Note: this drops action['context'], which is a dict stored as a string, which is not easy to update
action.update({'context': (u"{'journal_id': " + str(self.id) + u"}")})
return action
@@ -30,25 +30,24 @@ class AccountBankStatementImport(models.TransientModel):
def import_file(self):
""" Process the file chosen in the wizard, create bank statement(s) and go to reconciliation. """
self.ensure_one()
rec = self.with_context(active_id=self.ids[0])
#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)
data_file = self.data_file
# The appropriate implementation module returns the required data
currency_code, account_number, stmts_vals = rec._parse_file(base64.b64decode(data_file))
# Let the appropriate implementation module parse the file and return the required data
# The active_id is passed in context in case an implementation module requires information about the wizard state (see QIF)
currency_code, account_number, stmts_vals = self.with_context(active_id=self.ids[0])._parse_file(base64.b64decode(data_file))
# Check raw data
rec._check_parsed_data(stmts_vals)
# Try to find the bank account and currency in odoo
currency_id, bank_account_id = rec._find_additional_data(currency_code, account_number)
# Find or create the bank journal
journal_id = rec._get_journal(currency_id, bank_account_id, account_number)
# Create the bank account if not already existing
if not bank_account_id and account_number:
rec._create_bank_account(account_number, journal_id=journal_id)
self._check_parsed_data(stmts_vals)
# Try to find the currency and journal in odoo
currency, journal, bank_account = self._find_additional_data(currency_code, account_number)
# If no journal found, ask the user about creating one
if not journal:
# The active_id is passed in context so the wizard can call import_file again once the journal is created
return self.with_context(active_id=self.ids[0])._journal_creation_wizard(currency, account_number, bank_account)
# Prepare statement data to be used for bank statements creation
stmts_vals = rec._complete_stmts_vals(stmts_vals, journal_id, account_number)
stmts_vals = self._complete_stmts_vals(stmts_vals, journal, account_number)
# Create the bank statements
statement_ids, notifications = rec._create_bank_statements(stmts_vals)
statement_ids, notifications = self._create_bank_statements(stmts_vals)
# Finally dispatch to reconciliation interface
action = self.env.ref('account.action_bank_reconcile_bank_statements')
return {
@@ -61,6 +60,24 @@ class AccountBankStatementImport(models.TransientModel):
'type': 'ir.actions.client',
}
def _journal_creation_wizard(self, currency, account_number, bank_account):
""" Calls a wizard that allows the user to accept/refuse journal creation """
return {
'name': _('Journal Creation'),
'type': 'ir.actions.act_window',
'res_model': 'account.bank.statement.import.journal.creation',
'view_type': 'form',
'view_mode': 'form',
'target': 'new',
'context': {
'statement_import_transient_id': self.env.context['active_id'],
'default_currency_id': currency and currency.id or False,
'default_account_number': account_number,
'bank_account_id': bank_account and bank_account.id or False,
'default_name': _('Bank') + ' ' + account_number,
}
}
def _parse_file(self, data_file):
""" 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.
@@ -102,83 +119,71 @@ class AccountBankStatementImport(models.TransientModel):
def _find_additional_data(self, currency_code, account_number):
""" 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
company_currency = self.env.user.company_id.currency_id
journal_obj = self.env['account.journal']
currency = None
if currency_code:
currency = self.env['res.currency'].search([('name', '=ilike', currency_code)], limit=1)
company_currency = self.env.user.company_id.currency_id
if currency.id != company_currency.id:
currency_id = currency.id
if not currency:
raise osv.except_osv(_("No currency found matching '%s'.") % currency_code)
if currency == company_currency:
currency = False
bank_account_id = None
bank_account = None
if account_number and len(account_number) > 4:
account_number = account_number.replace(' ', '').replace('-', '')
self.env.cr.execute("select id from res_partner_bank where replace(replace(acc_number,' ',''),'-','') = %s", (account_number,))
bank_account_ids = [id[0] for id in self.env.cr.fetchall()]
bank_account_ids = self.env['res.partner.bank'].search([('id', 'in', bank_account_ids)], limit=1)
if bank_account_ids:
bank_account_id = bank_account_ids.id
return currency_id, bank_account_id
def _get_journal(self, currency_id, bank_account_id, account_number):
""" Find or create the journal """
ResPartnerBank = self.env['res.partner.bank']
bank_account = self.env['res.partner.bank'].search([('acc_number', '=', account_number)], limit=1)
# Find the journal from context or bank account
journal_id = self._context.get('journal_id')
if bank_account_id:
bank_account = ResPartnerBank.browse(bank_account_id)
if journal_id:
if bank_account.journal_id.id and bank_account.journal_id.id != journal_id:
journal = journal_obj.browse(self._context.get('journal_id', []))
if bank_account:
if journal:
if bank_account.journal_id and bank_account.journal_id != journal:
raise UserError(_('The account of this statement is linked to another journal.'))
if not bank_account.journal_id.id:
bank_account.write({'journal_id': journal_id})
if not bank_account.journal_id:
bank_account.write({'journal_id': journal.id})
else:
if bank_account.journal_id.id:
journal_id = bank_account.journal_id.id
if bank_account.journal_id:
journal = bank_account.journal_id
# 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.id
if currency_id and currency_id != journal_currency_id:
raise UserError(_('The currency of the bank statement is not the same as the currency of the journal !'))
# If importing into an existing journal
if journal:
# The bank account cannot belong to another journal
if not bank_account and account_number:
journal_account = self.env['res.partner.bank'].search([('journal_id', '=', journal.id)], limit=1)
if journal_account:
raise UserError(_('You are importing a file from account %s while the account of journal %s is %s.') % (account_number, journal.name, journal_account.acc_number))
# If there is no journal, create one (and its account)
if not journal_id and account_number:
company = self.env.user.company_id
journal_vals = self.env['account.journal']._prepare_bank_journal(company, {'account_type': 'bank', 'acc_name': account_number, 'currency_id': currency_id})
journal_id = self.env['account.journal'].create(journal_vals).id
if bank_account_id:
bank_account.write({'journal_id': journal_id})
# Its currency must be the same as the bank statement
journal_currency = journal.currency_id
if currency == None:
currency = journal_currency
if currency and currency != journal_currency:
statement_cur_code = currency == False and company_currency.name or currency.name
journal_cur_code = not journal_currency and company_currency.name or journal_currency.name
raise UserError(_('The currency of the bank statement (%s) is not the same as the currency of the journal (%s) !') % (statement_cur_code, journal_cur_code))
# If we couldn't find/create a journal, everything is lost
if not journal_id:
# If we couldn't find / can't create a journal, everything is lost
if not journal and not account_number:
raise UserError(_('Cannot find in which journal import this statement. Please manually select a journal.'))
return journal_id
def _create_bank_account(self, account_number, journal_id=False):
return currency, journal, bank_account
def _create_bank_account(self, account_number):
try:
bank_type = self.env.ref('bank.bank_normal')
bank_code = bank_type.code
except ValueError:
bank_code = 'bank'
account_number = account_number.replace(' ', '').replace('-', '')
vals_acc = {
return self.env['res.partner.bank'].create({
'acc_number': account_number,
'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.
if journal_id:
vals_acc['journal_id'] = journal_id
vals_acc['company_id'] = self.env.user.company_id.id
vals_acc['partner_id'] = self.env.user.company_id.partner_id.id
})
return self.env['res.partner.bank'].create(vals_acc)
def _complete_stmts_vals(self, stmts_vals, journal_id, account_number):
def _complete_stmts_vals(self, stmts_vals, journal, account_number):
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']:
unique_import_id = line_vals.get('unique_import_id')
@@ -19,7 +19,7 @@ class account_bank_statement_import_journal_creation(models.TransientModel):
currency_id = wiz.currency_id.id
account_number = wiz.account_number
bank_account_id = self.env._context.get('bank_account_id')
bank_account_id = self.env.context.get('bank_account_id')
if bank_account_id:
vals = {'currency_id': currency_id, 'acc_name': account_number, 'account_type': 'bank'}
vals_journal = journal_obj._prepare_bank_journal(company, vals)
@@ -38,5 +38,5 @@ class account_bank_statement_import_journal_creation(models.TransientModel):
self.env['res.partner.bank'].create(res_partner_bank_vals)
# Finish the statement import
statement_import_transient = import_wiz_obj.browse(self.env._context['statement_import_transient_id'])
statement_import_transient = import_wiz_obj.browse(self.env.context['statement_import_transient_id'])
return statement_import_transient.import_file()
@@ -11,7 +11,7 @@
<p>Just click OK to create the account/journal and finish the import. If this was a mistake, hit cancel to abort the import.</p>
<group>
<field name="name"/>
<field name="currency_id" options="{'no_create': True}"/>
<field name="currency_id" options="{'no_create': True}" attrs="{'invisible': [('currency_id', '=', False)]}"/>
<field name="account_number"/>
</group>
<footer>
@@ -11,19 +11,18 @@ class AccountBankStatementImport(models.TransientModel):
_inherit = "account.bank.statement.import"
def _get_hide_journal_field(self):
return self._context and 'journal_id' in self._context or False
return self.env.context and 'journal_id' in self.env.context or False
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(string='Hide the journal field in the view', default=_get_hide_journal_field)
def _get_journal(self, currency_id, bank_account_id, account_number):
def _find_additional_data(self, *args):
""" 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 self._context.get('active_id'):
record = self.browse(self._context.get('active_id'))
if record.journal_id:
return super(AccountBankStatementImport, self).with_context(journal_id=record.journal_id.id)._get_journal(currency_id, bank_account_id, account_number)
return super(AccountBankStatementImport, self)._get_journal(currency_id, bank_account_id, account_number)
We set it in context in the same way it's done when calling the import action from a journal.
"""
if self.journal_id:
self.env.context = dict(self.env.context, journal_id=self.journal_id.id)
return super(AccountBankStatementImport, self)._find_additional_data(*args)
def _check_qif(self, data_file):
return data_file.strip().startswith('!Type:')
@@ -10,11 +10,9 @@
<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'}"/>
<group attrs="{'invisible': [('hide_journal_field', '=', True)]}">
<field name="journal_id" domain="[('type', '=', 'bank')]" context="{'default_type':'bank'}"/>
</group>
</xpath>
</field>
</record>
-1
View File
@@ -57,7 +57,6 @@ Wizards provided by this module:
'account',
'base_vat',
'base_iban',
'l10n_be_coda',
'l10n_multilang',
],
'data': [
+2 -2
View File
@@ -71,14 +71,14 @@ description provided by the CODA configuration tables is based upon the CODA
V2.2 specifications.
If required, you can manually adjust the descriptions via the CODA configuration menu.
''',
'depends': ['account_voucher', 'base_iban', 'l10n_be_invoice_bba', 'account_bank_statement_import'],
'depends': ['account_accountant', 'l10n_be'],
'demo': [
'l10n_be_coda_demo.xml',
],
'data': [
'l10n_be_coda_view.xml',
],
'auto_install': False,
'auto_install': True,
'website': 'https://www.odoo.com/page/accounting',
'installable': True,
'license': 'AGPL-3',
@@ -22,28 +22,23 @@
import time
import re
from openerp.osv import osv
from openerp.tools.translate import _
from openerp import tools
from openerp import models, tools, _
from openerp.exceptions import UserError
import logging
class AccountBankStatementImport(models.TransientModel):
_inherit = 'account.bank.statement.import'
_logger = logging.getLogger(__name__)
class account_bank_statement_import(osv.TransientModel):
_inherit = "account.bank.statement.import"
def _check_coda(self, cr, uid, data_file, context=None):
def _check_coda(self, data_file):
# Matches the first 24 characters of a CODA file, as defined by the febelfin specifications
return re.match('0{5}\d{9}05[ D] +', data_file) != None
def _parse_file(self, cr, uid, data_file, context=None):
if not self._check_coda(cr, uid, data_file, context=context):
return super(account_bank_statement_import, self)._parse_file(cr, uid, data_file, context=context)
def _parse_file(self, data_file):
if not self._check_coda(data_file):
return super(AccountBankStatementImport, self)._parse_file(data_file)
def rmspaces(s):
return " ".join(s.split())
if context is None:
context = {}
recordlist = unicode(data_file, 'windows-1252', 'strict').split('\n')
statements = []
globalisation_comm = {}
@@ -251,5 +246,3 @@ class account_bank_statement_import(osv.TransientModel):
currency_code = statement['currency']
acc_number = statements[0] and statements[0]['acc_number'] or False
return currency_code, acc_number, ret_statements
def rmspaces(s):
return " ".join(s.split())