From 97c6dc77925090cb57e57b9edabcf81c93bea87c Mon Sep 17 00:00:00 2001 From: Christophe Matthieu Date: Wed, 5 Apr 2017 14:08:10 +0200 Subject: [PATCH] [FIX] account: fix various bugs in reconciliation * partial reconcile, display, compute, process_reconciliations * more information in tooltip * add domain to filter partner --- .../js/reconciliation/reconciliation_model.js | 81 ++++++++++------ .../reconciliation/reconciliation_renderer.js | 24 +++-- .../src/less/account_reconciliation.less | 1 + .../static/src/xml/account_reconciliation.xml | 5 +- .../static/test/reconciliation_tests.js | 96 +++++++++++++------ 5 files changed, 140 insertions(+), 67 deletions(-) diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js index 791d94e515d..f050ebfa8e5 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_model.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js @@ -402,9 +402,22 @@ var StatementModel = BasicModel.extend({ */ togglePartialReconcile: function (handle) { var line = this.getLine(handle); - var prop = _.find(line.reconciliation_proposition, {'invalid': false}); + var props = _.filter(line.reconciliation_proposition, {'invalid': false}); + if (props.length !== 1) { + return $.Deferred().reject(); + } + var prop = props[0]; prop.partial_reconcile = !prop.partial_reconcile; - return this._computeLine(line); + if (!prop.partial_reconcile) { + return this._computeLine(line); + } + return this._computeLine(line).then(function () { + if (prop.partial_reconcile) { + line.balance.amount = 0; + line.balance.type = 1; + line.mode = 'inactive'; + } + }); }, /** * Change the value of the editable proposition line or create a new one. @@ -472,9 +485,15 @@ var StatementModel = BasicModel.extend({ ids.push(line.id); values.push({ "partner_id": line.st_line.partner_id, - "counterpart_aml_dicts": _.map(_.filter(props, function (prop) {return !isNaN(prop.id) && !prop.already_paid;}), self._formatToProcessReconciliation), - "payment_aml_ids": _.map(_.filter(props, function (prop) {return !isNaN(prop.id) && prop.already_paid;}), self._formatToProcessReconciliation), - "new_aml_dicts": _.map(_.filter(props, function (prop) {return isNaN(prop.id);}), self._formatToProcessReconciliation), + "counterpart_aml_dicts": _.map(_.filter(props, function (prop) { + return !isNaN(prop.id) || prop.partial_reconcile; + }), self._formatToProcessReconciliation.bind(self, line)), + "payment_aml_ids": _.pluck(_.filter(props, function (prop) { + return !isNaN(prop.id) && !prop.partial_reconcile; + }), 'id'), + "new_aml_dicts": _.map(_.filter(props, function (prop) { + return isNaN(prop.id); + }), self._formatToProcessReconciliation.bind(self, line)), }); line.reconciled = true; self.valuenow++; @@ -514,8 +533,8 @@ var StatementModel = BasicModel.extend({ } line.reconciliation_proposition.push(prop); - _.each(line.reconciliation_proposition, function (line) { - line.partial_reconcile = false; + _.each(line.reconciliation_proposition, function (prop) { + prop.partial_reconcile = false; }); }, /** @@ -627,13 +646,6 @@ var StatementModel = BasicModel.extend({ 'account_code': self.accounts[line.st_line.open_balance_account_id], }; line.balance.type = line.balance.amount ? (line.balance.amount > 0 && line.st_line.partner_id ? 0 : -1) : 1; - - if (line.balance.type === -1) { - var props = _.filter(line.reconciliation_proposition, {'invalid': false}); - if (props.length === 1 && props[0].partial_reconcile) { - line.balance.type = 0; - } - } }); }, /** @@ -678,6 +690,7 @@ var StatementModel = BasicModel.extend({ prop.amount = prop.debit || -prop.credit; prop.label = prop.name; prop.account_id = self._formatNameGet(prop.account_id || line.account_id); + prop.is_partially_reconciled = prop.amount_str !== prop.total_amount_str; }); } }, @@ -791,7 +804,7 @@ var StatementModel = BasicModel.extend({ * @returns {Boolean} */ _isValid: function (prop) { - return prop.account_id && prop.amount && prop.label && !!prop.label.length; + return !isNaN(prop.id) || prop.account_id && prop.amount && prop.label && !!prop.label.length; }, /** * Fetch 'account.bank.statement.line' propositions. @@ -804,9 +817,11 @@ var StatementModel = BasicModel.extend({ */ _performMoveLine: function (handle) { var line = this.getLine(handle); - var excluded_ids = _.flatten(_.map(this.lines, function (line) { - return _.filter(_.pluck(line.reconciliation_proposition, 'id'), _.isNumber); - })); + var excluded_ids = _.compact(_.flatten(_.map(this.lines, function (line) { + return _.map(line.reconciliation_proposition, function (prop) { + return !prop.partial_reconcile && _.isNumber(prop.id) ? prop.id : null; + }); + }))); var filter = line.filter || ""; var offset = line.offset; var limit = 6; @@ -821,17 +836,25 @@ var StatementModel = BasicModel.extend({ * format the proposition to send information server side * * @private + * @param {object} line * @param {object} proposition * @returns {object} */ - _formatToProcessReconciliation: function (prop) { + _formatToProcessReconciliation: function (line, prop) { + var amount = prop.partial_reconcile ? -line.st_line.amount : prop.amount; var result = { - 'name' : prop.label, - 'debit' : prop.amount > 0 ? prop.amount : 0, - 'credit' : prop.amount < 0 ? -prop.amount : 0, - 'account_id' : prop.account_id.id, - 'journal_id' : prop.journal_id && prop.journal_id.id, + name : prop.label, + debit : amount > 0 ? amount : 0, + credit : amount < 0 ? -amount : 0, }; + if (!isNaN(prop.id)) { + result.counterpart_aml_id = prop.id; + } else { + result.account_id = prop.account_id.id; + if (prop.journal_id) { + result.journal_id = prop.account_id.id; + } + } if (!isNaN(prop.id)) result.counterpart_aml_id = prop.id; if (prop.analytic_account_id) result.analytic_account_id = prop.analytic_account_id.id; if (prop.tax_id) result.tax_ids = [[4, prop.tax_id.id, null]]; @@ -970,7 +993,7 @@ var ManualModel = StatementModel.extend({ }); } else { var mv_line_ids = _.pluck(_.filter(props, function (prop) {return !isNaN(prop.id);}), 'id'); - var new_mv_line_dicts = _.map(_.filter(props, function (prop) {return isNaN(prop.id);}), self._formatToProcessReconciliation); + var new_mv_line_dicts = _.map(_.filter(props, function (prop) {return isNaN(prop.id);}), self._formatToProcessReconciliation.bind(self, line)); process_reconciliations.push({ id: null, type: null, @@ -1127,9 +1150,11 @@ var ManualModel = StatementModel.extend({ */ _performMoveLine: function (handle) { var line = this.getLine(handle); - var excluded_ids = _.flatten(_.map(this.lines, function (line) { - return _.filter(_.pluck(line.reconciliation_proposition, 'id'), _.isNumber); - })); + var excluded_ids = _.compact(_.flatten(_.map(this.lines, function (line) { + return _.map(line.reconciliation_proposition, function (prop) { + return !prop.partial_reconcile && _.isNumber(prop.id) ? prop.id : null; + }); + }))); var filter = line.filter || ""; var offset = line.offset; var limit = 6; diff --git a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js index 011591afa29..541b3a1952a 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js @@ -194,7 +194,7 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { 'click .accounting_view tbody .mv_line td': '_onSelectProposition', 'click .o_reconcile_models button': '_onQuickCreateProposition', 'click .create .add_line': '_onCreateProposition', - 'click .accounting_view .cell_right .line_info_button': '_onTogglePartialReconcile', + 'click .accounting_view .line_info_button.fa-exclamation-triangle': '_onTogglePartialReconcile', 'click .reconcile_model_create': '_onCreateReconcileModel', 'click .reconcile_model_edit': '_onEditReconcileModel', }, @@ -290,12 +290,18 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { .attr("data-content", qweb.render('reconciliation.line.mv_line.details', {'line': line})); } - if (state.balance.amount < 0 && props.length === 1) { - var text = !line.partial_reconcile ? - _t("This move's amount is higher than the transaction's amount. Click to register a partial payment and keep the payment balance open."): - _t("Undo the partial reconciliation."); + if ((state.balance.amount !== 0 || line.partial_reconcile) && props.length === 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(state.st_line.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($line.find('.cell_right')) + .prependTo($cell) .attr("data-content", text); } $props.append($line); @@ -353,7 +359,11 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { type: 'many2one', name: 'partner_id', value: [partnerID, partnerName] - }]); + }], { + partner_id: { + domain: [["parent_id", "=", false], "|", ["customer", "=", true], ["supplier", "=", true]] + } + }); return this.model.get(recordID); }, diff --git a/addons/account/static/src/less/account_reconciliation.less b/addons/account/static/src/less/account_reconciliation.less index 5df97bb2e9f..253dc25996f 100644 --- a/addons/account/static/src/less/account_reconciliation.less +++ b/addons/account/static/src/less/account_reconciliation.less @@ -176,6 +176,7 @@ vertical-align: top; padding-right: 10px; font-weight: bold; + white-space: nowrap; } } diff --git a/addons/account/static/src/xml/account_reconciliation.xml b/addons/account/static/src/xml/account_reconciliation.xml index 07a9bbfea9d..33a08794bbc 100644 --- a/addons/account/static/src/xml/account_reconciliation.xml +++ b/addons/account/static/src/xml/account_reconciliation.xml @@ -112,7 +112,7 @@ - + @@ -211,7 +211,8 @@ Partner Date Due Date - ResidualAmount + Amount () + Residual () diff --git a/addons/account/static/test/reconciliation_tests.js b/addons/account/static/test/reconciliation_tests.js index 62ec17f9740..2e984dcf878 100644 --- a/addons/account/static/test/reconciliation_tests.js +++ b/addons/account/static/test/reconciliation_tests.js @@ -7,14 +7,16 @@ var data = { id: {string: "ID", type: 'integer'}, display_name: {string: "Displayed name", type: 'char'}, image: {string: "image", type: 'integer'}, + customer: {string: "customer", type: 'boolean'}, + supplier: {string: "supplier", type: 'boolean'}, }, records: [ - {id: 1, display_name: "partner 1", image: 'AAA'}, - {id: 2, display_name: "partner 2", image: 'BBB'}, - {id: 3, display_name: "partner 3", image: 'CCC'}, - {id: 4, display_name: "partner 4", image: 'DDD'}, - {id: 8, display_name: "Agrolait", image: 'EEE'}, - {id: 12, display_name: "Camptocamp", image: 'FFF'}, + {id: 1, display_name: "partner 1", image: 'AAA', customer: true}, + {id: 2, display_name: "partner 2", image: 'BBB', customer: true}, + {id: 3, display_name: "partner 3", image: 'CCC', customer: true}, + {id: 4, display_name: "partner 4", image: 'DDD', customer: true}, + {id: 8, display_name: "Agrolait", image: 'EEE', customer: true}, + {id: 12, display_name: "Camptocamp", image: 'FFF', supplier: true}, ], mark_as_reconciled: function () { return $.when(); @@ -132,14 +134,15 @@ var data = { return $.when(auto_reconciliation); }, process_reconciliations: function (args) { - var datas = args[0]; - for (var i in datas) { - var data = datas[i]; - for (var key in move_lines_for_manual_reconciliation) { - move_lines_for_manual_reconciliation[key] = _.filter(move_lines_for_manual_reconciliation[key], function (mv_line) { - return data.mv_line_ids.indexOf(mv_line.id) === -1; - }); - } + var datas = args[1]; + var ids = _.flatten(_.pluck(_.pluck(datas, 'counterpart_aml_dicts'), 'counterpart_aml_id')); + ids = ids.concat(_.flatten(_.pluck(datas, 'payment_aml_ids'))); + ids = _.compact(ids); + + for (var key in move_lines_for_manual_reconciliation) { + move_lines_for_manual_reconciliation[key] = _.filter(move_lines_for_manual_reconciliation[key], function (mv_line) { + return ids.indexOf(mv_line.id) === -1; + }); } return $.when(); }, @@ -554,11 +557,11 @@ QUnit.module('account', { QUnit.test('Reconciliation basic data', function (assert) { - assert.expect(20); + assert.expect(13); var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options); testUtils.addMockEnvironment(clientAction, { - 'data': this.params.data, + 'data': this.params.data }); clientAction.appendTo($('#qunit-fixture')); @@ -591,24 +594,57 @@ QUnit.module('account', { clientAction.widgets[3].$('.accounting_view thead td:first').trigger('click'); assert.strictEqual(clientAction.widgets[3].$el.data('mode'), 'create', "should switch to 'create' mode instead 'match' mode when 'match' mode is empty"); - widget = clientAction.widgets[2]; - - assert.notOk( widget.$('.cell_right .line_info_button').length, "should not display the partial reconciliation alert"); - assert.ok( widget.$('button.btn-primary:not(hidden)').length, "should display the reconcile button"); - widget.$('.accounting_view thead td:first').trigger('click'); - widget.$('.match td.cell_label:first').trigger('click'); - assert.ok( widget.$('.text-danger:not(hidden)').length, "should display counterpart alert"); - widget.$('.accounting_view tbody td.cell_label:first').trigger('click'); - assert.ok( widget.$('.text-danger:not(hidden)').length, "should display counterpart alert"); - assert.strictEqual(widget.$('.cell_right .line_info_button').length, 1, "should display a partial reconciliation alert"); - widget.$('.cell_right .line_info_button').trigger('click'); - assert.ok(widget.$('.cell_right .line_info_button').hasClass('do_partial_reconcile_false'), "should display the partial reconciliation information"); - assert.ok( widget.$('button.btn-default:not(hidden)').length, "should display the validate button"); - clientAction.destroy(); }); + QUnit.test('Reconciliation partial', function (assert) { + assert.expect(9); + + var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options); + testUtils.addMockEnvironment(clientAction, { + data: this.params.data, + mockRPC: function (route, args) { + if (args.method === 'process_reconciliations') { + assert.deepEqual(args.args, [ + [6], + [{ + partner_id: false, + counterpart_aml_dicts:[{ + name: "BNK1/2017/0002: SUPP.OUT/2017/0002", + debit: 32.58, + credit: 0, + counterpart_aml_id: 392 + }], + payment_aml_ids: [], + new_aml_dicts: [] + }] + ], "should call process_reconciliations with partial reconcile values"); + } + return this._super(route, args); + }, + }); + + clientAction.appendTo($('body')); + + var widget = clientAction.widgets[1]; + + assert.notOk( widget.$('.cell_left .line_info_button').length, "should not display the partial reconciliation alert"); + 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.ok( widget.$('button.btn-primary:not(hidden)').length, "should not display the reconcile button"); + assert.ok( widget.$('.text-danger:not(hidden)').length, "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"); + assert.ok(widget.$('.accounting_view .cell_left .line_info_button').hasClass('do_partial_reconcile_false'), "should display the partial reconciliation information"); + assert.ok( widget.$('button.btn-default:not(hidden)').length, "should display the validate button"); + assert.strictEqual( widget.$el.data('mode'), "inactive", "should be inactive mode"); + widget.$('button.btn-default:not(hidden)').trigger('click'); + + clientAction.destroy(); + }); + QUnit.test('Reconciliation title', function (assert) { assert.expect(6);