From 9945643158d360ea53e8abd6f93e0ad0d0712be4 Mon Sep 17 00:00:00 2001 From: "Ronald Portier (Therp BV)" Date: Thu, 2 Apr 2015 10:56:50 +0200 Subject: [PATCH] [ENH] Support multiple accounts/currencies. Copy of changes proposed for master. --- .../account_bank_statement_import.py | 286 ++++++++++-------- .../account_bank_statement_import_ofx.py | 8 +- .../account_bank_statement_import_qif.py | 4 +- 3 files changed, 160 insertions(+), 138 deletions(-) diff --git a/account_bank_statement_import/account_bank_statement_import.py b/account_bank_statement_import/account_bank_statement_import.py index 4344f3b..fc5e27e 100644 --- a/account_bank_statement_import/account_bank_statement_import.py +++ b/account_bank_statement_import/account_bank_statement_import.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +"""Framework for importing bank statement files.""" import base64 from openerp import api, models, fields @@ -50,7 +51,7 @@ class AccountBankStatementImport(models.TransientModel): @api.multi def import_file(self): """ Process the file chosen in the wizard, create bank statement(s) and - go to reconciliation. """ + go to reconciliation.""" self.ensure_one() data_file = base64.b64decode(self.data_file) statement_ids, notifications = self.with_context( @@ -70,15 +71,35 @@ class AccountBankStatementImport(models.TransientModel): @api.model 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 - 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 - 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 if not bank_account_id and account_number: journal_id = self.env.context.get('journal_id') @@ -90,13 +111,15 @@ class AccountBankStatementImport(models.TransientModel): 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) + 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 - 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 def _parse_file(self, data_file): @@ -105,69 +128,70 @@ class AccountBankStatementImport(models.TransientModel): 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) - - 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 - def _check_parsed_data(self, stmts_vals): + def _check_parsed_data(self, statements): """ Basic and structural verifications """ - if len(stmts_vals) == 0: + if len(statements) == 0: 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 - 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: 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[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 if account_number and len(account_number) > 4: bank_account_ids = self.env['res.partner.bank'].search( [('acc_number', '=', account_number)], limit=1) if bank_account_ids: bank_account_id = bank_account_ids[0].id - - return currency_id, bank_account_id + return bank_account_id @api.model 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: journal_id = bank_account.journal_id.id # 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_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 @api.model @@ -233,77 +263,68 @@ class AccountBankStatementImport(models.TransientModel): 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: - 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 - 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 """ bs_model = self.env['account.bank.statement'] 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 = [] - 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 notifications = [] num_ignored = len(ignored_statement_lines_import_ids) @@ -323,5 +344,4 @@ class AccountBankStatementImport(models.TransientModel): ignored_statement_lines_import_ids)]).ids } }] - - return statement_ids, notifications + return statement_id, notifications diff --git a/account_bank_statement_import_ofx/account_bank_statement_import_ofx.py b/account_bank_statement_import_ofx/account_bank_statement_import_ofx.py index 9358ae1..94bd49a 100644 --- a/account_bank_statement_import_ofx/account_bank_statement_import_ofx.py +++ b/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. " "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, 'transactions': transactions, '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] + }] diff --git a/account_bank_statement_import_qif/account_bank_statement_import_qif.py b/account_bank_statement_import_qif/account_bank_statement_import_qif.py index 0b9061e..e165473 100644 --- a/account_bank_statement_import_qif/account_bank_statement_import_qif.py +++ b/account_bank_statement_import_qif/account_bank_statement_import_qif.py @@ -84,7 +84,9 @@ class AccountBankStatementImport(models.TransientModel): 'not correctly formed.')) vals_bank_statement.update({ + 'currency_code': None, + 'account_number': None, 'balance_end_real': total, 'transactions': transactions }) - return None, None, [vals_bank_statement] + return [vals_bank_statement]