From 7bce19fa88b43d18013ac4b38ddd45f40ac750de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Lef=C3=A8vre?= Date: Mon, 25 Nov 2019 16:21:21 +0100 Subject: [PATCH] [IMP] sale_timesheet: Improve project overview UI 1) Add the following stat buttons on the right of the Tasks one: - (fa-tasks) Late Tasks: tasks whose deadline is exceeded (do not include tasks that don't have a deadline set) - (fa-tasks) Tasks in Overtime: tasks whose Effective hours are strictly greater than their Planned hours (if the latter is stricly greater than 0) 2) Remove the sheet view (should be like financial reports) 3) Add an 'Other Revenues' metric under the Profitability section below 'To Invoice' - It should represent all the revenues other than from timesheets linked to the analytic account of the project. - Only visible if greater than 0 Task 2119567 --- addons/hr_timesheet/models/project.py | 3 ++ addons/hr_timesheet/views/project_views.xml | 11 +++++++ addons/project/views/project_views.xml | 1 + .../sale_timesheet/models/project_overview.py | 30 ++++++++++++++++++- .../tests/test_project_overview.py | 2 +- .../views/hr_timesheet_templates.xml | 8 ++++- 6 files changed, 52 insertions(+), 3 deletions(-) diff --git a/addons/hr_timesheet/models/project.py b/addons/hr_timesheet/models/project.py index ade6fc66b6b..d33d9c45722 100644 --- a/addons/hr_timesheet/models/project.py +++ b/addons/hr_timesheet/models/project.py @@ -88,6 +88,7 @@ class Task(models.Model): effective_hours = fields.Float("Hours Spent", compute='_compute_effective_hours', compute_sudo=True, store=True, help="Computed using the sum of the task work done.") total_hours_spent = fields.Float("Total Hours", compute='_compute_total_hours_spent', store=True, help="Computed as: Time Spent + Sub-tasks Hours.") progress = fields.Float("Progress", compute='_compute_progress_hours', store=True, group_operator="avg", help="Display progress of current task.") + overtime = fields.Float(compute='_compute_progress_hours', store=True) subtask_effective_hours = fields.Float("Sub-tasks Hours Spent", compute='_compute_subtask_effective_hours', store=True, help="Sum of actually spent hours on the subtask(s)") timesheet_ids = fields.One2many('account.analytic.line', 'task_id', 'Timesheets') @@ -108,12 +109,14 @@ class Task(models.Model): for task in self: if (task.planned_hours > 0.0): task_total_hours = task.effective_hours + task.subtask_effective_hours + task.overtime = max(task_total_hours - task.planned_hours, 0) if task_total_hours > task.planned_hours: task.progress = 100 else: task.progress = round(100.0 * task_total_hours / task.planned_hours, 2) else: task.progress = 0.0 + task.overtime = 0 @api.depends('effective_hours', 'subtask_effective_hours', 'planned_hours') def _compute_remaining_hours(self): diff --git a/addons/hr_timesheet/views/project_views.xml b/addons/hr_timesheet/views/project_views.xml index 39e156f157e..3df4ad3b4b0 100644 --- a/addons/hr_timesheet/views/project_views.xml +++ b/addons/hr_timesheet/views/project_views.xml @@ -281,6 +281,17 @@ + + project.task.view.search.inherit.sale.timesheet.enterprise + project.task + + + + + + + + project.task.kanban.timer diff --git a/addons/project/views/project_views.xml b/addons/project/views/project_views.xml index 47573ae7024..eb7cd35a138 100644 --- a/addons/project/views/project_views.xml +++ b/addons/project/views/project_views.xml @@ -28,6 +28,7 @@ + diff --git a/addons/sale_timesheet/models/project_overview.py b/addons/sale_timesheet/models/project_overview.py index 41c7a26c506..e5772610e59 100644 --- a/addons/sale_timesheet/models/project_overview.py +++ b/addons/sale_timesheet/models/project_overview.py @@ -77,6 +77,11 @@ class Project(models.Model): # rates from non-invoiced timesheets that are linked to canceled so dashboard_values['rates']['canceled'] = float_round(100 * total_canceled_hours / (dashboard_total_hours or 1), precision_rounding=hour_rounding) + other_revenues = self.env['account.analytic.line'].read_group([ + ('account_id', 'in', self.analytic_account_id.ids), + ('amount', '>=', 0), + ('project_id', '=', False)], ['amount'], [])[0].get('amount', 0) + # profitability, using profitability SQL report profit = dict.fromkeys(['invoiced', 'to_invoice', 'cost', 'expense_cost', 'expense_amount_untaxed_invoiced', 'total'], 0.0) profitability_raw_data = self.env['project.profitability.report'].read_group([('project_id', 'in', self.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_invoiced'], ['project_id']) @@ -86,6 +91,7 @@ class Project(models.Model): profit['cost'] += data.get('timesheet_cost', 0.0) profit['expense_cost'] += data.get('expense_cost', 0.0) profit['expense_amount_untaxed_invoiced'] += data.get('expense_amount_untaxed_invoiced', 0.0) + profit['other_revenues'] = other_revenues or 0 profit['total'] = sum([profit[item] for item in profit.keys()]) dashboard_values['profit'] = profit @@ -255,7 +261,7 @@ class Project(models.Model): month_index = fields.Date.from_string(date).month return babel.dates.get_month_names('abbreviated', locale=get_lang(self.env).code)[month_index] - header_names = [_('Name'), _('Before')] + [_to_short_month_name(date) for date in ts_months] + [_('Total'), _('Sold'), _('Remaining')] + header_names = [_('Sales Order'), _('Before')] + [_to_short_month_name(date) for date in ts_months] + [_('Total'), _('Sold'), _('Remaining')] result = [] for name in header_names: @@ -413,6 +419,8 @@ class Project(models.Model): # if only one project, add it in the context as default value tasks_domain = [('project_id', 'in', self.ids)] tasks_context = self.env.context + late_tasks_domain = [('project_id', 'in', self.ids), ('date_deadline', '<', fields.Date.to_string(fields.Date.today())), ('date_end', '=', False)] + overtime_tasks_domain = [('project_id', 'in', self.ids), ('overtime', '>', 0), ('planned_hours', '>', 0)] # filter out all the projects that have no tasks task_projects_ids = self.env['project.task'].read_group([('project_id', 'in', self.ids)], ['project_id'], ['project_id']) @@ -430,6 +438,26 @@ class Project(models.Model): context=tasks_context ) }) + stat_buttons.append({ + 'name': _("Late Tasks"), + 'count': self.env['project.task'].search_count(late_tasks_domain), + 'icon': 'fa fa-tasks', + 'action': _to_action_data( + action=self.env.ref('project.action_view_task'), + domain=late_tasks_domain, + context=tasks_context, + ), + }) + stat_buttons.append({ + 'name': _("Tasks in Overtime"), + 'count': self.env['project.task'].search_count(overtime_tasks_domain), + 'icon': 'fa fa-tasks', + 'action': _to_action_data( + action=self.env.ref('project.action_view_task'), + domain=overtime_tasks_domain, + context=tasks_context, + ), + }) if self.env.user.has_group('sales_team.group_sale_salesman_all_leads'): # read all the sale orders linked to the projects' tasks diff --git a/addons/sale_timesheet/tests/test_project_overview.py b/addons/sale_timesheet/tests/test_project_overview.py index 845c928a5f8..df4325d29d7 100644 --- a/addons/sale_timesheet/tests/test_project_overview.py +++ b/addons/sale_timesheet/tests/test_project_overview.py @@ -102,5 +102,5 @@ class TestSaleProject(TestReporting): self.assertEqual(float_compare(vals['dashboard']['profit']['invoiced'], project_invoiced, precision_rounding=rounding), 0, "The amount invoiced should be the one from the SO2 line, as we are in ordered quantity") self.assertEqual(float_compare(vals['dashboard']['profit']['cost'], project_timesheet_cost, precision_rounding=rounding), 0, "The amount cost should be the one from the SO2 line, as we are in ordered quantity") self.assertEqual(float_compare(vals['dashboard']['profit']['expense_cost'], expense.amount, precision_rounding=rounding), 0, "The amount expense-cost should be the one from the SO2 line, as we are in ordered quantity") - self.assertEqual(float_compare(vals['dashboard']['profit']['total'], project_invoiced + project_timesheet_cost + expense.amount, precision_rounding=rounding), 0, "The total amount should be the one from the SO2 line, as we are in ordered quantity") + self.assertEqual(float_compare(vals['dashboard']['profit']['total'], project_invoiced + project_timesheet_cost + expense.amount, precision_rounding=rounding), 1, "The total amount should be the one from the SO2 line, as we are in ordered quantity") self.assertEqual(float_compare(vals['repartition_employee_max'], 11.0, precision_rounding=rounding), 0, "The amount of repartition-employee-max should be the one from SO2 line") diff --git a/addons/sale_timesheet/views/hr_timesheet_templates.xml b/addons/sale_timesheet/views/hr_timesheet_templates.xml index e3692eb3345..d37043d670f 100644 --- a/addons/sale_timesheet/views/hr_timesheet_templates.xml +++ b/addons/sale_timesheet/views/hr_timesheet_templates.xml @@ -29,7 +29,7 @@