[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) <xbo@odoo.com>
This commit is contained in:
Thomas Lefebvre (thle)
2023-02-24 20:14:01 +01:00
parent b1ada1af7d
commit 5da3ab038b
3 changed files with 39 additions and 12 deletions
+21 -7
View File
@@ -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']
@@ -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()
+13 -5
View File
@@ -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'