From 8823e01040f9522947e5d11c7312d09c50867cc4 Mon Sep 17 00:00:00 2001 From: hupo-odoo Date: Wed, 5 Apr 2023 15:05:30 +0000 Subject: [PATCH] [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) --- addons/account/models/account_move.py | 1 + addons/account/models/account_tax.py | 53 +++++++++---- .../account/static/src/css/report_invoice.css | 4 + .../account/tests/test_invoice_tax_totals.py | 77 +++++++++++++++++++ addons/account/views/report_invoice.xml | 64 ++++++++++++++- .../views/report_invoice_templates.xml | 35 +++------ .../layout_assets/layout_background.scss | 2 +- .../reports/layout_assets/layout_boxed.scss | 2 +- .../reports/layout_assets/layout_clean.scss | 2 +- addons/web/views/report_templates.xml | 12 ++- 10 files changed, 205 insertions(+), 47 deletions(-) diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 3f8d40dc5a1..a817b0b3f44 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -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: diff --git a/addons/account/models/account_tax.py b/addons/account/models/account_tax.py index b6db50691a0..ca4e317be12 100644 --- a/addons/account/models/account_tax.py +++ b/addons/account/models/account_tax.py @@ -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 diff --git a/addons/account/static/src/css/report_invoice.css b/addons/account/static/src/css/report_invoice.css index 7abb185c986..e4b15776f2a 100644 --- a/addons/account/static/src/css/report_invoice.css +++ b/addons/account/static/src/css/report_invoice.css @@ -16,3 +16,7 @@ height:18%; width:18%; } + +.tax_computation_company_currency { + margin-bottom: 5px; +} diff --git a/addons/account/tests/test_invoice_tax_totals.py b/addons/account/tests/test_invoice_tax_totals.py index 5c6253997a9..8584151abff 100644 --- a/addons/account/tests/test_invoice_tax_totals.py +++ b/addons/account/tests/test_invoice_tax_totals.py @@ -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"], + }) diff --git a/addons/account/views/report_invoice.xml b/addons/account/views/report_invoice.xml index 7cb2980a4b9..d9ea609a5e6 100644 --- a/addons/account/views/report_invoice.xml +++ b/addons/account/views/report_invoice.xml @@ -48,7 +48,7 @@ -
+

Invoice @@ -210,6 +210,7 @@

+
@@ -349,6 +350,67 @@ + + - -
- -
- Exchange Rate -

-

-
- Subtotal (AED) -

-

-
- VAT Amount (AED) -

-

-
- Total (AED) -

-

-
+ diff --git a/addons/web/static/src/webclient/actions/reports/layout_assets/layout_background.scss b/addons/web/static/src/webclient/actions/reports/layout_assets/layout_background.scss index f47d39e7f7d..ff5f5d4d70c 100644 --- a/addons/web/static/src/webclient/actions/reports/layout_assets/layout_background.scss +++ b/addons/web/static/src/webclient/actions/reports/layout_assets/layout_background.scss @@ -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; diff --git a/addons/web/static/src/webclient/actions/reports/layout_assets/layout_boxed.scss b/addons/web/static/src/webclient/actions/reports/layout_assets/layout_boxed.scss index 568f6c5e243..ea674142edf 100644 --- a/addons/web/static/src/webclient/actions/reports/layout_assets/layout_boxed.scss +++ b/addons/web/static/src/webclient/actions/reports/layout_assets/layout_boxed.scss @@ -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'); diff --git a/addons/web/static/src/webclient/actions/reports/layout_assets/layout_clean.scss b/addons/web/static/src/webclient/actions/reports/layout_assets/layout_clean.scss index 4933042efff..054f14448e8 100644 --- a/addons/web/static/src/webclient/actions/reports/layout_assets/layout_clean.scss +++ b/addons/web/static/src/webclient/actions/reports/layout_assets/layout_clean.scss @@ -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 { diff --git a/addons/web/views/report_templates.xml b/addons/web/views/report_templates.xml index e93243d82f1..828c5938238 100644 --- a/addons/web/views/report_templates.xml +++ b/addons/web/views/report_templates.xml @@ -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: ; } + .totals_taxes_company_currency strong{ + color: ; + } #informations strong { color: ; } @@ -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(, 0.9); @@ -652,7 +655,7 @@ } } } - #total { + #total, .totals_taxes_company_currency { strong { color: ; } @@ -670,6 +673,9 @@ #total strong{ color: ; } + .totals_taxes_company_currency strong{ + color: ; + } table { thead { color: ;