diff --git a/addons/project_purchase/models/project_project.py b/addons/project_purchase/models/project_project.py index 640da6112fb..c8d3f64d18c 100644 --- a/addons/project_purchase/models/project_project.py +++ b/addons/project_purchase/models/project_project.py @@ -143,7 +143,7 @@ class Project(models.Model): ('state', 'in', ['purchase', 'done']), '|', ('qty_invoiced', '>', 0), - '|', ('qty_to_invoice', '>', 0), ('product_uom_qty', '>', 0), + '|', ('qty_to_invoice', '>', 0), ('product_qty', '>', 0), ], order=self.env['purchase.order.line']._order) query.add_where( SQL( @@ -152,7 +152,7 @@ class Project(models.Model): self.env['purchase.order.line']._query_analytic_accounts(), ) ) - 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') + query_string, query_param = query.select('"purchase_order_line".id', 'qty_invoiced', 'qty_to_invoice', 'product_qty', 'price_subtotal', 'purchase_order_line.currency_id', '"purchase_order_line".analytic_distribution') self._cr.execute(query_string, query_param) purchase_order_line_read = [{ **pol, @@ -168,17 +168,18 @@ class Project(models.Model): for pol_read in purchase_order_line_read: purchase_order_line_invoice_line_ids.extend(pol_read['invoice_lines'].ids) currency = self.env['res.currency'].browse(pol_read['currency_id']).with_prefetch(currency_ids) - price_unit = currency._convert(pol_read['price_unit'], self.currency_id, self.company_id) + price_subtotal = currency._convert(pol_read['price_subtotal'], self.currency_id, self.company_id) + price_subtotal_unit = price_subtotal / pol_read['product_qty'] if pol_read['product_qty'] else 0.0 # an analytic account can appear several time in an analytic distribution with different repartition percentage analytic_contribution = sum( percentage for ids, percentage in pol_read['analytic_distribution'].items() if str(self.analytic_account_id.id) in ids.split(',') ) / 100. - amount_invoiced -= price_unit * pol_read['qty_invoiced'] * analytic_contribution if pol_read['qty_invoiced'] > 0 else 0.0 + amount_invoiced -= price_subtotal_unit * pol_read['qty_invoiced'] * analytic_contribution if pol_read['qty_invoiced'] > 0 else 0.0 if pol_read['qty_to_invoice'] > 0: - amount_to_invoice -= price_unit * pol_read['qty_to_invoice'] * analytic_contribution + amount_to_invoice -= price_subtotal_unit * pol_read['qty_to_invoice'] * analytic_contribution else: - amount_to_invoice -= price_unit * (pol_read['product_uom_qty'] - pol_read['qty_invoiced']) * analytic_contribution + amount_to_invoice -= price_subtotal_unit * (pol_read['product_qty'] - pol_read['qty_invoiced']) * analytic_contribution purchase_order_line_ids.append(pol_read['id']) costs = profitability_items['costs'] section_id = 'purchase_order' diff --git a/addons/project_purchase/tests/test_project_profitability.py b/addons/project_purchase/tests/test_project_profitability.py index 8f92b27c79a..209a40c6392 100644 --- a/addons/project_purchase/tests/test_project_profitability.py +++ b/addons/project_purchase/tests/test_project_profitability.py @@ -169,6 +169,7 @@ class TestProjectPurchaseProfitability(TestProjectProfitabilityCommon, TestPurch })], }) purchase_order.button_confirm() + purchase_order.order_line.flush_recordset() # we should have a new section "purchase_order", the total should be updated, # but the "other_purchase_costs" shouldn't change, as we don't take into # account bills from purchase orders, as those are already taken into calculations @@ -267,6 +268,7 @@ class TestProjectPurchaseProfitability(TestProjectProfitabilityCommon, TestPurch })], }) purchase_order.button_confirm() + purchase_order.order_line.flush_recordset() self.assertDictEqual( self.project._get_profitability_items(False)['costs'], { @@ -424,6 +426,7 @@ class TestProjectPurchaseProfitability(TestProjectProfitabilityCommon, TestPurch })], }) purchase_order_foreign.button_confirm() + purchase_order_foreign.order_line.flush_recordset() # We should have a new section "purchase_order", the total should be updated, # but the "other_purchase_costs" shouldn't change, as we don't take into @@ -460,6 +463,7 @@ class TestProjectPurchaseProfitability(TestProjectProfitabilityCommon, TestPurch })], }) purchase_order.button_confirm() + purchase_order.order_line.flush_recordset() # The 'to bill' section should be updated in the 'total' and 'purchase orders' sections. items = project._get_profitability_items(with_action=False)['costs'] self.assertEqual('purchase_order', items['data'][0]['id']) @@ -508,6 +512,103 @@ class TestProjectPurchaseProfitability(TestProjectProfitabilityCommon, TestPurch purchase_bill.invoice_date = datetime.today() purchase_bill.action_post() + def test_analytic_distribution_with_included_tax(self): + """When calculating the profitability of a project, included taxes should not be calculated""" + included_tax = self.env['account.tax'].create({ + 'name': 'included tax', + 'amount': '15.0', + 'amount_type': 'percent', + 'type_tax_use': 'purchase', + 'price_include': True + }) + + # create a purchase.order with the project account in analytic_distribution + purchase_order = self.env['purchase.order'].create({ + 'name': "A purchase order", + 'partner_id': self.partner_a.id, + 'order_line': [Command.create({ + 'analytic_distribution': {self.analytic_account.id: 100}, + 'product_id': self.product_order.id, + 'product_qty': 2, # plural value to check if the price is multiplied more than once + 'taxes_id': [included_tax.id], # set the included tax + 'price_unit': self.product_order.standard_price, + 'currency_id': self.env.company.currency_id.id, + })], + }) + purchase_order.button_confirm() + purchase_order.order_line.flush_recordset() + # the profitability should not take taxes into account + self.assertDictEqual( + self.project._get_profitability_items(False)['costs'], + { + 'data': [{ + 'id': 'purchase_order', + 'sequence': self.project._get_profitability_sequence_per_invoice_type()['purchase_order'], + 'to_bill': -(purchase_order.amount_untaxed), + 'billed': 0.0, + }], + 'total': { + 'to_bill': -(purchase_order.amount_untaxed), + 'billed': 0.0, + }, + }, + ) + + purchase_order.action_create_invoice() + purchase_bill = purchase_order.invoice_ids # get the bill from the purchase + purchase_bill.invoice_date = datetime.today() + purchase_bill.action_post() + # same here, taxes should not be calculated in the profitability + self.assertDictEqual( + self.project._get_profitability_items(False)['costs'], + { + 'data': [{ + 'id': 'purchase_order', + 'sequence': self.project._get_profitability_sequence_per_invoice_type()['purchase_order'], + 'to_bill': 0.0, + 'billed': -(purchase_bill.amount_untaxed), + }], + 'total': { + 'to_bill': 0.0, + 'billed': -(purchase_bill.amount_untaxed), + }, + }, + ) + + def test_analytic_distribution_with_mismatched_uom(self): + """When changing the unit of measure, the profitability should still match the price_subtotal of the order line""" + # create a purchase.order with the project account in analytic_distribution + purchase_order = self.env['purchase.order'].create({ + 'name': "A purchase order", + 'partner_id': self.partner_a.id, + 'order_line': [Command.create({ + 'analytic_distribution': {self.analytic_account.id: 100}, + '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() + # changing the uom to a higher number + purchase_order.order_line.product_uom = self.env.ref("uom.product_uom_dozen") + purchase_order.order_line.flush_recordset() + self.assertDictEqual( + self.project._get_profitability_items(False)['costs'], + { + 'data': [{ + 'id': 'purchase_order', + 'sequence': self.project._get_profitability_sequence_per_invoice_type()['purchase_order'], + 'to_bill': -(purchase_order.amount_untaxed), + 'billed': 0.0, + }], + 'total': { + 'to_bill': -(purchase_order.amount_untaxed), + 'billed': 0.0, + }, + }, + ) + def test_cross_analytics_contribution(self): cross_plan = self.env['account.analytic.plan'].create({'name': 'Cross Plan'}) cross_account = self.env['account.analytic.account'].create({ @@ -535,8 +636,9 @@ class TestProjectPurchaseProfitability(TestProjectProfitabilityCommon, TestPurch }) cross_order.button_confirm() - items = self.project._get_profitability_items() + cross_order.order_line.flush_recordset() + items = self.project._get_profitability_items(with_action=False)['costs'] self.assertEqual( - items['costs']['data'][0]['to_bill'], - self.product_order.standard_price *-cross_distribution/100 + items['data'][0]['to_bill'], + -(self.product_order.standard_price * cross_distribution / 100) )