From 5da3ab038bab86a4e3e3cb0d8267a6c28590a68d Mon Sep 17 00:00:00 2001 From: "Thomas Lefebvre (thle)" Date: Mon, 20 Feb 2023 14:01:37 +0000 Subject: [PATCH] [FIX] base, sale_project, project_purchase: convert currencies in profitability Steps to reproduce: - create a project with an analytic account; - create sale orders/invoices and purchase order/bills for a project; - use other currencies; - go to the "Project Updates". Issue: The amounts are used without being converted into the currency of the analytic account. Solution: Convert amounts used in the currency of the analytic account. Note: For performance reasons, it is acceptable to fetch all currency on the same date to get the conversion rate. In this case, today's date is the most appropriate (as the project is in progress). opw-3193676 closes odoo/odoo#113684 X-original-commit: 68b4ea69c173dd5894bcb42b99e6818f241f9734 Signed-off-by: Xavier Bol (xbo) --- addons/project_purchase/models/project.py | 28 ++++++++++++++----- .../tests/test_project_profitability.py | 5 ++++ addons/sale_project/models/project.py | 18 ++++++++---- 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/addons/project_purchase/models/project.py b/addons/project_purchase/models/project.py index e7a51393183..e7c808c65d0 100644 --- a/addons/project_purchase/models/project.py +++ b/addons/project_purchase/models/project.py @@ -6,6 +6,7 @@ import json from odoo import api, fields, models, _, _lt from odoo.osv import expression +from datetime import date class Project(models.Model): _inherit = "project.project" @@ -128,7 +129,7 @@ class Project(models.Model): '|', ('qty_to_invoice', '>', 0), ('product_uom_qty', '>', 0), ]) query.add_where('purchase_order_line.analytic_distribution ? %s', [str(self.analytic_account_id.id)]) - query_string, query_param = query.select('"purchase_order_line".id', 'qty_invoiced', 'qty_to_invoice', 'product_uom_qty', 'price_unit', '"purchase_order_line".analytic_distribution') + query_string, query_param = query.select('"purchase_order_line".id', 'qty_invoiced', 'qty_to_invoice', 'product_uom_qty', 'price_unit', 'purchase_order_line.currency_id', '"purchase_order_line".analytic_distribution') self._cr.execute(query_string, query_param) purchase_order_line_read = [{ **pol, @@ -136,11 +137,17 @@ class Project(models.Model): } for pol in self._cr.dictfetchall()] purchase_order_line_invoice_line_ids = self._get_already_included_profitability_invoice_line_ids() if purchase_order_line_read: + + # Get conversion rate from currencies to currency of analytic account + currency_ids = {pol['currency_id'] for pol in purchase_order_line_read + [{'currency_id': self.analytic_account_id.currency_id.id}]} + rates = self.env['res.currency'].browse(list(currency_ids))._get_rates(self.company_id, date.today()) + conversion_rates = {cid: rates[self.analytic_account_id.currency_id.id] / rate_from for cid, rate_from in rates.items()} + amount_invoiced = amount_to_invoice = 0.0 purchase_order_line_ids = [] for pol_read in purchase_order_line_read: purchase_order_line_invoice_line_ids.extend(pol_read['invoice_lines'].ids) - price_unit = pol_read['price_unit'] + price_unit = self.analytic_account_id.currency_id.round(pol_read['price_unit'] * conversion_rates[pol_read['currency_id']]) analytic_contribution = pol_read['analytic_distribution'][str(self.analytic_account_id.id)] / 100. amount_invoiced -= price_unit * pol_read['qty_invoiced'] * analytic_contribution if pol_read['qty_invoiced'] > 0 else 0.0 if pol_read['qty_to_invoice'] > 0: @@ -171,22 +178,29 @@ class Project(models.Model): # account_move_line__move_id is the alias of the joined table account_move in the query # we can use it, because of the "move_id.move_type" clause in the domain of the query, which generates the join # this is faster than a search_read followed by a browse on the move_id to retrieve the move_type of each account.move.line - query_string, query_param = query.select('price_subtotal', 'parent_state', 'account_move_line__move_id.move_type') + query_string, query_param = query.select('price_subtotal', 'parent_state', 'account_move_line.currency_id', 'account_move_line__move_id.move_type') self._cr.execute(query_string, query_param) bills_move_line_read = self._cr.dictfetchall() if bills_move_line_read: + + # Get conversion rate from currencies to currency of analytic account + currency_ids = {bml['currency_id'] for bml in bills_move_line_read + [{'currency_id': self.analytic_account_id.currency_id.id}]} + rates = self.env['res.currency'].browse(list(currency_ids))._get_rates(self.company_id, date.today()) + conversion_rates = {cid: rates[self.analytic_account_id.currency_id.id] / rate_from for cid, rate_from in rates.items()} + amount_invoiced = amount_to_invoice = 0.0 for moves_read in bills_move_line_read: + price_subtotal = self.analytic_account_id.currency_id.round(moves_read['price_subtotal'] * conversion_rates[moves_read['currency_id']]) if moves_read['parent_state'] == 'draft': if moves_read['move_type'] == 'in_invoice': - amount_to_invoice -= moves_read['price_subtotal'] + amount_to_invoice -= price_subtotal else: # moves_read['move_type'] == 'in_refund' - amount_to_invoice += moves_read['price_subtotal'] + amount_to_invoice += price_subtotal else: # moves_read['parent_state'] == 'posted' if moves_read['move_type'] == 'in_invoice': - amount_invoiced -= moves_read['price_subtotal'] + amount_invoiced -= price_subtotal else: # moves_read['move_type'] == 'in_refund' - amount_invoiced += moves_read['price_subtotal'] + amount_invoiced += price_subtotal # don't display the section if the final values are both 0 (bill -> vendor credit) if amount_invoiced != 0 or amount_to_invoice != 0: costs = profitability_items['costs'] diff --git a/addons/project_purchase/tests/test_project_profitability.py b/addons/project_purchase/tests/test_project_profitability.py index 75aa408c953..4bafc10e8f1 100644 --- a/addons/project_purchase/tests/test_project_profitability.py +++ b/addons/project_purchase/tests/test_project_profitability.py @@ -31,6 +31,7 @@ class TestProjectPurchaseProfitability(TestProjectProfitabilityCommon, TestPurch "quantity": 1, "product_uom_id": self.product_a.uom_id.id, "price_unit": self.product_a.standard_price, + "currency_id": self.env.company.currency_id.id, })], }) # the bill_1 is in draft, therefor it should have the cost "to_bill" same as the -product_price (untaxed) @@ -74,12 +75,14 @@ class TestProjectPurchaseProfitability(TestProjectProfitabilityCommon, TestPurch "quantity": 1, "product_uom_id": self.product_a.uom_id.id, "price_unit": self.product_a.standard_price, + "currency_id": self.env.company.currency_id.id, }), Command.create({ "analytic_distribution": {self.analytic_account.id: 100}, "product_id": self.product_b.id, "quantity": 2, "product_uom_id": self.product_b.uom_id.id, "price_unit": self.product_b.standard_price, + "currency_id": self.env.company.currency_id.id, })], }) # bill_2 is not posted, therefor its cost should be "to_billed" = - sum of all product_price * qty for each line @@ -125,6 +128,7 @@ class TestProjectPurchaseProfitability(TestProjectProfitabilityCommon, TestPurch "product_id": self.product_order.id, "product_qty": 1, "price_unit": self.product_order.standard_price, + "currency_id": self.env.company.currency_id.id, })], }) purchase_order.button_confirm() @@ -215,6 +219,7 @@ class TestProjectPurchaseProfitability(TestProjectProfitabilityCommon, TestPurch "product_id": self.product_order.id, "product_qty": 1, "price_unit": self.product_order.standard_price, + "currency_id": self.env.company.currency_id.id, })], }) purchase_order.button_confirm() diff --git a/addons/sale_project/models/project.py b/addons/sale_project/models/project.py index 1a6be7b15cd..659bb0f49c1 100755 --- a/addons/sale_project/models/project.py +++ b/addons/sale_project/models/project.py @@ -8,6 +8,7 @@ from odoo.exceptions import ValidationError, AccessError from odoo.osv import expression from odoo.tools import Query +from datetime import date class Project(models.Model): _inherit = 'project.project' @@ -433,22 +434,29 @@ class Project(models.Model): # account_move_line__move_id is the alias of the joined table account_move in the query # we can use it, because of the "move_id.move_type" clause in the domain of the query, which generates the join # this is faster than a search_read followed by a browse on the move_id to retrieve the move_type of each account.move.line - query_string, query_param = query.select('price_subtotal', 'parent_state', 'account_move_line__move_id.move_type') + query_string, query_param = query.select('price_subtotal', 'parent_state', 'account_move_line.currency_id', 'account_move_line__move_id.move_type') self._cr.execute(query_string, query_param) invoices_move_line_read = self._cr.dictfetchall() if invoices_move_line_read: + + # Get conversion rate from currencies to currency of analytic account + currency_ids = {iml['currency_id'] for iml in invoices_move_line_read + [{'currency_id': self.analytic_account_id.currency_id.id}]} + rates = self.env['res.currency'].browse(list(currency_ids))._get_rates(self.company_id, date.today()) + conversion_rates = {cid: rates[self.analytic_account_id.currency_id.id] / rate_from for cid, rate_from in rates.items()} + amount_invoiced = amount_to_invoice = 0.0 for moves_read in invoices_move_line_read: + price_subtotal = self.analytic_account_id.currency_id.round(moves_read['price_subtotal'] * conversion_rates[moves_read['currency_id']]) if moves_read['parent_state'] == 'draft': if moves_read['move_type'] == 'out_invoice': - amount_to_invoice += moves_read['price_subtotal'] + amount_to_invoice += price_subtotal else: # moves_read['move_type'] == 'out_refund' - amount_to_invoice -= moves_read['price_subtotal'] + amount_to_invoice -= price_subtotal else: # moves_read['parent_state'] == 'posted' if moves_read['move_type'] == 'out_invoice': - amount_invoiced += moves_read['price_subtotal'] + amount_invoiced += price_subtotal else: # moves_read['move_type'] == 'out_refund' - amount_invoiced -= moves_read['price_subtotal'] + amount_invoiced -= price_subtotal # don't display the section if the final values are both 0 (invoice -> credit note) if amount_invoiced != 0 or amount_to_invoice != 0: section_id = 'other_invoice_revenues'