diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index d038cfab430..81f1efa12a7 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -238,7 +238,7 @@ class AccountMove(models.Model): compute='_compute_amount', currency_field='currency_id') amount_residual_signed = fields.Monetary(string='Amount Due Signed', store=True, compute='_compute_amount', currency_field='company_currency_id') - amount_by_group = fields.Binary(string="Tax amount by group", + amount_by_group = fields.Char(string="Tax amount by group", compute='_compute_invoice_taxes_by_group', help='Edit Tax amounts if you encounter rounding issues.') payment_state = fields.Selection(PAYMENT_STATE_SELECTION, string="Payment Status", store=True, @@ -545,6 +545,28 @@ class AccountMove(models.Model): def _onchange_recompute_dynamic_lines(self): self._recompute_dynamic_lines() + @api.onchange('amount_by_group') + def _onchange_amount_by_group(self): + """ This method is triggered by the tax group widget. It allows us to edit the right + line_ids according to the edited tax group. + """ + for move in self: + amount_by_groups = json.loads(move.amount_by_group) + for amount_by_group in amount_by_groups: + tax_lines = move.line_ids.filtered(lambda line: line.tax_group_id and line.tax_group_id.id == amount_by_group['tax_group_id']) + if len(tax_lines): + first_tax_line = tax_lines[0] + tax_group_old_amount = sum(tax_lines.mapped('amount_currency')) + delta_amount = tax_group_old_amount - amount_by_group['tax_group_amount'] + delta_amount *= move.move_type == 'in_invoice' and -1 or 1 + + if delta_amount != 0: + first_tax_line.amount_currency = first_tax_line.amount_currency + delta_amount + # We have to trigger the on change manually because we don"t change the value of + # amount_currency in the view. + first_tax_line._onchange_amount_currency() + move._recompute_dynamic_lines() + @api.model def _get_tax_grouping_key_from_tax_line(self, tax_line): ''' Create the dictionary based on a tax line that will be used as key to group taxes together. @@ -1597,7 +1619,7 @@ class AccountMove(models.Model): else: move.invoice_payments_widget = json.dumps(False) - @api.depends('line_ids.price_subtotal', 'line_ids.tax_base_amount', 'line_ids.tax_line_id', 'partner_id', 'currency_id') + @api.depends('line_ids.amount_currency', 'line_ids.tax_base_amount', 'line_ids.tax_line_id', 'partner_id', 'currency_id') def _compute_invoice_taxes_by_group(self): for move in self: diff --git a/addons/account/static/src/css/tax_group.css b/addons/account/static/src/css/tax_group.css index 0b617056bf6..994f0ab7b01 100644 --- a/addons/account/static/src/css/tax_group.css +++ b/addons/account/static/src/css/tax_group.css @@ -1,25 +1,33 @@ -.oe_tax_group_editable { +.o_tax_group_editable { width: 100%; + padding: 1% 0% !important; } -.tax_group_edit { +.o_tax_group_edit { white-space: nowrap; } -.tax_group_edit:hover { +.o_tax_group_edit:hover { color: #00A09D; cursor: pointer; } -.oe_tax_group_name { - font-weight: bold; +.o_tax_group_name { + font-weight: bold; min-width: 150px; text-align: right; - padding-right: 20px; } -.oe_tax_group_editable .oe_tax_group_amount_value input { - width: 65%; - float: right; +.o_tax_group_name label { + padding-top: 2%; +} + +.o_tax_group_editable .o_tax_group_amount_value input { + width: 65%; + float: right; text-align: right; } + +.o_tax_group_editable .o_tax_group_amount_value::before { + content: ' '; +} diff --git a/addons/account/static/src/js/tax_group.js b/addons/account/static/src/js/tax_group.js index a9de7f9f587..1d0facf09ef 100644 --- a/addons/account/static/src/js/tax_group.js +++ b/addons/account/static/src/js/tax_group.js @@ -1,171 +1,190 @@ -odoo.define('account.tax_group', function (require) { - "use strict"; +/** @odoo-module alias=account.tax_group_owl **/ +"use strict"; - var core = require('web.core'); - var session = require('web.session'); - var fieldRegistry = require('web.field_registry'); - var AbstractField = require('web.AbstractField'); - var fieldUtils = require('web.field_utils'); - var QWeb = core.qweb; +const { Component } = owl; +const { useState, useRef } = owl.hooks; +import session from 'web.session'; +import AbstractFieldOwl from 'web.AbstractFieldOwl'; +import fieldUtils from 'web.field_utils'; +import field_registry from 'web.field_registry_owl'; - var TaxGroupCustomField = AbstractField.extend({ - events: { - 'click .tax_group_edit': '_onClick', - 'keydown .oe_tax_group_editable .tax_group_edit_input input': '_onKeydown', - 'blur .oe_tax_group_editable .tax_group_edit_input input': '_onBlur', - }, +class TaxGroupComponent extends Component { + constructor(parent, props) { + super(parent, props); + this.inputTax = useRef('taxValueInput'); + this.state = useState({value: 'readonly'}); + } - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- + //-------------------------------------------------------------------------- + // Life cycle methods + //-------------------------------------------------------------------------- - /** - * This method is called by "_setTaxGroups". It is - * responsible for calculating taxes based on - * tax groups and triggering an event to - * notify the ORM of a change. - * - * @param {Id} taxGroupId - * @param {Float} deltaAmount - */ - _changeTaxValueByTaxGroup: function (taxGroupId, deltaAmount) { - var self = this; - // Search for the first tax line with the same tax group and modify its value - var line_id = self.record.data.line_ids.data.find(elem => elem.data.tax_group_id && elem.data.tax_group_id.data.id === taxGroupId); + willUpdateProps(nextProps) { + this.setState('readonly'); // If props are edited, we set the state to readonly + } - var debitAmount = 0; - var creditAmount = 0; - var amount_currency = 0; - if (line_id.data.currency_id) { // If multi currency enable - if (this.record.data.move_type === "in_invoice") { - amount_currency = line_id.data.amount_currency - deltaAmount; - } else { - amount_currency = line_id.data.amount_currency + deltaAmount; - } - } else { - var balance = line_id.data.price_subtotal; - balance -= deltaAmount; - if (this.record.data.move_type === "in_invoice") { // For vendor bill - if (balance > 0) { - debitAmount = balance; - } else if (balance < 0) { - creditAmount = -balance; - } - } else { // For refund - if (balance > 0) { - creditAmount = balance; - } else if (balance < 0) { - debitAmount = -balance; - } - } - } - // Trigger ORM - self.trigger_up('field_changed', { - dataPointID: self.record.id, - changes: { line_ids: { operation: "UPDATE", id: line_id.id, data: { amount_currency: amount_currency, debit: debitAmount, credit: creditAmount } } }, // account.move change - initialEvent: { dataPointID: line_id.id, changes: { amount_currency: amount_currency, debit: debitAmount, credit: creditAmount }, }, // account.move.line change - }); - }, + patched() { + if (this.state.value === 'edit') { + this.inputTax.el.focus(); // Focus the input + this.inputTax.el.value = this.props.taxGroup.tax_group_amount; + } + } - /** - * This method checks that the document where the widget - * is located is of the "in_invoice" or "in_refund" type. - * This makes it possible to know if it is a purchase - * document. - * - * @returns boolean (true if the invoice is a purchase document) - */ - _isPurchaseDocument: function () { - return this.record.data.move_type === "in_invoice" || this.record.data.move_type === 'in_refund'; - }, + //-------------------------------------------------------------------------- + // Main methods + //-------------------------------------------------------------------------- - /** - * This method is part of the widget life cycle and allows you to render - * the widget. - * - * @private - * @override - */ - _render: function () { - var self = this; - // Display the pencil and allow the event to click and edit only on purchase that are not posted and in edit mode. - // since the field is readonly its mode will always be readonly. Therefore we have to use a trick by checking the - // formRenderer (the parent) and check if it is in edit in order to know the correct mode. - var displayEditWidget = self._isPurchaseDocument() && this.record.data.state === 'draft' && this.getParent().mode === 'edit'; - this.$el.html($(QWeb.render('AccountTaxGroupTemplate', { - lines: self.value, - displayEditWidget: displayEditWidget, - }))); - }, + /** + * The purpose of this method is to change the state of the component. + * It can have one of the following three states: + * - readonly: display in read-only mode of the field, + * - edit: display with a html input field, + * - disable: display with a html input field that is disabled. + * + * If a value other than one of these 3 states is passed as a parameter, + * the component is set to readonly by default. + * + * @param {String} value + */ + setState(value) { + if (['readonly', 'edit', 'disable'].includes(value)) { + this.state.value = value; + } + else { + this.state.value = 'readonly'; + } + } - //-------------------------------------------------------------------------- - // Handler - //-------------------------------------------------------------------------- + /** + * This method handles the "_onChangeTaxValue" event. In this method, + * we get the new value for the tax group, we format it and we call + * the method to recalculate the tax lines. At the moment the method + * is called, we disable the html input field. + * + * In case the value has not changed or the tax group is equal to 0, + * the modification does not take place. + */ + _onChangeTaxValue() { + this.setState('disable'); // Disable the input + let newValue = this.inputTax.el.value; // Get the new value + let currency = session.get_currency(this.props.record.data.currency_id.data.id); + try { + newValue = fieldUtils.parse.float(newValue); // Need a float for format the value + newValue = fieldUtils.format.float(newValue, null, {digits: currency.digits}); // Return a string rounded to currency precision + newValue = fieldUtils.parse.float(newValue); // Convert back to Float to compare with oldValue to know if value has changed + } catch (err) { + $(this.inputTax.el).addClass('o_field_invalid'); + this.setState('edit'); + return; + } + // The newValue can't be equals to 0 + if (newValue === this.props.taxGroup.tax_group_amount || newValue === 0) { + this.setState('readonly'); + return; + } + this.trigger('change-tax-group', { + oldValue: this.props.taxGroup.tax_group_amount, + newValue: newValue, + taxGroupId: this.props.taxGroup.tax_group_id + }); + } +} +TaxGroupComponent.props = ['taxGroup', 'displayEditWidget', 'record']; +TaxGroupComponent.template = 'account.TaxGroupComponent'; - /** - * This method is called when the user is in edit mode and - * leaves the field. Then, we execute the code that - * modifies the information. - * - * @param {event} ev - */ - _onBlur: function (ev) { - ev.preventDefault(); - var $input = $(ev.target); - var newValue = $input.val(); - var currency = session.get_currency(this.record.data.currency_id.data.id); - try { - newValue = fieldUtils.parse.float(newValue); // Need a float for format the value. - newValue = fieldUtils.format.float(newValue, null, {digits: currency.digits}); // return a string rounded to currency precision - newValue = fieldUtils.parse.float(newValue); // convert back to Float to compare with oldValue to know if value has changed - } catch (err) { - $input.addClass('o_field_invalid'); - return; - } - var oldValue = $input.data('originalValue'); - if (newValue === oldValue || newValue === 0) { - return this._render(); - } - var taxGroupId = $input.parents('.oe_tax_group_editable').data('taxGroupId'); - this._changeTaxValueByTaxGroup(taxGroupId, oldValue-newValue); - }, +class TaxGroupListComponent extends AbstractFieldOwl { + constructor(...args) { + super(...args); + this.taxGroups = useState({value: JSON.parse(this.value)}); + this.displayEditWidget = this._displayEditWidget(); + } - /** - * This method is called when the user clicks on a specific
| - + | + | -
- |
+ |