diff --git a/addons/pos_sale/report/sale_report.py b/addons/pos_sale/report/sale_report.py index 606d04d9a8c..0eec84f92d1 100644 --- a/addons/pos_sale/report/sale_report.py +++ b/addons/pos_sale/report/sale_report.py @@ -34,19 +34,19 @@ class SaleReport(models.Model): CASE WHEN pos.state != 'invoiced' THEN SUM(l.qty) ELSE 0 END AS qty_to_invoice, SUM(l.price_subtotal_incl) / MIN({self._case_value_or_one('pos.currency_rate')}) - / {self._case_value_or_one('currency_table.rate')} + * {self._case_value_or_one('currency_table.rate')} AS price_total, SUM(l.price_subtotal) / MIN({self._case_value_or_one('pos.currency_rate')}) - / {self._case_value_or_one('currency_table.rate')} + * {self._case_value_or_one('currency_table.rate')} AS price_subtotal, (CASE WHEN pos.state != 'invoiced' THEN SUM(l.price_subtotal) ELSE 0 END) / MIN({self._case_value_or_one('pos.currency_rate')}) - / {self._case_value_or_one('currency_table.rate')} + * {self._case_value_or_one('currency_table.rate')} AS amount_to_invoice, (CASE WHEN pos.state = 'invoiced' THEN SUM(l.price_subtotal) ELSE 0 END) / MIN({self._case_value_or_one('pos.currency_rate')}) - / {self._case_value_or_one('currency_table.rate')} + * {self._case_value_or_one('currency_table.rate')} AS amount_invoiced, count(*) AS nbr, pos.name AS name, @@ -74,7 +74,7 @@ class SaleReport(models.Model): l.discount AS discount, SUM((l.price_unit * l.discount * l.qty / 100.0 / {self._case_value_or_one('pos.currency_rate')} - / {self._case_value_or_one('currency_table.rate')})) + * {self._case_value_or_one('currency_table.rate')})) AS discount_amount, concat('pos.order', ',', pos.id) AS order_reference""" diff --git a/addons/sale/report/sale_report.py b/addons/sale/report/sale_report.py index 38e6da62a06..d94f62b51ff 100644 --- a/addons/sale/report/sale_report.py +++ b/addons/sale/report/sale_report.py @@ -104,22 +104,22 @@ class SaleReport(models.Model): CASE WHEN l.product_id IS NOT NULL THEN SUM(l.qty_to_invoice / u.factor * u2.factor) ELSE 0 END AS qty_to_invoice, CASE WHEN l.product_id IS NOT NULL THEN SUM(l.price_total / {self._case_value_or_one('s.currency_rate')} - / {self._case_value_or_one('currency_table.rate')} + * {self._case_value_or_one('currency_table.rate')} ) ELSE 0 END AS price_total, CASE WHEN l.product_id IS NOT NULL THEN SUM(l.price_subtotal / {self._case_value_or_one('s.currency_rate')} - / {self._case_value_or_one('currency_table.rate')} + * {self._case_value_or_one('currency_table.rate')} ) ELSE 0 END AS price_subtotal, CASE WHEN l.product_id IS NOT NULL THEN SUM(l.untaxed_amount_to_invoice / {self._case_value_or_one('s.currency_rate')} - / {self._case_value_or_one('currency_table.rate')} + * {self._case_value_or_one('currency_table.rate')} ) ELSE 0 END AS untaxed_amount_to_invoice, CASE WHEN l.product_id IS NOT NULL THEN SUM(l.untaxed_amount_invoiced / {self._case_value_or_one('s.currency_rate')} - / {self._case_value_or_one('currency_table.rate')} + * {self._case_value_or_one('currency_table.rate')} ) ELSE 0 END AS untaxed_amount_invoiced, COUNT(*) AS nbr, @@ -148,7 +148,7 @@ class SaleReport(models.Model): l.discount AS discount, CASE WHEN l.product_id IS NOT NULL THEN SUM(l.price_unit * l.product_uom_qty * l.discount / 100.0 / {self._case_value_or_one('s.currency_rate')} - / {self._case_value_or_one('currency_table.rate')} + * {self._case_value_or_one('currency_table.rate')} ) ELSE 0 END AS discount_amount, concat('sale.order', ',', s.id) AS order_reference""" diff --git a/addons/sale/tests/test_sale_report.py b/addons/sale/tests/test_sale_report.py index 49ad6ca370c..e02c1860946 100644 --- a/addons/sale/tests/test_sale_report.py +++ b/addons/sale/tests/test_sale_report.py @@ -1,8 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from freezegun import freeze_time - from odoo import fields from odoo.fields import Command from odoo.tests import tagged @@ -17,66 +15,113 @@ class TestSaleReportCurrencyRate(SaleCommon): def setUpClass(cls): super().setUpClass() - cls.company = cls.env['res.company'].create({ - 'name': 'Test Company', - 'currency_id': cls.env.ref('base.USD').id, + cls.usd_cmp = cls.env['res.company'].create({ + 'name': 'USD Company', 'currency_id': cls.env.ref('base.USD').id, + }) + cls.eur_cmp = cls.env['res.company'].create({ + 'name': 'EUR Company', 'currency_id': cls.env.ref('base.EUR').id, }) def test_sale_report_foreign_currency(self): - # Test the amounts shown in the sales report for orders in foreign currency - currency_eur = self._enable_currency('EUR') + """ + Test that amounts are correctly converted between currencies. + There are two different conversions to take into account: + - currency of the sale order pricelist -> currency of the sale order company + - currency of sale order company -> currency of the current user company + Adjustment between past and present rates must also be taken into account. + """ - eur_pricelist = self.env['product.pricelist'].create({ - 'name': 'Pricelist (EUR)', - 'currency_id': currency_eur.id, - }) + companies = self.usd_cmp + self.eur_cmp + today = fields.Date.today() + past_day = fields.Date.to_date('2020-01-01') + usd = self.usd_cmp.currency_id + eur = self.eur_cmp.currency_id + ars = self._enable_currency('ARS') - with freeze_time('2022-02-22'): - self.env['res.currency.rate'].create({ - 'name': fields.Date.today(), - 'company_rate': 3.0, - 'currency_id': currency_eur.id, - 'company_id': self.company.id, - }) - eur_order = self.env['sale.order'].with_company(self.company).create({ - 'partner_id': self.partner.id, - 'pricelist_id': eur_pricelist.id, - 'date_order': fields.Date.today(), - 'order_line': [ - Command.create({ - 'product_id': self.product.id, - 'product_uom_qty': 1.0, - }), - ] - }) - # the sales order's amount is in foreign currency - self.assertEqual(eur_order.amount_total, 60.0) + # Create corresponding pricelists and rates. + pricelists = self.env['product.pricelist'].create([ + {'name': 'Pricelist (USD)', 'currency_id': usd.id}, + {'name': 'Pricelist (EUR)', 'currency_id': eur.id}, + {'name': 'Pricelist (ARS)', 'currency_id': ars.id}, + ]) + self.env['res.currency.rate'].create([ + {'name': past_day, 'rate': 555, 'currency_id': ars.id, 'company_id': self.eur_cmp.id}, + {'name': past_day, 'rate': 1.0, 'currency_id': eur.id, 'company_id': self.eur_cmp.id}, + {'name': past_day, 'rate': 999, 'currency_id': usd.id, 'company_id': self.eur_cmp.id}, + {'name': past_day, 'rate': 3.0, 'currency_id': ars.id, 'company_id': self.usd_cmp.id}, + {'name': past_day, 'rate': 0.1, 'currency_id': eur.id, 'company_id': self.usd_cmp.id}, + {'name': past_day, 'rate': 1.0, 'currency_id': usd.id, 'company_id': self.usd_cmp.id}, + {'name': today, 'rate': 222, 'currency_id': ars.id, 'company_id': self.eur_cmp.id}, + {'name': today, 'rate': 1.0, 'currency_id': eur.id, 'company_id': self.eur_cmp.id}, + {'name': today, 'rate': 2.9, 'currency_id': usd.id, 'company_id': self.eur_cmp.id}, + {'name': today, 'rate': 101, 'currency_id': ars.id, 'company_id': self.usd_cmp.id}, + {'name': today, 'rate': 0.6, 'currency_id': eur.id, 'company_id': self.usd_cmp.id}, + {'name': today, 'rate': 1.0, 'currency_id': usd.id, 'company_id': self.usd_cmp.id}, + ]) - # the report should show the amount in company currency - report_line = self.env['sale.report'].sudo().search( - [('order_reference', '=', f"sale.order,{eur_order.id}")]) - self.assertEqual(report_line.price_total, 20.0) + self.assertEqual(self.product.currency_id, usd) - def test_sale_report_company_currency(self): - usd_pricelist = self.env['product.pricelist'].create({ - 'name': 'Pricelist (USD)', - 'currency_id': self.company.currency_id.id, - }) + # Needed to get conversion rates between companies. + currency_rates = (companies + self.env.company).mapped('currency_id')._get_rates( + self.env.company, today + ) - usd_order = self.env['sale.order'].with_company(self.company).create({ - 'partner_id': self.partner.id, - 'pricelist_id': usd_pricelist.id, - 'order_line': [ - Command.create({ - 'product_id': self.product.id, - 'product_uom_qty': 1.0, - }), - ] - }) - # the sales order's amount is in company currency - self.assertEqual(usd_order.amount_total, 20.0) + sale_orders = self.env['sale.order'] + expected_reported_amount = 0 # The total amount of all sale orders in the report. + qty = 0 # to add variety to the data - # the report should match the amount on the SO - report_line = self.env['sale.report'].sudo().search( - [('order_reference', '=', f"sale.order,{usd_order.id}")]) - self.assertEqual(report_line.price_total, 20.0) + # Create sale orders + for company in companies: + SaleOrder = self.env['sale.order'].with_company(company) + for date in (past_day, today): + for pricelist in pricelists: + qty += 1 + order = SaleOrder.create({ + 'partner_id': self.partner.id, + 'pricelist_id': pricelist.id, + 'date_order': date, + 'order_line': [Command.create( + {'product_id': self.product.id, 'product_uom_qty': qty} + )], + }) + sale_orders |= order + + expected_so_currency_rate = self.env['res.currency.rate'].search([ + ('name', '=', date), + ('currency_id', '=', pricelist.currency_id.id), + ('company_id', '=', company.id), + ]).rate + expected_product_currency_rate = self.env['res.currency.rate'].search([ + ('name', '=', date), + ('currency_id', '=', self.product.currency_id.id), + ('company_id', '=', company.id), + ]).rate + + # To find the total amount we convert the price of the product from its currency + # to the currency of the so company and then from it to the currency of the so + # pricelist. + price_for_so_company = self.product.list_price / expected_product_currency_rate + expected_rounded_price = pricelist.currency_id.round( + price_for_so_company * expected_so_currency_rate + ) + + expected_amount_total = qty * expected_rounded_price + self.assertAlmostEqual(order.currency_rate, expected_so_currency_rate) + self.assertAlmostEqual(order.amount_total, expected_amount_total) + + # The amount in the report is converted first to the currency of the company and + # then to the currency of the current company (self.env.company). + current_company_rate = currency_rates[self.env.company.currency_id.id] + so_company_rate = currency_rates[company.currency_id.id] + conversion_rate = (current_company_rate / so_company_rate) + expected_reported_amount += ( + order.amount_total / order.currency_rate * conversion_rate + ) + + # The report should show the amount in the current (in this case usd) company currency. + report_lines = self.env['sale.report'].sudo().with_context( + allow_company_ids=[self.usd_cmp.id, self.eur_cmp.id] + ).search([('order_reference', 'in', [f'sale.order,{so_id}' for so_id in sale_orders.ids])]) + + price_total = sum(report_lines.mapped('price_total')) + self.assertAlmostEqual(price_total, expected_reported_amount) diff --git a/addons/sale_margin/report/sale_report.py b/addons/sale_margin/report/sale_report.py index 75d5b41aac1..71f40c351c7 100644 --- a/addons/sale_margin/report/sale_report.py +++ b/addons/sale_margin/report/sale_report.py @@ -13,6 +13,6 @@ class SaleReport(models.Model): res = super()._select_additional_fields() res['margin'] = f"""SUM(l.margin / {self._case_value_or_one('s.currency_rate')} - / {self._case_value_or_one('currency_table.rate')}) + * {self._case_value_or_one('currency_table.rate')}) """ return res