[IMP] account: Refactor tax group widget with OWL

Refactor the tax group widget with the new JavaScript Odoo Framework:
OWL. This widget has 2 different components, one representing a tax group
and one representing the list of tax groups.

Task ID: #2312900
Initial Task ID: #1986412

Part-of: odoo/odoo#74138
This commit is contained in:
Florian Gilbert
2021-09-02 14:15:12 +00:00
committed by oco-odoo
parent fa1c05e7bb
commit 0f90e80f9e
6 changed files with 375 additions and 322 deletions
+24 -2
View File
@@ -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:
+17 -9
View File
@@ -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: ' ';
}
+178 -159
View File
@@ -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 <input> 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 <td>.
* it will hide the edit button and display the field to be edited.
*
* @param {event} ev
*/
_onClick: function (ev) {
ev.preventDefault();
var $taxGroupElement = $(ev.target).parents('.oe_tax_group_editable');
// Show input and hide previous element
$taxGroupElement.find('.tax_group_edit').addClass('d-none');
$taxGroupElement.find('.tax_group_edit_input').removeClass('d-none');
var $input = $taxGroupElement.find('.tax_group_edit_input input');
// Get original value and display it in user locale in the input
var formatedOriginalValue = fieldUtils.format.float($input.data('originalValue'), {}, {});
$input.focus(); // Focus the input
$input.val(formatedOriginalValue); //add value in user locale to the input
},
//--------------------------------------------------------------------------
// Life cycle method
//--------------------------------------------------------------------------
/**
* This method is called when the user is in edit mode and pressing
* a key on his keyboard. If this key corresponds to ENTER or TAB,
* the code that modifies the information is executed.
*
* @param {event} ev
*/
_onKeydown: function (ev) {
switch (ev.which) {
// Trigger only if the user clicks on ENTER or on TAB.
case $.ui.keyCode.ENTER:
case $.ui.keyCode.TAB:
// trigger blur to prevent the code being executed twice
$(ev.target).blur();
}
},
willUpdateProps(nextProps) {
// We only reformat tax groups if there are changed
if (nextProps.fieldName === 'amount_by_group') {
this.taxGroups.value = JSON.parse(this.value);
}
}
});
fieldRegistry.add('tax-group-custom-field', TaxGroupCustomField)
});
//--------------------------------------------------------------------------
// Events
//--------------------------------------------------------------------------
_onKeydown(ev) {
switch (ev.which) {
// Trigger only if the user clicks on ENTER or on TAB.
case $.ui.keyCode.ENTER:
case $.ui.keyCode.TAB:
// trigger blur to prevent the code being executed twice
$(ev.target).blur();
}
}
//--------------------------------------------------------------------------
// Private methods
//--------------------------------------------------------------------------
/**
* Tricky method to get the parentWidget. It necessary to do that because
* we need to know the view mode. (If we are in readonly or edit).
*/
_getParentWidget() {
return this.__owl__.parent.parentWidget;
}
/**
* 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() {
let purchaseMoveTypes = ['in_invoice', 'in_refund'];
return purchaseMoveTypes.includes(this.record.data.move_type)
}
/**
* This method verifies that the account move is a purchase document, that the document is in draft and
* that the edit mode is enabled.
*/
_displayEditWidget() {
return this._isPurchaseDocument() && this.record.data.state === 'draft' && this._getParentWidget().mode === 'edit';
}
/**
* This method is the main function of the tax group widget.
* It is called by an event trigger (from the TaxGroupComponent) and receives
* a particular payload.
*
* It is responsible for calculating taxes based on tax groups and triggering
* an event to notify the ORM of a change.
*
* @param {*} ev
* @param {*} ev.details A payload with the tax group id, the old value of the
* tax group and the new value.
*/
_onChangeTaxValueByTaxGroup(ev) {
let detail = ev.detail;
this.taxGroups.value.forEach(taxGroup => {
if (taxGroup.tax_group_id === detail.taxGroupId) {
taxGroup.tax_group_amount = detail.newValue;
}
});
this.trigger('field-changed', {
dataPointID: this.record.id,
changes: { amount_by_group: JSON.stringify(this.taxGroups.value) }
})
}
}
TaxGroupListComponent.template = 'account.TaxGroupCustomField';
TaxGroupListComponent.components = { TaxGroupComponent };
field_registry.add('tax-group-custom-field', TaxGroupListComponent);
export default TaxGroupListComponent
+34 -25
View File
@@ -1,35 +1,44 @@
<?xml version='1.0' encoding='utf-8'?>
<?xml version="1.0" encoding="utf-8"?>
<templates>
<t t-name="AccountTaxGroupTemplate">
<table class="o_group o_inner_group oe_subtotal_footer border-0 my-0" style="min-width: 100%;">
<div t-name="account.TaxGroupCustomField" owl="1">
<table class="o_inner_group oe_subtotal_footer border-0 my-O" style="min-width: 100%;">
<tbody>
<t t-foreach="lines" t-as="line">
<t t-foreach="taxGroups.value" t-as="taxGroup">
<tr>
<td class="o_td_label oe_tax_group_name">
<label class="o_form_label" t-esc="line[0]"/>
<td class="o_td_label o_tax_group_name">
<label class="o_form_label" t-esc="taxGroup.tax_group_name"/>
</td>
<td class="oe_tax_group_editable" t-att-data-tax-group-id="line[6]">
<t t-if="displayEditWidget and line[1] !== 0">
<span class="tax_group_edit">
<i class="fa fa-pencil"></i>
<span class="oe_tax_group_amount_value">
<t t-esc="line[3]"/>
</span>
</span>
<span class="tax_group_edit_input d-none">
<input type="text" class="o_field_float o_field_number o_input" t-att-data-original-value="line[1]"/>
</span>
</t>
<t t-if="!displayEditWidget or line[1] === 0">
<span class="oe_tax_group_amount_value">
<t t-esc="line[3]"/>
</span>
</t>
<td class="o_tax_group_editable" t-att-data-tax-group-id="taxGroup.tax_group_id">
<TaxGroupComponent taxGroup="taxGroup" record="record" displayEditWidget="displayEditWidget" t-on-change-tax-group="_onChangeTaxValueByTaxGroup"/>
</td>
</tr>
</t>
</tbody>
</table>
</t>
</div>
<div t-name="account.TaxGroupComponent" owl="1">
<t t-if="props.displayEditWidget">
<t t-if="['edit', 'disable'].includes(state.value)">
<span class="o_tax_group_edit_input">
<input type="text" t-ref="taxValueInput" class="o_field_float o_field_number o_input" t-att-disabled="state.value === 'disable'" t-on-blur.prevent="_onChangeTaxValue()"/>
</span>
</t>
<t t-else="">
<span class="o_tax_group_edit" t-on-click.prevent="setState('edit')">
<i class="fa fa-pencil"></i>
<span class="o_tax_group_amount_value">
<t t-esc="props.taxGroup.formated_tax_group_amount"/>
</span>
</span>
</t>
</t>
<t t-else="">
<span class="o_tax_group_amount_value">
<t t-esc="props.taxGroup.formated_tax_group_amount"/>
</span>
</t>
</div>
</templates>
@@ -1,124 +1,119 @@
odoo.define('account.tax.group.tour.tests', function (require) {
"use strict";
/** @odoo-module alias=account.tax.group.tour.tests */
"use strict";
var core = require('web.core');
var tour = require('web_tour.tour');
var _t = core._t;
import tour from 'web_tour.tour';
tour.register('account_tax_group', {
test: true,
url: "/web",
}, [tour.stepUtils.showAppsMenuItem(),
{
content: "Go to Invoicing",
trigger: '.o_app[data-menu-xmlid="account.menu_finance"]',
edition: 'community',
tour.register('account_tax_group', {
test: true,
url: "/web",
}, [tour.stepUtils.showAppsMenuItem(),
{
content: "Go to Invoicing",
trigger: '.o_app[data-menu-xmlid="account.menu_finance"]',
edition: 'community',
},
{
content: "Go to Accounting",
trigger: '.o_app[data-menu-xmlid="account_accountant.menu_accounting"]',
edition: 'enterprise',
},
{
content: "Go to Vendors",
trigger: 'a:contains("Vendors")',
},
{
content: "Go to Bills",
trigger: 'span:contains("Bills")',
},
{
extra_trigger: '.breadcrumb:contains("Bills")',
content: "Create new bill",
trigger: '.o_list_button_add',
},
// Set a vendor
{
content: "Add vendor",
trigger: 'div.o_field_widget.o_field_many2one[name="partner_id"] div input',
run: 'text Azure Interior',
},
{
content: "Valid vendor",
trigger: '.ui-menu-item a:contains("Azure Interior")',
},
// Add First product
{
content: "Add items",
trigger: 'div[name="invoice_line_ids"] .o_field_x2many_list_row_add a:contains("Add a line")',
},
{
content: "Select input",
trigger: 'div[name="invoice_line_ids"] .o_list_view .o_selected_row .o_list_many2one:first input',
},
{
content: "Type item",
trigger: 'div[name="invoice_line_ids"] .o_list_view .o_selected_row .o_list_many2one:first input',
run: "text Large Desk",
},
{
content: "Valid item",
trigger: '.ui-menu-item-wrapper:contains("Large Desk")',
},
// Save account.move
{
content: "Save the account move",
trigger: '.o_form_button_save',
},
// Edit account.move
{
content: "Edit the account move",
trigger: '.o_form_button_edit',
},
// Edit tax group amount
{
content: "Edit tax group amount",
trigger: '.o_tax_group_edit',
},
{
content: "Modify the input value",
trigger: '.o_tax_group_edit_input input',
run: function (actions) {
$('.o_tax_group_edit_input input').val(200);
$('.o_tax_group_edit_input input').select();
$('.o_tax_group_edit_input input').blur();
},
{
content: "Go to Accounting",
trigger: '.o_app[data-menu-xmlid="account_accountant.menu_accounting"]',
edition: 'enterprise',
},
// Check new value for total (with modified tax_group_amount).
{
content: "Valid total amount",
trigger: 'span[name="amount_total"]:contains("1,499.00")',
},
// Modify the quantity of the object
{
content: "Select item quantity",
trigger: 'div[name="invoice_line_ids"] .o_list_view tbody tr.o_data_row .o_list_number[title="1.00"]',
},
{
content: "Change item quantity",
trigger: 'div[name="invoice_line_ids"] .o_list_view tbody tr.o_data_row .o_list_number[title="1.00"] input',
run: 'text 2',
},
{
content: "Valid the new value",
trigger: 'div[name="invoice_line_ids"] .o_list_view tbody tr.o_data_row .o_list_number[title="1.00"] input',
run: function (actions) {
let keydownEvent = jQuery.Event('keydown');
keydownEvent.which = 13;
this.$anchor.trigger(keydownEvent);
},
{
content: "Go to Vendors",
trigger: 'a:contains("Vendors")',
},
{
content: "Go to Bills",
trigger: 'span:contains("Bills")',
},
{
extra_trigger: '.breadcrumb:contains("Bills")',
content: "Create new bill",
trigger: '.o_list_button_add',
},
// Set a vendor
{
content: "Add vendor",
trigger: 'div.o_field_widget.o_field_many2one[name="partner_id"] div input',
run: 'text Azure Interior',
},
{
content: "Valid vendor",
trigger: '.ui-menu-item a:contains("Azure Interior")',
},
// Add First product
{
content: "Add items",
trigger: 'div[name="invoice_line_ids"] .o_field_x2many_list_row_add a:contains("Add a line")',
},
{
content: "Select input",
trigger: 'div[name="invoice_line_ids"] .o_list_view .o_selected_row .o_list_many2one:first input',
},
{
content: "Type item",
trigger: 'div[name="invoice_line_ids"] .o_list_view .o_selected_row .o_list_many2one:first input',
run: "text Large Desk",
},
{
content: "Valid item",
trigger: '.ui-menu-item-wrapper:contains("Large Desk")',
},
// Save account.move
{
content: "Save the account move",
trigger: '.o_form_button_save',
},
// Edit account.move
{
content: "Edit the account move",
trigger: '.o_form_button_edit',
},
// Edit tax group amount
{
content: "Edit tax group amount",
trigger: '.oe_tax_group_amount_value',
},
{
content: "Modify the input value",
trigger: '.tax_group_edit_input input',
run: function (actions) {
$('.tax_group_edit_input input').val(200);
$('.tax_group_edit_input input').select();
var keydownEvent = jQuery.Event('keydown');
keydownEvent.which = 13;
this.$anchor.trigger(keydownEvent);
},
},
// Check new value for total (with modified tax_group_amount).
{
content: "Valid total amount",
trigger: 'span[name="amount_total"]:contains("1,499.00")',
},
// Modify the quantity of the object
{
content: "Select item quantity",
trigger: 'div[name="invoice_line_ids"] .o_list_view tbody tr.o_data_row .o_list_number[title="1.000"]',
},
{
content: "Change item quantity",
trigger: 'div[name="invoice_line_ids"] .o_list_view tbody tr.o_data_row .o_list_number[title="1.000"] input',
run: 'text 2',
},
{
content: "Valid the new value",
trigger: 'div[name="invoice_line_ids"] .o_list_view tbody tr.o_data_row .o_list_number[title="1.000"] input',
run: function (actions) {
var keydownEvent = jQuery.Event('keydown');
keydownEvent.which = 13;
this.$anchor.trigger(keydownEvent);
},
},
// Save form
{
content: "Save the account move",
trigger: '.o_form_button_save',
},
// Check new tax group value
{
content: "Check new value of tax group",
trigger: '.oe_tax_group_amount_value:contains("389.70")',
},
]);
});
},
// Save form
{
content: "Save the account move",
trigger: '.o_form_button_save',
},
// Check new tax group value
{
content: "Check new value of tax group",
trigger: '.o_tax_group_amount_value:contains("389.70")',
},
]);
+7 -7
View File
@@ -131,23 +131,23 @@
<span t-field="o.amount_untaxed"/>
</td>
</tr>
<t t-foreach="o.amount_by_group" t-as="amount_by_group">
<t t-foreach="json.loads(o.amount_by_group)" t-as="amount_by_group">
<tr style="">
<t t-if="len(o.line_ids.filtered(lambda line: line.tax_line_id)) in [0, 1] and o.amount_untaxed == amount_by_group[2]">
<td><span class="text-nowrap" t-esc="amount_by_group[0]"/></td>
<t t-if="len(o.line_ids.filtered(lambda line: line.tax_line_id)) in [0, 1] and o.amount_untaxed == amount_by_group['tax_group_base_amount']">
<td><span class="text-nowrap" t-esc="amount_by_group['tax_group_name']"/></td>
<td class="text-right o_price_total">
<span class="text-nowrap" t-esc="amount_by_group[3]" />
<span class="text-nowrap" t-esc="amount_by_group['formated_tax_group_amount']" />
</td>
</t>
<t t-else="">
<td>
<span t-esc="amount_by_group[0]"/>
<span t-esc="amount_by_group['tax_group_name']"/>
<span class="text-nowrap"> on
<t t-esc="amount_by_group[4]"/>
<t t-esc="amount_by_group['formated_tax_group_base_amount']"/>
</span>
</td>
<td class="text-right o_price_total">
<span class="text-nowrap" t-esc="amount_by_group[3]"/>
<span class="text-nowrap" t-esc="amount_by_group['formated_tax_group_amount']"/>
</td>
</t>
</tr>