[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
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -281,6 +281,17 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="project_task_view_search" model="ir.ui.view">
|
||||
<field name="name">project.task.view.search.inherit.sale.timesheet.enterprise</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="inherit_id" ref="project.view_task_search_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//filter[@name='late']" position='after'>
|
||||
<filter string="Tasks in Overtime" name="overtime" domain="[('overtime', '>', 0)]"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Improve the task kanban in Project -->
|
||||
<record id="project_task_view_kanban" model="ir.ui.view">
|
||||
<field name="name">project.task.kanban.timer</field>
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
<filter string="Unassigned" name="unassigned" domain="[('user_id', '=', False)]"/>
|
||||
<separator/>
|
||||
<filter string="Starred" name="starred" domain="[('priority', 'in', [1, 2])]"/>
|
||||
<filter string="Late Tasks" name="late" domain="[('date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"/>
|
||||
<separator/>
|
||||
<filter string="Unread Messages" name="message_needaction" domain="[('message_needaction', '=', True)]"/>
|
||||
<separator/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<div class="o_timesheet_plan_sale_timesheet">
|
||||
<div class="o_timesheet_plan_sale_timesheet_dashboard">
|
||||
|
||||
<div class="o_timesheet_plan_stat_buttons oe_button_box">
|
||||
<div class="o_timesheet_plan_stat_buttons oe_button_box o_not_full">
|
||||
<t t-foreach="stat_buttons" t-as="stat_button">
|
||||
<a class="btn oe_stat_button"
|
||||
type="action"
|
||||
@@ -154,6 +154,12 @@
|
||||
</td>
|
||||
<td>To invoice</td>
|
||||
</tr>
|
||||
<tr t-if="dashboard['profit']['other_revenues'] > 0">
|
||||
<td class="o_timesheet_plan_dashboard_cell">
|
||||
<t t-esc="dashboard['profit']['other_revenues']" t-options='{"widget": "monetary", "display_currency": currency}'/>
|
||||
</td>
|
||||
<td title="All revenues that are not from timesheets and that are linked to the analytic account of the project.">Other Revenues</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="o_timesheet_plan_dashboard_cell">
|
||||
<t t-esc="dashboard['profit']['cost']" t-options='{"widget": "monetary", "display_currency": currency}'/>
|
||||
|
||||
Reference in New Issue
Block a user