[IMP] sale_timesheet: overview usability and styling

In order to make the overview more understandable, we provide some modifications:

- remove some value when they are zero ("No task found", ...)
- the whole middle column (percentages) is merged inside the hours one
- provide some tooltips
- renaming titles
- ...

To do so and to avoid leaving empty lines or doing complicated template branching, the
above table is split in two different tables, so that we can remove lines independently.

We also added a new category in the overview, concerning timesheets that are related to a cancelled so. This meant tweaking the project_overview plan_prepare_value method, in order to have the new numbers calculated and keep the others consitent.

Task-1968121

closes odoo/odoo#33015

Signed-off-by: Jérome Maes (jem) <jem@openerp.com>
This commit is contained in:
MaxLarue
2019-05-29 07:29:21 +00:00
parent 1f3a00fb3e
commit e0b33e280e
5 changed files with 248 additions and 148 deletions
+3 -3
View File
@@ -15,9 +15,9 @@ class AccountAnalyticLine(models.Model):
return expression.OR([domain, [('qty_delivered_method', '=', 'timesheet')]])
timesheet_invoice_type = fields.Selection([
('billable_time', 'Billable Time'),
('billable_fixed', 'Billable Fixed'),
('non_billable', 'Non Billable'),
('billable_time', 'Billed on Timesheets'),
('billable_fixed', 'Billed at a Fixed price'),
('non_billable', 'Non Billable Tasks'),
('non_billable_project', 'No task found')], string="Billable Type", compute='_compute_timesheet_invoice_type', compute_sudo=True, store=True, readonly=True)
timesheet_invoice_id = fields.Many2one('account.invoice', string="Invoice", readonly=True, copy=False, help="Invoice created from the timesheet")
@@ -53,10 +53,16 @@ class Project(models.Model):
}
}
# hours from non-invoiced timesheets that are linked to canceled so
canceled_hours_domain = [('project_id', 'in', self.ids), ('timesheet_invoice_type', '!=', False), ('so_line.state', '=', 'cancel')]
total_canceled_hours = sum(self.env['account.analytic.line'].search(canceled_hours_domain).mapped('unit_amount'))
dashboard_values['hours']['canceled'] = float_round(total_canceled_hours, precision_rounding=hour_rounding)
dashboard_values['hours']['total'] += float_round(total_canceled_hours, precision_rounding=hour_rounding)
# hours (from timesheet) and rates (by billable type)
dashboard_domain = [('project_id', 'in', self.ids), ('timesheet_invoice_type', '!=', False)] # force billable type
dashboard_domain = [('project_id', 'in', self.ids), ('timesheet_invoice_type', '!=', False), '|', ('so_line', '=', False), ('so_line.state', '!=', 'cancel')] # force billable type
dashboard_data = self.env['account.analytic.line'].read_group(dashboard_domain, ['unit_amount', 'timesheet_invoice_type'], ['timesheet_invoice_type'])
dashboard_total_hours = sum([data['unit_amount'] for data in dashboard_data])
dashboard_total_hours = sum([data['unit_amount'] for data in dashboard_data]) + total_canceled_hours
for data in dashboard_data:
billable_type = data['timesheet_invoice_type']
dashboard_values['hours'][billable_type] = float_round(data.get('unit_amount'), precision_rounding=hour_rounding)
@@ -66,6 +72,9 @@ class Project(models.Model):
dashboard_values['rates'][billable_type] = rate
dashboard_values['rates']['total'] += rate
# 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)
# 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'])
@@ -90,7 +99,11 @@ class Project(models.Model):
employee_ids = list(itertools.chain.from_iterable([employee_id['employee_ids'] for employee_id in employee_ids]))
employees = self.env['hr.employee'].sudo().browse(employee_ids) | self.env['account.analytic.line'].search([('project_id', 'in', self.ids)]).mapped('employee_id')
repartition_domain = [('project_id', 'in', self.ids), ('employee_id', '!=', False), ('timesheet_invoice_type', '!=', False)] # force billable type
repartition_data = self.env['account.analytic.line'].read_group(repartition_domain, ['employee_id', 'timesheet_invoice_type', 'unit_amount'], ['employee_id', 'timesheet_invoice_type'], lazy=False)
# repartition data, without timesheet on cancelled so
repartition_data = self.env['account.analytic.line'].read_group(repartition_domain + ['|', ('so_line', '=', False), ('so_line.state', '!=', 'cancel')], ['employee_id', 'timesheet_invoice_type', 'unit_amount'], ['employee_id', 'timesheet_invoice_type'], lazy=False)
# read timesheet on cancelled so
cancelled_so_timesheet = self.env['account.analytic.line'].read_group(repartition_domain + [('so_line.state', '=', 'cancel')], ['employee_id', 'unit_amount'], ['employee_id'], lazy=False)
repartition_data += [{**canceled, 'timesheet_invoice_type': 'canceled'} for canceled in cancelled_so_timesheet]
# set repartition per type per employee
repartition_employee = {}
@@ -102,6 +115,7 @@ class Project(models.Model):
non_billable=0.0,
billable_time=0.0,
billable_fixed=0.0,
canceled=0.0,
total=0.0,
)
for data in repartition_data:
@@ -113,14 +127,13 @@ class Project(models.Model):
non_billable=0.0,
billable_time=0.0,
billable_fixed=0.0,
canceled=0.0,
total=0.0,
))[data['timesheet_invoice_type']] = float_round(data.get('unit_amount', 0.0), precision_rounding=hour_rounding)
repartition_employee[employee_id]['__domain_' + data['timesheet_invoice_type']] = data['__domain']
# compute total
for employee_id, vals in repartition_employee.items():
repartition_employee[employee_id]['total'] = sum([vals[inv_type] for inv_type in billable_types])
repartition_employee[employee_id]['total'] = sum([vals[inv_type] for inv_type in [*billable_types, 'canceled']])
hours_per_employee = [repartition_employee[employee_id]['total'] for employee_id in repartition_employee]
values['repartition_employee_max'] = (max(hours_per_employee) if hours_per_employee else 1) or 1
values['repartition_employee'] = repartition_employee
@@ -158,8 +171,10 @@ class Project(models.Model):
if row[0]['sale_order_id']:
sale_order_ids.add(row[0]['sale_order_id'])
sale_orders = self.env['sale.order'].sudo().browse(sale_order_ids | empty_order_ids)
sale_order_lines = self.env['sale.order.line'].sudo().browse(sale_line_ids | empty_line_ids)
map_so_names = {so.id: so.name for so in self.env['sale.order'].sudo().browse(sale_order_ids | empty_order_ids)}
map_so_names = {so.id: so.name for so in sale_orders}
map_so_cancel = {so.id: so.state == 'cancel' for so in sale_orders}
map_sol = {sol.id: sol for sol in sale_order_lines}
map_sol_names = {sol.id: sol.name.split('\n')[0] if sol.name else _('No Sales Order Line') for sol in sale_order_lines}
map_sol_so = {sol.id: sol.order_id.id for sol in sale_order_lines}
@@ -199,7 +214,7 @@ class Project(models.Model):
sale_order_id = row_key[0]
# sale order row
if sale_order_id not in rows_sale_order:
rows_sale_order[sale_order_id] = [{'label': map_so_names.get(sale_order_id, _('No Sales Order')), 'res_id': sale_order_id, 'res_model': 'sale.order', 'type': 'sale_order'}] + default_row_vals[:] # INFO, before, M1, M2, M3, Done, M3, M4, M5, After, Forecasted
rows_sale_order[sale_order_id] = [{'label': map_so_names.get(sale_order_id, _('No Sales Order')), 'canceled': map_so_cancel.get(sale_order_id, False), 'res_id': sale_order_id, 'res_model': 'sale.order', 'type': 'sale_order'}] + default_row_vals[:] # INFO, before, M1, M2, M3, Done, M3, M4, M5, After, Forecasted
for index in range(len(rows_sale_line[row_key])):
if index != 0:
@@ -239,7 +254,7 @@ class Project(models.Model):
month_index = fields.Date.from_string(date).month
return babel.dates.get_month_names('abbreviated', locale=self.env.context.get('lang', 'en_US'))[month_index]
header_names = [_('Name'), _('Before')] + [_to_short_month_name(date) for date in ts_months] + [_('Done'), _('Sold'), _('Remaining')]
header_names = [_('Name'), _('Before')] + [_to_short_month_name(date) for date in ts_months] + [_('Total'), _('Sold'), _('Remaining')]
result = []
for name in header_names:
@@ -1,6 +1,13 @@
$canceled-color: gray;
.o_timesheet_plan_sale_timesheet {
.o_title {
margin-top: 30px;
margin-bottom: 30px;
}
[type='action'] {
color: $o-brand-primary;
cursor: pointer;
@@ -12,6 +19,19 @@
width: auto;
}
.o_profitability_wrapper {
display: flex;
flex: 1;
flex-wrap: wrap;
.o_profitability_section {
display: flex;
flex: 1;
justify-content: flex-start;
}
}
.o_timesheet_plan_sale_timesheet_dashboard {
.table {
@@ -32,6 +52,10 @@
text-align: right;
}
.section {
}
}
.o_timesheet_plan_sale_timesheet_people_time {
@@ -52,7 +76,12 @@
cursor: pointer;
}
.o_progress_non_billable {
background-color: gray;
background-color: purple;
color: white;
cursor: pointer;
}
.o_progress_canceled {
background-color: $canceled-color;
color: white;
cursor: pointer;
}
@@ -78,6 +107,9 @@
}
.o_project_plan_project_timesheet_forecast {
margin-top: 50px;
th{
text-align: center;
vertical-align: top;
@@ -111,6 +143,12 @@
.o_timesheet_plan_redirect {
cursor: pointer;
}
.o_canceled_tag {
background-color: $canceled-color;
color: white;
border: 0px;
}
}
}
@@ -46,146 +46,175 @@
</t>
</div>
<div class="o_title">
<h2>Hours recorded and Profitability</h2>
</div>
<t t-set="display_cost" t-value="dashboard['profit']['expense_cost'] != 0.0"/>
<div class="table-responsive">
<table class="table">
<tbody>
<tr>
<th rowspan="6">
<a type="action" data-model="account.analytic.line" t-att-data-domain="json.dumps(timesheet_domain)" data-context='{"pivot_row_groupby": ["date:month"],"pivot_column_groupby": ["timesheet_invoice_type"], "pivot_measures": ["unit_amount"]}' data-views='[[0, "pivot"], [0, "list"]]' tabindex="-1">Hours</a>
</th>
<td class="o_timesheet_plan_dashboard_cell">
<t t-esc="dashboard['hours']['billable_time']" t-options="{'widget': 'float_time'}"/>
</td>
<td>Timesheets</td>
<th rowspan="6">
<a type="action" data-model="account.analytic.line" t-att-data-domain="json.dumps(timesheet_domain)" data-context='{"pivot_row_groupby": ["date:month", "employee_id"], "pivot_column_groupby": ["timesheet_invoice_type"],"pivot_measures": ["unit_amount"]}' data-views='[[0, "pivot"], [0, "list"]]' tabindex="-1">Rates</a>
</th>
<td class="o_timesheet_plan_dashboard_cell">
<t t-esc="dashboard['rates']['billable_time']"/> %
</td>
<td>Timesheets</td>
<th rowspan="6">
<a type="action" data-model="project.profitability.report" t-att-data-domain="json.dumps(profitability_domain)" data-context="{'group_by_no_leaf':1, 'group_by':[], 'sale_show_order_product_name': 1}" data-views='[[0, "pivot"], [0, "graph"]]' tabindex="-1">Profitability</a>
</th>
<td class="o_timesheet_plan_dashboard_cell">
<t t-esc="dashboard['profit']['invoiced']" t-options='{"widget": "monetary", "display_currency": currency}'/>
</td>
<td>Time invoiced</td>
</tr>
<tr>
<td class="o_timesheet_plan_dashboard_cell">
<t t-esc="dashboard['hours']['billable_fixed']" t-options="{'widget': 'float_time'}"/>
</td>
<td>Fixed</td>
<td class="o_timesheet_plan_dashboard_cell">
<t t-esc="dashboard['rates']['billable_fixed']"/> %
</td>
<td>Fixed</td>
<td class="o_timesheet_plan_dashboard_cell">
<t t-esc="dashboard['profit']['to_invoice']" t-options='{"widget": "monetary", "display_currency": currency}'/>
</td>
<td>Time to invoice</td>
</tr>
<tr>
<td class="o_timesheet_plan_dashboard_cell">
<t t-esc="dashboard['hours']['non_billable_project']" t-options="{'widget': 'float_time'}"/>
</td>
<td>No task found</td>
<td class="o_timesheet_plan_dashboard_cell">
<t t-esc="dashboard['rates']['non_billable_project']"/> %
</td>
<td>No task found</td>
<td class="o_timesheet_plan_dashboard_cell">
<t t-esc="dashboard['profit']['cost']" t-options='{"widget": "monetary", "display_currency": currency}'/>
</td>
<td>Timesheet costs</td>
</tr>
<tr>
<td class="o_timesheet_plan_dashboard_cell">
<t t-esc="dashboard['hours']['non_billable']" t-options="{'widget': 'float_time'}"/>
</td>
<td><a type="action"
title="Non Billable Tasks"
data-model="project.task"
data-views='[[false, "list"], [false, "form"]]'
t-att-data-domain="json.dumps([['project_id', 'in', projects.ids], ['sale_line_id', '=', False]])"
>Non billable</a></td>
<td class="o_timesheet_plan_dashboard_cell">
<t t-esc="dashboard['rates']['non_billable']"/> %
</td>
<td>Non billable</td>
<t t-if="display_cost">
<td class="o_timesheet_plan_dashboard_cell">
<t t-esc="dashboard['profit']['expense_cost']" t-options='{"widget": "monetary", "display_currency": currency}'/>
</td>
<td>Other costs</td>
</t>
<t t-else="">
<td class="o_timesheet_plan_dashboard_total">
<t t-esc="dashboard['profit']['total']" t-options='{"widget": "monetary", "display_currency": currency}'/>
</td>
<td><b>Total</b></td>
</t>
</tr>
<tr>
<td class="o_timesheet_plan_dashboard_total">
<t t-esc="dashboard['hours']['total']" t-options="{'widget': 'float_time'}"/>
</td>
<td><b>Total</b></td>
<td></td>
<td></td>
<t t-if="display_cost">
<td class="o_timesheet_plan_dashboard_cell">
<t t-esc="dashboard['profit']['expense_amount_untaxed_invoiced']" t-options='{"widget": "monetary", "display_currency": currency}'/>
</td>
<td>Re-invoiced costs</td>
</t>
<t t-else="">
<td></td>
<td></td>
</t>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<t t-if="display_cost">
<td class="o_timesheet_plan_dashboard_total">
<t t-esc="dashboard['profit']['total']" t-options='{"widget": "monetary", "display_currency": currency}'/>
</td>
<td><b>Total</b></td>
</t>
<t t-else="">
<td></td>
<td></td>
</t>
</tr>
</tbody>
</table>
<div class="o_profitability_wrapper">
<div class="o_profitability_section">
<div>
<table class="table">
<tbody>
<th>
<a type="action" data-model="account.analytic.line" t-att-data-domain="json.dumps(timesheet_domain)" data-context='{"pivot_row_groupby": ["date:month"],"pivot_column_groupby": ["timesheet_invoice_type"], "pivot_measures": ["unit_amount"]}' data-views='[[0, "pivot"], [0, "list"]]' tabindex="-1">Hours</a>
</th>
<tr>
<td class="o_timesheet_plan_dashboard_cell">
<t t-esc="dashboard['hours']['billable_time']" t-options="{'widget': 'float_time'}"/>
</td>
<td class="o_timesheet_plan_dashboard_cell">
(<t t-esc="dashboard['rates']['billable_time']"/> %)
</td>
<td title=" Includes the time logged into tasks for which you invoice based on ordered quantities or on milestones.">
Billed on Timesheets
</td>
</tr>
<tr>
<td class="o_timesheet_plan_dashboard_cell">
<t t-esc="dashboard['hours']['billable_fixed']" t-options="{'widget': 'float_time'}"/>
</td>
<td class="o_timesheet_plan_dashboard_cell">
(<t t-esc="dashboard['rates']['billable_fixed']"/> %)
</td>
<td title="Includes the time logged into tasks for which you invoice based on timesheets on tasks.">
Billed at a Fixed price
</td>
</tr>
<tr t-if="dashboard['hours']['non_billable_project'] != 0">
<td class="o_timesheet_plan_dashboard_cell">
<t t-esc="dashboard['hours']['non_billable_project']" t-options="{'widget': 'float_time'}"/>
</td>
<td class="o_timesheet_plan_dashboard_cell">
(<t t-esc="dashboard['rates']['non_billable_project']"/> %)
</td>
<td title="Includes the time logged from the Timesheet module that is linked to a project, but not to a task.">
No task found
</td>
</tr>
<tr>
<td class="o_timesheet_plan_dashboard_cell">
<t t-esc="dashboard['hours']['non_billable']" t-options="{'widget': 'float_time'}"/>
</td>
<td class="o_timesheet_plan_dashboard_cell">
(<t t-esc="dashboard['rates']['non_billable']"/> %)
</td>
<td>
<a type="action"
data-model="project.task"
data-views='[[false, "list"], [false, "form"]]'
t-att-data-domain="json.dumps([['project_id', 'in', projects.ids], ['sale_line_id', '=', False]])"
>
<span class="btn-link"
style="font-weight:normal;"
title="Includes the time logged into a task which is not linked to any Sales Order.">
Non Billable Tasks
</span>
</a>
</td>
</tr>
<tr t-if="dashboard['hours']['canceled'] > 0">
<td class="o_timesheet_plan_dashboard_cell">
<t t-esc="dashboard['hours']['canceled']" t-options="{'widget': 'float_time'}"/>
</td>
<td class="o_timesheet_plan_dashboard_cell">
(<t t-esc="dashboard['rates']['canceled']"/> %)
</td>
<td title="Includes the time logged into a task which is linked to a cancelled Sales Order.">
Cancelled
</td>
</tr>
<tr>
<td class="o_timesheet_plan_dashboard_total"><b><t t-esc="dashboard['hours']['total']" t-options="{'widget': 'float_time'}"/></b></td>
<td><b>Total</b></td>
<td></td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="o_profitability_section">
<div>
<table class="table">
<tbody>
<th>
<a type="action" data-model="project.profitability.report" t-att-data-domain="json.dumps(profitability_domain)" data-context="{'group_by_no_leaf':1, 'group_by':[], 'sale_show_order_product_name': 1}" data-views='[[0, "pivot"], [0, "graph"]]' tabindex="-1">Profitability</a>
</th>
<tr>
<td class="o_timesheet_plan_dashboard_cell">
<t t-esc="dashboard['profit']['invoiced']" t-options='{"widget": "monetary", "display_currency": currency}'/>
</td>
<td>Invoiced</td>
</tr>
<tr>
<td class="o_timesheet_plan_dashboard_cell">
<t t-esc="dashboard['profit']['to_invoice']" t-options='{"widget": "monetary", "display_currency": currency}'/>
</td>
<td>To invoice</td>
</tr>
<tr>
<td class="o_timesheet_plan_dashboard_cell">
<t t-esc="dashboard['profit']['cost']" t-options='{"widget": "monetary", "display_currency": currency}'/>
</td>
<td title="This cost is based on the &quot;Timesheet cost&quot; set in the HR Settings of your employees.">
Timesheet costs
</td>
</tr>
<tr t-if="display_cost">
<td class="o_timesheet_plan_dashboard_cell">
<t t-esc="dashboard['profit']['expense_cost']" t-options='{"widget": "monetary", "display_currency": currency}'/>
</td>
<td>Other costs</td>
</tr>
<tr t-if="display_cost &amp; (dashboard['profit']['expense_amount_untaxed_invoiced'] != 0)">
<td class="o_timesheet_plan_dashboard_cell">
<t t-esc="dashboard['profit']['expense_amount_untaxed_invoiced']" t-options='{"widget": "monetary", "display_currency": currency}'/>
</td>
<td>Re-invoiced costs</td>
</tr>
<tr>
<td class="o_timesheet_plan_dashboard_total">
<b>
<t t-esc="dashboard['profit']['total']" t-options='{"widget": "monetary", "display_currency": currency}'/>
</b>
</td>
<td><b>Total</b></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="o_title">
<h2>Time by people</h2>
</div>
<div class="o_timesheet_plan_sale_timesheet_people_time">
<h3>Time by people</h3>
<t t-if="not repartition_employee">
<p>There is no timesheet for now.</p>
</t>
<t t-if="repartition_employee">
<div class="float-right o_timesheet_plan_badge">
<span class="badge badge-pill o_progress_billable_fixed">
<a type="action" data-model="account.analytic.line" t-att-data-domain="json.dumps(timesheet_domain + [('timesheet_invoice_type','=','billable_fixed')])" tabindex="-1">Billable fixed</a>
</span>
<span class="badge badge-pill o_progress_billable_time">
<a type="action" data-model="account.analytic.line" t-att-data-domain="json.dumps(timesheet_domain + [('timesheet_invoice_type','=','billable_time')])" tabindex="-1">Billable time</a>
<a type="action" data-model="account.analytic.line" t-att-data-domain="json.dumps(timesheet_domain + [('timesheet_invoice_type','=','billable_time')])" tabindex="-1">Billed on Timesheets</a>
</span>
<span class="badge badge-pill o_progress_non_billable_project">
<span class="badge badge-pill o_progress_billable_fixed">
<a type="action" data-model="account.analytic.line" t-att-data-domain="json.dumps(timesheet_domain + [('timesheet_invoice_type','=','billable_fixed')])" tabindex="-1">Billed at a Fixed price</a>
</span>
<span t-if="dashboard['hours']['non_billable_project'] != 0" class="badge badge-pill o_progress_non_billable_project">
<a type="action" data-model="account.analytic.line" t-att-data-domain="json.dumps(timesheet_domain + [('timesheet_invoice_type','=','non_billable_project')])" tabindex="-1">No task found</a>
</span>
<span class="badge badge-pill o_progress_non_billable">
<a type="action" data-model="account.analytic.line" t-att-data-domain="json.dumps(timesheet_domain + [('timesheet_invoice_type','=','non_billable')])" tabindex="-1">Non billable</a>
<a type="action" data-model="account.analytic.line" t-att-data-domain="json.dumps(timesheet_domain + [('timesheet_invoice_type','=','non_billable')])" tabindex="-1">Non billable tasks</a>
</span>
<!-- only show the canceled pill if there were timesheets on canceled so -->
<t t-if="sum([employee.get('canceled', 0.0) for employee in repartition_employee.values()]) > 0">
<span class="badge badge-pill o_progress_canceled">
<a type="action" data-model="account.analytic.line" t-att-data-domain="json.dumps(timesheet_domain + [('so_line.state', '=', 'cancel')])" tabindex="-1">Cancelled</a>
</span>
</t>
</div>
<div class="table-responsive">
<table class="table">
@@ -206,21 +235,25 @@
<t t-set="total" t-value="employee['total'] or 1.0" />
<t t-call="sale_timesheet.progressbar">
<t t-set="label">Billable fixed</t>
<t t-set="key">billable_fixed</t>
<t t-set="label">Billed on Timesheets</t>
<t t-set="key">billable_time</t>
</t>
<t t-call="sale_timesheet.progressbar">
<t t-set="label">Billable time</t>
<t t-set="key">billable_time</t>
<t t-set="label">Billed at a Fixed price</t>
<t t-set="key">billable_fixed</t>
</t>
<t t-call="sale_timesheet.progressbar">
<t t-set="label">No task found</t>
<t t-set="key">non_billable_project</t>
</t>
<t t-call="sale_timesheet.progressbar">
<t t-set="label">Non billable</t>
<t t-set="label">Non billable tasks</t>
<t t-set="key">non_billable</t>
</t>
<t t-call="sale_timesheet.progressbar">
<t t-set="label">Cancelled</t>
<t t-set="key">canceled</t>
</t>
</div>
</td>
</tr>
@@ -231,13 +264,21 @@
</t>
</div>
<div class="o_title">
<h2>Timesheets</h2>
</div>
<!-- NOTE: this template to display a table works whatever the length of the rows, as project_timesheet_forecast_sale extends the table to add forecasts -->
<div class="o_project_plan_project_timesheet_forecast">
<t t-if="timesheet_forecast_table and timesheet_forecast_table['rows']">
<h3 name="table_plan_title">Timesheets</h3>
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th></th>
<th colspan="5" id="table_plan_title" class="o_right_bordered"><h3>Timesheets</h3></th>
<th colspan="2" id="table_plan_total"></th>
</tr>
<tr>
<t t-foreach="timesheet_forecast_table['header']" t-as="header_val">
<th t-att-class="'o_right_bordered' if header_val_index in [5,10] else ''">
@@ -261,6 +302,9 @@
<a type="action" t-att-data-model="row_value['res_model']" t-att-data-res-id="row_value['res_id']" t-att-class="'o_timesheet_plan_redirect' if row_value['res_id'] else ''">
<t t-esc="row_value.get('label')"/>
</a>
<span t-if="row_value.get('canceled')" class="badge badge-pill o_canceled_tag">
Cancelled
</span>
</t>
<t t-if="row_type != 'sale_order'">
<t t-if="not row_is_milestone">
@@ -309,10 +353,13 @@
<t t-set="title"><t t-esc="label"/>: <t t-esc="amount" t-options="{'widget': 'float_time'}"/></t>
<a t-attf-class="progress-bar o_progress_{{key}}"
t-attf-style="width: {{amount / total * 100}}%"
t-att-title="title"
type="action" data-model="account.analytic.line"
t-att-data-domain="json.dumps(employee['__domain_' + key])"
/>
>
<span t-att-title="title" style="font-size: 0px; width: 100%; height: 100%;">
<t t-esc="label" />
</span>
</a>
</t>
</template>
@@ -9,9 +9,9 @@
<xpath expr="//filter[@name='month']" position="before">
<field name="timesheet_invoice_type"/>
<field name="timesheet_invoice_id"/>
<filter name="billable_time" string="Billable Hours" domain="[('timesheet_invoice_type', '=', 'billable_time')]"/>
<filter name="billable_fixed" string="Fixed Price Projects" domain="[('timesheet_invoice_type', '=', 'billable_fixed')]"/>
<filter name="non_billable" string="Non Billable Hours" domain="[('timesheet_invoice_type', '=', 'non_billable')]"/>
<filter name="billable_time" string="Billed on Timesheets" domain="[('timesheet_invoice_type', '=', 'billable_time')]"/>
<filter name="billable_fixed" string="Billed at a Fixed Price" domain="[('timesheet_invoice_type', '=', 'billable_fixed')]"/>
<filter name="non_billable" string="Non Billable Tasks" domain="[('timesheet_invoice_type', '=', 'non_billable')]"/>
<separator/>
</xpath>
</field>