[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:
@@ -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:
|
||||
|
||||
@@ -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"],
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
@@ -573,7 +573,7 @@
|
||||
}
|
||||
}
|
||||
.row > div > table,
|
||||
div#total table {
|
||||
div#total table, .totals_taxes_company_currency table {
|
||||
&:first-child,
|
||||
&:last-child,
|
||||
&.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 {
|
||||
&.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'/>;
|
||||
|
||||
Reference in New Issue
Block a user