From 10b9d06689c55e5273d2fdff560403cac59278be Mon Sep 17 00:00:00 2001 From: Laurent Smet Date: Wed, 8 Aug 2018 13:45:11 +0200 Subject: [PATCH] [ADD] account: allow forcing tax included tax in reconciliation widget Before this commit, the only way to deal with the same tax either with price included or not was to duplicate it, one with the price included checked. This commit add a new boolean on the reconciliation widget propositions to decide if the tax must be used like a price included tax or not. PR: 17040 task: 32717 --- addons/account/models/account.py | 37 +++++++++++-- addons/account/models/chart_template.py | 6 +++ .../js/reconciliation/reconciliation_model.js | 54 ++++++++++++++++--- .../reconciliation/reconciliation_renderer.js | 22 +++++++- .../static/src/xml/account_reconciliation.xml | 4 ++ addons/account/views/account_view.xml | 19 ++++++- .../static/src/js/fields/abstract_field.js | 3 ++ .../static/src/js/fields/relational_fields.js | 3 ++ 8 files changed, 135 insertions(+), 13 deletions(-) diff --git a/addons/account/models/account.py b/addons/account/models/account.py index 8e297c04407..9c8bdc6bfeb 100644 --- a/addons/account/models/account.py +++ b/addons/account/models/account.py @@ -975,11 +975,14 @@ class AccountTax(models.Model): return math.copysign(quantity, base_amount) * self.amount else: return quantity * self.amount - if (self.amount_type == 'percent' and not self.price_include) or (self.amount_type == 'division' and self.price_include): + + price_include = self.price_include or self._context.get('force_price_include') + + if (self.amount_type == 'percent' and not price_include) or (self.amount_type == 'division' and price_include): return base_amount * self.amount / 100 - if self.amount_type == 'percent' and self.price_include: + if self.amount_type == 'percent' and price_include: return base_amount - (base_amount / (1 + self.amount / 100)) - if self.amount_type == 'division' and not self.price_include: + if self.amount_type == 'division' and not price_include: return base_amount / (1 - self.amount / 100) - base_amount @api.multi @@ -1055,6 +1058,10 @@ class AccountTax(models.Model): # depending on the context. This domain might filter out some taxes from self, e.g. in the # case of group taxes. for tax in self.sorted(key=lambda r: r.sequence): + # Allow forcing price_include/include_base_amount through the context for the reconciliation widget. + # See task 24014. + price_include = self._context.get('force_price_include', tax.price_include) + if tax.amount_type == 'group': children = tax.children_tax_ids.with_context(base_values=(total_excluded, total_included, base)) ret = children.compute_all(price_unit, currency, quantity, product, partner) @@ -1071,7 +1078,7 @@ class AccountTax(models.Model): else: tax_amount = currency.round(tax_amount) - if tax.price_include: + if price_include: total_excluded -= tax_amount base -= tax_amount else: @@ -1136,6 +1143,12 @@ class AccountReconcileModel(models.Model): ('fixed', 'Fixed'), ('percentage', 'Percentage of balance') ], required=True, default='percentage') + is_tax_price_included = fields.Boolean(string='Is Tax Included in Price', related='tax_id.price_include', + help='Technical field used inside the view to make the force_tax_included field readonly if the tax is already price included.') + tax_amount_type = fields.Selection(string='Tax Amount Type', related='tax_id.amount_type', + help='Technical field used inside the view to make the force_tax_included field invisible if the tax is a group.') + force_tax_included = fields.Boolean(string='Tax Included in Price', + help='Force the tax to be managed as a price included tax.') amount = fields.Float(digits=0, required=True, default=100.0, help="Fixed amount will count as a debit if it is negative, as a credit if it is positive.") tax_id = fields.Many2one('account.tax', string='Tax', ondelete='restrict') analytic_account_id = fields.Many2one('account.analytic.account', string='Analytic Account', ondelete='set null') @@ -1148,6 +1161,12 @@ class AccountReconcileModel(models.Model): ('fixed', 'Fixed'), ('percentage', 'Percentage of amount') ], string="Second Amount type",required=True, default='percentage') + is_second_tax_price_included = fields.Boolean(string='Is Second Tax Included in Price', related='second_tax_id.price_include', + help='Technical field used inside the view to make the force_second_tax_included field readonly if the tax is already price included.') + second_tax_amount_type = fields.Selection(string='Second Tax Amount Type', related='second_tax_id.amount_type', + help='Technical field used inside the view to make the force_second_tax_included field invisible if the tax is a group.') + force_second_tax_included = fields.Boolean(string='Second Tax Included in Price', + help='Force the second tax to be managed as a price included tax.') second_amount = fields.Float(string='Second Amount', digits=0, required=True, default=100.0, help="Fixed amount will count as a debit if it is negative, as a credit if it is positive.") second_tax_id = fields.Many2one('account.tax', string='Second Tax', ondelete='restrict', domain=[('type_tax_use', '=', 'purchase')]) second_analytic_account_id = fields.Many2one('account.analytic.account', string='Second Analytic Account', ondelete='set null') @@ -1156,3 +1175,13 @@ class AccountReconcileModel(models.Model): @api.onchange('name') def onchange_name(self): self.label = self.name + + @api.onchange('tax_id') + def _onchange_tax_id(self): + if self.tax_id: + self.force_tax_included = self.tax_id.price_include + + @api.onchange('second_tax_id') + def _onchange_second_tax_id(self): + if self.second_tax_id: + self.force_second_tax_included = self.second_tax_id.price_include diff --git a/addons/account/models/chart_template.py b/addons/account/models/chart_template.py index 2eb3ecc6683..30f764799c2 100644 --- a/addons/account/models/chart_template.py +++ b/addons/account/models/chart_template.py @@ -648,11 +648,13 @@ class AccountChartTemplate(models.Model): 'account_id': acc_template_ref[account_reconcile_model.account_id.id], 'label': account_reconcile_model.label, 'amount_type': account_reconcile_model.amount_type, + 'force_tax_included': account_reconcile_model.force_tax_included, 'amount': account_reconcile_model.amount, 'tax_id': account_reconcile_model.tax_id and tax_template_ref[account_reconcile_model.tax_id.id] or False, 'second_account_id': account_reconcile_model.second_account_id and acc_template_ref[account_reconcile_model.second_account_id.id] or False, 'second_label': account_reconcile_model.second_label, 'second_amount_type': account_reconcile_model.second_amount_type, + 'force_second_tax_included': account_reconcile_model.force_second_tax_included, 'second_amount': account_reconcile_model.second_amount, 'second_tax_id': account_reconcile_model.second_tax_id and tax_template_ref[account_reconcile_model.second_tax_id.id] or False, } @@ -904,6 +906,8 @@ class AccountReconcileModelTemplate(models.Model): ('percentage', 'Percentage of balance') ], required=True, default='percentage') amount = fields.Float(digits=0, required=True, default=100.0, help="Fixed amount will count as a debit if it is negative, as a credit if it is positive.") + force_tax_included = fields.Boolean(string='Tax Included in Price', + help='Force the tax to be managed as a price included tax.') tax_id = fields.Many2one('account.tax.template', string='Tax', ondelete='restrict', domain=[('type_tax_use', '=', 'purchase')]) second_account_id = fields.Many2one('account.account.template', string='Second Account', ondelete='cascade', domain=[('deprecated', '=', False)]) second_label = fields.Char(string='Second Journal Item Label') @@ -912,4 +916,6 @@ class AccountReconcileModelTemplate(models.Model): ('percentage', 'Percentage of amount') ], string="Second Amount type",required=True, default='percentage') second_amount = fields.Float(string='Second Amount', digits=0, required=True, default=100.0, help="Fixed amount will count as a debit if it is negative, as a credit if it is positive.") + force_second_tax_included = fields.Boolean(string='Second Tax Included in Price', + help='Force the second tax to be managed as a price included tax.') second_tax_id = fields.Many2one('account.tax.template', string='Second Tax', ondelete='restrict', domain=[('type_tax_use', '=', 'purchase')]) diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js index f42943f6027..6f0f756930c 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_model.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js @@ -94,7 +94,7 @@ var _t = core._t; */ var StatementModel = BasicModel.extend({ avoidCreate: false, - quickCreateFields: ['account_id', 'amount', 'analytic_account_id', 'label', 'tax_id', 'analytic_tag_ids'], + quickCreateFields: ['account_id', 'amount', 'analytic_account_id', 'label', 'tax_id', 'force_tax_included', 'analytic_tag_ids'], /** * @override @@ -409,7 +409,21 @@ var StatementModel = BasicModel.extend({ .then(function (accounts) { self.accounts = _.object(_.pluck(accounts, 'id'), _.pluck(accounts, 'code')); }); - return $.when(def_statement, def_reconcileModel, def_account).then(function () { + self.taxes = {}; + var def_taxes = this._rpc({ + model: 'account.tax', + method: 'search_read', + fields: ['price_include', 'amount_type'], + }) + .then(function (taxes) { + _.each(taxes, function(tax){ + self.taxes[tax.id] = { + price_include: tax.price_include, + amount_type: tax.amount_type, + } + }) + }); + return $.when(def_statement, def_reconcileModel, def_account, def_taxes).then(function () { _.each(self.lines, function (line) { line.reconcileModels = self.reconcileModels; }); @@ -465,7 +479,7 @@ 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', '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']; this._blurProposition(handle); var focus = this._formatQuickCreate(line, _.pick(reconcileModel, fields)); @@ -628,10 +642,21 @@ var StatementModel = BasicModel.extend({ this._computeReconcileModels(handle, prop.reconcileModelId); } } - if ('account_id' in values || 'amount' in values || 'tax_id' in values) { + if ('account_id' in values || 'amount' in values || 'tax_id' in values || 'force_tax_included' in values) { prop.__tax_to_recompute = true; + + if(values.tax_id){ + values.tax_id.amount_type = this.taxes[values.tax_id.id].amount_type; + values.tax_id.price_include = prop.force_tax_included = this.taxes[values.tax_id.id].price_include; + }else if('tax_id' in values && prop.base_amount && prop.base_amount != prop.amount) + // When removing a price_included tax, reset the amount to the base_amount. + prop.amount = prop.base_amount; } line.createForm = _.pick(prop, this.quickCreateFields); + + // If you check/uncheck the force_tax_included box, reset the createForm amount. + if(prop.base_amount) + line.createForm.amount = prop.base_amount; return this._computeLine(line); }, /** @@ -816,11 +841,15 @@ var StatementModel = BasicModel.extend({ }); var args = [[prop.tax_id.id], prop.base_amount, formatOptions.currency_id]; + var add_context = {'round': true}; + debugger; + if(line.createForm.force_tax_included && prop.tax_id.amount_type !== "group") + add_context.force_price_include = true; tax_defs.push(self._rpc({ model: 'account.tax', method: 'json_friendly_compute_all', args: args, - context: $.extend(self.context || {}, {'round': true}), + context: $.extend({}, self.context || {}, add_context), }) .then(function (result) { _.each(result.taxes, function(tax){ @@ -1033,6 +1062,19 @@ var StatementModel = BasicModel.extend({ var amount = field_utils.format.monetary(Math.abs(prop.base_amount), {}, formatOptions); prop.base_amount = sign * field_utils.parse.monetary(amount, {}, formatOptions); } + + if(prop.tax_id){ + // Set the amount_type value. + prop.tax_id.amount_type = this.taxes[prop.tax_id.id].amount_type; + // Set the price_include value. + prop.tax_id.price_include = this.taxes[prop.tax_id.id].price_include; + } + + // Set the force_tax_included value. + if(prop.tax_id && values.force_tax_included !== undefined) + prop.force_tax_included = values.force_tax_included; + else if(prop.tax_id && this.taxes[prop.tax_id.id].price_include) + prop.force_tax_included = this.taxes[prop.tax_id.id].price_include; prop.amount = prop.base_amount; return prop; }, @@ -1144,7 +1186,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', 'analytic_tag_ids'], + quickCreateFields: ['account_id', 'journal_id', 'amount', 'analytic_account_id', 'label', 'tax_id', 'force_tax_included', 'analytic_tag_ids'], //-------------------------------------------------------------------------- // Public diff --git a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js index ef1a91a8c74..0653581a36b 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js @@ -391,6 +391,7 @@ 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})); if (!isNaN(line.id)) { @@ -467,10 +468,22 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { if ((data[fieldName] || state.createForm[fieldName]) && !_.isEqual(state.createForm[fieldName], data[fieldName])) { field.reset(record); } + if (fieldName === 'tax_id') { + if (!state.createForm[fieldName] || state.createForm[fieldName].amount_type === "group") { + $('.create_force_tax_included').addClass('d-none'); + } + else { + $('.create_force_tax_included').removeClass('d-none'); + } + } }); }); }); }); + if(state.createForm.tax_id){ + // Set the 'Tax Include' field editable or not depending of the 'price_include' value. + this.$('.create_force_tax_included input').attr('disabled', state.createForm.tax_id.price_include); + } } this.$('.create .add_line').toggle(!!state.balance.amount_currency); }, @@ -545,6 +558,9 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { relation: 'account.analytic.tag', type: 'many2many', name: 'analytic_tag_ids', + }, { + type: 'boolean', + name: 'force_tax_included', }, { type: 'char', name: 'label', @@ -566,7 +582,7 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { 'journal_id', record, {mode: 'edit'}); self.fields.tax_id = new relational_fields.FieldMany2One(self, - 'tax_id', record, {mode: 'edit'}); + 'tax_id', record, {mode: 'edit', additionalContext: {append_type_to_tax_name: true}}); self.fields.analytic_account_id = new relational_fields.FieldMany2One(self, 'analytic_account_id', record, {mode: 'edit'}); @@ -574,6 +590,9 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { self.fields.analytic_tag_ids = new relational_fields.FieldMany2ManyTags(self, 'analytic_tag_ids', record, {mode: 'edit'}); + self.fields.force_tax_included = new basic_fields.FieldBoolean(self, + 'force_tax_included', record, {mode: 'edit'}); + self.fields.label = new basic_fields.FieldChar(self, 'label', record, {mode: 'edit'}); @@ -587,6 +606,7 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { self.fields.tax_id.appendTo($create.find('.create_tax_id .o_td_field')); self.fields.analytic_account_id.appendTo($create.find('.create_analytic_account_id .o_td_field')); self.fields.analytic_tag_ids.appendTo($create.find('.create_analytic_tag_ids .o_td_field')); + self.fields.force_tax_included.appendTo($create.find('.create_force_tax_included .o_td_field')) self.fields.label.appendTo($create.find('.create_label .o_td_field')) .then(addRequiredStyle.bind(self, self.fields.label)); self.fields.amount.appendTo($create.find('.create_amount .o_td_field')) diff --git a/addons/account/static/src/xml/account_reconciliation.xml b/addons/account/static/src/xml/account_reconciliation.xml index bccd3ebde9c..4f1916da1e8 100644 --- a/addons/account/static/src/xml/account_reconciliation.xml +++ b/addons/account/static/src/xml/account_reconciliation.xml @@ -202,6 +202,10 @@ Amount + + Tax Included in Price + +
diff --git a/addons/account/views/account_view.xml b/addons/account/views/account_view.xml index e6cd89960ea..d102f48f96a 100644 --- a/addons/account/views/account_view.xml +++ b/addons/account/views/account_view.xml @@ -854,7 +854,14 @@ - + + + + @@ -877,7 +884,15 @@ - + + + + diff --git a/addons/web/static/src/js/fields/abstract_field.js b/addons/web/static/src/js/fields/abstract_field.js index 7cf72791be5..2ea40a2602b 100644 --- a/addons/web/static/src/js/fields/abstract_field.js +++ b/addons/web/static/src/js/fields/abstract_field.js @@ -107,6 +107,9 @@ var AbstractField = Widget.extend({ var fieldsInfo = record.fieldsInfo[this.viewType]; this.attrs = options.attrs || (fieldsInfo && fieldsInfo[name]) || {}; + // the 'additionalContext' property contains the attributes to pass through the context. + this.additionalContext = options.additionalContext || {}; + // this property tracks the current (parsed if needed) value of the field. // Note that we don't use an event system anymore, using this.get('value') // is no longer valid. diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js index ba83de415e7..5f10d1bf0cb 100644 --- a/addons/web/static/src/js/fields/relational_fields.js +++ b/addons/web/static/src/js/fields/relational_fields.js @@ -366,6 +366,9 @@ var FieldMany2One = AbstractField.extend({ var context = this.record.getContext(this.recordParams); var domain = this.record.getDomain(this.recordParams); + // Add the additionalContext + _.extend(context, this.additionalContext) + var blacklisted_ids = this._getSearchBlacklist(); if (blacklisted_ids.length > 0) { domain.push(['id', 'not in', blacklisted_ids]);