[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:
@@ -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()
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user