From b2c06ac3242be2c3da9ff854862e82a07557731b Mon Sep 17 00:00:00 2001 From: "Pieter J. Kersten" Date: Tue, 16 Feb 2010 23:15:11 +0100 Subject: [PATCH] [FIX] account_banking: no results in wizard when no errors in process [FIX] account_banking: currency from transaction was not taken into account [FIX] account_banking: last resulting bank statements tree was immutable [FIX] account_banking: right menu action in bank_statement triggered backtrace [IMP] account_banking: sorting now on id to maintain order of import --- account_banking/account_banking.py | 3 + account_banking/account_banking_view.xml | 36 +- account_banking/wizard/bank_import.py | 1017 +++++++++++----------- account_banking/wizard/banktools.py | 44 +- 4 files changed, 579 insertions(+), 521 deletions(-) diff --git a/account_banking/account_banking.py b/account_banking/account_banking.py index 4d296ffc3..c9a51b574 100644 --- a/account_banking/account_banking.py +++ b/account_banking/account_banking.py @@ -111,6 +111,7 @@ class account_banking_imported_file(osv.osv): '''Imported Bank Statements File''' _name = 'account.banking.imported.file' _description = __doc__ + _rec_name = 'date' _columns = { 'company_id': fields.many2one('res.company', 'Company', select=True, readonly=True @@ -147,8 +148,10 @@ class account_bank_statement(osv.osv): 2. Extended 'button_confirm' trigger to cope with the period per statement_line situation. 3. Added optional relation with imported statements file + 4. Ordering is based on auto generated id. ''' _inherit = 'account.bank.statement' + _order = 'id' #def _currency(self, cursor, user, ids, name, args, context=None): # ''' diff --git a/account_banking/account_banking_view.xml b/account_banking/account_banking_view.xml index 39a9a527b..42bcd1b61 100644 --- a/account_banking/account_banking_view.xml +++ b/account_banking/account_banking_view.xml @@ -29,7 +29,7 @@ id="menu_action_account_banking_bank_accounts" parent="account_banking.menu_finance_banking_settings" action="action_account_banking_res_partner_banks" - sequence="12" + sequence="10" /> @@ -74,7 +74,7 @@ id="menu_action_account_banking_bank_journals" parent="account_banking.menu_finance_banking_settings" action="action_account_banking_journals" - sequence="12" + sequence="20" /> @@ -142,7 +142,7 @@ + + + account.bank.statement.tree.banking-2 + + account.bank.statement + tree + + + + + + + + + + account.bank.statement.form.banking-4 + + account.bank.statement + form + + + + + + + account.bank.statement.form.banking-4 diff --git a/account_banking/wizard/bank_import.py b/account_banking/wizard/bank_import.py index 0f9d6258b..48e60a9da 100644 --- a/account_banking/wizard/bank_import.py +++ b/account_banking/wizard/bank_import.py @@ -31,543 +31,548 @@ from tools import config from tools.translate import _ from account_banking.parsers import models from account_banking.parsers.convert import * +from account_banking.struct import struct from banktools import * -def _get_move_info(pool, cursor, uid, move_line): - reconcile_obj = pool.get('account.bank.statement.reconcile') - type_map = { - 'out_invoice': 'customer', - 'in_invoice': 'supplier', - 'out_refund': 'customer', - 'in_refund': 'supplier', - } - retval = struct(move_line=move_line) - retval.reference = move_line.ref - if move_line.invoice: - retval.invoice = move_line.invoice - retval.type = type_map[move_line.invoice.type] - else: - retval.type = 'general' - move_line.reconcile_id = reconcile_obj.create( - cursor, uid, {'line_ids': [(6, 0, [move_line.id])]} - ) - return retval - -def _link_payment(pool, cursor, uid, trans, payment_lines, - partner_id, bank_account_id, log): - ''' - Find the payment order belonging to this reference - if there is one - This is the easiest part: when sending payments, the returned bank info - should be identical to ours. - ''' - # TODO: Not sure what side effects are created when payments are done - # for credited customer invoices, which will be matched later on too. - digits = int(config['price_accuracy']) - candidates = [x for x in payment_lines - if x.communication == trans.reference - and round(x.amount, digits) == -round(trans.transferred_amount, digits) - and trans.remote_account in (x.bank_id.acc_number, - x.bank_id.iban) - ] - if len(candidates) == 1: - candidate = candidates[0] - payment_line_obj = pool.get('payment.line') - payment_line_obj.write(cursor, uid, [candidate.id], { - 'export_state': 'done', - 'date_done': trans.effective_date.strftime('%Y-%m-%d')} - ) - - return _get_move_info(pool, cursor, uid, candidate.move_line_id) - - return False - -def _link_invoice(pool, cursor, uid, trans, move_lines, - partner_id, bank_account_id, log): - ''' - Find the invoice belonging to this reference - if there is one - Use the sales journal to check. - - Challenges we're facing: - 1. The sending or receiving party is not necessarily the same as the - partner the payment relates to. - 2. References can be messed up during manual encoding and inexact - matching can link the wrong invoices. - 3. Amounts can or can not match the expected amount. - 4. Multiple invoices can be paid in one transaction. - .. There are countless more, but these we'll try to address. - - Assumptions for matching: - 1. There are no payments for invoices not sent. These are dealt with - later on. - 1. Debit amounts are either customer invoices or credited supplier - invoices. - 2. Credit amounts are either supplier invoices or credited customer - invoices. - 3. Payments are either below expected amount or only slightly above - (abs). - 4. Payments from partners that are matched, pay their own invoices. - - Worst case scenario: - 1. No match was made. - No harm done. Proceed with manual matching as usual. - 2. The wrong match was made. - Statements are encoded in draft. You will have the opportunity to - manually correct the wrong assumptions. - ''' - # First on partner - candidates = [x for x in move_lines if x.partner_id.id == partner_id] - - # Next on reference/invoice number. Mind that this uses the invoice - # itself, as the move_line references have been fiddled with on invoice - # creation. This also enables us to search for the invoice number in the - # reference instead of the other way around, as most human interventions - # *add* text. - if not candidates: - ref = trans.reference.upper() - msg = trans.message.upper() - candidates = [x for x in move_lines - if x.invoice.number.upper() in ref or - x.invoice.number.upper() in msg - ] - - if len(candidates) > 1: - # TODO: currency coercing - digits = int(config['price_accuracy']) - if trans.transferred_amount < 0: - func = lambda x, y=abs(trans.transferred_amount), z=digits:\ - round(x.debit, z) == round(y, z) - else: - func = lambda x, y=abs(trans.transferred_amount), z=digits:\ - round(x.credit, z) == round(y, z) - best = [x for x in move_lines if func(x)] - if len(best) != 1: - log.append( - _('Unable to link transaction %(trans)s to invoice: ' - '%(no_candidates)s candidates found; can\'t choose.') % { - 'trans': trans.id, - 'no_candidates': len(best) - }) - return False - - if len(candidates) == 1: - return _get_move_info(pool, cursor, uid, candidates[0]) - - return False - -def _link_canceled_debit(pool, cursor, uid, trans, payment_lines, - partner_id, bank_account_id, log): - ''' - Direct debit transfers can be canceled by the remote owner within a - legaly defined time period. These 'payments' are most likely - already marked 'done', which makes them harder to match. Also the - reconciliation has to be reversed. - ''' - # TODO: code _link_canceled_debit - return False - -def _banking_import_statements_file(self, cursor, uid, data, context): - ''' - Import bank statements / bank transactions file. - This module/function represents the business logic, the parser modules - represent the decoding logic. - ''' - form = data['form'] - statements_file = form['file'] - data = base64.decodestring(statements_file) - - pool = pooler.get_pool(cursor.dbname) - company_obj = pool.get('res.company') - user_obj = pool.get('res.user') - journal_obj = pool.get('account.journal') - move_line_obj = pool.get('account.move.line') - payment_line_obj = pool.get('payment.line') - statement_obj = pool.get('account.bank.statement') - statement_line_obj = pool.get('account.bank.statement.line') - statement_file_obj = pool.get('account.banking.imported.file') - #account_obj = pool.get('account.account') - #payment_order_obj = pool.get('payment.order') - #currency_obj = pool.get('res.currency') - - # get the parser to parse the file - parser_code = form['parser'] - parser = models.create_parser(parser_code) - if not parser: - raise wizard.except_wizard( - _('ERROR!'), - _('Unable to import parser %(parser)s. Parser class not found.') % - {'parser':parser_code} - ) - - # Get the company - company = form['company'] - if not company: - user_data = user_obj.browse(cursor, uid, uid, context) - company = company_obj.browse( - cursor, uid, company or user_data.company_id.id, context - ) - - # Parse the file - statements = parser.parse(data) - - if any([x for x in statements if not x.is_valid()]): - raise wizard.except_wizard( - _('ERROR!'), - _('The imported statements appear to be invalid! Check your file.') - ) - - # Create the file now, as the statements need to be linked to it - import_id = statement_file_obj.create(cursor, uid, dict( - company_id = company.id, - file = statements_file, - date = time.strftime('%Y-%m-%d'), - user_id = uid, - state = 'unfinished' - )) - - # Results - no_stat_loaded = 0 - no_trans_loaded = 0 - no_stat_skipped = 0 - no_trans_skipped = 0 - no_trans_matched = 0 - no_errors = 0 - log = [] - - # Caching - error_accounts = {} - info = {} - imported_statement_ids = [] - - if statements: - # Get interesting journals once - if company: - journal_ids = journal_obj.search(cursor, uid, [ - ('type', 'in', ('sale','purchase')), - ('company_id', '=', company.id), - ]) - else: - journal_ids = None - if not journal_ids: - journal_ids = journal_obj.search(cursor, uid, [ - ('type', 'in', ('sale','purchase')), - ('active', '=', True), - ('company_id', '=', False), - ]) - # Get all unreconciled moves predating the last statement in one big - # swoop. Assumption: the statements in the file are sorted in ascending - # order of date. - move_line_ids = move_line_obj.search(cursor, uid, [ - ('reconcile_id', '=', False), - ('journal_id', 'in', journal_ids), - ('account_id.reconcile', '=', True), - ('date', '<=', date2str(statements[-1].date)), - ]) - move_lines = move_line_obj.browse(cursor, uid, move_line_ids) - - # Get all unreconciled sent payment lines in one big swoop. - # No filtering can be done, as empty dates carry value for C2B - # communication. Most likely there are much less sent payments - # than reconciled and open/draft payments. - cursor.execute("SELECT l.id FROM payment_order o, payment_line l " - "WHERE l.order_id = o.id AND " - "o.state = 'sent' AND " - "l.date_done IS NULL" - ) - payment_line_ids = [x[0] for x in cursor.fetchall()] - if payment_line_ids: - payment_lines = payment_line_obj.browse(cursor, uid, payment_line_ids) - else: - payment_lines = [] - - for statement in statements: - if statement.local_account in error_accounts: - # Don't repeat messages - no_stat_skipped += 1 - no_trans_skipped += len(statement.transactions) - continue - - if not statement.local_account in info: - account_info = get_company_bank_account( - pool, cursor, uid, statement.local_account, company, log - ) - if not account_info: - log.append( - _('Statements found for unknown account %(bank_account)s') % - {'bank_account': statement.local_account} - ) - error_accounts[statement.local_account] = True - no_errors += 1 - continue - if 'journal_id' not in account_info: - log.append( - _('Statements found for account %(bank_account)s, ' - 'but no default journal was defined.' - ) % {'bank_account': statement.local_account} - ) - error_accounts[statement.local_account] = True - no_errors += 1 - continue - info[statement.local_account] = account_info - else: - account_info = info[statement.local_account] - - if statement.local_currency \ - and account_info.journal_id.code != statement.local_currency: - # TODO: convert currencies? - log.append( - _('Statement for account %(bank_account)s uses different ' - 'currency than the defined bank journal.') % - {'bank_account': statement.local_account} - ) - error_accounts[statement.local_account] = True - no_errors += 1 - continue - - # Check existence of previous statement - statement_ids = statement_obj.search(cursor, uid, [ - ('name', '=', statement.id), - ('date', '=', date2str(statement.date)), - ]) - if statement_ids: - log.append( - _('Statement %(id)s known - skipped') % { - 'id': statement.id - } - ) - continue - - statement_id = statement_obj.create(cursor, uid, dict( - name = statement.id, - journal_id = account_info.journal_id.id, - date = date2str(statement.date), - balance_start = statement.start_balance, - balance_end_real = statement.end_balance, - balance_end = statement.end_balance, - state = 'draft', - currency = account_info.journal_id.currency.id, - user_id = uid, - banking_id = import_id, - )) - imported_statement_ids.append(statement_id) - - # move each line to the right period and try to match it with an - # invoice or payment - subno = 0 - for transaction in statement.transactions: - move_info = False - - # Keep a tracer for identification of order in a statement in case - # of missing transaction ids. - subno += 1 - - # Link remote partner, import account when needed - partner_bank = get_bank_account( - pool, cursor, uid, transaction.remote_account, log, fail=True - ) - if partner_bank: - partner_id = partner_bank.partner_id.id - partner_bank_id = partner_bank.id - elif transaction.remote_owner: - partner_id = get_or_create_partner( - pool, cursor, uid, transaction.remote_owner - ) - if transaction.remote_account: - partner_bank_id = create_bank_account( - pool, cursor, uid, partner_id, - transaction.remote_account, transaction.remote_owner, - log - ) - else: - partner_id = False - partner_bank_id = False - - # Link accounting period - period_id = get_period(pool, cursor, uid, - transaction.effective_date, company, - log) - if not period_id: - no_trans_skipped += 1 - continue - - # Credit means payment... isn't it? - if transaction.transferred_amount < 0 and payment_lines: - # Link open payment - if any - move_info = _link_payment(pool, cursor, uid, transaction, - payment_lines, partner_id, - partner_bank_id, log - ) - - # Second guess, invoice - if not move_info: - # Link invoice - if any - move_info = _link_invoice(pool, cursor, uid, transaction, - move_lines, partner_id, partner_bank_id, - log - ) - if not move_info: - if transaction.transferred_amount < 0: - account_id = account_info.default_credit_account_id - else: - account_id = account_info.default_debit_account_id - else: - account_id = move_info.move_line.account_id - no_trans_matched += 1 - - values = struct( - name = '%s.%s' % (statement.id, transaction.id or subno), - date = transaction.effective_date, - amount = transaction.transferred_amount, - account_id = account_id.id, - statement_id = statement_id, - note = transaction.message, - ref = transaction.reference, - period_id = period_id, - ) - if partner_id: - values.partner_id = partner_id - if partner_bank_id: - values.partner_bank_id = partner_bank_id - if move_info: - values.type = move_info.type - values.reconcile_id = move_info.move_line.reconcile_id - - statement_line_id = statement_line_obj.create(cursor, uid, values) - no_trans_loaded += 1 - - no_stat_loaded += 1 - - if payment_lines: - # As payments lines are treated as individual transactions, the - # batch as a whole is only marked as 'done' when all payment lines - # have been reconciled. - cursor.execute( - "UPDATE payment_order o " - "SET state = 'done', " - "date_done = '%s' " - "FROM payment_line l " - "WHERE o.state = 'sent' " - "AND o.id = l.order_id " - "AND o.id NOT IN (" - "SELECT DISTINCT id FROM payment_line " - "WHERE date_done IS NULL " - "AND id IN (%s)" - ")" % ( - time.strftime('%Y-%m-%d'), - ','.join([str(x) for x in payment_line_ids]) - ) - ) - report = [ - '%s: %s' % (_('Total number of statements'), no_stat_skipped + no_stat_loaded), - '%s: %s' % (_('Total number of transactions'), no_trans_skipped + no_trans_loaded), - '%s: %s' % (_('Number of errors found'), no_errors), - '%s: %s' % (_('Number of statements skipped due to errors'), no_stat_skipped), - '%s: %s' % (_('Number of transactions skipped due to errors'), no_trans_skipped), - '%s: %s' % (_('Number of statements loaded'), no_stat_loaded), - '%s: %s' % (_('Number of transactions loaded'), no_trans_loaded), - '', - '%s:' % ('Error report'), - '', - ] - text_log = '\n'.join(report + log) - state = no_errors and 'error' or 'ready' - statement_file_obj.write(cursor, uid, import_id, dict( - state = state, log = text_log, - )) - self._nextstate = no_errors and 'view_error' or 'view_statements' - self._import_id = import_id - self._log = text_log - self._statement_ids = imported_statement_ids - return {} - -banking_import_form = ''' -
- - - - - - -''' - def parser_types(*args, **kwargs): '''Delay evaluation of parser types until start of wizard, to allow depending modules to initialize and add their parsers to the list ''' return models.parser_type.get_parser_types() -banking_import_fields = dict( - company = dict( - string = 'Company', - type = 'many2one', - relation = 'res.company', - required = True, - ), - file = dict( - string = 'Statements File', - type = 'binary', - required = True, - help = ('The Transactions File to import. Please note that while it is ' - 'perfectly safe to reload the same file multiple times or to load in ' - 'timeframe overlapping statements files, there are formats that may ' - 'introduce different sequencing, which may create double entries.\n\n' - 'To stay on the safe side, always load bank statements files using the ' - 'same format.') - ), - parser = dict( - string = 'File Format', - type = 'selection', - selection = parser_types, - required = True, - ), -) - -result_form = ''' -
- - - -''' - -result_fields = dict( - log = dict(string='Log', type='text', readonly=True) -) - class banking_import(wizard.interface): ''' Wizard to import bank statements. Generic code, parsing is done in the parser modules. ''' + result_form = ''' +
+ + + + ''' + + result_fields = dict( + log = dict(string='Log', type='text', readonly=True) + ) + + banking_import_form = ''' +
+ + + + + + ''' + + banking_import_fields = dict( + company = dict( + string = 'Company', + type = 'many2one', + relation = 'res.company', + required = True, + ), + file = dict( + string = 'Statements File', + type = 'binary', + required = True, + help = ('The Transactions File to import. Please note that while it is ' + 'perfectly safe to reload the same file multiple times or to load in ' + 'timeframe overlapping statements files, there are formats that may ' + 'introduce different sequencing, which may create double entries.\n\n' + 'To stay on the safe side, always load bank statements files using the ' + 'same format.') + ), + parser = dict( + string = 'File Format', + type = 'selection', + selection = parser_types, + required = True, + ), + ) + def __init__(self, *args, **kwargs): super(banking_import, self).__init__(*args, **kwargs) self.__state = '' - def _fill_results(self, cursor, uid, data, context): + def _fill_results(self, *args, **kwargs): return {'log': self._log} + def _get_move_info(cursor, uid, move_line): + reconcile_obj = self.pool.get('account.bank.statement.reconcile') + type_map = { + 'out_invoice': 'customer', + 'in_invoice': 'supplier', + 'out_refund': 'customer', + 'in_refund': 'supplier', + } + retval = struct(move_line=move_line) + retval.reference = move_line.ref + if move_line.invoice: + retval.invoice = move_line.invoice + retval.type = type_map[move_line.invoice.type] + else: + retval.type = 'general' + move_line.reconcile_id = reconcile_obj.create( + cursor, uid, {'line_ids': [(6, 0, [move_line.id])]} + ) + return retval + + def _link_payment(self, cursor, uid, trans, payment_lines, + partner_id, bank_account_id, log): + ''' + Find the payment order belonging to this reference - if there is one + This is the easiest part: when sending payments, the returned bank info + should be identical to ours. + ''' + # TODO: Not sure what side effects are created when payments are done + # for credited customer invoices, which will be matched later on too. + digits = int(config['price_accuracy']) + candidates = [x for x in payment_lines + if x.communication == trans.reference + and round(x.amount, digits) == -round(trans.transferred_amount, digits) + and trans.remote_account in (x.bank_id.acc_number, + x.bank_id.iban) + ] + if len(candidates) == 1: + candidate = candidates[0] + payment_line_obj = self.pool.get('payment.line') + payment_line_obj.write(cursor, uid, [candidate.id], { + 'export_state': 'done', + 'date_done': trans.effective_date.strftime('%Y-%m-%d')} + ) + + return self._get_move_info(cursor, uid, candidate.move_line_id) + + return False + + def _link_invoice(self, cursor, uid, trans, move_lines, + partner_id, bank_account_id, log): + ''' + Find the invoice belonging to this reference - if there is one + Use the sales journal to check. + + Challenges we're facing: + 1. The sending or receiving party is not necessarily the same as the + partner the payment relates to. + 2. References can be messed up during manual encoding and inexact + matching can link the wrong invoices. + 3. Amounts can or can not match the expected amount. + 4. Multiple invoices can be paid in one transaction. + .. There are countless more, but these we'll try to address. + + Assumptions for matching: + 1. There are no payments for invoices not sent. These are dealt with + later on. + 1. Debit amounts are either customer invoices or credited supplier + invoices. + 2. Credit amounts are either supplier invoices or credited customer + invoices. + 3. Payments are either below expected amount or only slightly above + (abs). + 4. Payments from partners that are matched, pay their own invoices. + + Worst case scenario: + 1. No match was made. + No harm done. Proceed with manual matching as usual. + 2. The wrong match was made. + Statements are encoded in draft. You will have the opportunity to + manually correct the wrong assumptions. + ''' + # First on partner + candidates = [x for x in move_lines if x.partner_id.id == partner_id] + + # Next on reference/invoice number. Mind that this uses the invoice + # itself, as the move_line references have been fiddled with on invoice + # creation. This also enables us to search for the invoice number in the + # reference instead of the other way around, as most human interventions + # *add* text. + if not candidates: + ref = trans.reference.upper() + msg = trans.message.upper() + candidates = [x for x in move_lines + if x.invoice.number.upper() in ref or + x.invoice.number.upper() in msg + ] + + if len(candidates) > 1: + # TODO: currency coercing + digits = int(config['price_accuracy']) + if trans.transferred_amount < 0: + func = lambda x, y=abs(trans.transferred_amount), z=digits:\ + round(x.debit, z) == round(y, z) + else: + func = lambda x, y=abs(trans.transferred_amount), z=digits:\ + round(x.credit, z) == round(y, z) + best = [x for x in move_lines if func(x)] + if len(best) != 1: + log.append( + _('Unable to link transaction %(trans)s to invoice: ' + '%(no_candidates)s candidates found; can\'t choose.') % { + 'trans': trans.id, + 'no_candidates': len(best) + }) + return False + + if len(candidates) == 1: + return self._get_move_info(cursor, uid, candidates[0]) + + return False + + def _link_canceled_debit(self, cursor, uid, trans, payment_lines, + partner_id, bank_account_id, log): + ''' + Direct debit transfers can be canceled by the remote owner within a + legaly defined time period. These 'payments' are most likely + already marked 'done', which makes them harder to match. Also the + reconciliation has to be reversed. + ''' + # TODO: code _link_canceled_debit + return False + + def _banking_import_statements_file(self, cursor, uid, data, context): + ''' + Import bank statements / bank transactions file. + This module/function represents the business logic, the parser modules + represent the decoding logic. + ''' + form = data['form'] + statements_file = form['file'] + data = base64.decodestring(statements_file) + + self.pool = pooler.get_pool(cursor.dbname) + company_obj = self.pool.get('res.company') + user_obj = self.pool.get('res.user') + journal_obj = self.pool.get('account.journal') + move_line_obj = self.pool.get('account.move.line') + payment_line_obj = self.pool.get('payment.line') + statement_obj = self.pool.get('account.bank.statement') + statement_line_obj = self.pool.get('account.bank.statement.line') + statement_file_obj = self.pool.get('account.banking.imported.file') + #account_obj = self.pool.get('account.account') + #payment_order_obj = self.pool.get('payment.order') + currency_obj = self.pool.get('res.currency') + + # get the parser to parse the file + parser_code = form['parser'] + parser = models.create_parser(parser_code) + if not parser: + raise wizard.except_wizard( + _('ERROR!'), + _('Unable to import parser %(parser)s. Parser class not found.') % + {'parser':parser_code} + ) + + # Get the company + company = form['company'] + if not company: + user_data = user_obj.browse(cursor, uid, uid, context) + company = company_obj.browse( + cursor, uid, company or user_data.company_id.id, context + ) + + # Parse the file + statements = parser.parse(data) + + if any([x for x in statements if not x.is_valid()]): + raise wizard.except_wizard( + _('ERROR!'), + _('The imported statements appear to be invalid! Check your file.') + ) + + # Create the file now, as the statements need to be linked to it + import_id = statement_file_obj.create(cursor, uid, dict( + company_id = company.id, + file = statements_file, + date = time.strftime('%Y-%m-%d'), + user_id = uid, + state = 'unfinished' + )) + + # Results + no_stat_loaded = 0 + no_trans_loaded = 0 + no_stat_skipped = 0 + no_trans_skipped = 0 + no_trans_matched = 0 + no_errors = 0 + log = [] + + # Caching + error_accounts = {} + info = {} + imported_statement_ids = [] + + if statements: + # Get interesting journals once + if company: + journal_ids = journal_obj.search(cursor, uid, [ + ('type', 'in', ('sale','purchase')), + ('company_id', '=', company.id), + ]) + else: + journal_ids = None + if not journal_ids: + journal_ids = journal_obj.search(cursor, uid, [ + ('type', 'in', ('sale','purchase')), + ('active', '=', True), + ('company_id', '=', False), + ]) + # Get all unreconciled moves predating the last statement in one big + # swoop. Assumption: the statements in the file are sorted in ascending + # order of date. + move_line_ids = move_line_obj.search(cursor, uid, [ + ('reconcile_id', '=', False), + ('journal_id', 'in', journal_ids), + ('account_id.reconcile', '=', True), + ('date', '<=', date2str(statements[-1].date)), + ]) + if move_line_ids: + move_lines = move_line_obj.browse(cursor, uid, move_line_ids) + else: + move_lines = [] + + # Get all unreconciled sent payment lines in one big swoop. + # No filtering can be done, as empty dates carry value for C2B + # communication. Most likely there are much less sent payments + # than reconciled and open/draft payments. + cursor.execute("SELECT l.id FROM payment_order o, payment_line l " + "WHERE l.order_id = o.id AND " + "o.state = 'sent' AND " + "l.date_done IS NULL" + ) + payment_line_ids = [x[0] for x in cursor.fetchall()] + if payment_line_ids: + payment_lines = payment_line_obj.browse(cursor, uid, payment_line_ids) + else: + payment_lines = [] + + for statement in statements: + if statement.local_account in error_accounts: + # Don't repeat messages + no_stat_skipped += 1 + no_trans_skipped += len(statement.transactions) + continue + + if not statement.local_account in info: + account_info = get_company_bank_account( + self.pool, cursor, uid, statement.local_account, + statement.local_currency, company, log + ) + if not account_info: + log.append( + _('Statements found for unknown account %(bank_account)s') % + {'bank_account': statement.local_account} + ) + error_accounts[statement.local_account] = True + no_errors += 1 + continue + if 'journal_id' not in account_info: + log.append( + _('Statements found for account %(bank_account)s, ' + 'but no default journal was defined.' + ) % {'bank_account': statement.local_account} + ) + error_accounts[statement.local_account] = True + no_errors += 1 + continue + info[statement.local_account] = account_info + else: + account_info = info[statement.local_account] + + if statement.local_currency \ + and account_info.currency_id.code != statement.local_currency: + # TODO: convert currencies? + log.append( + _('Statement for account %(bank_account)s uses different ' + 'currency than the defined bank journal.') % + {'bank_account': statement.local_account} + ) + error_accounts[statement.local_account] = True + no_errors += 1 + continue + + # Check existence of previous statement + statement_ids = statement_obj.search(cursor, uid, [ + ('name', '=', statement.id), + ('date', '=', date2str(statement.date)), + ]) + if statement_ids: + log.append( + _('Statement %(id)s known - skipped') % { + 'id': statement.id + } + ) + continue + + statement_id = statement_obj.create(cursor, uid, dict( + name = statement.id, + journal_id = account_info.journal_id.id, + date = date2str(statement.date), + balance_start = statement.start_balance, + balance_end_real = statement.end_balance, + balance_end = statement.end_balance, + state = 'draft', + user_id = uid, + banking_id = import_id, + )) + imported_statement_ids.append(statement_id) + + # move each line to the right period and try to match it with an + # invoice or payment + subno = 0 + for transaction in statement.transactions: + move_info = False + + # Keep a tracer for identification of order in a statement in case + # of missing transaction ids. + subno += 1 + + # Link remote partner, import account when needed + partner_bank = get_bank_account( + self.pool, cursor, uid, transaction.remote_account, log, fail=True + ) + if partner_bank: + partner_id = partner_bank.partner_id.id + partner_bank_id = partner_bank.id + elif transaction.remote_owner: + partner_id = get_or_create_partner( + self.pool, cursor, uid, transaction.remote_owner + ) + if transaction.remote_account: + partner_bank_id = create_bank_account( + self.pool, cursor, uid, partner_id, + transaction.remote_account, transaction.remote_owner, + log + ) + else: + partner_id = False + partner_bank_id = False + + # Link accounting period + period_id = get_period(self.pool, cursor, uid, + transaction.effective_date, company, + log) + if not period_id: + no_trans_skipped += 1 + continue + + # Credit means payment... isn't it? + if transaction.transferred_amount < 0 and payment_lines: + # Link open payment - if any + move_info = self._link_payment( + cursor, uid, transaction, + payment_lines, partner_id, + partner_bank_id, log + ) + + # Second guess, invoice + if not move_info: + # Link invoice - if any + move_info = self._link_invoice( + cursor, uid, transaction, move_lines, partner_id, + partner_bank_id, log + ) + if not move_info: + if transaction.transferred_amount < 0: + account_id = account_info.default_credit_account_id + else: + account_id = account_info.default_debit_account_id + else: + account_id = move_info.move_line.account_id + no_trans_matched += 1 + + values = struct( + name = '%s.%s' % (statement.id, transaction.id or subno), + date = transaction.effective_date, + amount = transaction.transferred_amount, + account_id = account_id.id, + statement_id = statement_id, + note = transaction.message, + ref = transaction.reference, + period_id = period_id, + currency = account_info.currency_id.id, + ) + if partner_id: + values.partner_id = partner_id + if partner_bank_id: + values.partner_bank_id = partner_bank_id + if move_info: + values.type = move_info.type + values.reconcile_id = move_info.move_line.reconcile_id + + statement_line_id = statement_line_obj.create(cursor, uid, values) + no_trans_loaded += 1 + + no_stat_loaded += 1 + + if payment_lines: + # As payments lines are treated as individual transactions, the + # batch as a whole is only marked as 'done' when all payment lines + # have been reconciled. + cursor.execute( + "UPDATE payment_order o " + "SET state = 'done', " + "date_done = '%s' " + "FROM payment_line l " + "WHERE o.state = 'sent' " + "AND o.id = l.order_id " + "AND o.id NOT IN (" + "SELECT DISTINCT id FROM payment_line " + "WHERE date_done IS NULL " + "AND id IN (%s)" + ")" % ( + time.strftime('%Y-%m-%d'), + ','.join([str(x) for x in payment_line_ids]) + ) + ) + report = [ + '%s: %s' % (_('Total number of statements'), no_stat_skipped + no_stat_loaded), + '%s: %s' % (_('Total number of transactions'), no_trans_skipped + no_trans_loaded), + '%s: %s' % (_('Number of errors found'), no_errors), + '%s: %s' % (_('Number of statements skipped due to errors'), no_stat_skipped), + '%s: %s' % (_('Number of transactions skipped due to errors'), no_trans_skipped), + '%s: %s' % (_('Number of statements loaded'), no_stat_loaded), + '%s: %s' % (_('Number of transactions loaded'), no_trans_loaded), + '', + '%s:' % ('Error report'), + '', + ] + text_log = '\n'.join(report + log) + state = no_errors and 'error' or 'ready' + statement_file_obj.write(cursor, uid, import_id, dict( + state = state, log = text_log, + )) + if no_errors or not imported_statement_ids: + self._nextstate = 'view_error' + else: + self._nextstate = 'view_statements' + self._import_id = import_id + self._log = text_log + self._statement_ids = imported_statement_ids + return {} + def _action_open_window(self, cursor, uid, data, context): ''' Open a window with the resulting bank statements ''' # TODO: this needs fiddling. The resulting window is informative, # but not very usefull... - form = data['form'] - return dict( - domain = "[('id','in',(%s,))]" % (','.join(map(str, form['statement_ids']))), - name = 'Statement', - view_type = 'tree', - view_mode = 'form,tree', - res_model = 'account.bank.statement', - view_id = False, - type = 'ir.actions.act_window', - res_id = form['statement_ids'], + module_obj = self.pool.get('ir.model.data') + action_obj = self.pool.get('ir.actions.act_window') + result = module_obj._get_id( + cursor, uid, 'account', 'action_bank_statement_tree' ) + id = module_obj.read(cursor, uid, [result], ['res_id'])[0]['res_id'] + result = action_obj.read(cursor, uid, [id])[0] + result['context'] = str({'banking_id': self._import_id}) + return result def _action_open_import(self, cursor, uid, data, context): ''' Open a window with the resulting import in error ''' - form = data['form'] return dict( view_type = 'form', view_mode = 'form,tree', @@ -600,7 +605,7 @@ class banking_import(wizard.interface): } }, 'view_statements' : { - #'actions': [_banking_import_statements_file], + 'actions': [_fill_results], 'result': { 'type': 'form', 'arch': result_form, diff --git a/account_banking/wizard/banktools.py b/account_banking/wizard/banktools.py index 9b9529165..c7e30a0eb 100644 --- a/account_banking/wizard/banktools.py +++ b/account_banking/wizard/banktools.py @@ -56,14 +56,14 @@ def get_period(pool, cursor, uid, date, company, log): ]) if not fiscalyear_ids: log.append( - _('No suitable fiscal year found for company %(company_name)s') - % dict(company_name=company.name) + _('No suitable fiscal year found for date %(date)s and company %(company_name)s') + % dict(company_name=company.name, date=date) ) return False elif len(fiscalyear_ids) > 1: log.append( - _('Multiple overlapping fiscal years found for date %(date)s') - % dict(date=date) + _('Multiple overlapping fiscal years found for date %(date)s and company %(company_name)s') + % dict(company_name=company.name, date=date) ) return False @@ -73,13 +73,13 @@ def get_period(pool, cursor, uid, date, company, log): ('fiscalyear_id','=',fiscalyear_id), ('state','=','draft') ]) if not period_ids: - log.append(_('No suitable period found for date %(date)s') - % dict(date=date) + log.append(_('No suitable period found for date %(date)s and company %(company_name)s') + % dict(company_name=company.name, date=date) ) return False if len(period_ids) != 1: - log.append(_('Multiple overlapping periods for date %(date)s') - % dict(date=date) + log.append(_('Multiple overlapping periods for date %(date)s and company %(company_name)s') + % dict(company_name=company.name, date=date) ) return False return period_ids[0] @@ -135,10 +135,11 @@ def get_or_create_partner(pool, cursor, uid, name, log): partner_id = partner_ids[0] return partner_obj.browse(cursor, uid, partner_id)[0] -def get_company_bank_account(pool, cursor, uid, account_number, +def get_company_bank_account(pool, cursor, uid, account_number, currency, company, log): ''' - Get the matching bank account for this company. + Get the matching bank account for this company. Currency is the ISO code + for the requested currency. ''' results = struct() bank_account = get_bank_account(pool, cursor, uid, account_number, log, @@ -154,12 +155,31 @@ def get_company_bank_account(pool, cursor, uid, account_number, return False results.account = bank_account bank_settings_obj = pool.get('account.banking.account.settings') - bank_settings_ids = bank_settings_obj.search(cursor, uid, [ - ('partner_bank_id', '=', bank_account.id) + criteria = [('partner_bank_id', '=', bank_account.id)] + + # Find matching journal for currency + journal_obj = pool.get('account.journal') + journal_ids = journal_obj.search(cursor, uid, [ + ('type', '=', 'cash'), + ('currency', '=', currency or company.currency_id.code) ]) + if not journal_ids and currency == company.currency_id.code: + journal_ids = journal_obj.search(cursor, uid, [ + ('type', '=', 'cash'), ('currency', '=', False) + ]) + if journal_ids: + criteria.append(('journal_id', 'in', journal_ids)) + + # Find bank account settings + bank_settings_ids = bank_settings_obj.search(cursor, uid, criteria) if bank_settings_ids: settings = bank_settings_obj.browse(cursor, uid, bank_settings_ids)[0] results.journal_id = settings.journal_id + # Take currency from settings or from company + if settings.journal_id.currency.id: + results.currency_id = settings.journal_id.currency + else: + results.currency_id = company.currency_id results.default_debit_account_id = settings.default_debit_account_id results.default_credit_account_id = settings.default_credit_account_id return results