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)