[IMP] sale_timesheet: clean display of Days for timesheets

Display Days instead of Hours in project overview if encoding unit for timesheets is set to Days

[IMP] sale_timesheet: update some labels in Project Overview

[IMP] sale_timesheet: fix style in Project Overview

[IMP] sale_timesheet: clean code

[IMP] sale_timesheet: add some rounding in project overview
This commit is contained in:
Anh Thao Pham (pta)
2020-07-23 13:54:11 +02:00
parent 6752e43232
commit 61cbee3703
6 changed files with 121 additions and 52 deletions
@@ -14,4 +14,5 @@ class PortalAccount(PortalAccount):
domain = request.env['account.analytic.line']._timesheet_get_portal_domain()
domain = expression.AND([domain, [('timesheet_invoice_id', '=', invoice.id)]])
values['timesheets'] = request.env['account.analytic.line'].sudo().search(domain)
values['is_uom_day'] = request.env['account.analytic.line'].sudo()._is_timesheet_encode_uom_day()
return values
@@ -30,6 +30,8 @@ class Project(models.Model):
def _plan_prepare_values(self):
currency = self.env.company.currency_id
uom_hour = self.env.ref('uom.product_uom_hour')
company_uom = self.env.company.timesheet_encode_uom_id
is_uom_day = company_uom == self.env.ref('uom.product_uom_day')
hour_rounding = uom_hour.rounding
billable_types = ['non_billable', 'non_billable_project', 'billable_time', 'billable_fixed']
@@ -39,13 +41,14 @@ class Project(models.Model):
'timesheet_domain': [('project_id', 'in', self.ids)],
'profitability_domain': [('project_id', 'in', self.ids)],
'stat_buttons': self._plan_get_stat_button(),
'is_uom_day': is_uom_day,
}
#
# Hours, Rates and Profitability
#
dashboard_values = {
'hours': dict.fromkeys(billable_types + ['total'], 0.0),
'time': dict.fromkeys(billable_types + ['total'], 0.0),
'rates': dict.fromkeys(billable_types + ['total'], 0.0),
'profit': {
'invoiced': 0.0,
@@ -58,8 +61,12 @@ 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)
canceled_hours = float_round(total_canceled_hours, precision_rounding=hour_rounding)
if is_uom_day:
# convert time from hours to days
canceled_hours = round(uom_hour._compute_quantity(canceled_hours, company_uom, raise_if_failure=False), 2)
dashboard_values['time']['canceled'] = canceled_hours
dashboard_values['time']['total'] += canceled_hours
# hours (from timesheet) and rates (by billable type)
dashboard_domain = [('project_id', 'in', self.ids), ('timesheet_invoice_type', '!=', False), '|', ('so_line', '=', False), ('so_line.state', '!=', 'cancel')] # force billable type
@@ -67,12 +74,17 @@ class Project(models.Model):
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)
dashboard_values['hours']['total'] += float_round(data.get('unit_amount'), precision_rounding=hour_rounding)
amount = float_round(data.get('unit_amount'), precision_rounding=hour_rounding)
if is_uom_day:
# convert time from hours to days
amount = round(uom_hour._compute_quantity(amount, company_uom, raise_if_failure=False), 2)
dashboard_values['time'][billable_type] = amount
dashboard_values['time']['total'] += amount
# rates
rate = round(data.get('unit_amount') / dashboard_total_hours * 100, 2) if dashboard_total_hours else 0.0
dashboard_values['rates'][billable_type] = rate
dashboard_values['rates']['total'] += rate
dashboard_values['time']['total'] = round(dashboard_values['time']['total'], 2)
# 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)
@@ -148,6 +160,11 @@ class Project(models.Model):
# 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, 'canceled']])
if is_uom_day:
# convert all times from hours to days
for time_type in ['non_billable_project', 'non_billable', 'billable_time', 'billable_fixed', 'canceled', 'total']:
if repartition_employee[employee_id][time_type]:
repartition_employee[employee_id][time_type] = round(uom_hour._compute_quantity(repartition_employee[employee_id][time_type], company_uom, raise_if_failure=False), 2)
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
@@ -166,6 +183,8 @@ class Project(models.Model):
return False
uom_hour = self.env.ref('uom.product_uom_hour')
company_uom = self.env.company.timesheet_encode_uom_id
is_uom_day = company_uom and company_uom == self.env.ref('uom.product_uom_day')
# build SQL query and fetch raw data
query, query_params = self._table_rows_sql_query()
@@ -250,6 +269,11 @@ class Project(models.Model):
sale_line_row[0]['has_children'] = True
timesheet_forecast_table_rows.append(employee_row)
if is_uom_day:
# convert all values from hours to days
for row in timesheet_forecast_table_rows:
for index in range(1, len(row)):
row[index] = round(uom_hour._compute_quantity(row[index], company_uom, raise_if_failure=False), 2)
# complete table data
return {
'header': self._table_header(),
@@ -89,7 +89,7 @@ $canceled-color: gray;
border-radius: 3px;
}
>.o_timesheet_plan_badge {
.o_timesheet_plan_badge {
margin-bottom: 10px;
> .badge {
border: none;
@@ -100,14 +100,16 @@ $canceled-color: gray;
}
}
.table {
margin-top:15px;
}
.table > thead > tr > th, .table > thead > tr > td,
.table > thead > tr > td,
.table > tbody > tr > th, .table > tbody > tr > td {
border: none;
padding: 0px;
}
.table > thead > tr > th {
border: none;
padding-left: 0px;;
}
}
.o_project_plan_project_timesheet_forecast {
@@ -89,11 +89,11 @@ class TestSaleProject(TestReporting):
project_invoiced = self.so_line_order_project.price_unit * self.so_line_order_project.product_uom_qty * timesheet1.unit_amount
project_timesheet_cost = timesheet2.amount + timesheet3.amount + timesheet4.amount + timesheet5.amount + timesheet1.amount
self.assertEqual(float_compare(vals['dashboard']['hours']['non_billable'], timesheet5.unit_amount, precision_rounding=rounding), 0, "The hours non-billable should be the one from the SO2 line, as we are in ordered quantity")
self.assertEqual(float_compare(vals['dashboard']['hours']['non_billable_project'], timesheet2.unit_amount, precision_rounding=rounding), 0, "The hours non-billable-project should be the one from the SO2 line, as we are in ordered quantity")
self.assertEqual(float_compare(vals['dashboard']['hours']['billable_time'], timesheet1.unit_amount, precision_rounding=rounding), 0, "The hours billable-time should be the one from the SO2 line, as we are in ordered quantity")
self.assertEqual(float_compare(vals['dashboard']['hours']['billable_fixed'], project_so_timesheet_sold_unit, precision_rounding=rounding), 0, "The hours billable-fixed should be the one from the SO2 line, as we are in ordered quantity")
self.assertEqual(float_compare(vals['dashboard']['hours']['total'], dashboard_value, precision_rounding=rounding), 0, "The total hours should be the one from the SO2 line, as we are in ordered quantity")
self.assertEqual(float_compare(vals['dashboard']['time']['non_billable'], timesheet5.unit_amount, precision_rounding=rounding), 0, "The hours non-billable should be the one from the SO2 line, as we are in ordered quantity")
self.assertEqual(float_compare(vals['dashboard']['time']['non_billable_project'], timesheet2.unit_amount, precision_rounding=rounding), 0, "The hours non-billable-project should be the one from the SO2 line, as we are in ordered quantity")
self.assertEqual(float_compare(vals['dashboard']['time']['billable_time'], timesheet1.unit_amount, precision_rounding=rounding), 0, "The hours billable-time should be the one from the SO2 line, as we are in ordered quantity")
self.assertEqual(float_compare(vals['dashboard']['time']['billable_fixed'], project_so_timesheet_sold_unit, precision_rounding=rounding), 0, "The hours billable-fixed should be the one from the SO2 line, as we are in ordered quantity")
self.assertEqual(float_compare(vals['dashboard']['time']['total'], dashboard_value, precision_rounding=rounding), 0, "The total hours should be the one from the SO2 line, as we are in ordered quantity")
self.assertEqual(float_compare(vals['dashboard']['rates']['non_billable'], project_rate_non_billable, precision_rounding=rounding), 0, "The rate non-billable should be the one from the SO2 line, as we are in ordered quantity")
self.assertEqual(float_compare(vals['dashboard']['rates']['non_billable_project'], project_rate_non_billable_project, precision_rounding=rounding), 0, "The rate non-billable-project should be the one from the SO2 line, as we are in ordered quantity")
self.assertEqual(float_compare(vals['dashboard']['rates']['billable_time'], project_rate_billable_time, precision_rounding=rounding), 0, "The rate billable-time should be the one from the SO2 line, as we are in ordered quantity")
@@ -64,7 +64,8 @@
</div>
<div class="o_title">
<h2>Recorded Hours and Profitability</h2>
<h2 t-if="is_uom_day">Recorded Days and Profitability</h2>
<h2 t-else="">Recorded Hours and Profitability</h2>
</div>
<t t-set="display_cost" t-value="dashboard['profit']['expense_cost'] != 0.0"/>
@@ -74,11 +75,17 @@
<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>
<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">
<span t-if="is_uom_day">Days recorded</span>
<span t-else="">Hours recorded</span>
</a>
</th>
<tr>
<td class="o_timesheet_plan_dashboard_cell">
<t t-esc="dashboard['hours']['billable_time']" t-options="{'widget': 'float_time'}"/>
<t t-if="is_uom_day" t-esc="dashboard['time']['billable_time']" t-options="{'widget': 'timesheet_uom'}"/>
<t t-else="" t-esc="dashboard['time']['billable_time']" t-options="{'widget': 'float_time'}"/>
</td>
<td class="o_timesheet_plan_dashboard_cell">
(<t t-esc="dashboard['rates']['billable_time']"/> %)
@@ -89,7 +96,8 @@
</tr>
<tr>
<td class="o_timesheet_plan_dashboard_cell">
<t t-esc="dashboard['hours']['billable_fixed']" t-options="{'widget': 'float_time'}"/>
<t t-if="is_uom_day" t-esc="dashboard['time']['billable_fixed']" t-options="{'widget': 'timesheet_uom'}"/>
<t t-else="" t-esc="dashboard['time']['billable_fixed']" t-options="{'widget': 'float_time'}"/>
</td>
<td class="o_timesheet_plan_dashboard_cell">
(<t t-esc="dashboard['rates']['billable_fixed']"/> %)
@@ -98,9 +106,10 @@
Billed at a Fixed price
</td>
</tr>
<tr t-if="dashboard['hours']['non_billable_project'] != 0">
<tr t-if="dashboard['time']['non_billable_project'] != 0">
<td class="o_timesheet_plan_dashboard_cell">
<t t-esc="dashboard['hours']['non_billable_project']" t-options="{'widget': 'float_time'}"/>
<t t-if="is_uom_day" t-esc="dashboard['time']['non_billable_project']" t-options="{'widget': 'timesheet_uom'}"/>
<t t-else="" t-esc="dashboard['time']['non_billable_project']" t-options="{'widget': 'float_time'}"/>
</td>
<td class="o_timesheet_plan_dashboard_cell">
(<t t-esc="dashboard['rates']['non_billable_project']"/> %)
@@ -111,7 +120,8 @@
</tr>
<tr>
<td class="o_timesheet_plan_dashboard_cell">
<t t-esc="dashboard['hours']['non_billable']" t-options="{'widget': 'float_time'}"/>
<t t-if="is_uom_day" t-esc="dashboard['time']['non_billable']" t-options="{'widget': 'timesheet_uom'}"/>
<t t-else="" t-esc="dashboard['time']['non_billable']" t-options="{'widget': 'float_time'}"/>
</td>
<td class="o_timesheet_plan_dashboard_cell">
(<t t-esc="dashboard['rates']['non_billable']"/> %)
@@ -130,9 +140,10 @@
</a>
</td>
</tr>
<tr t-if="dashboard['hours']['canceled'] > 0">
<tr t-if="dashboard['time']['canceled'] > 0">
<td class="o_timesheet_plan_dashboard_cell">
<t t-esc="dashboard['hours']['canceled']" t-options="{'widget': 'float_time'}"/>
<t t-if="is_uom_day" t-esc="dashboard['time']['canceled']" t-options="{'widget': 'timesheet_uom'}"/>
<t t-else="" t-esc="dashboard['time']['canceled']" t-options="{'widget': 'float_time'}"/>
</td>
<td class="o_timesheet_plan_dashboard_cell">
(<t t-esc="dashboard['rates']['canceled']"/> %)
@@ -142,7 +153,12 @@
</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 class="o_timesheet_plan_dashboard_total">
<b>
<t t-if="is_uom_day" t-esc="dashboard['time']['total']" t-options="{'widget': 'timesheet_uom'}"/>
<t t-else="" t-esc="dashboard['time']['total']" t-options="{'widget': 'float_time'}"/>
</b>
</td>
<td><b>Total</b></td>
<td></td>
</tr>
@@ -227,28 +243,38 @@
<p>There are no timesheets 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_time">
<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_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 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">
<thead>
<tr>
<th></th>
<th>Employee</th>
<th t-if="is_uom_day" class="text-nowrap text-right pr-5">Days Spent</th>
<th t-else="" class="text-nowrap text-right pr-5">Hours Spent</th>
<td>
<div class="float-right o_timesheet_plan_badge">
<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">Billed on Timesheets</a>
</span>
<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['time']['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 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>
</td>
</tr>
</thead>
<tbody>
<t t-foreach="repartition_employee" t-as="employee_id">
<t t-set="employee" t-value="repartition_employee[employee_id]"/>
@@ -262,7 +288,8 @@
</a>
</td>
<td class="text-right pr-5" style="width: 10%; vertical-align: middle;">
<t t-esc="employee['total']" t-options="{'widget': 'float_time'}"/>
<t t-if="is_uom_day" t-esc="employee['total']" t-options="{'widget': 'timesheet_uom'}"/>
<t t-else="" t-esc="employee['total']" t-options="{'widget': 'float_time'}"/>
</td>
<td style="vertical-align:middle">
<div class="border rounded">
@@ -367,15 +394,24 @@
<t t-if="row_value_index != 0">
<t t-if="row_value_index &lt; len(row)-2">
<t t-if="row_is_milestone">
<i t-att-class="'text-muted' if not row_value else ''"><t t-esc="row_value" t-options="{'widget': 'float_time'}"/></i>
<i t-att-class="'text-muted' if not row_value else ''">
<t t-if="is_uom_day" t-esc="row_value" t-options="{'widget': 'timesheet_uom'}"/>
<t t-else="" t-esc="row_value" t-options="{'widget': 'float_time'}"/>
</i>
</t>
<t t-else="">
<span t-att-class="'text-muted' if not row_value else ''"><t t-esc="row_value" t-options="{'widget': 'float_time'}"/></span>
<span t-att-class="'text-muted' if not row_value else ''">
<t t-if="is_uom_day" t-esc="row_value" t-options="{'widget': 'timesheet_uom'}"/>
<t t-else="" t-esc="row_value" t-options="{'widget': 'float_time'}"/>
</span>
</t>
</t>
<t t-else="">
<t t-if="not row_is_milestone and not row[0].get('type') == 'hr_employee'">
<span t-att-class="'text-muted' if not row_value else ''"><t t-esc="row_value" t-options="{'widget': 'float_time'}"/></span>
<span t-att-class="'text-muted' if not row_value else ''">
<t t-if="is_uom_day" t-esc="row_value" t-options="{'widget': 'timesheet_uom'}"/>
<t t-else="" t-esc="row_value" t-options="{'widget': 'float_time'}"/>
</span>
</t>
</t>
</t>
@@ -406,7 +442,9 @@
<template id="progressbar" name="project overview progressbar segments">
<t t-set="amount" t-value="employee[key]"/>
<t t-if="amount &gt; 0">
<t t-set="title"><t t-esc="label"/>: <t t-esc="amount" t-options="{'widget': 'float_time'}"/></t>
<t t-set="title">
<t t-esc="label"/>: <t t-if="is_uom_day" t-esc="amount" t-options="{'widget': 'timesheet_uom'}"/><t t-else="" 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}}%"
type="action" data-model="account.analytic.line"
@@ -36,7 +36,8 @@
<th t-if="nr_projects &gt; 1">Project</th>
<th t-if="nr_tasks &gt; 0">Task</th>
<th>Description</th>
<th class="text-right">Duration</th>
<th t-if="timesheets[0]._is_timesheet_encode_uom_day()" class="text-right">Duration (days)</th>
<th t-else="" class="text-right">Duration (hours)</th>
</tr>
</thead>
<tr t-foreach="timesheets" t-as="timesheet">
@@ -45,7 +46,10 @@
<td t-if="nr_projects &gt; 1"><span t-field="timesheet.project_id"/></td>
<td t-if="nr_tasks &gt; 0"><span t-field="timesheet.task_id"/></td>
<td><t t-esc="timesheet.name"/></td>
<td class="text-right"><span t-field="timesheet.unit_amount" t-options='{"widget": "float_time"}'/></td>
<td class="text-right">
<span t-if="timesheet._is_timesheet_encode_uom_day()" t-esc="timesheet._get_timesheet_time_day()" t-options='{"widget": "timesheet_uom"}'/>
<span t-else="" t-field="timesheet.unit_amount" t-options='{"widget": "float_time"}'/>
</td>
</tr>
</table>
</div>