diff options
| author | wan <wan@odoo.com> | 2019-02-08 10:11:41 +0000 |
|---|---|---|
| committer | wan <wan@odoo.com> | 2019-02-08 10:11:41 +0000 |
| commit | 969705fa2ae8bfd5e2d2cfce0241fcd56bebf01f (patch) | |
| tree | ddfc35a278f022c3c62962a14b587de49bb67f4a /addons/account/static | |
| parent | 723789b6b1997568943bcf2fa879b8471838ec19 (diff) | |
[ADD] account: suspense account management
Task 1930403
Purpose:
A suspense account is an account in the general ledger that temporarily stores any transactions for which there is uncertainty about the account in which they should be recorded. Once the accounting staff investigates and clarifies the purpose of this type of transaction, it shifts the transaction out of the suspense account and into the correct account(s). An entry into a suspense account may be a debit or a credit.
The Chosen Odoo approach (macro) for this case is
1) from bank statement -> reconciliation widget -> reconciliation model
2) create move that have a dedicated checkbox
3) be able to reopen the bank statement line "like if there is no linked account move" and finally reconcile the transaction with the good invoice/bill (we can reprocess account move "to check")
4) when we reprocess, Instead of creating an account move : Check if there is an "to check" existing linked account move and remove all the account move lines then replace them by the good ones --> we want to keep the sequence
closes odoo/odoo#30486
Diffstat (limited to 'addons/account/static')
6 files changed, 165 insertions, 16 deletions
diff --git a/addons/account/static/src/js/reconciliation/reconciliation_action.js b/addons/account/static/src/js/reconciliation/reconciliation_action.js index e43667be0e4..4690f15d801 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_action.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_action.js @@ -32,6 +32,9 @@ var StatementAction = AbstractAction.extend({ load_more: '_onLoadMore', reload: 'reload', }, + events: { + 'change .reconciliation_search_input': '_onSearch', + }, config: _.extend({}, AbstractAction.prototype.config, { // used to instantiate the model Model: ReconciliationModel.StatementModel, @@ -40,7 +43,7 @@ var StatementAction = AbstractAction.extend({ // used to instantiate each widget line LineRenderer: ReconciliationRenderer.LineRenderer, // used context params - params: ['statement_ids'], + params: ['statement_line_ids'], // number of statements/partners/accounts to display defaultDisplayQty: 10, // number of moves lines displayed in 'match' mode @@ -66,12 +69,12 @@ var StatementAction = AbstractAction.extend({ // Adding values from the context is necessary to put this information in the url via the action manager so that // you can retrieve it if the person shares his url or presses f5 _.each(params.params, function (value, name) { - params.context[name] = name.indexOf('_ids') !== -1 ? _.map((value+'').split(), parseFloat) : value; + params.context[name] = name.indexOf('_ids') !== -1 ? _.map((value+'').split(','), parseFloat) : value; }); params.params = {}; _.each(this.config.params, function (name) { if (params.context[name]) { - params.params[name] = name.indexOf('_ids') !== -1 && _.isArray(params.context[name]) ? params.context[name].join() : params.context[name]; + params.params[name] = params.context[name]; } }); }, @@ -249,6 +252,16 @@ var StatementAction = AbstractAction.extend({ } }); }, + + /** + * @private + * @param {OdooEvent} ev + */ + _onSearch: function (ev) { + var self = this; + ev.stopPropagation(); + this.reload(); + }, _onActionPartialAmount: function(event) { var self = this; diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js index 3714884473f..9f667221621 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_model.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js @@ -62,6 +62,7 @@ var _t = core._t; * } * [ref]: string * [is_partially_reconciled]: boolean + * [to_check]: boolean * [amount_currency_str]: string|false (amount in record currency) * } * mv_lines: object - idem than reconciliation_proposition @@ -94,7 +95,7 @@ var _t = core._t; */ var StatementModel = BasicModel.extend({ avoidCreate: false, - quickCreateFields: ['account_id', 'amount', 'analytic_account_id', 'label', 'tax_id', 'force_tax_included', 'analytic_tag_ids'], + quickCreateFields: ['account_id', 'amount', 'analytic_account_id', 'label', 'tax_id', 'force_tax_included', 'analytic_tag_ids', 'to_check'], /** * @override @@ -339,14 +340,13 @@ var StatementModel = BasicModel.extend({ * - 'account.reconciliation.widget' fetch each line data * * @param {Object} context - * @param {number[]} context.statement_ids + * @param {number[]} context.statement_line_ids * @returns {Deferred} */ load: function (context) { var self = this; - // var statement_ids = context.statement_ids; - this.statement_ids = context.statement_ids; - if (!this.statement_ids) { + this.statement_line_ids = context.statement_line_ids; + if (!this.statement_line_ids) { return $.when(); } this.context = context; @@ -393,14 +393,16 @@ var StatementModel = BasicModel.extend({ self.alreadyDisplayed = []; self.lines = {}; self.pagerIndex = 0; + self.search_str = $('.reconciliation_search_input').val() var def_statement = this._rpc({ model: 'account.reconciliation.widget', method: 'get_bank_statement_data', - args: [self.statement_ids], + kwargs: {"bank_statement_line_ids":self.statement_line_ids, "search_str":self.search_str}, + context: self.context, }) .then(function (statement) { self.statement = statement; - self.bank_statement_id = self.statement_ids.length === 1 ? {id: self.statement_ids[0], display_name: statement.statement_name} : false; + self.bank_statement_id = self.statement_line_ids.length === 1 ? {id: self.statement_line_ids[0], display_name: statement.statement_name} : false; self.valuenow = self.valuenow || statement.value_min; self.valuemax = self.valuemax || statement.value_max; self.context.journal_id = statement.journal_id; @@ -486,11 +488,18 @@ var StatementModel = BasicModel.extend({ quickCreateProposition: function (handle, reconcileModelId) { var line = this.getLine(handle); var reconcileModel = _.find(this.reconcileModels, function (r) {return r.id === reconcileModelId;}); - var fields = ['account_id', 'amount', 'amount_type', 'analytic_account_id', 'journal_id', 'label', 'force_tax_included', 'tax_id', 'analytic_tag_ids']; + var fields = ['account_id', 'amount', 'amount_type', 'analytic_account_id', 'journal_id', 'label', 'force_tax_included', 'tax_id', 'analytic_tag_ids', 'to_check']; this._blurProposition(handle); var focus = this._formatQuickCreate(line, _.pick(reconcileModel, fields)); focus.reconcileModelId = reconcileModelId; + if (!line.reconciliation_proposition.every(function(prop) {return prop.to_check == focus.to_check})) { + new CrashManager().show_warning({data: { + exception_type: _t("Incorrect Operation"), + message: _t("You cannot mix items with and without the 'To Check' checkbox ticked.") + }}); + return $.when(); + } line.reconciliation_proposition.push(focus); if (reconcileModel.has_second_line) { @@ -615,6 +624,14 @@ var StatementModel = BasicModel.extend({ prop = this._formatQuickCreate(line); line.reconciliation_proposition.push(prop); } + if (!line.reconciliation_proposition.slice(0,-1).every(function(prop) {return prop.to_check == values.to_check})) { + new CrashManager().show_warning({data: { + exception_type: _t("Incorrect Operation"), + message: _t("You cannot mix items with and without the 'To Check' checkbox ticked.") + }}); + $('.create_to_check input').click(); + return $.when(); + } _.each(values, function (value, fieldName) { if (fieldName === 'analytic_tag_ids') { switch (value.operation) { @@ -708,6 +725,15 @@ var StatementModel = BasicModel.extend({ return isNaN(prop.id) && prop.display; }), self._formatToProcessReconciliation.bind(self, line)), }; + line.reconciliation_proposition.some(function(prop) { + if (prop.to_check) { + values_dict['to_check'] = true; + return true; + } + }) + if (line.reconciliation_proposition[0].to_check) { + values_dict['to_check'] = true; + } // If the lines are not fully balanced, create an unreconciled amount. // line.st_line.currency_id is never false here because its equivalent to @@ -733,6 +759,7 @@ var StatementModel = BasicModel.extend({ model: 'account.reconciliation.widget', method: 'process_bank_statement_line', args: [ids, values], + context: self.context, }) .then(function () { return {handles: handles}; @@ -1019,6 +1046,16 @@ var StatementModel = BasicModel.extend({ .then(function(){ return data.write_off ? self.quickCreateProposition(line.handle, data.model_id) : true; }) + .then(function() { + // If still no partner set, take the one from context, if it exists + if (!line.st_line.partner_id && self.context.partner_id && self.context.partner_name) { + return self.changePartner(line.handle, { + 'id': self.context.partner_id, + 'display_name': self.context.partner_name, + }, true); + } + return true; + }) ); }); return $.when.apply($, defs); @@ -1090,6 +1127,7 @@ var StatementModel = BasicModel.extend({ 'link': values.link, 'display': true, 'invalid': true, + 'to_check': values.to_check, '__tax_to_recompute': true, 'is_tax': values.is_tax, '__focus': '__focus' in values ? values.__focus : true, @@ -1235,7 +1273,7 @@ var StatementModel = BasicModel.extend({ * datas allowing manual reconciliation */ var ManualModel = StatementModel.extend({ - quickCreateFields: ['account_id', 'journal_id', 'amount', 'analytic_account_id', 'label', 'tax_id', 'force_tax_included', 'analytic_tag_ids', 'date'], + quickCreateFields: ['account_id', 'journal_id', 'amount', 'analytic_account_id', 'label', 'tax_id', 'force_tax_included', 'analytic_tag_ids', 'date', 'to_check'], //-------------------------------------------------------------------------- // Public diff --git a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js index 23c680ddcda..abc5a5aa2e8 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js @@ -66,7 +66,9 @@ var StatementRenderer = Widget.extend(FieldManagerMixin, { } this.$('h1.statement_name').text(this._initialState.title || _t('No Title')); - + if (this.model.context && this.model.context.args && this.model.context.args.search) { + this.$('.reconciliation_search_input').val(self.model.context.args.search); + } return $.when.apply($, defs); }, /** @@ -335,9 +337,11 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { update: function (state) { var self = this; // isValid - this.$('caption .o_buttons button.o_validate').toggleClass('d-none', !!state.balance.type); - this.$('caption .o_buttons button.o_reconcile').toggleClass('d-none', state.balance.type <= 0); + var to_check_checked = !!(state.reconciliation_proposition[0] && state.reconciliation_proposition[0].to_check); + this.$('caption .o_buttons button.o_validate').toggleClass('d-none', !!state.balance.type && !to_check_checked); + this.$('caption .o_buttons button.o_reconcile').toggleClass('d-none', state.balance.type <= 0 || to_check_checked); this.$('caption .o_buttons .o_no_valid').toggleClass('d-none', state.balance.type >= 0); + self.$('caption .o_buttons button.o_validate').toggleClass('text-warning', to_check_checked); // partner_id this._makePartnerRecord(state.st_line.partner_id, state.st_line.partner_name).then(function (recordID) { @@ -545,6 +549,9 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { }, { type: 'char', //TODO is it a bug or a feature when type date exists ? name: 'date', + }, { + type: 'boolean', + name: 'to_check', }], { account_id: {string: _t("Account")}, label: {string: _t("Label")}, @@ -580,6 +587,9 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { self.fields.date = new basic_fields.FieldDate(self, 'date', record, {mode: 'edit'}); + self.fields.to_check = new basic_fields.FieldBoolean(self, + 'to_check', record, {mode: 'edit'}); + var $create = $(qweb.render("reconciliation.line.create", {'state': state})); self.fields.account_id.appendTo($create.find('.create_account_id .o_td_field')) .then(addRequiredStyle.bind(self, self.fields.account_id)); @@ -593,6 +603,7 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { self.fields.amount.appendTo($create.find('.create_amount .o_td_field')) .then(addRequiredStyle.bind(self, self.fields.amount)); self.fields.date.appendTo($create.find('.create_date .o_td_field')) + self.fields.to_check.appendTo($create.find('.create_to_check .o_td_field')) self.$('.create').append($create); function addRequiredStyle(widget) { diff --git a/addons/account/static/src/scss/account_reconciliation.scss b/addons/account/static/src/scss/account_reconciliation.scss index 1d94d0cbcb7..92a033b211c 100644 --- a/addons/account/static/src/scss/account_reconciliation.scss +++ b/addons/account/static/src/scss/account_reconciliation.scss @@ -18,6 +18,23 @@ } } + .o_filter_input_wrapper { + position: relative; + float: right; + width: 150px; + margin-top: 0px; + margin-bottom: 20px; + float: right; + .searchIcon { + position: absolute; + right: 10px; + } + .o_filter_input { + border: none; + border-bottom: 1px black solid; + } + } + .progress { width: 100%; margin: 10px 0 2px 0; diff --git a/addons/account/static/src/xml/account_reconciliation.xml b/addons/account/static/src/xml/account_reconciliation.xml index 33c778a9b7c..0ea973ca2c0 100644 --- a/addons/account/static/src/xml/account_reconciliation.xml +++ b/addons/account/static/src/xml/account_reconciliation.xml @@ -19,6 +19,10 @@ <t t-else=""> <h1 class="statement_name" /> </t> + <div class="o_filter_input_wrapper"> + <span class="searchIcon"><i class="fa fa-search" role="img" aria-label="Search" title="Search"></i></span> + <input class="reconciliation_search_input o_filter_input" type="text" placeholder="Search..." /> + </div> <h1 class="statement_name_edition" style="display: none;"/> <div class="progress progress-striped"> <div class="progress-text"> @@ -44,6 +48,7 @@ <attribute name="class">o_manual_statement</attribute> </t> <t t-jquery=".o_view_noreconciliation p" t-operation="replace"></t> + <t t-jquery=".o_filter_input_wrapper" t-operation="replace"></t> <t t-jquery=".o_view_noreconciliation" t-operation="append"> <p><b>Good Job!</b> There is nothing to reconcile.</p> <p>All invoices and payments have been matched, your accounts' balances are clean.</p> @@ -216,6 +221,10 @@ <td class="o_td_label">Writeoff Date</td> <td class="o_td_field"></td> </tr> + <tr class="create_to_check"> <!--TODO d-none maybe see with avw--> + <td class="o_td_label">To Check</td> + <td class="o_td_field"></td> + </tr> </tbody> </table> <div class="add_line_container"> diff --git a/addons/account/static/tests/reconciliation_tests.js b/addons/account/static/tests/reconciliation_tests.js index 8b48d52a723..d32d5a51233 100644 --- a/addons/account/static/tests/reconciliation_tests.js +++ b/addons/account/static/tests/reconciliation_tests.js @@ -63,6 +63,7 @@ var db = { {id: 287, code: 101200, name: "101200 Account Receivable", company_id: 1}, {id: 288, code: 101300, name: "101300 Tax Paid", company_id: 1}, {id: 308, code: 101401, name: "101401 Bank", company_id: 1}, + {id: 499, code: 499001, name: "499001 Suspense Account", company_id: 1}, {id: 500, code: 500, name: "500 Account", company_id: 1}, {id: 501, code: 501, name: "501 Account", company_id: 1}, {id: 502, code: 502, name: "502 Account", company_id: 1}, @@ -553,7 +554,7 @@ var session = { var options = { context: { - statement_ids: [4] + statement_line_ids: [4] }, params: { limitMoveLines: 5, @@ -1857,5 +1858,65 @@ QUnit.module('account', { clientAction.destroy(); }); + + QUnit.test('Reconcile temporarily and ask to check', function(assert){ + assert.expect(4); + this.params.options.context['to_check'] = true; + var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options); + + testUtils.mock.addMockEnvironment(clientAction, { + data: this.params.data, + session: { + currencies: { + 3: { + digits: [69, 2], + position: "before", + symbol: "$" + } + } + }, + }); + clientAction.appendTo($('#qunit-fixture')); + var widget = clientAction.widgets[0]; + + // Add a line as proposition + // open the first line + widget.$('.accounting_view tfoot td.cell_label').click() + + var $reconcileForm = widget.$(".create"); + $reconcileForm.find('.create_account_id input').val('499001 Suspense Account').keydown().keyup() + $reconcileForm.find('.create_account_id input').click(); + $('.ui-autocomplete .ui-menu-item a:contains(499001 Suspense Account)') + .trigger('mouseover') + .trigger('click'); + + assert.equal($("button.o_validate.btn.btn-secondary.text-warning:first").length, 0, "should not display reconcile button in orange"); + $reconcileForm.find('.create_to_check input').click() + assert.equal($("button.o_validate.btn.btn-secondary.text-warning:first").length, 1, "should display reconcile button in orange"); + + testUtils.mock.intercept(clientAction, 'call_service', function (event) { + assert.deepEqual(event.data.args[1].args, + [[5],[{partner_id: 8, + counterpart_aml_dicts: [], + payment_aml_ids: [], + new_aml_dicts: [{account_id: 499, + credit: 1175, + debit: 0, + analytic_tag_ids: [[6, null, []]], + name: 'SAJ/2014/002 and SAJ/2014/003', + }], + to_check: true, + }]], + "Should call process_bank_statement_line with to_check set to true"); + var def = $.Deferred(); + def.abort = function () {}; + event.data.callback(def); + }); + + widget.$("button.o_reconcile.btn.btn-primary:first").click(); + assert.ok(true, "No error in reconciliation"); + + clientAction.destroy(); + }); }); }); |
