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]);