Browse Source

[ENH] Support multiple accounts/currencies.

Copy of changes proposed for master.
pull/15/head
Ronald Portier (Therp BV) 9 years ago
parent
commit
9945643158
  1. 286
      account_bank_statement_import/account_bank_statement_import.py
  2. 8
      account_bank_statement_import_ofx/account_bank_statement_import_ofx.py
  3. 4
      account_bank_statement_import_qif/account_bank_statement_import_qif.py

286
account_bank_statement_import/account_bank_statement_import.py

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Framework for importing bank statement files."""
import base64 import base64
from openerp import api, models, fields from openerp import api, models, fields
@ -50,7 +51,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 +71,35 @@ 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 = []
statements = self._parse_file(data_file)
# Check raw data:
self._check_parsed_data(statements)
# Import all statements:
for statement in statements:
(statement_id, new_notifications) = (
self._import_statement(statement))
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, statement):
"""Import a single bank-statement.
Return ids of created statements and notifications.
"""
currency_code = statement.pop('currency_code')
account_number = statement.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,13 +111,15 @@ 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)
statement = self._complete_statement(
statement, journal_id, account_number)
# Create the bank statement
return self._create_bank_statement(statement)
@api.model @api.model
def _parse_file(self, data_file): def _parse_file(self, data_file):
@ -105,69 +128,70 @@ class AccountBankStatementImport(models.TransientModel):
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
items marked by o) :
- 'name': string (e.g: '000000123')
- 'date': date (e.g: 2013-06-26)
-o 'balance_start': float (e.g: 8368.56)
-o 'balance_end_real': float (e.g: 8888.88)
- 'transactions': list of dict containing :
- 'name': string
(e.g: 'KBC-INVESTERINGSKREDIET 787-5562831-01')
- 'date': date
- 'amount': float
- 'unique_import_id': string
-o 'account_number': string
Will be used to find/create the res.partner.bank
in odoo
-o 'note': string
-o 'partner_name': string
-o 'ref': string
- bank statements data: list of dict containing (optional
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')
- 'date': date (e.g: 2013-06-26)
-o 'balance_start': float (e.g: 8368.56)
-o 'balance_end_real': float (e.g: 8888.88)
- 'transactions': list of dict containing :
- 'name': string
(e.g: 'KBC-INVESTERINGSKREDIET 787-5562831-01')
- 'date': date
- 'amount': float
- 'unique_import_id': string
-o 'account_number': string
Will be used to find/create the res.partner.bank
in odoo
-o 'note': string
-o 'partner_name': 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 ?'
))
@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:
raise Warning(_('This file doesn\'t contain any transaction.'))
for statement in statements:
if 'transactions' in statement and statement['transactions']:
return
# If we get here, no transaction was found:
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, account_number):
@ -189,17 +213,23 @@ 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:
# as the bank statement. When journal has no currency, currency must
# be equal to company currency.
if journal_id and currency_id:
journal_currency_id = self.env['account.journal'].browse( journal_currency_id = self.env['account.journal'].browse(
journal_id).currency.id 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.'))
if 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 !'
))
else:
if currency_id != self.env.user.company_id.currency_id.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
@ -233,77 +263,68 @@ 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
def _complete_statement(self, statement, journal_id, account_number):
statement['journal_id'] = journal_id
for line_vals in statement['transactions']:
unique_import_id = line_vals.get('unique_import_id', False)
if unique_import_id:
line_vals['unique_import_id'] = (
account_number and account_number + '-' or '') + \
unique_import_id
for line_vals in st_vals['transactions']:
unique_import_id = line_vals.get('unique_import_id', False)
if unique_import_id:
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
bank_account_id = False
identifying_string = line_vals.get('account_number')
if identifying_string:
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:
bank_account_id = self._create_bank_account(
identifying_string).id
line_vals['partner_id'] = partner_id
line_vals['bank_account_id'] = bank_account_id
return stmts_vals
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
bank_account_id = False
identifying_string = line_vals.get('account_number')
if identifying_string:
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:
bank_account_id = self._create_bank_account(
identifying_string).id
line_vals['partner_id'] = partner_id
line_vals['bank_account_id'] = bank_account_id
return statement
@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, statement):
""" 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 = []
# Filter out already imported transactions and create statement
ignored_statement_lines_import_ids = [] ignored_statement_lines_import_ids = []
for st_vals in stmts_vals:
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)):
filtered_st_lines.append(line_vals)
else:
ignored_statement_lines_import_ids.append(
line_vals['unique_import_id'])
if len(filtered_st_lines) > 0:
# Remove values that won't be used to create records
st_vals.pop('transactions', None)
for line_vals in filtered_st_lines:
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.'))
filtered_st_lines = []
for line_vals in statement['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)
else:
ignored_statement_lines_import_ids.append(unique_id)
statement_id = False
if len(filtered_st_lines) > 0:
# Remove values that won't be used to create records
statement.pop('transactions', None)
for line_vals in filtered_st_lines:
line_vals.pop('account_number', None)
# Create the statement
statement['line_ids'] = [
[0, False, line] for line in filtered_st_lines]
statement_id = bs_model.create(statement).id
# Prepare import feedback # Prepare import feedback
notifications = [] notifications = []
num_ignored = len(ignored_statement_lines_import_ids) num_ignored = len(ignored_statement_lines_import_ids)
@ -323,5 +344,4 @@ class AccountBankStatementImport(models.TransientModel):
ignored_statement_lines_import_ids)]).ids ignored_statement_lines_import_ids)]).ids
} }
}] }]
return statement_ids, notifications
return statement_id, notifications

8
account_bank_statement_import_ofx/account_bank_statement_import_ofx.py

@ -67,12 +67,12 @@ class AccountBankStatementImport(models.TransientModel):
raise Warning(_("The following problem occurred during import. " raise Warning(_("The following problem occurred during import. "
"The file might not be valid.\n\n %s" % e.message)) "The file might not be valid.\n\n %s" % e.message))
vals_bank_statement = {
return [{
'currency_code': ofx.account.statement.currency,
'account_number': ofx.account.number,
'name': ofx.account.routing_number, 'name': ofx.account.routing_number,
'transactions': transactions, 'transactions': transactions,
'balance_start': ofx.account.statement.balance, 'balance_start': ofx.account.statement.balance,
'balance_end_real': 'balance_end_real':
float(ofx.account.statement.balance) + total_amt, float(ofx.account.statement.balance) + total_amt,
}
return ofx.account.statement.currency, ofx.account.number, [
vals_bank_statement]
}]

4
account_bank_statement_import_qif/account_bank_statement_import_qif.py

@ -84,7 +84,9 @@ class AccountBankStatementImport(models.TransientModel):
'not correctly formed.')) 'not correctly formed.'))
vals_bank_statement.update({ vals_bank_statement.update({
'currency_code': None,
'account_number': None,
'balance_end_real': total, 'balance_end_real': total,
'transactions': transactions 'transactions': transactions
}) })
return None, None, [vals_bank_statement]
return [vals_bank_statement]
Loading…
Cancel
Save