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