From e26bdb32822e27e1a6a0a171566bb78ba0eebe2d Mon Sep 17 00:00:00 2001 From: "Xavier BOL (xbo)" Date: Fri, 1 Apr 2022 12:02:03 +0200 Subject: [PATCH] [IMP] project,*: sort the row displayed in profitability section Before this commit, the order of rows shown in costs and revenues is not fixed and depends on the order of the modules. This commit adds a sequence on each row and then sort the rows to fix the order of those rows. task-2710808 --- addons/project/models/project.py | 9 ++++++++- addons/project_hr_expense/models/project.py | 7 ++++++- .../tests/test_project_profitability.py | 6 +++++- addons/project_mrp/models/project.py | 6 ++++++ addons/project_purchase/models/project.py | 7 ++++++- .../models/project_project.py | 3 +++ .../tests/test_project_profitability.py | 15 +++++++++------ addons/sale_project/models/project.py | 10 +++++++++- .../tests/test_project_profitability.py | 8 ++++++++ addons/sale_timesheet/models/project.py | 14 +++++++++++++- .../tests/test_project_profitability.py | 17 ++++++++++++++--- 11 files changed, 87 insertions(+), 15 deletions(-) diff --git a/addons/project/models/project.py b/addons/project/models/project.py index e9dcb90a99b..c6a2abbdd64 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -734,13 +734,20 @@ class Project(models.Model): 'currency_id': self.currency_id.id, } if self._show_profitability(): - panel_data['profitability_items'] = self._get_profitability_items() + profitability_items = self._get_profitability_items() + if self._get_profitability_sequence_per_invoice_type() and profitability_items and 'revenues' in profitability_items and 'costs' in profitability_items: # sort the data values + profitability_items['revenues']['data'] = sorted(profitability_items['revenues']['data'], key=lambda k: k['sequence']) + profitability_items['costs']['data'] = sorted(profitability_items['costs']['data'], key=lambda k: k['sequence']) + panel_data['profitability_items'] = profitability_items panel_data['profitability_labels'] = self._get_profitability_labels() return panel_data def _get_profitability_labels(self): return {} + def _get_profitability_sequence_per_invoice_type(self): + return {} + def _get_user_values(self): return { 'is_project_user': self.user_has_groups('project.group_project_user'), diff --git a/addons/project_hr_expense/models/project.py b/addons/project_hr_expense/models/project.py index e91472174a1..0a4e98bfca3 100644 --- a/addons/project_hr_expense/models/project.py +++ b/addons/project_hr_expense/models/project.py @@ -66,6 +66,11 @@ class Project(models.Model): labels['expenses'] = _lt('Expenses') return labels + def _get_profitability_sequence_per_invoice_type(self): + sequence_per_invoice_type = super()._get_profitability_sequence_per_invoice_type() + sequence_per_invoice_type['expenses'] = 11 + return sequence_per_invoice_type + def _get_expenses_profitability_items(self, with_action=True): if not self.analytic_account_id: return {} @@ -82,7 +87,7 @@ class Project(models.Model): expense_data = expenses_read_group[0] section_id = 'expenses' expense_profitability_items = { - 'costs': {'id': section_id, 'billed': -expense_data['untaxed_amount'], 'to_bill': 0.0}, + 'costs': {'id': section_id, 'sequence': self._get_profitability_sequence_per_invoice_type()[section_id], 'billed': -expense_data['untaxed_amount'], 'to_bill': 0.0}, } if can_see_expense: expense_profitability_items['action'] = {'name': 'action_profitability_items', 'type': 'object', 'section': section_id, 'domain': json.dumps([('id', 'in', expense_data['ids'])])} diff --git a/addons/project_hr_expense/tests/test_project_profitability.py b/addons/project_hr_expense/tests/test_project_profitability.py index fdbd4b2b671..6c4ed965e41 100644 --- a/addons/project_hr_expense/tests/test_project_profitability.py +++ b/addons/project_hr_expense/tests/test_project_profitability.py @@ -47,11 +47,15 @@ class TestProjectHrExpenseProfitability(TestProjectProfitabilityCommon, TestProj self.project, self.project_profitability_items_empty) + sequence_per_invoice_type = self.project._get_profitability_sequence_per_invoice_type() + self.assertIn('expenses', sequence_per_invoice_type) + expense_sequence = sequence_per_invoice_type['expenses'] + self.assertDictEqual( self.project._get_profitability_items(False), { 'costs': { - 'data': [{'id': 'expenses', 'to_bill': 0.0, 'billed': -expense.untaxed_amount}], + 'data': [{'id': 'expenses', 'sequence': expense_sequence, 'to_bill': 0.0, 'billed': -expense.untaxed_amount}], 'total': {'to_bill': 0.0, 'billed': -expense.untaxed_amount}, }, 'revenues': {'data': [], 'total': {'to_invoice': 0.0, 'invoiced': 0.0}}, diff --git a/addons/project_mrp/models/project.py b/addons/project_mrp/models/project.py index aee89564c2b..9367841e636 100644 --- a/addons/project_mrp/models/project.py +++ b/addons/project_mrp/models/project.py @@ -42,6 +42,11 @@ class Project(models.Model): labels['manufacturing_order'] = _lt('Manufacturing Orders') return labels + def _get_profitability_sequence_per_invoice_type(self): + sequence_per_invoice_type = super()._get_profitability_sequence_per_invoice_type() + sequence_per_invoice_type['manufacturing_order'] = 10 + return sequence_per_invoice_type + def _get_profitability_aal_domain(self): return expression.AND([ super()._get_profitability_aal_domain(), @@ -60,6 +65,7 @@ class Project(models.Model): can_see_manufactoring_order = with_action and len(self) == 1 and self.user_has_groups('mrp.group_mrp_user') mrp_costs = { 'id': mrp_category, + 'sequence': self._get_profitability_sequence_per_invoice_type()[mrp_category], 'billed': sum([res['amount'] for res in mrp_aal_read_group]), 'to_bill': 0.0, } diff --git a/addons/project_purchase/models/project.py b/addons/project_purchase/models/project.py index 64b88fe2d90..d3ef89974f2 100644 --- a/addons/project_purchase/models/project.py +++ b/addons/project_purchase/models/project.py @@ -94,6 +94,11 @@ class Project(models.Model): labels['purchase_order'] = _lt('Purchase Orders') return labels + def _get_profitability_sequence_per_invoice_type(self): + sequence_per_invoice_type = super()._get_profitability_sequence_per_invoice_type() + sequence_per_invoice_type['purchase_order'] = 9 + return sequence_per_invoice_type + def _get_profitability_items(self, with_action=True): profitability_items = super()._get_profitability_items(with_action) if self.analytic_account_id: @@ -112,7 +117,7 @@ class Project(models.Model): purchase_order_line_ids.append(pol_read['id']) costs = profitability_items['costs'] section_id = 'purchase_order' - purchase_order_costs = {'id': section_id, 'billed': amount_invoiced, 'to_bill': amount_to_invoice} + purchase_order_costs = {'id': section_id, 'sequence': self._get_profitability_sequence_per_invoice_type()[section_id], 'billed': amount_invoiced, 'to_bill': amount_to_invoice} if with_action and purchase_order_line_ids and self.user_has_groups('purchase.group_purchase_user'): action = {'name': 'action_profitability_items', 'type': 'object', 'section': section_id, 'domain': json.dumps([('id', 'in', purchase_order_line_ids)])} if len(purchase_order_line_ids) == 1: diff --git a/addons/project_sale_expense/models/project_project.py b/addons/project_sale_expense/models/project_project.py index c7da95b37fe..725d2aa57b4 100644 --- a/addons/project_sale_expense/models/project_project.py +++ b/addons/project_sale_expense/models/project_project.py @@ -54,9 +54,11 @@ class Project(models.Model): total_amount_expense_invoiced += res['untaxed_amount_invoiced'] reinvoice_expense_ids += expense_data_per_product_id[product_id] section_id = 'expenses' + sequence = self._get_profitability_sequence_per_invoice_type()[section_id] expense_data = { 'costs': { 'id': section_id, + 'sequence': sequence, 'billed': -amount_billed, 'to_bill': 0.0, }, @@ -64,6 +66,7 @@ class Project(models.Model): if reinvoice_expense_ids: expense_data['revenues'] = { 'id': section_id, + 'sequence': sequence, 'invoiced': total_amount_expense_invoiced, 'to_invoice': total_amount_expense_to_invoice, } diff --git a/addons/project_sale_expense/tests/test_project_profitability.py b/addons/project_sale_expense/tests/test_project_profitability.py index 828ad467700..115d5cab34b 100644 --- a/addons/project_sale_expense/tests/test_project_profitability.py +++ b/addons/project_sale_expense/tests/test_project_profitability.py @@ -27,6 +27,9 @@ class TestProjectSaleExpenseProfitability(TestProjectProfitabilityCommon, TestPr self.project_profitability_items_empty) expense_profitability = self.project._get_expenses_profitability_items(False) + sequence_per_invoice_type = self.project._get_profitability_sequence_per_invoice_type() + self.assertIn('expenses', sequence_per_invoice_type) + expense_sequence = sequence_per_invoice_type['expenses'] self.assertDictEqual( expense_profitability.get('revenues', {}), @@ -34,7 +37,7 @@ class TestProjectSaleExpenseProfitability(TestProjectProfitabilityCommon, TestPr ) self.assertDictEqual( expense_profitability['costs'], - {'id': 'expenses', 'billed': -280.0, 'to_bill': 0.0}, + {'id': 'expenses', 'sequence': expense_sequence, 'billed': -280.0, 'to_bill': 0.0}, ) expense_sheet.action_sheet_move_create() @@ -59,11 +62,11 @@ class TestProjectSaleExpenseProfitability(TestProjectProfitabilityCommon, TestPr expense_profitability = self.project._get_expenses_profitability_items(False) self.assertDictEqual( expense_profitability.get('revenues', {}), - {'id': 'expenses', 'invoiced': 0.0, 'to_invoice': expense_sol.untaxed_amount_to_invoice}, + {'id': 'expenses', 'sequence': expense_sequence, 'invoiced': 0.0, 'to_invoice': expense_sol.untaxed_amount_to_invoice}, ) self.assertDictEqual( expense_profitability['costs'], - {'id': 'expenses', 'billed': -280.0, 'to_bill': 0.0}, + {'id': 'expenses', 'sequence': expense_sequence, 'billed': -280.0, 'to_bill': 0.0}, ) self.assertDictEqual( @@ -92,7 +95,7 @@ class TestProjectSaleExpenseProfitability(TestProjectProfitabilityCommon, TestPr expense_profitability = self.project._get_expenses_profitability_items(False) self.assertDictEqual( expense_profitability.get('revenues', {}), - {'id': 'expenses', 'invoiced': expense_sol.untaxed_amount_invoiced, 'to_invoice': 0.0}, + {'id': 'expenses', 'sequence': expense_sequence, 'invoiced': expense_sol.untaxed_amount_invoiced, 'to_invoice': 0.0}, ) credit_note = invoice._reverse_moves() @@ -101,7 +104,7 @@ class TestProjectSaleExpenseProfitability(TestProjectProfitabilityCommon, TestPr expense_profitability = self.project._get_expenses_profitability_items(False) self.assertDictEqual( expense_profitability.get('revenues', {}), - {'id': 'expenses', 'invoiced': 0.0, 'to_invoice': expense_sol.untaxed_amount_to_invoice}, + {'id': 'expenses', 'sequence': expense_sequence, 'invoiced': 0.0, 'to_invoice': expense_sol.untaxed_amount_to_invoice}, ) self.sale_order.action_cancel() @@ -112,7 +115,7 @@ class TestProjectSaleExpenseProfitability(TestProjectProfitabilityCommon, TestPr ) self.assertDictEqual( expense_profitability['costs'], - {'id': 'expenses', 'billed': -280.0, 'to_bill': 0.0}, + {'id': 'expenses', 'sequence': expense_sequence, 'billed': -280.0, 'to_bill': 0.0}, ) expense_sheet.refuse_sheet('Test Cancel Expense') diff --git a/addons/sale_project/models/project.py b/addons/sale_project/models/project.py index 5e3a3c3896e..39ad9d2a7fd 100644 --- a/addons/sale_project/models/project.py +++ b/addons/sale_project/models/project.py @@ -299,6 +299,13 @@ class Project(models.Model): 'other_revenues': _lt('Material'), } + def _get_profitability_sequence_per_invoice_type(self): + return { + **super()._get_profitability_sequence_per_invoice_type(), + 'service_revenues': 6, + 'other_revenues': 7, + } + def _get_service_policy_to_invoice_type(self): return { 'ordered_prepaid': 'service_revenues', @@ -372,8 +379,9 @@ class Project(models.Model): if len(sol_ids) == 1: action_params['res_id'] = sol_ids[0] other_revenues['action'] = action_params + sequence_per_invoice_type = self._get_profitability_sequence_per_invoice_type() return { - 'data': [{'id': invoice_type, **vals} for invoice_type, vals in revenues_dict.items()], + 'data': [{'id': invoice_type, 'sequence': sequence_per_invoice_type[invoice_type], **vals} for invoice_type, vals in revenues_dict.items()], 'total': {'to_invoice': total_to_invoice, 'invoiced': total_invoiced}, } diff --git a/addons/sale_project/tests/test_project_profitability.py b/addons/sale_project/tests/test_project_profitability.py index e5fffadd6e3..2eec2858baf 100644 --- a/addons/sale_project/tests/test_project_profitability.py +++ b/addons/sale_project/tests/test_project_profitability.py @@ -76,6 +76,8 @@ class TestSaleProjectProfitability(TestProjectProfitabilityCommon, TestSaleCommo invoice_type, ['billable_manual', 'service_revenues'], 'invoice_type="billable_manual" if sale_timesheet is installed otherwise it is equal to "service_revenues"') + sequence_per_invoice_type = self.project._get_profitability_sequence_per_invoice_type() + self.assertIn('service_revenues', sequence_per_invoice_type) self.assertDictEqual( self.project._get_profitability_items(False), { @@ -84,6 +86,7 @@ class TestSaleProjectProfitability(TestProjectProfitabilityCommon, TestSaleCommo { # id should be equal to "billable_manual" if "sale_timesheet" module is installed otherwise "service_revenues" 'id': invoice_type, + 'sequence': sequence_per_invoice_type[invoice_type], 'to_invoice': self.delivery_service_order_line.untaxed_amount_to_invoice, 'invoiced': self.delivery_service_order_line.untaxed_amount_invoiced, }, @@ -128,6 +131,7 @@ class TestSaleProjectProfitability(TestProjectProfitabilityCommon, TestSaleCommo 'data': [ { 'id': invoice_type, + 'sequence': sequence_per_invoice_type[invoice_type], 'to_invoice': 0.0, 'invoiced': self.delivery_service_order_line.untaxed_amount_invoiced, }, @@ -169,11 +173,13 @@ class TestSaleProjectProfitability(TestProjectProfitabilityCommon, TestSaleCommo 'data': [ { 'id': invoice_type, + 'sequence': sequence_per_invoice_type[invoice_type], 'to_invoice': sum(service_sols.mapped('untaxed_amount_to_invoice')), 'invoiced': sum(service_sols.mapped('untaxed_amount_invoiced')), }, { 'id': 'other_revenues', + 'sequence': sequence_per_invoice_type['other_revenues'], 'to_invoice': material_order_line.untaxed_amount_to_invoice, 'invoiced': material_order_line.untaxed_amount_invoiced, }, @@ -203,11 +209,13 @@ class TestSaleProjectProfitability(TestProjectProfitabilityCommon, TestSaleCommo 'data': [ { 'id': invoice_type, + 'sequence': sequence_per_invoice_type[invoice_type], 'to_invoice': sum(service_sols.mapped('untaxed_amount_to_invoice')), 'invoiced': manual_service_order_line.untaxed_amount_invoiced, }, { 'id': 'other_revenues', + 'sequence': sequence_per_invoice_type['other_revenues'], 'to_invoice': material_order_line.untaxed_amount_to_invoice, 'invoiced': material_order_line.untaxed_amount_invoiced, }, diff --git a/addons/sale_timesheet/models/project.py b/addons/sale_timesheet/models/project.py index e25449e2cc8..a98963c9bea 100644 --- a/addons/sale_timesheet/models/project.py +++ b/addons/sale_timesheet/models/project.py @@ -394,6 +394,16 @@ class Project(models.Model): 'non_billable': _lt('Timesheets (Non Billable)'), } + def _get_profitability_sequence_per_invoice_type(self): + return { + **super()._get_profitability_sequence_per_invoice_type(), + 'billable_fixed': 1, + 'billable_time': 2, + 'billable_milestones': 3, + 'billable_manual': 4, + 'non_billable': 5, + } + def _get_profitability_aal_domain(self): domain = ['|', ('project_id', 'in', self.ids), ('so_line', 'in', self._fetch_sale_order_item_ids())] return expression.AND([ @@ -451,6 +461,8 @@ class Project(models.Model): action_params['res_id'] = record_ids[0] return action_params + sequence_per_invoice_type = self._get_profitability_sequence_per_invoice_type() + def convert_dict_into_profitability_data(d, cost=True): profitability_data = [] key1, key2 = ['to_bill', 'billed'] if cost else ['to_invoice', 'invoiced'] @@ -458,7 +470,7 @@ class Project(models.Model): if not vals[key1] and not vals[key2]: continue record_ids = vals.pop('record_ids', []) - data = {'id': invoice_type, **vals} + data = {'id': invoice_type, 'sequence': sequence_per_invoice_type[invoice_type], **vals} if record_ids: if invoice_type not in ['other_costs', 'other_revenues'] and can_see_timesheets: # action to see the timesheets action = get_timesheets_action(invoice_type, record_ids) diff --git a/addons/sale_timesheet/tests/test_project_profitability.py b/addons/sale_timesheet/tests/test_project_profitability.py index 99c08176245..7ec867e8319 100644 --- a/addons/sale_timesheet/tests/test_project_profitability.py +++ b/addons/sale_timesheet/tests/test_project_profitability.py @@ -74,6 +74,12 @@ class TestSaleTimesheetProjectProfitability(TestCommonSaleTimesheet): 'unit_amount': 2.0, }) + sequence_per_invoice_type = self.project_task_rate._get_profitability_sequence_per_invoice_type() + self.assertIn('billable_time', sequence_per_invoice_type) + self.assertIn('billable_fixed', sequence_per_invoice_type) + self.assertIn('billable_milestones', sequence_per_invoice_type) + self.assertIn('billable_manual', sequence_per_invoice_type) + self.assertEqual(self.task.sale_line_id, delivery_service_order_line) self.assertEqual((timesheet1 + timesheet2).so_line, delivery_service_order_line) self.assertEqual(delivery_service_order_line.qty_delivered, 0.0, 'The service type is not timesheet but manual so the quantity delivered is not increased by the timesheets linked.') @@ -88,6 +94,7 @@ class TestSaleTimesheetProjectProfitability(TestCommonSaleTimesheet): 'data': [ { 'id': 'billable_manual', + 'sequence': sequence_per_invoice_type['billable_manual'], 'billed': (timesheet1.unit_amount + timesheet2.unit_amount) * -self.employee_user.timesheet_cost, 'to_bill': 0.0, }, @@ -121,11 +128,13 @@ class TestSaleTimesheetProjectProfitability(TestCommonSaleTimesheet): 'data': [ { 'id': 'billable_manual', + 'sequence': sequence_per_invoice_type['billable_manual'], 'billed': (timesheet1.unit_amount + timesheet2.unit_amount) * -self.employee_user.timesheet_cost, 'to_bill': 0.0, }, { 'id': 'non_billable', + 'sequence': sequence_per_invoice_type['non_billable'], 'billed': timesheet3.unit_amount * -self.employee_manager.timesheet_cost, 'to_bill': 0.0, }, @@ -154,7 +163,7 @@ class TestSaleTimesheetProjectProfitability(TestCommonSaleTimesheet): { 'revenues': { 'data': [ - {'id': 'billable_time', 'to_invoice': delivery_timesheet_order_line.untaxed_amount_to_invoice, 'invoiced': 0.0}, + {'id': 'billable_time', 'sequence': sequence_per_invoice_type['billable_time'], 'to_invoice': delivery_timesheet_order_line.untaxed_amount_to_invoice, 'invoiced': 0.0}, ], 'total': {'to_invoice': delivery_timesheet_order_line.untaxed_amount_to_invoice, 'invoiced': 0.0}, }, @@ -162,11 +171,13 @@ class TestSaleTimesheetProjectProfitability(TestCommonSaleTimesheet): 'data': [ { 'id': 'billable_time', + 'sequence': sequence_per_invoice_type['billable_time'], 'billed': (timesheet1.unit_amount + timesheet2.unit_amount) * -self.employee_user.timesheet_cost, 'to_bill': 0.0, }, { 'id': 'non_billable', + 'sequence': sequence_per_invoice_type['non_billable'], 'billed': timesheet3.unit_amount * -self.employee_manager.timesheet_cost, 'to_bill': 0.0, }, @@ -200,14 +211,14 @@ class TestSaleTimesheetProjectProfitability(TestCommonSaleTimesheet): self.assertFalse([data for data in profitability_items['revenues']['data'] if data['id'] == 'billable_milestones']) self.assertDictEqual( [data for data in profitability_items['costs']['data'] if data['id'] == 'billable_milestones'][0], - {'id': 'billable_milestones', 'to_bill': 0.0, 'billed': task2_timesheet.amount}, + {'id': 'billable_milestones', 'sequence': sequence_per_invoice_type['billable_milestones'], 'to_bill': 0.0, 'billed': task2_timesheet.amount}, ) milestone_order_line.qty_delivered = 1 profitability_items = self.project_task_rate._get_profitability_items(False) self.assertDictEqual( [data for data in profitability_items['revenues']['data'] if data['id'] == 'billable_milestones'][0], - {'id': 'billable_milestones', 'to_invoice': milestone_order_line.untaxed_amount_to_invoice, 'invoiced': 0.0}, + {'id': 'billable_milestones', 'sequence': sequence_per_invoice_type['billable_milestones'], 'to_invoice': milestone_order_line.untaxed_amount_to_invoice, 'invoiced': 0.0}, ) task2_timesheet.unlink() profitability_items = self.project_task_rate._get_profitability_items(False)