[IMP] account: Express VAT in local currency on invoices

Display the totals and taxes on pdf invoices in the company currency when the invoice is in foreign currency. This is a requirement from some countries where local authorities need the VAT to be expressed on the invoice in the national currency even if the invoice has been issued in another currency. The tax groups details are also required. The new information is expressed using a rectangle at the bottom right of an invoice. It includes the same information as the detail of totals and taxes above, with the difference that it is in company currency. If cash rounding is activated, the total rounded must not be displayed in company currency.

task : 3256515

closes odoo/odoo#118234

Signed-off-by: Florian Gilbert (flg) <flg@odoo.com>
This commit is contained in:
hupo-odoo
2023-05-09 04:44:28 +02:00
parent 6525d5fba4
commit 8823e01040
10 changed files with 205 additions and 47 deletions
+1
View File
@@ -1250,6 +1250,7 @@ class AccountMove(models.Model):
is_refund=move.move_type in ('out_refund', 'in_refund'),
handle_price_include=False,
))
kwargs['is_company_currency_requested'] = move.currency_id != move.company_id.currency_id
move.tax_totals = self.env['account.tax']._prepare_tax_totals(**kwargs)
rounding_line = move.line_ids.filtered(lambda l: l.display_type == 'rounding')
if rounding_line:
+39 -14
View File
@@ -1154,13 +1154,17 @@ class AccountTax(models.Model):
return res
@api.model
def _prepare_tax_totals(self, base_lines, currency, tax_lines=None):
def _prepare_tax_totals(self, base_lines, currency, tax_lines=None, is_company_currency_requested=False):
""" Compute the tax totals details for the business documents.
:param base_lines: A list of python dictionaries created using the '_convert_to_tax_base_line_dict' method.
:param currency: The currency set on the business document.
:param tax_lines: Optional list of python dictionaries created using the '_convert_to_tax_line_dict' method.
If specified, the taxes will be recomputed using them instead of recomputing the taxes on
the provided base lines.
:param base_lines: A list of python dictionaries created using the '_convert_to_tax_base_line_dict' method.
:param currency: The currency set on the business document.
:param tax_lines: Optional list of python dictionaries created using the '_convert_to_tax_line_dict'
method. If specified, the taxes will be recomputed using them instead of
recomputing the taxes on the provided base lines.
:param is_company_currency_requested : Optional boolean which indicates whether or not the company currency is
requested from the function. This can typically be used when using an
invoice in foreign currency and the company currency is required.
:return: A dictionary in the following form:
{
'amount_total': The total amount to be displayed on the document, including every total
@@ -1175,14 +1179,20 @@ class AccountTax(models.Model):
default one: 'Untaxed Amount'.
And groups_data is a list of dict in the following form:
{
'tax_group_name': The name of the tax groups this total is made for.
'tax_group_amount': The total tax amount in this tax group.
'tax_group_base_amount': The base amount for this tax group.
'formatted_tax_group_amount': Same as tax_group_amount, but as a string formatted accordingly
with partner's locale.
'formatted_tax_group_base_amount': Same as tax_group_base_amount, but as a string formatted
accordingly with partner's locale.
'tax_group_id': The id of the tax group corresponding to this dict.
'tax_group_name': The name of the tax groups this total is made for.
'tax_group_amount': The total tax amount in this tax group.
'tax_group_base_amount': The base amount for this tax group.
'formatted_tax_group_amount': Same as tax_group_amount, but as a string formatted accordingly
with partner's locale.
'formatted_tax_group_base_amount': Same as tax_group_base_amount, but as a string formatted
accordingly with partner's locale.
'tax_group_id': The id of the tax group corresponding to this dict.
'tax_group_base_amount_company_currency': OPTIONAL: the base amount of the tax group expressed in
the company currency when the parameter
is_company_currency_requested is True
'tax_group_amount_company_currency': OPTIONAL: the tax amount of the tax group expressed in
the company currency when the parameter
is_company_currency_requested is True
}
'subtotals': A list of dictionaries in the following form, one for each subtotal in
'groups_by_subtotals' keys.
@@ -1192,6 +1202,8 @@ class AccountTax(models.Model):
belonging to preceding subtotals and the base amount
'formatted_amount': Same as amount, but as a string formatted accordingly with
partner's locale.
'amount_company_currency': OPTIONAL: The total amount in company currency when the
parameter is_company_currency_requested is True
}
'subtotals_order': A list of keys of `groups_by_subtotals` defining the order in which it needs
to be displayed
@@ -1218,6 +1230,9 @@ class AccountTax(models.Model):
'base_amount': tax_detail['base_amount_currency'],
'tax_amount': tax_detail['tax_amount_currency'],
}
if is_company_currency_requested:
tax_group_vals['base_amount_company_currency'] = tax_detail['base_amount']
tax_group_vals['tax_amount_company_currency'] = tax_detail['tax_amount']
# Handle a manual edition of tax lines.
if tax_lines is not None:
@@ -1238,6 +1253,9 @@ class AccountTax(models.Model):
amount_untaxed = global_tax_details['base_amount_currency']
amount_tax = 0.0
amount_untaxed_company_currency = global_tax_details['base_amount']
amount_tax_company_currency = 0.0
subtotal_order = {}
groups_by_subtotal = defaultdict(list)
for tax_group_vals in tax_group_vals_list:
@@ -1256,6 +1274,9 @@ class AccountTax(models.Model):
'formatted_tax_group_amount': formatLang(self.env, tax_group_vals['tax_amount'], currency_obj=currency),
'formatted_tax_group_base_amount': formatLang(self.env, tax_group_vals['base_amount'], currency_obj=currency),
})
if is_company_currency_requested:
groups_by_subtotal[subtotal_title][-1]['tax_group_amount_company_currency'] = tax_group_vals['tax_amount_company_currency']
groups_by_subtotal[subtotal_title][-1]['tax_group_base_amount_company_currency'] = tax_group_vals['base_amount_company_currency']
# ==== Build the final result ====
@@ -1267,6 +1288,10 @@ class AccountTax(models.Model):
'amount': amount_total,
'formatted_amount': formatLang(self.env, amount_total, currency_obj=currency),
})
if is_company_currency_requested:
subtotals[-1]['amount_company_currency'] = amount_untaxed_company_currency + amount_tax_company_currency
amount_tax_company_currency += sum(x['tax_group_amount_company_currency'] for x in groups_by_subtotal[subtotal_title])
amount_tax += sum(x['tax_group_amount'] for x in groups_by_subtotal[subtotal_title])
amount_total = amount_untaxed + amount_tax
@@ -16,3 +16,7 @@
height:18%;
width:18%;
}
.tax_computation_company_currency {
margin-bottom: 5px;
}
@@ -732,3 +732,80 @@ class TestTaxTotals(AccountTestInvoicingCommon):
]
run_case('round_per_line', lines, [16.60])
run_case('round_globally', lines, [16.59])
def test_invoice_foreign_currency_tax_totals(self):
self.env['res.currency.rate'].create({
'name': '2018-01-01',
'rate': 0.2,
'currency_id': self.currency_data['currency'].id,
'company_id': self.env.company.id,
})
tax_10 = self.env['account.tax'].create({
'name': "tax_10",
'amount_type': 'percent',
'amount': 10.0,
'tax_group_id': self.tax_group1.id,
})
tax_20 = self.env['account.tax'].create({
'name': "tax_20",
'amount_type': 'percent',
'amount': 20.0,
'tax_group_id': self.tax_group2.id,
})
invoice = self.env['account.move'].create({
'move_type': 'out_invoice',
'partner_id': self.partner_a.id,
'invoice_date': '2019-01-01',
'currency_id': self.currency_data['currency'].id,
})
lines_data = [(100, tax_10), (300, tax_20)]
invoice_lines_vals = [
Command.create({
'name': 'line',
'display_type': 'product',
'account_id': self.company_data['default_account_revenue'].id,
'price_unit': amount,
'tax_ids': [Command.set(taxes.ids)],
})
for amount, taxes in lines_data
]
invoice['invoice_line_ids'] = invoice_lines_vals
self.assertTaxTotals(invoice, {
'amount_total': 470,
'amount_untaxed': 400,
'display_tax_base': True,
'groups_by_subtotal': {
'Untaxed Amount': [
{
'tax_group_name': self.tax_group1.name,
'tax_group_amount': 10,
'tax_group_base_amount': 100,
'tax_group_id': self.tax_group1.id,
'tax_group_amount_company_currency': 50,
'tax_group_base_amount_company_currency': 500,
},
{
'tax_group_name': self.tax_group2.name,
'tax_group_amount': 60,
'tax_group_base_amount': 300,
'tax_group_id': self.tax_group2.id,
'tax_group_amount_company_currency': 300,
'tax_group_base_amount_company_currency': 1500,
}
]
},
'subtotals': [
{
'name': "Untaxed Amount",
'amount': 400,
'amount_company_currency': 2000,
}
],
'subtotals_order': ["Untaxed Amount"],
})
+63 -1
View File
@@ -48,7 +48,7 @@
</div>
</t>
</div>
<div class="mt-5">
<div class="mt-5 clearfix">
<div class="page mb-4">
<h2>
<span t-if="o.move_type == 'out_invoice' and o.state == 'posted'">Invoice</span>
@@ -210,6 +210,7 @@
<small class="text-muted lh-sm"><span t-field="o.amount_total_words"/></small>
</p>
</div>
<t t-call="account.document_tax_totals_company_currency_template"/>
</div>
<div id="payment_term">
<div class="justify-text">
@@ -349,6 +350,67 @@
</t>
</template>
<template id="document_tax_totals_company_currency_template">
<div class="mb-2 mt-3 border p-2 avoid-page-break-inside totals_taxes_company_currency"
t-if="o.company_id.currency_id != o.currency_id and o.amount_tax != 0 and o.move_type in ['out_invoice', 'out_refund']">
<table class="table table-sm table-borderless mb-0">
<p class="tax_computation_company_currency">
Tax computation in <span t-field="o.company_currency_id"/>
</p>
<t t-foreach="tax_totals['subtotals']" t-as="subtotal">
<tr class="border-black o_subtotal">
<td><strong t-out="subtotal['name']"/></td>
<td class="text-end">
<span
t-out="subtotal['amount_company_currency']"
t-options='{"widget": "monetary", "display_currency": o.company_currency_id}'
/>
</td>
</tr>
<t t-set="subtotal_to_show" t-value="subtotal['name']"/>
<t t-foreach="tax_totals['groups_by_subtotal'][subtotal_to_show]" t-as="amount_by_group">
<tr>
<t t-if="tax_totals['display_tax_base']">
<td>
<span t-out="amount_by_group['tax_group_name']"/>
<span class="text-nowrap"> on
<t t-out="amount_by_group['tax_group_base_amount_company_currency']"
t-options='{"widget": "monetary", "display_currency": o.company_currency_id}'
/>
</span>
</td>
<td class="text-end o_price_total">
<span class="text-nowrap"
t-out="amount_by_group['tax_group_amount_company_currency']"
t-options='{"widget": "monetary", "display_currency": o.company_currency_id}'
/>
</td>
</t>
<t t-else="">
<td><span class="text-nowrap" t-out="amount_by_group['tax_group_name']"/></td>
<td class="text-end o_price_total">
<span class="text-nowrap"
t-out="amount_by_group['tax_group_amount_company_currency']"
t-options='{"widget": "monetary", "display_currency": o.company_currency_id}'
/>
</td>
</t>
</tr>
</t>
</t>
<!--Total amount with all taxes-->
<tr class="border-black o_total">
<td><strong>Total</strong></td>
<td class="text-end">
<span t-out="abs(o.amount_total_signed)"
t-options='{"widget": "monetary", "display_currency": o.company_currency_id}'
/>
</td>
</tr>
</table>
</div>
</template>
<template id="report_invoice">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="o">
@@ -31,33 +31,16 @@
<span t-field="line.l10n_ae_vat_amount" id="line_tax_amount"/>
</td>
</xpath>
</template>
<xpath expr="//div[@id='right-elements']" position="after">
<div t-if="o.company_id.country_id.code == 'AE' and o.currency_id != o.company_id.currency_id"
id="aed_amounts" class="row clearfix ms-auto my-3 text-nowrap table">
<t t-set="aed_rate"
t-value="o.env['res.currency']._get_conversion_rate(o.currency_id, o.company_id.currency_id, o.company_id, o.invoice_date or datetime.date.today())"/>
<div name="exchange_rate" class="col-auto">
<strong>Exchange Rate</strong>
<p class="m-0" t-out="aed_rate" t-options='{"widget": "float", "precision": 5}'/>
</div>
<div name="aed_subtotal" class="col-auto">
<strong>Subtotal (AED)</strong>
<p class="m-0" t-out="o.amount_untaxed_signed"
t-options='{"widget": "monetary", "display_currency": o.company_currency_id}'/>
</div>
<div name="aed_vat_amount" class="col-auto">
<strong>VAT Amount (AED)</strong>
<p class="m-0"
t-out="o.currency_id._convert(o.amount_tax, o.company_id.currency_id, o.company_id, o.invoice_date or datetime.date.today())"
t-options='{"widget": "monetary", "display_currency": o.company_currency_id}'/>
</div>
<div name="aed_total" class="col-auto">
<strong>Total (AED)</strong>
<p class="m-0" t-out="o.amount_total_signed"
t-options='{"widget": "monetary", "display_currency": o.company_currency_id}'/>
</div>
</div>
<template id="document_tax_totals_company_currency_template" inherit_id="account.document_tax_totals_company_currency_template">
<xpath expr="//p[hasclass('tax_computation_company_currency')]" position="after">
<tr t-if="o.company_id.country_id.code == 'AE'">
<t t-set="exchange_rate"
t-value="abs(o.amount_total_signed) / o.amount_total"/>
<td>Exchange Rate</td>
<td class="text-end" t-out="exchange_rate" t-options='{"widget": "float", "precision": 5}'/>
</tr>
</xpath>
</template>
</odoo>
@@ -69,7 +69,7 @@
/*Total table*/
/* row div rule compat 12.0 */
.row > div > table,
div#total table {
div#total table, .totals_taxes_company_currency table {
tr {
&:nth-child(odd) {
background-color: transparent !important;
@@ -82,7 +82,7 @@
/*Total table*/
/* row div rule compat 12.0 */
.row > div > table,
div#total table {
div#total table, .totals_taxes_company_currency table {
thead tr:first-child,
tr.o_subtotal {
border-bottom: 1px solid map-get($grays, '700');
@@ -70,7 +70,7 @@
}
/* row col rule compat 12.0 */
.row > div > table,
div#total table {
div#total table, .totals_taxes_company_currency table {
tr {
&:first-child td,
&.o_subtotal {
+9 -3
View File
@@ -573,7 +573,7 @@
}
}
.row > div > table,
div#total table {
div#total table, .totals_taxes_company_currency table {
&amp;:first-child,
&amp;:last-child,
&amp;.o_subtotal,
@@ -589,6 +589,9 @@
#total strong {
color: <t t-esc='primary'/>;
}
.totals_taxes_company_currency strong{
color: <t t-esc='primary'/>;
}
#informations strong {
color: <t t-esc='secondary'/>;
}
@@ -613,7 +616,7 @@
}
}
.row > div > table,
div#total table {
div#total table, .totals_taxes_company_currency table {
tr {
&amp;.o_total td {
background-color: rgba(<t t-esc='primary'/>, 0.9);
@@ -652,7 +655,7 @@
}
}
}
#total {
#total, .totals_taxes_company_currency {
strong {
color: <t t-esc='secondary'/>;
}
@@ -670,6 +673,9 @@
#total strong{
color: <t t-esc='primary'/>;
}
.totals_taxes_company_currency strong{
color: <t t-esc='primary'/>;
}
table {
thead {
color: <t t-esc='secondary'/>;