From b0a29c9bad4fb9180ddcdae80529e244c6b61d49 Mon Sep 17 00:00:00 2001 From: Laurent Smet Date: Fri, 17 Aug 2018 15:30:51 +0200 Subject: [PATCH] [IMP] account, bank statement reconciliation widget: improve suggestions Suggestions made to reconcile bank statement lines are now more accurate (based on communication and amount) There's no automatic mode anymore (button + shortcut removed) Partners are set automatically when selecting a line in the widget (that will help the creation of a res.partner.bank for him, for a better partner detection next time) Was task: 1847703 Was PR #25438 --- .../account/models/account_bank_statement.py | 80 ----- .../account/models/reconciliation_widget.py | 337 ++++++++++++------ .../reconciliation/reconciliation_action.js | 10 +- .../js/reconciliation/reconciliation_model.js | 117 +++--- .../reconciliation/reconciliation_renderer.js | 39 +- .../js/reconciliation/tour_reconciliation.js | 23 +- .../static/src/xml/account_reconciliation.xml | 36 +- .../static/tests/reconciliation_tests.js | 64 +--- .../test_bank_statement_reconciliation.py | 6 +- 9 files changed, 379 insertions(+), 333 deletions(-) diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py index 57dc68415c9..0a91e072607 100644 --- a/addons/account/models/account_bank_statement.py +++ b/addons/account/models/account_bank_statement.py @@ -398,86 +398,6 @@ class AccountBankStatementLine(models.Model): return select_clause, from_clause, where_clause return select_clause + from_clause + where_clause - # todo: remove this => coverage task - def _get_move_lines_for_auto_reconcile(self): - """ Returns the move lines that the method auto_reconcile can use to try to reconcile the statement line """ - pass - - # this method is only call by the widget reconciliation, move it or not ? - @api.multi - def auto_reconcile(self): - """ Try to automatically reconcile the statement.line ; return the counterpart journal entry/ies if the automatic reconciliation succeeded, False otherwise. - TODO : this method could be greatly improved and made extensible - """ - self.ensure_one() - match_recs = self.env['account.move.line'] - - amount = self.amount_currency or self.amount - company_currency = self.journal_id.company_id.currency_id - st_line_currency = self.currency_id or self.journal_id.currency_id - currency = (st_line_currency and st_line_currency != company_currency) and st_line_currency.id or False - precision = st_line_currency and st_line_currency.decimal_places or company_currency.decimal_places - params = {'company_id': self.env.user.company_id.id, - 'account_payable_receivable': (self.journal_id.default_credit_account_id.id, self.journal_id.default_debit_account_id.id), - 'amount': float_round(amount, precision_digits=precision), - 'partner_id': self.partner_id.id, - 'ref': self.name, - } - field = currency and 'amount_residual_currency' or 'amount_residual' - liquidity_field = currency and 'amount_currency' or amount > 0 and 'debit' or 'credit' - # Look for structured communication match - if self.name: - sql_query = self._get_common_sql_query() + \ - " AND aml.ref = %(ref)s AND ("+field+" = %(amount)s OR (acc.internal_type = 'liquidity' AND "+liquidity_field+" = %(amount)s)) \ - ORDER BY date_maturity asc, aml.id asc" - self.env.cr.execute(sql_query, params) - match_recs = self.env.cr.dictfetchall() - if len(match_recs) > 1: - return False - - # Look for a single move line with the same partner, the same amount - if not match_recs: - if self.partner_id: - sql_query = self._get_common_sql_query() + \ - " AND ("+field+" = %(amount)s OR (acc.internal_type = 'liquidity' AND "+liquidity_field+" = %(amount)s)) \ - ORDER BY date_maturity asc, aml.id asc" - self.env.cr.execute(sql_query, params) - match_recs = self.env.cr.dictfetchall() - if len(match_recs) > 1: - return False - - if not match_recs: - return False - - match_recs = self.env['account.move.line'].browse([aml.get('id') for aml in match_recs]) - # Now reconcile - counterpart_aml_dicts = [] - payment_aml_rec = self.env['account.move.line'] - for aml in match_recs: - if aml.account_id.internal_type == 'liquidity': - payment_aml_rec = (payment_aml_rec | aml) - else: - amount = aml.currency_id and aml.amount_residual_currency or aml.amount_residual - counterpart_aml_dicts.append({ - 'name': aml.name if aml.name != '/' else aml.move_id.name, - 'debit': amount < 0 and -amount or 0, - 'credit': amount > 0 and amount or 0, - 'move_line': aml - }) - - try: - with self._cr.savepoint(): - counterpart = self.process_reconciliation(counterpart_aml_dicts=counterpart_aml_dicts, payment_aml_rec=payment_aml_rec) - return counterpart - except UserError: - # A configuration / business logic error that makes it impossible to auto-reconcile should not be raised - # since automatic reconciliation is just an amenity and the user will get the same exception when manually - # reconciling. Other types of exception are (hopefully) programming errors and should cause a stacktrace. - self.invalidate_cache() - self.env['account.move'].invalidate_cache() - self.env['account.move.line'].invalidate_cache() - return False - def _prepare_reconciliation_move(self, move_ref): """ Prepare the dict of values to create the move from a statement line. This method may be overridden to adapt domain logic through model inheritance (make sure to call super() to establish a clean extension chain). diff --git a/addons/account/models/reconciliation_widget.py b/addons/account/models/reconciliation_widget.py index 7183c3607fa..11d6b2a3577 100644 --- a/addons/account/models/reconciliation_widget.py +++ b/addons/account/models/reconciliation_widget.py @@ -3,8 +3,7 @@ from odoo import api, fields, models, _ from odoo.exceptions import UserError from odoo.osv import expression -from odoo.tools import pycompat -from odoo.tools import float_round, float_repr +from odoo.tools import pycompat, float_is_zero from odoo.tools.misc import formatLang @@ -12,52 +11,236 @@ class AccountReconciliation(models.AbstractModel): _name = 'account.reconciliation.widget' #################################################### - # Public + # Search propositions #################################################### @api.model - def auto_reconcile(self, st_line_ids, num_already_reconciled_lines=0): - """ Use statement line auto_reconcile method and return the details - to the widget interface. + def _get_matching_amls_query(self, st_lines, excluded_ids=None): + ''' Base query used by the matching rules. + Main things about this huge query: + - Try to join the res.partner table: + 1) On st_line.partner_id. + 2) On partner_id found on a res.partner.bank sharing the same account number. + 3) On a partner having the same case insensitive name. + - Basic filters on account_move_line. - :param st_line_ids: ids of the statement lines to reconciliate - :param num_already_reconciled_lines: number of reconcilied lines - used to increment the progress bar in the interface in function - of the real number of reconcilied lines. - """ - st_lines = self.env['account.bank.statement.line'].browse(st_line_ids) - automatic_reconciliation_entries = self.env['account.bank.statement.line'] - unreconciled = self.env['account.bank.statement.line'] + :param st_lines: Account.bank.statement.lines recordset. + :param excluded_ids: Account.move.lines to exclude. + :return: (query, params) + ''' + params = [tuple(st_lines.ids)] - for stl in st_lines: - res = stl.auto_reconcile() - if res: - automatic_reconciliation_entries += stl - else: - unreconciled += stl + # N.B: The first part of the CASE is about 'blue lines' while the second part is about 'black lines'. + query = ''' + SELECT + st_line.id AS id, + aml.id AS aml_id, + aml.currency_id AS aml_currency_id, + aml.amount_residual AS aml_amount_residual, + aml.amount_residual_currency AS aml_amount_residual_currency + FROM account_bank_statement_line st_line + LEFT JOIN account_journal journal ON journal.id = st_line.journal_id + LEFT JOIN res_company company ON company.id = st_line.company_id + LEFT JOIN res_partner_bank bank ON bank.id = st_line.bank_account_id OR bank.acc_number = st_line.account_number + LEFT JOIN res_partner partner ON ( + CASE WHEN st_line.partner_id IS NOT NULL THEN + partner.id = st_line.partner_id + WHEN bank.partner_id IS NOT NULL THEN + partner.id = bank.partner_id + ELSE + partner.name ILIKE st_line.partner_name + END + ) + , account_move_line aml + LEFT JOIN res_company aml_company ON aml_company.id = aml.company_id + LEFT JOIN account_account aml_account ON aml_account.id = aml.account_id + WHERE st_line.id IN %s + AND aml.company_id = st_line.company_id + AND aml.statement_id IS NULL + + AND ( + company.account_bank_reconciliation_start IS NULL + OR + aml.date > company.account_bank_reconciliation_start + ) + + AND CASE WHEN journal.default_credit_account_id IS NOT NULL + AND journal.default_debit_account_id IS NOT NULL + THEN + ( + aml.account_id IN (journal.default_credit_account_id, journal.default_debit_account_id) + AND aml.payment_id IS NOT NULL + ) + OR + ( + aml_account.reconcile IS TRUE + AND aml.reconciled IS FALSE + ) + END + ''' - # Collect various information for the reconciliation widget - notifications = [] - num_auto_reconciled = len(automatic_reconciliation_entries) - if num_auto_reconciled > 0: - auto_reconciled_message = num_auto_reconciled > 1 \ - and _("%d transactions were automatically reconciled.") % num_auto_reconciled \ - or _("1 transaction was automatically reconciled.") - notifications += [{ - 'type': 'info', - 'message': auto_reconciled_message, - 'details': { - 'name': _("Automatically reconciled items"), - 'model': 'account.move', - 'ids': automatic_reconciliation_entries.mapped('journal_entry_ids').mapped('move_id').ids - } - }] - return { - 'st_lines_ids': unreconciled.ids, - 'notifications': notifications, - 'statement_name': False, - 'num_already_reconciled_lines': num_auto_reconciled + num_already_reconciled_lines, - } + if excluded_ids: + query += 'AND aml.id NOT IN %s' + params.append(tuple(excluded_ids)) + return query, params + + @api.model + def _get_matching_amls_invoice_rule(self, st_lines, excluded_ids=None): + ''' RULE 1: Match an account.move.line automatically if linked to an invoice having a number or reference quite + similar. + + This rule automatically match when invoice_reference match when: + - matching only one invoice. + - matching multiple invoices but having the same total amount residual. + + :param st_lines: Account.bank.statement.lines recordset. + :param excluded_ids: Account.move.lines to exclude. + :return: (query, params, automatic_match_func) + ''' + + def automatic_match_func(st_line, fetched_amls): + # Match only one invoice. + if len(fetched_amls) == 1: + return True + + # Match multiple invoices but having the same total amount residual. + total_residual = sum(aml['aml_currency_id'] and aml['aml_amount_residual_currency'] or aml['aml_amount_residual'] for aml in fetched_amls) + line_residual = st_line.currency_id and st_line.amount_currency or st_line.amount + line_currency = st_line.currency_id or st_line.journal_id.currency_id or st_line.company_id.currency_id + return float_is_zero(total_residual - line_residual, precision_rounding=line_currency.rounding) + + query, params = self._get_matching_amls_query(st_lines, excluded_ids=excluded_ids) + + # Join the account_invoice table. + query = query.replace( + 'account_move_line aml', + ''' + account_move_line aml + LEFT JOIN account_move move ON move.id = aml.move_id + LEFT JOIN account_invoice invoice ON invoice.move_name = move.name + ''' + ) + + # Add where clause. + # N.B: invoice_reference could be a list of invoice reference/number (e.g. 'INV/2018/0001,INV/2018/0002'). + query += ''' + AND invoice.state = 'open' + AND CASE WHEN st_line.amount >= 0.0 THEN + invoice.type IN ('out_invoice', 'in_refund') + ELSE + invoice.type IN ('in_invoice', 'out_refund') + END + AND ( + REGEXP_REPLACE(st_line.name, '[^0-9]', '', 'g') ~ REGEXP_REPLACE(invoice.number, '[^0-9]', '', 'g') + OR ( + invoice.reference IS NOT NULL + AND + REGEXP_REPLACE(st_line.name, '[^0-9]', '', 'g') ~ REGEXP_REPLACE(invoice.reference, '[^0-9]', '', 'g') + ) + ) + AND CASE WHEN partner.id IS NOT NULL THEN + invoice.partner_id = partner.id + ELSE + TRUE + END + ''' + return query, params, automatic_match_func + + @api.model + def _get_matching_amls_amount_rule(self, st_lines, excluded_ids=None): + ''' RULE 2: Match one or more account.move.lines automatically if either the statement line has exactly the same amount + or either the statement line matchs a single line having a greater or equals amount. + + This only work if a partner has been found on the statement line. + + :param st_lines: Account.bank.statement.lines recordset. + :param excluded_ids: Account.move.lines to exclude. + :return: (query, params, automatic_match_func) + ''' + + def automatic_match_func(st_line, fetched_amls): + total_residual = sum(aml['aml_currency_id'] and aml['aml_amount_residual_currency'] or aml['aml_amount_residual'] for aml in fetched_amls) + line_residual = st_line.currency_id and st_line.amount_currency or st_line.amount + line_currency = st_line.currency_id or st_line.journal_id.currency_id or st_line.company_id.currency_id + + # Match the total residual amount. + if float_is_zero(total_residual - line_residual, precision_rounding=line_currency.rounding): + return True + + # Match only one line having a greater residual amount. + return len(fetched_amls) == 1 and line_residual < total_residual + + query, params = self._get_matching_amls_query(st_lines, excluded_ids=excluded_ids) + + # Add where clause. + # N.B: move.line currency_id is either set or either got from the company. + # N.B2: statement.line currency_id is either set, either got from the journal or either got from the company. + query += ''' + AND partner.id IS NOT NULL + AND aml.partner_id = partner.id + AND ( + CASE WHEN st_line.currency_id IS NOT NULL AND st_line.currency_id != aml_company.currency_id THEN + aml.currency_id = st_line.currency_id + WHEN journal.currency_id IS NOT NULL AND journal.currency_id != aml_company.currency_id THEN + aml.currency_id = journal.currency_id + ELSE + aml.currency_id IS NULL + END + ) + ''' + return query, params, automatic_match_func + + @api.model + def _get_matching_amls(self, st_lines, excluded_ids=None): + ''' Apply reconciliation matching rules in order to find matching account.move.lines. + + :param st_lines: Account.bank.statement.lines recordset. + :param excluded_ids: Account.move.lines ids to exclude. + :return: A dictionnary mapping each id with: + * line: The account.bank.statement.line record. + * aml_ids: The matching account.move.line ids. + ''' + results = dict((r.id, {'line': r, 'aml_ids': []}) for r in st_lines) + excluded_ids = excluded_ids or [] + + rules = [ + self._get_matching_amls_invoice_rule, + self._get_matching_amls_amount_rule, + ] + for rule in rules: + query, params, automatic_match_func = rule(st_lines, excluded_ids=excluded_ids) + self._cr.execute(query, params) + query_res = self._cr.dictfetchall() + + # Map statement line with candidates. + candidates_map = {} + for res in query_res: + candidates_map.setdefault(res['id'], []) + candidates_map[res['id']].append(res) + + for line_id, fetched_amls in candidates_map.items(): + st_line = results[line_id]['line'] + + candidate_amls = [] + candidate_amls_ids = [] + for aml in fetched_amls: + if aml['aml_id'] not in excluded_ids: + candidate_amls.append(aml) + candidate_amls_ids.append(aml['aml_id']) + + if automatic_match_func(results[line_id]['line'], candidate_amls): + results[line_id]['aml_ids'] = candidate_amls_ids + + # Mark statement line as already processed. + st_lines -= st_line + + # Exclude move lines. + excluded_ids += candidate_amls_ids + return results + + #################################################### + # Public + #################################################### @api.model def process_bank_statement_line(self, st_line_ids, data): @@ -133,15 +316,15 @@ class AccountReconciliation(models.AbstractModel): excluded_ids = excluded_ids or [] ret = [] st_lines = self.env['account.bank.statement.line'].browse(st_line_ids) + matching_amls = self._get_matching_amls(st_lines, excluded_ids=excluded_ids) - for st_line in st_lines: - aml_recs = self._get_statement_line_reconciliation_proposition(st_line, excluded_ids=excluded_ids) - target_currency = st_line.currency_id or st_line.journal_id.currency_id or st_line.journal_id.company_id.currency_id - rp = self._prepare_move_lines(aml_recs, target_currency=target_currency, target_date=st_line.date) - excluded_ids += [move_line['id'] for move_line in rp] + # Iterate on st_lines to keep the same order in the results list. + for line in st_lines: + aml_ids = matching_amls[line.id]['aml_ids'] + amls = aml_ids and self.env['account.move.line'].browse(aml_ids) ret.append({ - 'st_line': self._get_statement_line(st_line), - 'reconciliation_proposition': rp + 'st_line': self._get_statement_line(line), + 'reconciliation_proposition': aml_ids and self._prepare_move_lines(amls) or [], }) return ret @@ -641,62 +824,6 @@ class AccountReconciliation(models.AbstractModel): return data - @api.model - def _get_statement_line_reconciliation_proposition(self, st_line, excluded_ids=None): - """ Returns move lines that constitute the best guess to reconcile a statement line - Note: it only looks for move lines in the same currency as the statement line. - """ - Account_move_line = self.env['account.move.line'] - - if not excluded_ids: - excluded_ids = [] - amount = st_line.amount_currency or st_line.amount - company_currency = st_line.journal_id.company_id.currency_id - st_line_currency = st_line.currency_id or st_line.journal_id.currency_id - currency = (st_line_currency and st_line_currency != company_currency) and st_line_currency.id or False - precision = st_line_currency and st_line_currency.decimal_places or company_currency.decimal_places - - params = {'company_id': st_line.company_id.id, - 'account_payable_receivable': (st_line.journal_id.default_credit_account_id.id, st_line.journal_id.default_debit_account_id.id), - 'amount': float_repr(float_round(amount, precision_digits=precision), precision_digits=precision), - 'partner_id': st_line.partner_id.id, - 'excluded_ids': tuple(excluded_ids), - 'ref': st_line.name, - 'bank_rec_threshold': st_line.journal_id.company_id.account_bank_reconciliation_start, - } - - # Look for structured communication match - if st_line.name: - add_to_select = ", CASE WHEN aml.ref = %(ref)s THEN 1 ELSE 2 END as temp_field_order " - add_to_from = " JOIN account_move m ON m.id = aml.move_id " - select_clause, from_clause, where_clause = st_line._get_common_sql_query(overlook_partner=True, excluded_ids=excluded_ids, split=True) - sql_query = select_clause + add_to_select + from_clause + add_to_from + where_clause - - if params['bank_rec_threshold']: - sql_query += "AND aml.date > %(bank_rec_threshold)s" - sql_query += " AND (aml.ref= %(ref)s or m.name = %(ref)s) \ - ORDER BY temp_field_order, date_maturity desc, aml.id desc" - self.env.cr.execute(sql_query, params) - results = self.env.cr.fetchone() - if results: - return Account_move_line.browse(results[0]) - - # Look for a single move line with the same amount - field = currency and 'amount_residual_currency' or 'amount_residual' - liquidity_field = currency and 'amount_currency' or amount > 0 and 'debit' or 'credit' - liquidity_amt_clause = currency and '%(amount)s::numeric' or 'abs(%(amount)s::numeric)' - sql_query = st_line._get_common_sql_query(excluded_ids=excluded_ids) + \ - " AND (" + field + " = %(amount)s::numeric OR (acc.internal_type = 'liquidity' AND " + liquidity_field + " = " + liquidity_amt_clause + "))" - if params['bank_rec_threshold']: - sql_query += "AND aml.date > %(bank_rec_threshold)s " - sql_query += "ORDER BY date_maturity desc, aml.id desc LIMIT 1" - self.env.cr.execute(sql_query, params) - results = self.env.cr.fetchone() - if results: - return Account_move_line.browse(results[0]) - - return Account_move_line - @api.model def _get_move_line_reconciliation_proposition(self, account_id, partner_id=None): """ Returns two lines whose amount are opposite """ diff --git a/addons/account/static/src/js/reconciliation/reconciliation_action.js b/addons/account/static/src/js/reconciliation/reconciliation_action.js index 153decff42a..88668a2da97 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_action.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_action.js @@ -27,9 +27,7 @@ var StatementAction = AbstractAction.extend(ControlPanelMixin, { create_proposition: '_onAction', quick_create_proposition: '_onAction', toggle_partial_reconcile: '_onAction', - auto_reconciliation: '_onValidate', validate: '_onValidate', - validate_all_balanced: '_onValidate', change_name: '_onChangeName', close_statement: '_onCloseStatement', load_more: '_onLoadMore', @@ -271,7 +269,7 @@ var StatementAction = AbstractAction.extend(ControlPanelMixin, { return this._loadMore(this.model.defaultDisplayQty); }, /** - * call 'validate' or 'autoReconciliation' model method then destroy the + * call 'validate' model method then destroy the * validated lines and update the action renderer with the new status bar * values and notifications then open the first available line * @@ -281,7 +279,7 @@ var StatementAction = AbstractAction.extend(ControlPanelMixin, { _onValidate: function (event) { var self = this; var handle = event.target.handle; - var method = event.name.indexOf('auto_reconciliation') === -1 ? 'validate' : 'autoReconciliation'; + var method = 'validate'; this.model[method](handle).then(function (result) { self.renderer.update({ 'valuenow': self.model.valuenow, @@ -327,7 +325,7 @@ var ManualAction = StatementAction.extend({ //-------------------------------------------------------------------------- /** - * call 'validate' or 'autoReconciliation' model method then destroy the + * call 'validate' model method then destroy the * reconcilied lines, update the not reconcilied and update the action * renderer with the new status bar values and notifications then open the * first available line @@ -338,7 +336,7 @@ var ManualAction = StatementAction.extend({ _onValidate: function (event) { var self = this; var handle = event.target.handle; - var method = event.name.indexOf('auto_reconciliation') === -1 ? 'validate' : 'autoReconciliation'; + var method = 'validate'; this.model[method](handle).then(function (result) { _.each(result.reconciled, function (handle) { self._getWidget(handle).destroy(); diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js index ddb87fed11d..9bc336af4b8 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_model.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js @@ -127,40 +127,22 @@ var StatementModel = BasicModel.extend({ * @returns {Deferred} */ addProposition: function (handle, mv_line_id) { + var self = this; var line = this.getLine(handle); var prop = _.clone(_.find(line.mv_lines, {'id': mv_line_id})); this._addProposition(line, prop); line.limit_override = (line.offset + 1) * this.limitMoveLines; - return $.when(this._computeLine(line), this._performMoveLine(handle)); - }, - /** - * send information 'account.reconciliation.widget' model to reconciliate - * lines, call rpc to 'auto_reconcile' - * Update the number of validated line - * - * @returns {Deferred} resolved with an object who contains - * 'handles' key and 'notifications' - */ - autoReconciliation: function () { - var self = this; - var ids = _.pluck(_.filter(this.lines, {'reconciled': false}), 'id'); - return this._rpc({ - model: 'account.reconciliation.widget', - method: 'auto_reconcile', - args: [ids, self.valuenow], - }) - .then(function (result) { - var reconciled_ids = _.difference(ids, result.st_lines_ids); - self.valuenow += reconciled_ids.length; - result.handles = []; - _.each(self.lines, function (line, handle) { - if (reconciled_ids.indexOf(line.id) !== -1) { - line.reconciled = true; - result.handles.push(handle); - } + + // Onchange the partner if not already set on the statement line. + if(!line.st_line.partner_id && line.reconciliation_proposition + && line.reconciliation_proposition.length == 1 && prop.partner_id){ + return this.changePartner(handle, {'id': prop.partner_id, 'display_name': prop.partner_name}, true) + .then(function (result) { + return $.when(self._computeLine(line), self._performMoveLine(handle)); }); - return result; - }); + } + + return $.when(this._computeLine(line), this._performMoveLine(handle)); }, /** * change the filter for the target line and fetch the new matched lines @@ -236,27 +218,38 @@ var StatementModel = BasicModel.extend({ * change the partner on the line and fetch the new matched lines * * @param {string} handle + * @param {bool} preserveMode * @param {Object} partner * @param {string} partner.display_name * @param {number} partner.id * @returns {Deferred} */ - changePartner: function (handle, partner) { + changePartner: function (handle, partner, preserveMode) { var self = this; var line = this.getLine(handle); line.st_line.partner_id = partner && partner.id; line.st_line.partner_name = partner && partner.display_name || ''; return $.when(partner && this._changePartner(handle, partner.id)) .then(function() { - line.reconciliation_proposition = []; - self._computeLine(line); - return self.changeMode(handle, 'match'); + if(line.st_line.partner_id){ + _.each(line.reconciliation_proposition, function(prop){ + if(prop.partner_id != line.st_line.partner_id){ + line.reconciliation_proposition = []; + return false; + } + }) + } + return self._computeLine(line); }) .then(function () { - if (line.mode === 'create') { - return self.createProposition(handle); - } - }); + if (!preserveMode) + return self.changeMode(handle, 'match'); + return false; + }) + .then(function () { + return line.mode === 'create' ? self.createProposition(handle) : false; + }) + }, /** * close the statement @@ -285,7 +278,7 @@ var StatementModel = BasicModel.extend({ var prop = _.filter(line.reconciliation_proposition, '__focus'); var last = prop[prop.length-1]; if (last && !this._isValid(last)) { - return $.Deferred().reject(); + return $.Deferred().resolve(false); } prop = this._formatQuickCreate(line); @@ -369,8 +362,10 @@ var StatementModel = BasicModel.extend({ self.valuemax = statement.st_lines_ids.length; self.context.journal_id = statement.journal_id; _.each(statement.st_lines_ids, function (id) { - self.lines[_.uniqueId('rline')] = { + var handle = _.uniqueId('rline'); + self.lines[handle] = { id: id, + handle: handle, reconciled: false, mode: 'inactive', mv_lines: [], @@ -496,6 +491,7 @@ var StatementModel = BasicModel.extend({ removeProposition: function (handle, id) { var self = this; var line = this.getLine(handle); + var defs = []; // new limit = previous limit + 1, the one put back line.limit_override = (line.offset + 1) * this.limitMoveLines; var prop = _.find(line.reconciliation_proposition, {'id' : id}); @@ -503,17 +499,21 @@ var StatementModel = BasicModel.extend({ line.reconciliation_proposition = _.filter(line.reconciliation_proposition, function (p) { return p.id !== prop.id && p.id !== prop.link && p.link !== prop.id && (!p.link || p.link !== prop.link); }); + + // No proposition left and then, reset the st_line partner. + if(line.reconciliation_proposition.length == 0 && line.st_line.has_no_partner) + defs.push(self.changePartner(line.handle)); } line.mode = (id || line.mode !== "create") && isNaN(id) && !this.avoidCreate ? 'create' : 'match'; - var def = this._computeLine(line); + defs.push(this._computeLine(line)); if (line.mode === 'create') { - return def.then(function () { + return $.when(defs).then(function () { return self.createProposition(handle); }); } else if (line.mode === 'match') { - return $.when(def, self._performMoveLine(handle)); + return $.when(defs, self._performMoveLine(handle)); } - return def; + return $.when(defs); }, searchBalanceAmount: function (handle) { var line = this.getLine(handle); @@ -977,7 +977,38 @@ var StatementModel = BasicModel.extend({ self._formatLineProposition(line, line.reconciliation_proposition); if (!line.reconciliation_proposition.length) { delete line.reconciliation_proposition; + }else{ + // loop state propositions + var debit_props = _.filter(line.reconciliation_proposition, function(p){ + return p.amount > 0 && line.st_line.amount; + }); + var credit_props = _.filter(line.reconciliation_proposition, function(p){ + return p.amount < 0 && line.st_line.amount < 0; + }); + var sum_debit_props = debit_props.length > 0 ? debit_props.map(function(p){return p.amount}).reduce(function(p1, p2){return p1 + p2}) : 0; + var sum_credit_props = credit_props.length > 0 ? credit_props.map(function(p){return p.amount}).reduce(function(p1, p2){return p1 + p2}) : 0; + + // Want to display the triangle for partial reconciliation or not. + if(line.st_line.amount_currency > 0 && line.st_line.amount_currency < sum_debit_props && debit_props.length == 1) + debit_props[0].display_triangle = true; + else if(line.st_line.amount_currency < 0 && line.st_line.amount_currency > sum_credit_props && credit_props.length == 1) + credit_props[0].display_triangle = true; } + + // No partner set on st_line and all matching amls have the same one: set it on the st_line. + if(!line.st_line.partner_id && line.reconciliation_proposition){ + var hasDifferentPartners = function(prop){ + return !prop.partner_id || prop.partner_id != line.reconciliation_proposition[0].partner_id; + } + + if(!_.any(line.reconciliation_proposition, hasDifferentPartners)){ + defs.push(self.changePartner(line.handle, { + 'id': line.reconciliation_proposition[0].partner_id, + 'display_name': line.reconciliation_proposition[0].partner_name, + }, true)); + } + } + defs.push(self._computeLine(line)); }); return $.when.apply($, defs); diff --git a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js index 116a6073a8b..e7b9067ca3d 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js @@ -19,7 +19,6 @@ var _t = core._t; var StatementRenderer = Widget.extend(FieldManagerMixin, { template: 'reconciliation.statement', events: { - 'click div:first button.o_automatic_reconciliation': '_onAutoReconciliation', 'click div:first h1.statement_name': '_onClickStatementName', "click *[rel='do_action']": "_onDoAction", 'click button.js_load_more': '_onLoadMore', @@ -67,13 +66,6 @@ var StatementRenderer = Widget.extend(FieldManagerMixin, { this.$('h1.statement_name').text(this._initialState.title || _t('No Title')); - this.enterHandler = function (e) { - if ((e.which === 13 || e.which === 10) && (e.ctrlKey || e.metaKey)) { - this.trigger_up('validate_all_balanced'); - } - }.bind(this); - $('body').on('keyup', this.enterHandler); - return $.when.apply($, defs); }, /** @@ -166,12 +158,6 @@ var StatementRenderer = Widget.extend(FieldManagerMixin, { // Handlers //-------------------------------------------------------------------------- - /** - * @private - */ - _onAutoReconciliation: function () { - this.trigger_up('auto_reconciliation'); - }, /** * @private */ @@ -396,30 +382,17 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { var targetLineAmount = state.st_line.amount; _.each(props, function (line) { - var $line = $(qweb.render("reconciliation.line.mv_line", {'line': line, 'state': state})); + var display_triangle = (line.already_paid === false && + ((state.balance.amount_currency < 0 || line.partial_reconcile) + && line.amount > 0 && state.st_line.amount > 0 && targetLineAmount < line.amount && partialDebitProps <= 1) || + ((state.balance.amount_currency > 0 || line.partial_reconcile) + && line.amount < 0 && state.st_line.amount < 0 && targetLineAmount > line.amount && partialCreditProps <= 1)); + var $line = $(qweb.render("reconciliation.line.mv_line", {'line': line, 'state': state, 'display_triangle': display_triangle})); if (!isNaN(line.id)) { $('') .appendTo($line.find('.cell_info_popover')) .attr("data-content", qweb.render('reconciliation.line.mv_line.details', {'line': line})); } - if (line.already_paid === false && - ((state.balance.amount_currency < 0 || line.partial_reconcile) - && line.amount > 0 && state.st_line.amount > 0 && targetLineAmount < line.amount && partialDebitProps <= 1) || - ((state.balance.amount_currency > 0 || line.partial_reconcile) - && line.amount < 0 && state.st_line.amount < 0 && targetLineAmount > line.amount && partialCreditProps <= 1)) { - var $cell = $line.find(line.amount > 0 ? '.cell_right' : '.cell_left'); - var text; - if (line.partial_reconcile) { - text = _t("Undo the partial reconciliation."); - $cell.text(line.write_off_amount_str); - } else { - text = _t("This move's amount is higher than the transaction's amount. Click to register a partial payment and keep the payment balance open."); - } - - $('') - .prependTo($cell) - .attr("data-content", text); - } targetLineAmount -= line.amount; $props.append($line); diff --git a/addons/account/static/src/js/reconciliation/tour_reconciliation.js b/addons/account/static/src/js/reconciliation/tour_reconciliation.js index 9cb7fd8fa88..3da88d2c7ea 100644 --- a/addons/account/static/src/js/reconciliation/tour_reconciliation.js +++ b/addons/account/static/src/js/reconciliation/tour_reconciliation.js @@ -20,7 +20,7 @@ Tour.register('bank_statement_reconciliation', { method: 'search', args: [[['name', '=', 'BNK/2014/001']]], // account in l10n_generic_coa }).then(function(ids) { - var path = "/web#statement_ids=" + ids[0] + "&action=bank_statement_reconciliation_view"; + var path = "/web?debug=assets#statement_ids=" + ids[0] + "&action=bank_statement_reconciliation_view"; console.log("'bank_statement_reconciliation' url is: '" + path + "'"); window.location.href = path; }).fail(function () { @@ -35,28 +35,23 @@ Tour.register('bank_statement_reconciliation', { run: function () {}, }, - // open a line and reconcile a line proposed by the server + // Reconciliation of 'SAJ/2018/002' + // Select the 'INV/2018/0002' line and click on reconcile. { content: "open the last line in match mode to test the reconcile button", - extra_trigger: '.o_reconciliation_line:last .o_reconcile:visible', - trigger: '.o_reconciliation_line:last .accounting_view thead .cell_label:contains("/002")', + trigger: '.toggle_match:last', }, { - content: "deselect the proposed line", - extra_trigger: '.o_reconciliation_line:last[data-mode="match"]', - trigger: '.o_reconciliation_line:last .accounting_view .cell_label:contains("/0002")' - }, - { - content: "re-select the line", - extra_trigger: '.o_reconciliation_line:last .o_no_valid:visible', // the user can't validate the line and display the write-off line - trigger: '.o_reconciliation_line:last .match .cell_label:contains("INV"):contains("/0002")' + content: "select the 'INV/2018/0002' line", + trigger: '.o_reconciliation_line:last .match .cell_label:contains("INV/2018/0002")' }, { content: "reconcile the line", trigger: '.o_reconciliation_line:last .o_reconcile:visible', }, + // Reconciliation of 'First 2000 € of SAJ/2014/0001' // Make a partial reconciliation { @@ -66,7 +61,7 @@ Tour.register('bank_statement_reconciliation', { }, { content: "select a line with with a higher amount", - trigger: '.o_reconciliation_line:last .match .cell_right:contains($ 4,610.00)' + trigger: '.o_reconciliation_line:last .match .cell_label:contains("INV/2018/0001")' }, { content: "click on partial reconcile", @@ -77,6 +72,7 @@ Tour.register('bank_statement_reconciliation', { trigger: '.o_reconciliation_line:last .o_reconcile:visible', }, + // Reconciliation of 'Prepayment' // Test changing the partner { @@ -91,7 +87,6 @@ Tour.register('bank_statement_reconciliation', { }, { content: "use filter", - extra_trigger: '.o_reconciliation_line:nth-child(2) .match:not(:has(tr:eq(2))) tr:eq(1)', trigger: '.o_reconciliation_line:nth-child(2) .match .match_controls .filter', run: 'text 4610' }, diff --git a/addons/account/static/src/xml/account_reconciliation.xml b/addons/account/static/src/xml/account_reconciliation.xml index 4fed71ce5cb..f2cdcda847d 100644 --- a/addons/account/static/src/xml/account_reconciliation.xml +++ b/addons/account/static/src/xml/account_reconciliation.xml @@ -26,7 +26,6 @@
-
@@ -45,7 +44,6 @@ o_manual_statement -

Good Job! There is nothing to reconcile.

@@ -215,6 +213,20 @@
+ + + + + + + + + + + + @@ -230,8 +242,24 @@ : - - + + + + + + + + + + + + + + diff --git a/addons/account/static/tests/reconciliation_tests.js b/addons/account/static/tests/reconciliation_tests.js index 0895598fe83..92cd9f47405 100644 --- a/addons/account/static/tests/reconciliation_tests.js +++ b/addons/account/static/tests/reconciliation_tests.js @@ -666,23 +666,22 @@ QUnit.module('account', { }); clientAction.appendTo($('#qunit-fixture')); - var widget = clientAction.widgets[0]; assert.strictEqual(widget.$('.mv_line').length, 2, "should display 2 account move lines"); assert.strictEqual(widget.$('.mv_line').text().replace(/[\n\r\s]+/g, ' '), " 101200 2017-02-07 INV/2017/0002 $ 650.00 101200 2017-02-07 INV/2017/0003 $ 525.00 ", "should display 4 account move lines who contains the account_code, due_date, label and the credit"); - assert.strictEqual(widget.$('.mv_line .cell_right:not(:empty)').length, 2, "should display only the credit account move lines (hide the debit)"); + assert.strictEqual(widget.$('.mv_line .cell_right:contains(".")').length, 2, "should display only the credit account move lines (hide the debit)"); clientAction.widgets[1].$('.accounting_view thead td:first').trigger('click'); assert.strictEqual(clientAction.widgets[1].$('.mv_line').length, 5, "should display 5 account move lines"); - assert.strictEqual(clientAction.widgets[1].$('.mv_line .cell_right:not(:empty)').length, 4, "should display only the credit account move lines (hide the debit)"); + assert.strictEqual(clientAction.widgets[1].$('.mv_line .cell_right:contains(".")').length, 4, "should display only the credit account move lines (hide the debit)"); assert.strictEqual(clientAction.widgets[1].$('.mv_line.already_reconciled').length, 3, "should display 3 already reconciled account move lines"); assert.strictEqual(clientAction.widgets[1].$('.mv_line').text().replace(/[\n\r\s]+/g, ' '), " 101401 2017-01-23 ASUSTeK: BNK1/2017/0002: SUPP.OUT/2017/0002 : BILL/2017/0003 $ 376.00 101401 2017-01-23 Agrolait: BNK1/2017/0003: CUST.IN/2017/0001 $ 100.00 101401 2017-01-23 Agrolait: BNK1/2017/0004: CUST.IN/2017/0002 : INV/2017/0003 $ 525.50 101200 2017-02-07 Agrolait: INV/2017/0002 $ 650.00 101200 2017-02-22 Agrolait: INV/2017/0004 $ 525.00 ", "should display 4 account move lines who contains the account_code, due_date, label and the credit"); - assert.strictEqual(clientAction.widgets[1].$('.mv_line .cell_left:not(:empty)').length, 1, "should display only 1 debit account move lines"); + assert.strictEqual(clientAction.widgets[1].$('.mv_line .cell_left:contains(".")').length, 1, "should display only 1 debit account move lines"); // load more assert.ok(clientAction.widgets[1].$('.match div.load-more a:visible').length, "should display the 'load more' button"); @@ -833,10 +832,11 @@ QUnit.module('account', { mockRPC: function (route, args) { console.log(args.method); if (args.method === 'process_bank_statement_line') { + var lines = args.args['1']; assert.deepEqual(args.args, [ [6], [{ - partner_id: false, + partner_id: lines.length == 1 ? lines[0].partner_id : false, counterpart_aml_dicts:[], payment_aml_ids: [392], new_aml_dicts: [], @@ -869,7 +869,12 @@ QUnit.module('account', { widget.$('.accounting_view thead td:first').trigger('click'); widget.$('.match .cell_account_code:first').trigger('click'); assert.equal( widget.$('.accounting_view tbody .cell_left .line_info_button').length, 1, "should display the partial reconciliation alert"); - assert.strictEqual(widget.$('button.btn-primary:visible').length, 0, "should not display the reconcile button"); + + // The partner has been set automatically, remove it. + widget.$('.o_input_dropdown input').trigger('click'); + widget.$('.o_input_dropdown input').val('').trigger('keyup').trigger('blur'); + + assert.strictEqual(widget.$('button.btn-primary:visible').length, 0, "should display the reconcile model buttons"); assert.strictEqual(widget.$('.text-danger:visible').length, 1, "should display counterpart alert"); widget.$('.accounting_view .cell_left .line_info_button').trigger('click'); assert.strictEqual(widget.$('.accounting_view .cell_left .line_info_button').length, 1, "should display a partial reconciliation alert"); @@ -1054,7 +1059,7 @@ QUnit.module('account', { widget.$('.create .create_amount input').val('1100.00').trigger('input'); - assert.strictEqual(widget.$('.accounting_view tbody .cell_right').text(), "$ 1100.00", "should display the value 1100.00 in right column"); + assert.strictEqual(widget.$('.accounting_view tbody .cell_right').text().trim().replace(/[\n\r\s\u00a0]+/g, ' '), "$ 1100.00", "should display the value 1100.00 in right column"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_right').text(), "$ 75.00", "should display 'Open Balance' line because the rest to reconcile is 75.00"); assert.strictEqual(widget.$('.accounting_view tbody tr').length, 1, "should have ever only the created reconcile line"); assert.strictEqual(widget.$('.accounting_view tbody tr').text().replace(/[\n\r\s$,]+/g, ' '), " 101200 SAJ/2014/002 and SAJ/2014/003 1100.00 ", @@ -1067,7 +1072,7 @@ QUnit.module('account', { $('.ui-autocomplete .ui-menu-item a:contains(101000 Current Assets)').trigger('mouseenter').trigger('click'); widget.$('.create .create_label input').val('test0').trigger('input'); - assert.strictEqual(widget.$('.accounting_view tbody .cell_left:last').text(), "$ 100.00", "should display the value 100.00 in left column"); + assert.strictEqual(widget.$('.accounting_view tbody .cell_left:last').text().trim().replace(/[\n\r\s\u00a0]+/g, ' '), "$ 100.00", "should display the value 100.00 in left column"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Open balance", "should display 'Open Balance'"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_right').text(), "$ 175.00", "should display 'Open Balance' line because the rest to reconcile is 175.00"); assert.strictEqual(widget.$('.accounting_view tbody tr').length, 2, "should have 2 created reconcile lines"); @@ -1084,7 +1089,7 @@ QUnit.module('account', { $('.ui-autocomplete .ui-menu-item a:contains(101000 Current Assets)').trigger('mouseenter').trigger('click'); widget.$('.create .create_label input').val('test1').trigger('input'); - assert.strictEqual(widget.$('.accounting_view tbody .cell_right:last').text(), "$ 200.00", "should display the value 200.00 in left column"); + assert.strictEqual(widget.$('.accounting_view tbody .cell_right:last').text().trim().replace(/[\n\r\s\u00a0]+/g, ' '), "$ 200.00", "should display the value 200.00 in left column"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Open balance", "should display 'Open balance'"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "$ 25.00", "should display 'Open balance' with 25.00 in left column"); assert.strictEqual(widget.$('.accounting_view tbody tr').length, 3, "should have 3 created reconcile lines"); @@ -1174,7 +1179,7 @@ QUnit.module('account', { widget.$('.create .create_label input').val('test1').trigger('input'); widget.$('.create .create_amount input').val('1100').trigger('input'); - assert.strictEqual(widget.$('.accounting_view tbody .cell_right:last').text(), "$\u00a01100.00", "should display the value 1100.00 in left column"); + assert.strictEqual(widget.$('.accounting_view tbody .cell_right:last').text().trim().replace(/[\n\r\s\u00a0]+/g, ' '), "$ 1100.00", "should display the value 1100.00 in left column"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Open balance", "should display 'Open Balance'"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_right').text(), "$\u00a075.00", "should display 'Open Balance' with 75.00 in right column"); assert.strictEqual(widget.$('.accounting_view tbody tr').length, 1, "should have 1 created reconcile lines"); @@ -1182,7 +1187,7 @@ QUnit.module('account', { widget.$('.create .create_tax_id input').trigger('click'); $('.ui-autocomplete .ui-menu-item a:contains(10.00%)').trigger('mouseenter').trigger('click'); - assert.strictEqual(widget.$('.accounting_view tbody .cell_right').text().replace('$_', ''), "$\u00a01000.00$\u00a0100.00", "should have 2 created reconcile lines with right column values"); + assert.strictEqual(widget.$('.accounting_view tbody .cell_right').text().trim().replace(/[\n\r\s\u00a0]+/g, ' '), "$ 1000.00 $ 100.00", "should have 2 created reconcile lines with right column values"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Open balance", "should display 'Open Balance'"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_right').text(), "$\u00a075.00", "should display 'Open Balance' with 75.00 in right column"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "", "should display 'Open Balance' without any value in left column"); @@ -1191,7 +1196,7 @@ QUnit.module('account', { widget.$('.create .create_tax_id input').trigger('click'); $('.ui-autocomplete .ui-menu-item a:contains(20.00%)').trigger('mouseenter').trigger('click'); - assert.strictEqual(widget.$('.accounting_view tbody .cell_right').text().replace('$_', ''), "$\u00a01100.00$\u00a0220.00", "should have 2 created reconcile lines with right column values"); + assert.strictEqual(widget.$('.accounting_view tbody .cell_right').text().trim().replace(/[\n\r\s\u00a0]+/g, ' '), "$ 1100.00 $ 220.00", "should have 2 created reconcile lines with right column values"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Open balance", "should display 'Open balance'"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "$\u00a0145.00", "should display 'Open balance' with 145.00 in right column"); assert.strictEqual(widget.$('.accounting_view tbody tr').length, 2, "should have 2 created reconcile lines"); @@ -1215,7 +1220,7 @@ QUnit.module('account', { widget.$('.create .quick_add button:contains(ATOS)').trigger('click'); assert.strictEqual(widget.$('.accounting_view tbody .cell_label, .accounting_view tbody .cell_right').text().replace(/[\n\r\s$,]+/g, ' '), - " ATOS Banque 1145.62 Tax 20.00% 229.12 ATOS Frais 26.71 Tax 10.00% include 2.67", "should display 4 lines"); + " ATOS Banque 1145.62 Tax 20.00% 229.12 ATOS Frais 26.71 Tax 10.00% include 2.67 ", "should display 4 lines"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_label, .accounting_view tfoot .cell_left').text().replace(/[\n\r\s$,]+/g, ' '), "Open balance229.12", "should display the 'Open balance' line with value in left column"); @@ -1241,37 +1246,6 @@ QUnit.module('account', { clientAction.destroy(); }); - QUnit.test('Reconciliation auto reconciliation', function (assert) { - assert.expect(6); - - var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options); - - testUtils.addMockEnvironment(clientAction, { - 'data': this.params.data, - }); - clientAction.appendTo($('#qunit-fixture')); - - clientAction.$('button.o_automatic_reconciliation').trigger('click'); - - assert.strictEqual(clientAction.$('.progress .valuenow').text(), "1", "should reconciled one line"); - assert.strictEqual(clientAction.$('.progress .valuemax').text(), "4", "should have maximum 4 lines to reconciled"); - assert.strictEqual(clientAction.$('thead .cell_label').text().replace(/[\n\r\s$,]+/g, ' '), - "SAJ/2014/002 and SAJ/2014/003 Bank fees First 2000 € of SAJ/2014/001 ", "should rest 3 lines to reconciled"); - assert.ok(clientAction.$('.notification_area .notification').length, "should display the notification"); - var do_action = false; - clientAction.on('do_action', clientAction, function (data) { - do_action = data.data.action; - }); - clientAction.$('.notification_area a.fa-external-link').trigger('click'); - assert.deepEqual(_.pick(do_action, "res_model", "domain", "type"), - {"res_model":"account.move", "domain":[['id',"in",[143]]], "type":"ir.actions.act_window"}, - "should try to open the record form view"); - clientAction.$('.notification_area .close').trigger('click'); - assert.notOk(clientAction.$('.notification_area .notification').length, "should close the notification"); - - clientAction.destroy(); - }); - QUnit.test('Reconciliation manual', function (assert) { assert.expect(13); @@ -1296,7 +1270,7 @@ QUnit.module('account', { " Agrolait 101200 ", "should display the partner and the account code as title"); - assert.strictEqual(clientAction.$('.o_reconciliation_line:first .match tr:first .cell_right').text(), + assert.strictEqual(clientAction.$('.o_reconciliation_line:first .match tr:first .cell_right').text().trim().replace(/[\n\r\s\u00a0]+/g, ' '), "$ 11,000.00", "sould display the line in $"); assert.strictEqual(clientAction.$('.o_reconciliation_line:first .match tr:first .cell_right .o_multi_currency').data('content'), "10,222.00 €", "sould display the monetary information in €"); diff --git a/addons/account/tests/test_bank_statement_reconciliation.py b/addons/account/tests/test_bank_statement_reconciliation.py index 6da03b3c69e..118127ee222 100644 --- a/addons/account/tests/test_bank_statement_reconciliation.py +++ b/addons/account/tests/test_bank_statement_reconciliation.py @@ -12,7 +12,7 @@ class TestBankStatementReconciliation(AccountingTestCase): self.bs_model = self.env['account.bank.statement'] self.bsl_model = self.env['account.bank.statement.line'] self.reconciliation_widget = self.env['account.reconciliation.widget'] - self.partner_agrolait = self.env.ref("base.res_partner_2") + self.partner = self.env['res.partner'].create({'name': 'test'}) def test_reconciliation_proposition(self): rcv_mv_line = self.create_invoice(100) @@ -53,7 +53,7 @@ class TestBankStatementReconciliation(AccountingTestCase): def create_invoice(self, amount): """ Return the move line that gets to be reconciled (the one in the receivable account) """ - vals = {'partner_id': self.partner_agrolait.id, + vals = {'partner_id': self.partner.id, 'type': 'out_invoice', 'name': '-', 'currency_id': self.env.user.company_id.currency_id.id, @@ -89,7 +89,7 @@ class TestBankStatementReconciliation(AccountingTestCase): bank_stmt_line = self.bsl_model.create({ 'name': '_', 'statement_id': bank_stmt.id, - 'partner_id': self.partner_agrolait.id, + 'partner_id': self.partner.id, 'amount': st_line_amount, })