|
@ -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 |
|
@ -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,14 +128,13 @@ 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 |
|
|
- bank statements data: list of dict containing (optional |
|
|
items marked by o) : |
|
|
items marked by o) : |
|
|
|
|
|
-o currency code: string (e.g: 'EUR') |
|
|
|
|
|
The ISO 4217 currency code, case insensitive |
|
|
|
|
|
-o account number: string (e.g: 'BE1234567890') |
|
|
|
|
|
The number of the bank account which the statement |
|
|
|
|
|
belongs to |
|
|
- 'name': string (e.g: '000000123') |
|
|
- 'name': string (e.g: '000000123') |
|
|
- 'date': date (e.g: 2013-06-26) |
|
|
- 'date': date (e.g: 2013-06-26) |
|
|
-o 'balance_start': float (e.g: 8368.56) |
|
|
-o 'balance_start': float (e.g: 8368.56) |
|
@ -130,44 +152,46 @@ class AccountBankStatementImport(models.TransientModel): |
|
|
-o 'partner_name': string |
|
|
-o 'partner_name': string |
|
|
-o 'ref': string |
|
|
-o 'ref': string |
|
|
""" |
|
|
""" |
|
|
raise Warning(_('Could not make sense of the given file.\nDid you ' |
|
|
|
|
|
'install the module to support this type of file ?')) |
|
|
|
|
|
|
|
|
raise Warning(_( |
|
|
|
|
|
'Could not make sense of the given file.\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: |
|
|
|
|
|
|
|
|
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.')) |
|
|
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 journal_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 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.')) |
|
|
|
|
|
|
|
|
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,11 +263,9 @@ class AccountBankStatementImport(models.TransientModel): |
|
|
default_currency=currency_id).create(vals_acc) |
|
|
default_currency=currency_id).create(vals_acc) |
|
|
|
|
|
|
|
|
@api.model |
|
|
@api.model |
|
|
def _complete_stmts_vals(self, stmts_vals, journal_id, account_number): |
|
|
|
|
|
for st_vals in stmts_vals: |
|
|
|
|
|
st_vals['journal_id'] = journal_id |
|
|
|
|
|
|
|
|
|
|
|
for line_vals in st_vals['transactions']: |
|
|
|
|
|
|
|
|
def _complete_statement(self, 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) |
|
|
unique_import_id = line_vals.get('unique_import_id', False) |
|
|
if unique_import_id: |
|
|
if unique_import_id: |
|
|
line_vals['unique_import_id'] = ( |
|
|
line_vals['unique_import_id'] = ( |
|
@ -264,46 +292,39 @@ class AccountBankStatementImport(models.TransientModel): |
|
|
identifying_string).id |
|
|
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 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 = [] |
|
|
filtered_st_lines = [] |
|
|
for line_vals in st_vals['transactions']: |
|
|
|
|
|
if 'unique_import_id' not in line_vals \ |
|
|
|
|
|
or not line_vals['unique_import_id'] \ |
|
|
|
|
|
or not bool(bsl_model.sudo().search( |
|
|
|
|
|
[('unique_import_id', '=', |
|
|
|
|
|
line_vals['unique_import_id'])], |
|
|
|
|
|
limit=1)): |
|
|
|
|
|
|
|
|
for line_vals in 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) |
|
|
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(unique_id) |
|
|
|
|
|
statement_id = False |
|
|
if len(filtered_st_lines) > 0: |
|
|
if len(filtered_st_lines) > 0: |
|
|
# Remove values that won't be used to create records |
|
|
# Remove values that won't be used to create records |
|
|
st_vals.pop('transactions', None) |
|
|
|
|
|
|
|
|
statement.pop('transactions', None) |
|
|
for line_vals in filtered_st_lines: |
|
|
for line_vals in filtered_st_lines: |
|
|
line_vals.pop('account_number', None) |
|
|
line_vals.pop('account_number', None) |
|
|
# Create the satement |
|
|
|
|
|
st_vals['line_ids'] = [[0, False, line] for line in |
|
|
|
|
|
filtered_st_lines] |
|
|
|
|
|
statement_ids.append(bs_model.create(st_vals).id) |
|
|
|
|
|
if len(statement_ids) == 0: |
|
|
|
|
|
raise Warning(_('You have already imported that file.')) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Create the statement |
|
|
|
|
|
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 |