From 1adaaba90ea0193fa73c323476ee383fe128aa05 Mon Sep 17 00:00:00 2001 From: "Victor Piryns (pivi)" Date: Thu, 30 Mar 2023 06:22:40 +0000 Subject: [PATCH] [FIX] (sale_)project(_purchase): proj.prof. w/ AA distr. for inv/bills Current behaviour: The project profitability panel doesn't take into account the AA distribution of the project's AA for solo invoices and bills. Expected behaviour: It should take the distribution set into account. Steps to reproduce: - Install Accounting, Sales, Project, Purchase - Settings > Activate AA - Create a new project with a new AA for it. - Create a new bill or invoice, on the line set the 2 AA, one with the project's AA with some X%, and the other AA is irrelevant (the sum of all AA on the line should be 100%) - Confirm the invoice/bill - Go to the project profitability report (project updates), see that we have 100% of the line value that contributed, when it should have. Reason for the problem: The contribution of the AA was not taken into account when computing the project profitability for solo invoices/bills (invoices/bills w/o SO/PO respectively) Fix: Additionally get the analytic_distribution when fetch the account_move_lines to compute the project's profitability. Affected versions: - 16.0 - saas-16.1 - saas-16.2 - master opw-3246199 closes odoo/odoo#117163 X-original-commit: c3c66d05d039514c9dd02ebdede20a3dff757728 Signed-off-by: Xavier Bol (xbo) Signed-off-by: Piryns Victor (pivi) --- .../models/project_project.py | 11 +++-- .../tests/test_project_profitability.py | 47 ++++++++++--------- addons/sale_project/models/project.py | 11 +++-- .../tests/test_project_profitability.py | 31 +++++++----- 4 files changed, 56 insertions(+), 44 deletions(-) diff --git a/addons/project_purchase/models/project_project.py b/addons/project_purchase/models/project_project.py index 8240cfc9aa8..69cff41c095 100644 --- a/addons/project_purchase/models/project_project.py +++ b/addons/project_purchase/models/project_project.py @@ -177,7 +177,7 @@ 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.currency_id', '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.analytic_distribution', '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: @@ -190,16 +190,17 @@ class Project(models.Model): 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']]) + analytic_contribution = moves_read['analytic_distribution'][str(self.analytic_account_id.id)] / 100. if moves_read['parent_state'] == 'draft': if moves_read['move_type'] == 'in_invoice': - amount_to_invoice -= price_subtotal + amount_to_invoice -= price_subtotal * analytic_contribution else: # moves_read['move_type'] == 'in_refund' - amount_to_invoice += price_subtotal + amount_to_invoice += price_subtotal * analytic_contribution else: # moves_read['parent_state'] == 'posted' if moves_read['move_type'] == 'in_invoice': - amount_invoiced -= price_subtotal + amount_invoiced -= price_subtotal * analytic_contribution else: # moves_read['move_type'] == 'in_refund' - amount_invoiced += price_subtotal + amount_invoiced += price_subtotal * analytic_contribution # 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 4bafc10e8f1..26e8249c662 100644 --- a/addons/project_purchase/tests/test_project_profitability.py +++ b/addons/project_purchase/tests/test_project_profitability.py @@ -17,7 +17,12 @@ class TestProjectPurchaseProfitability(TestProjectProfitabilityCommon, TestPurch """ A bill that has an AAL on one of its line should be taken into account for the profitability of the project. + The contribution of the line should only be dependent + on the project's analytic account % that was set on the line """ + # a custom analytic contribution (number between 1 -> 100 included) + analytic_distribution = 42 + analytic_contribution = analytic_distribution / 100. # create a bill_1 with the AAL bill_1 = self.env['account.move'].create({ "name": "Bill_1 name", @@ -26,7 +31,7 @@ class TestProjectPurchaseProfitability(TestProjectProfitabilityCommon, TestPurch "partner_id": self.partner.id, "invoice_date": datetime.today(), "invoice_line_ids": [Command.create({ - "analytic_distribution": {self.analytic_account.id: 100}, + "analytic_distribution": {self.analytic_account.id: analytic_distribution}, "product_id": self.product_a.id, "quantity": 1, "product_uom_id": self.product_a.uom_id.id, @@ -41,10 +46,10 @@ class TestProjectPurchaseProfitability(TestProjectProfitabilityCommon, TestPurch 'data': [{ 'id': 'other_purchase_costs', 'sequence': self.project._get_profitability_sequence_per_invoice_type()['other_purchase_costs'], - 'to_bill': -self.product_a.standard_price, + 'to_bill': -self.product_a.standard_price * analytic_contribution, 'billed': 0.0, }], - 'total': {'to_bill': -self.product_a.standard_price, 'billed': 0.0}, + 'total': {'to_bill': -self.product_a.standard_price * analytic_contribution, 'billed': 0.0}, }, ) # post bill_1 @@ -57,9 +62,9 @@ class TestProjectPurchaseProfitability(TestProjectProfitabilityCommon, TestPurch 'id': 'other_purchase_costs', 'sequence': self.project._get_profitability_sequence_per_invoice_type()['other_purchase_costs'], 'to_bill': 0.0, - 'billed': -self.product_a.standard_price, + 'billed': -self.product_a.standard_price * analytic_contribution, }], - 'total': {'to_bill': 0.0, 'billed': -self.product_a.standard_price}, + 'total': {'to_bill': 0.0, 'billed': -self.product_a.standard_price * analytic_contribution}, }, ) # create another bill, with 2 lines, 2 diff products, the second line has 2 as quantity @@ -70,14 +75,14 @@ class TestProjectPurchaseProfitability(TestProjectProfitabilityCommon, TestPurch "partner_id": self.partner.id, "invoice_date": datetime.today(), "invoice_line_ids": [Command.create({ - "analytic_distribution": {self.analytic_account.id: 100}, + "analytic_distribution": {self.analytic_account.id: analytic_distribution}, "product_id": self.product_a.id, "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}, + "analytic_distribution": {self.analytic_account.id: analytic_distribution}, "product_id": self.product_b.id, "quantity": 2, "product_uom_id": self.product_b.uom_id.id, @@ -92,12 +97,12 @@ class TestProjectPurchaseProfitability(TestProjectProfitabilityCommon, TestPurch 'data': [{ 'id': 'other_purchase_costs', 'sequence': self.project._get_profitability_sequence_per_invoice_type()['other_purchase_costs'], - 'to_bill': -(self.product_a.standard_price + 2 * self.product_b.standard_price), - 'billed': -self.product_a.standard_price, + 'to_bill': -(self.product_a.standard_price + 2 * self.product_b.standard_price) * analytic_contribution, + 'billed': -self.product_a.standard_price * analytic_contribution, }], 'total': { - 'to_bill': -(self.product_a.standard_price + 2 * self.product_b.standard_price), - 'billed': -self.product_a.standard_price, + 'to_bill': -(self.product_a.standard_price + 2 * self.product_b.standard_price) * analytic_contribution, + 'billed': -self.product_a.standard_price * analytic_contribution, }, }, ) @@ -111,11 +116,11 @@ class TestProjectPurchaseProfitability(TestProjectProfitabilityCommon, TestPurch 'id': 'other_purchase_costs', 'sequence': self.project._get_profitability_sequence_per_invoice_type()['other_purchase_costs'], 'to_bill': 0.0, - 'billed': -2 * (self.product_a.standard_price + self.product_b.standard_price), + 'billed': -2 * (self.product_a.standard_price + self.product_b.standard_price) * analytic_contribution, }], 'total': { 'to_bill': 0.0, - 'billed': -2 * (self.product_a.standard_price + self.product_b.standard_price), + 'billed': -2 * (self.product_a.standard_price + self.product_b.standard_price) * analytic_contribution, }, }, ) @@ -124,7 +129,7 @@ class TestProjectPurchaseProfitability(TestProjectProfitabilityCommon, TestPurch "name": "A purchase order", "partner_id": self.partner_a.id, "order_line": [Command.create({ - "analytic_distribution": {self.analytic_account.id: 100}, + "analytic_distribution": {self.analytic_account.id: analytic_distribution}, "product_id": self.product_order.id, "product_qty": 1, "price_unit": self.product_order.standard_price, @@ -142,17 +147,17 @@ class TestProjectPurchaseProfitability(TestProjectProfitabilityCommon, TestPurch 'data': [{ 'id': 'purchase_order', 'sequence': self.project._get_profitability_sequence_per_invoice_type()['purchase_order'], - 'to_bill': -self.product_order.standard_price, + 'to_bill': -self.product_order.standard_price * analytic_contribution, 'billed': 0.0, }, { 'id': 'other_purchase_costs', 'sequence': self.project._get_profitability_sequence_per_invoice_type()['other_purchase_costs'], 'to_bill': 0.0, - 'billed': -2 * (self.product_a.standard_price + self.product_b.standard_price), + 'billed': -2 * (self.product_a.standard_price + self.product_b.standard_price) * analytic_contribution, }], 'total': { - 'to_bill': -self.product_order.standard_price, - 'billed': -2 * (self.product_a.standard_price + self.product_b.standard_price), + 'to_bill': -self.product_order.standard_price * analytic_contribution, + 'billed': -2 * (self.product_a.standard_price + self.product_b.standard_price) * analytic_contribution, }, }, ) @@ -169,18 +174,18 @@ class TestProjectPurchaseProfitability(TestProjectProfitabilityCommon, TestPurch 'id': 'purchase_order', 'sequence': self.project._get_profitability_sequence_per_invoice_type()['purchase_order'], 'to_bill': 0.0, - 'billed': -self.product_order.standard_price, + 'billed': -self.product_order.standard_price * analytic_contribution, }, { 'id': 'other_purchase_costs', 'sequence': self.project._get_profitability_sequence_per_invoice_type()['other_purchase_costs'], 'to_bill': 0.0, - 'billed': -2 * (self.product_a.standard_price + self.product_b.standard_price), + 'billed': -2 * (self.product_a.standard_price + self.product_b.standard_price) * analytic_contribution, }], 'total': { 'to_bill': 0.0, 'billed': -(2 * self.product_a.standard_price + 2 * self.product_b.standard_price + - self.product_order.standard_price), + self.product_order.standard_price) * analytic_contribution, }, }, ) diff --git a/addons/sale_project/models/project.py b/addons/sale_project/models/project.py index 656c6d9da5f..bf09a908a60 100755 --- a/addons/sale_project/models/project.py +++ b/addons/sale_project/models/project.py @@ -434,7 +434,7 @@ 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.currency_id', '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.analytic_distribution', '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: @@ -447,16 +447,17 @@ class Project(models.Model): 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']]) + analytic_contribution = moves_read['analytic_distribution'][str(self.analytic_account_id.id)] / 100. if moves_read['parent_state'] == 'draft': if moves_read['move_type'] == 'out_invoice': - amount_to_invoice += price_subtotal + amount_to_invoice += price_subtotal * analytic_contribution else: # moves_read['move_type'] == 'out_refund' - amount_to_invoice -= price_subtotal + amount_to_invoice -= price_subtotal * analytic_contribution else: # moves_read['parent_state'] == 'posted' if moves_read['move_type'] == 'out_invoice': - amount_invoiced += price_subtotal + amount_invoiced += price_subtotal * analytic_contribution else: # moves_read['move_type'] == 'out_refund' - amount_invoiced -= price_subtotal + amount_invoiced -= price_subtotal * analytic_contribution # 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' diff --git a/addons/sale_project/tests/test_project_profitability.py b/addons/sale_project/tests/test_project_profitability.py index c68a6c333be..c0fc6787086 100644 --- a/addons/sale_project/tests/test_project_profitability.py +++ b/addons/sale_project/tests/test_project_profitability.py @@ -245,8 +245,13 @@ class TestSaleProjectProfitability(TestProjectProfitabilityCommon, TestSaleCommo """ An invoice that has an AAL on one of its line should be taken into account for the profitability of the project. + The contribution of the line should only be dependent + on the project's analytic account % that was set on the line """ self.project.allow_billable = True + # a custom analytic contribution (number between 1 -> 100 included) + analytic_distribution = 42 + analytic_contribution = analytic_distribution / 100. # create a invoice_1 with the AAL invoice_1 = self.env['account.move'].create({ "name": "Invoice_1", @@ -255,7 +260,7 @@ class TestSaleProjectProfitability(TestProjectProfitabilityCommon, TestSaleCommo "partner_id": self.partner.id, "invoice_date": datetime.today(), "invoice_line_ids": [Command.create({ - "analytic_distribution": {self.analytic_account.id: 100}, + "analytic_distribution": {self.analytic_account.id: analytic_distribution}, "product_id": self.product_a.id, "quantity": 1, "product_uom_id": self.product_a.uom_id.id, @@ -269,10 +274,10 @@ class TestSaleProjectProfitability(TestProjectProfitabilityCommon, TestSaleCommo 'data': [{ 'id': 'other_invoice_revenues', 'sequence': self.project._get_profitability_sequence_per_invoice_type()['other_invoice_revenues'], - 'to_invoice': self.product_a.standard_price, + 'to_invoice': self.product_a.standard_price * analytic_contribution, 'invoiced': 0.0, }], - 'total': {'to_invoice': self.product_a.standard_price, 'invoiced': 0.0}, + 'total': {'to_invoice': self.product_a.standard_price * analytic_contribution, 'invoiced': 0.0}, }, ) # post invoice_1 @@ -285,9 +290,9 @@ class TestSaleProjectProfitability(TestProjectProfitabilityCommon, TestSaleCommo 'id': 'other_invoice_revenues', 'sequence': self.project._get_profitability_sequence_per_invoice_type()['other_invoice_revenues'], 'to_invoice': 0.0, - 'invoiced': self.product_a.standard_price, + 'invoiced': self.product_a.standard_price * analytic_contribution, }], - 'total': {'to_invoice': 0.0, 'invoiced': self.product_a.standard_price}, + 'total': {'to_invoice': 0.0, 'invoiced': self.product_a.standard_price * analytic_contribution}, }, ) # create another invoice, with 2 lines, 2 diff products, the second line has 2 as quantity @@ -298,13 +303,13 @@ class TestSaleProjectProfitability(TestProjectProfitabilityCommon, TestSaleCommo "partner_id": self.partner.id, "invoice_date": datetime.today(), "invoice_line_ids": [Command.create({ - "analytic_distribution": {self.analytic_account.id: 100}, + "analytic_distribution": {self.analytic_account.id: analytic_distribution}, "product_id": self.product_a.id, "quantity": 1, "product_uom_id": self.product_a.uom_id.id, "price_unit": self.product_a.standard_price, }), Command.create({ - "analytic_distribution": {self.analytic_account.id: 100}, + "analytic_distribution": {self.analytic_account.id: analytic_distribution}, "product_id": self.product_b.id, "quantity": 2, "product_uom_id": self.product_b.uom_id.id, @@ -318,12 +323,12 @@ class TestSaleProjectProfitability(TestProjectProfitabilityCommon, TestSaleCommo 'data': [{ 'id': 'other_invoice_revenues', 'sequence': self.project._get_profitability_sequence_per_invoice_type()['other_invoice_revenues'], - 'to_invoice': (self.product_a.standard_price + 2 * self.product_b.standard_price), - 'invoiced': self.product_a.standard_price, + 'to_invoice': (self.product_a.standard_price + 2 * self.product_b.standard_price) * analytic_contribution, + 'invoiced': self.product_a.standard_price * analytic_contribution, }], 'total': { - 'to_invoice': (self.product_a.standard_price + 2 * self.product_b.standard_price), - 'invoiced': self.product_a.standard_price, + 'to_invoice': (self.product_a.standard_price + 2 * self.product_b.standard_price) * analytic_contribution, + 'invoiced': self.product_a.standard_price * analytic_contribution, }, }, ) @@ -337,11 +342,11 @@ class TestSaleProjectProfitability(TestProjectProfitabilityCommon, TestSaleCommo 'id': 'other_invoice_revenues', 'sequence': self.project._get_profitability_sequence_per_invoice_type()['other_invoice_revenues'], 'to_invoice': 0.0, - 'invoiced': 2 * (self.product_a.standard_price + self.product_b.standard_price), + 'invoiced': 2 * (self.product_a.standard_price + self.product_b.standard_price) * analytic_contribution, }], 'total': { 'to_invoice': 0.0, - 'invoiced': 2 * (self.product_a.standard_price + self.product_b.standard_price), + 'invoiced': 2 * (self.product_a.standard_price + self.product_b.standard_price) * analytic_contribution, }, }, )