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 @@