[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:
@@ -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 "Timesheet cost" 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 & (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>
|
||||
|
||||
Reference in New Issue
Block a user