[IMP] hr_timesheet: hide the "Round timesheets" feature in the settings of Timesheets if encoding unit is Days

[IMP] hr_timesheet: hide the "Timesheet Timer" feature in the project form if encoding unit is not Hours

[IMP] hr_timesheet: clean display of Days for timesheets

A computed field has to be added to retrieve the encoding uom of the current company and be used in the views.
FieldTimesheetFactor field has to be registered to "float_factor" widget for the "planned_hours" field to work correctly with Days
(for the conversion from hours to days to be computed and to be able to set a greater value than 1 day)

If encoding unit for timesheets is set to Days:
- Display Days instead of Hours in Portal > Timesheets
- Display Days instead of Hours in Portal > Tasks
- Display Days instead of Hours in Portal > Invoices & Bills
- Display Days instead of Hours in Portal > Tickets

[IMP] hr_timesheet: display clean duration in Timesheet Entries

Display Days instead of Hours in Timesheet Entries (print action) if encoding unit for timesheets is set to Days

[IMP] hr_timesheet: correctly display timesheets in Days if configured to

- correctly display remaining days in project.task kanban and list view
- hide timer start button if timesheet is configured in Days
- update several column names

[IMP] hr_timesheet: clean code

[IMP] hr_timesheet: small improvements when displaying timesheets in days

- Rename some labels
- Fixes style
- Prevent fields displayed with widget "timesheet_uom" to change when timer is on
- Display correctly "Initially Planned Days" field in task creation form
This commit is contained in:
Anh Thao Pham (pta)
2020-07-23 13:12:12 +02:00
parent c6d5948c75
commit 7e49130d89
13 changed files with 181 additions and 35 deletions
@@ -141,5 +141,6 @@ class TimesheetCustomerPortal(CustomerPortal):
'searchbar_groupby': searchbar_groupby,
'searchbar_filters': OrderedDict(sorted(searchbar_filters.items())),
'filterby': filterby,
'is_uom_day': request.env['account.analytic.line']._is_timesheet_encode_uom_day(),
})
return request.render("hr_timesheet.portal_my_timesheets", values)
@@ -22,4 +22,5 @@ class ProjectCustomerPortal(CustomerPortal):
timesheets_by_subtask[timesheet.task_id] |= timesheet
values['timesheets'] = timesheets
values['timesheets_by_subtask'] = timesheets_by_subtask
values['is_uom_day'] = request.env['account.analytic.line']._is_timesheet_encode_uom_day()
return values
@@ -189,3 +189,15 @@ class AccountAnalyticLine(models.Model):
'amount': amount_converted,
})
return result
def _is_timesheet_encode_uom_day(self):
company_uom = self.env.company.timesheet_encode_uom_id
return company_uom == self.env.ref('uom.product_uom_day')
def _convert_hours_to_days(self, time):
uom_hour = self.env.ref('uom.product_uom_hour')
uom_day = self.env.ref('uom.product_uom_day')
return round(uom_hour._compute_quantity(time, uom_day, raise_if_failure=False), 2)
def _get_timesheet_time_day(self):
return self._convert_hours_to_days(self.unit_amount)
+38 -1
View File
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from lxml import etree
from odoo import models, fields, api, _
from odoo.exceptions import UserError, ValidationError, RedirectWarning
@@ -24,6 +26,10 @@ class Project(models.Model):
total_timesheet_time = fields.Integer(
compute='_compute_total_timesheet_time',
help="Total number of time (in the proper UoM) recorded in the project, rounded to the unit.")
encode_uom_in_days = fields.Boolean(compute='_compute_encode_uom_in_days')
def _compute_encode_uom_in_days(self):
self.encode_uom_in_days = self.env.company.timesheet_encode_uom_id == self.env.ref('uom.product_uom_day')
@api.depends('analytic_account_id')
def _compute_allow_timesheets(self):
@@ -106,6 +112,13 @@ class Task(models.Model):
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="Time spent on the sub-tasks (and their own sub-tasks) of this task.")
timesheet_ids = fields.One2many('account.analytic.line', 'task_id', 'Timesheets')
encode_uom_in_days = fields.Boolean(compute='_compute_encode_uom_in_days', default=lambda self: self._uom_in_days())
def _uom_in_days(self):
return self.env.company.timesheet_encode_uom_id == self.env.ref('uom.product_uom_day')
def _compute_encode_uom_in_days(self):
self.encode_uom_in_days = self._uom_in_days()
@api.depends('project_id.analytic_account_id.active')
def _compute_analytic_account_active(self):
@@ -180,7 +193,10 @@ class Task(models.Model):
if self.env.context.get('hr_timesheet_display_remaining_hours'):
name_mapping = dict(super().name_get())
for task in self:
if task.allow_timesheets and task.planned_hours > 0:
if task.allow_timesheets and task.planned_hours > 0 and task.encode_uom_in_days:
days_left = _("(%s days remaining)") % task._convert_hours_to_days(task.remaining_hours)
name_mapping[task.id] = name_mapping.get(task.id, '') + " ‒ " + days_left
elif task.allow_timesheets and task.planned_hours > 0:
hours, mins = (str(int(duration)).rjust(2, '0') for duration in divmod(abs(task.remaining_hours) * 60, 60))
hours_left = _(
"(%(sign)s%(hours)s:%(minutes)s remaining)",
@@ -197,8 +213,24 @@ class Task(models.Model):
""" Set the correct label for `unit_amount`, depending on company UoM """
result = super(Task, self)._fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu)
result['arch'] = self.env['account.analytic.line']._apply_timesheet_label(result['arch'])
if view_type == 'tree' and self.env.company.timesheet_encode_uom_id == self.env.ref('uom.product_uom_day'):
result['arch'] = self._apply_time_label(result['arch'])
return result
@api.model
def _apply_time_label(self, view_arch):
doc = etree.XML(view_arch)
encoding_uom = self.env.company.timesheet_encode_uom_id
for node in doc.xpath("//field[@name='planned_hours'][@widget='timesheet_uom'][not(@string)]"):
node.set('string', _('Planned %s') % encoding_uom.name or '')
for node in doc.xpath("//field[@name='effective_hours'][@widget='timesheet_uom'][not(@string)]"):
node.set('string', _('%s Spent') % encoding_uom.name or '')
for node in doc.xpath("//field[@name='remaining_hours'][@widget='timesheet_uom'][not(@string)]"):
node.set('string', _('Remaining %s') % encoding_uom.name or '')
return etree.tostring(doc, encoding='unicode')
def unlink(self):
"""
If some tasks to unlink have some timesheets entries, these
@@ -216,3 +248,8 @@ class Task(models.Model):
warning_msg, self.env.ref('hr_timesheet.timesheet_action_task').id,
_('See timesheet entries'), {'active_ids': tasks_with_timesheets.ids})
return super(Task, self).unlink()
def _convert_hours_to_days(self, time):
uom_hour = self.env.ref('uom.product_uom_hour')
uom_day = self.env.ref('uom.product_uom_day')
return round(uom_hour._compute_quantity(time, uom_day, raise_if_failure=False), 2)
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
from odoo import api, fields, models
class ResConfigSettings(models.TransientModel):
@@ -20,3 +20,10 @@ class ResConfigSettings(models.TransientModel):
and widgets to help the encoding. All reporting will still be expressed in hours (default value).""")
timesheet_min_duration = fields.Integer('Minimal duration', default=15, config_parameter='hr_timesheet.timesheet_min_duration')
timesheet_rounding = fields.Integer('Rounding up', default=15, config_parameter='hr_timesheet.timesheet_rounding')
is_encode_uom_days = fields.Boolean(compute='_compute_is_encode_uom_days')
@api.depends('timesheet_encode_uom_id')
def _compute_is_encode_uom_days(self):
product_uom_day = self.env.ref('uom.product_uom_day')
for settings in self:
settings.is_encode_uom_days = settings.timesheet_encode_uom_id == product_uom_day
@@ -20,15 +20,19 @@
<div class="row" style="margin-top:10px;">
<div class="col-lg-12">
<t t-set='is_uom_day' t-value='docs._is_timesheet_encode_uom_day()'/>
<table class="table table-sm">
<thead>
<tr>
<th><span>Date</span></th>
<th><span>Responsible</span></th>
<th><span>Description</span></th>
<th t-if="show_project"><span>Project</span></th>
<th t-if="show_task"><span>Task</span></th>
<th class="text-right"><span>Time</span></th>
<th class="align-middle"><span>Date</span></th>
<th class="align-middle"><span>Responsible</span></th>
<th class="align-middle"><span>Description</span></th>
<th class="align-middle" t-if="show_project"><span>Project</span></th>
<th class="align-middle" t-if="show_task"><span>Task</span></th>
<th class="text-right">
<span t-if="is_uom_day">Time Spent (Days)</span>
<span t-else="">Time Spent (Hours)</span>
</th>
</tr>
</thead>
<tbody>
@@ -49,16 +53,24 @@
<t t-if="l.task_id"><span t-field="l.task_id.name"/></t>
</td>
<td class="text-right">
<span t-field="l.unit_amount" t-options="{'widget': 'duration', 'digital': True, 'unit': 'hour', 'round': 'minute'}"/>
<span t-if="is_uom_day" t-esc="l._get_timesheet_time_day()" t-options="{'widget': 'timesheet_uom'}"/>
<span t-else="" t-field="l.unit_amount" t-options="{'widget': 'duration', 'digital': True, 'unit': 'hour', 'round': 'minute'}"/>
</td>
</tr>
<tr>
<td />
<td />
<td t-if="show_project"/>
<td t-if="show_task"/>
<td class="text-right"><strong>Total</strong></td>
<td class="text-right"><strong t-esc="sum(docs.mapped('unit_amount'))" t-options="{'widget': 'duration', 'digital': True, 'unit': 'hour', 'round': 'minute'}"/></td>
<t t-set="nbCols" t-value="4"/>
<t t-if="show_project" t-set="nbCols" t-value="nbCols + 1"/>
<t t-if="show_task" t-set="nbCols" t-value="nbCols + 1"/>
<td class="text-right" t-attf-colspan="{{nbCols}}">
<strong t-if="is_uom_day">
<span style="margin-right: 15px;">Total (Days)</span>
<t t-esc="docs._convert_hours_to_days(sum(docs.mapped('unit_amount')))" t-options="{'widget': 'timesheet_uom'}"/>
</strong>
<strong t-else="">
<span style="margin-right: 15px;">Total (Hours)</span>
<t t-esc="sum(docs.mapped('unit_amount'))" t-options="{'widget': 'duration', 'digital': True, 'unit': 'hour', 'round': 'minute'}"/>
</strong>
</td>
</tr>
</tbody>
</table>
@@ -0,0 +1,20 @@
odoo.define('hr_timesheet.timesheet_factor', function (require) {
'use strict';
const timesheetUomFields = require('hr_timesheet.timesheet_uom');
const fieldUtils = require('web.field_utils');
const fieldRegistry = require('web.field_registry');
fieldRegistry.add('timesheet_factor', timesheetUomFields.FieldTimesheetFactor);
fieldUtils.format.timesheet_factor = function(value, field, options) {
const formatter = fieldUtils.format[timesheetUomFields.FieldTimesheetFactor.prototype.formatType];
return formatter(value, field, options);
};
fieldUtils.parse.timesheet_factor = function(value, field, options) {
const parser = fieldUtils.parse[timesheetUomFields.FieldTimesheetFactor.prototype.formatType];
return parser(value, field, options);
};
});
@@ -0,0 +1,4 @@
.o_form_subtask_button {
border: none;
padding-left: 0px;
}
+2
View File
@@ -2,8 +2,10 @@
<odoo>
<template id="assets_backend" name="timesheet assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<link type="text/scss" href="/hr_timesheet/static/src/scss/timesheets_task_form.scss" rel="stylesheet"/>
<script type="text/javascript" src="/hr_timesheet/static/src/js/task_with_hours.js"></script>
<script type="text/javascript" src="/hr_timesheet/static/src/js/timesheet_uom.js"/>
<script type="text/javascript" src="/hr_timesheet/static/src/js/timesheet_factor.js"/>
</xpath>
</template>
</odoo>
@@ -61,7 +61,8 @@
<th t-if="not groupby == 'project'">Project</th>
<th t-if="not groupby == 'task'">Task</th>
<th>Description</th>
<th class="text-right">Hours Spent <small class="text-muted">(Total: <span t-esc="hours_spent" t-options='{"widget": "float_time"}'/>)</small></th>
<th t-if="is_uom_day" class="text-right">Days Spent <small class="text-muted">(Total: <span t-esc="timesheets[0]._convert_hours_to_days(hours_spent)" t-options='{"widget": "timesheet_uom"}'/>)</small></th>
<th t-else="" class="text-right">Hours Spent <small class="text-muted">(Total: <span t-esc="hours_spent" t-options='{"widget": "float_time"}'/>)</small></th>
</tr>
</thead>
<tbody>
@@ -72,7 +73,10 @@
<td t-if="not groupby == 'project'"><span t-field="timesheet.project_id"/></td>
<td t-if="not groupby == 'task'"><span t-field="timesheet.task_id"/></td>
<td><span 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="is_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>
</t>
</tbody>
@@ -21,14 +21,18 @@
<th>Date</th>
<th>Employee</th>
<th>Description</th>
<th class="text-right">Duration</th>
<th t-if="is_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">
<td><t t-esc="timesheet.date" t-options='{"widget": "date"}'/></td>
<td><t t-esc="timesheet.employee_id.name"/></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="is_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>
<tfoot>
<tr>
+56 -15
View File
@@ -79,11 +79,26 @@
<field name="analytic_account_active" invisible="1"/>
<field name="allow_timesheets" invisible="1"/>
<field name="allow_subtasks" invisible="1"/>
<field name="encode_uom_in_days" invisible="1"/>
<page string="Timesheets" id="timesheets_tab" attrs="{'invisible': [('allow_timesheets', '=', False)]}">
<group>
<group>
<field name="planned_hours" widget="float_time"/>
<field name="subtask_planned_hours" widget="float_time" groups="project.group_subtask_project" attrs="{'invisible': ['|', ('allow_subtasks', '=', False), ('subtask_count', '=', 0)]}"/>
<label for="planned_hours" string="Initially Planned Hours" attrs="{'invisible': [('encode_uom_in_days', '=', True)]}"/>
<field name="planned_hours" widget="float_time" attrs="{'invisible': [('encode_uom_in_days', '=', True)]}" nolabel="1" />
<label for="planned_hours" string="Initially Planned Days" attrs="{'invisible': [('encode_uom_in_days', '=', False)]}"/>
<field name="planned_hours" widget="timesheet_factor" attrs="{'invisible': [('encode_uom_in_days', '=', False)]}" nolabel="1" />
<label for="subtask_planned_hours" groups="project.group_subtask_project" string="Sub-tasks Planned Hours"
attrs="{'invisible': ['|', '|', ('allow_subtasks', '=', False), ('subtask_count', '=', 0), ('encode_uom_in_days', '=', True)]}"/>
<span class="o_row" groups="project.group_subtask_project"
attrs="{'invisible': ['|', '|', ('allow_subtasks', '=', False), ('subtask_count', '=', 0), ('encode_uom_in_days', '=', True)]}">
<field name="subtask_planned_hours" widget="float_time"/>
</span>
<label for="subtask_planned_hours" groups="project.group_subtask_project" string="Sub-tasks Planned Days"
attrs="{'invisible': ['|', '|', ('allow_subtasks', '=', False), ('subtask_count', '=', 0), ('encode_uom_in_days', '=', False)]}"/>
<div class="o_row" groups="project.group_subtask_project"
attrs="{'invisible': ['|', '|', ('allow_subtasks', '=', False), ('subtask_count', '=', 0), ('encode_uom_in_days', '=', False)]}">
<field name="subtask_planned_hours" widget="timesheet_uom"/>
</div>
</group>
<group>
<field name="progress" widget="progressbar"/>
@@ -155,11 +170,34 @@
</field>
<group attrs="{'invisible': [('analytic_account_active', '=', False)]}">
<group class="oe_subtotal_footer oe_right" name="project_hours">
<field name="effective_hours" widget="float_time" />
<button name="action_view_subtask_timesheet" string="Sub-tasks Hours Spent" type="object" class="o_td_label o_form_label oe_inline oe_link mr-0" attrs="{'invisible' : ['|', ('allow_subtasks', '=', False), ('subtask_effective_hours', '=', 0.0)]}"/>
<field name="subtask_effective_hours" class="mt-2" widget="float_time" attrs="{'invisible' : ['|', ('allow_subtasks', '=', False), ('subtask_effective_hours', '=', 0.0)]}" nolabel="1"/>
<field name="total_hours_spent" widget="float_time" class="oe_subtotal_footer_separator" attrs="{'invisible' : ['|', ('allow_subtasks', '=', False), ('subtask_effective_hours', '=', 0.0)]}" />
<field name="remaining_hours" widget="float_time" class="oe_subtotal_footer_separator" attrs="{'invisible' : [('planned_hours', '=', 0.0)]}"/>
<span>
<label class="font-weight-bold" for="effective_hours" string="Hours Spent" attrs="{'invisible': [('encode_uom_in_days', '=', True)]}"/>
<label class="font-weight-bold" for="effective_hours" string="Days Spent" attrs="{'invisible': [('encode_uom_in_days', '=', False)]}"/>
</span>
<field name="effective_hours" widget="timesheet_uom" nolabel="1"/>
<button name="action_view_subtask_timesheet" type="object" class="o_td_label o_form_label o_form_subtask_button oe_inline oe_link mr-0" attrs="{'invisible' : ['|', ('allow_subtasks', '=', False), ('subtask_effective_hours', '=', 0.0)]}">
<span class="text-nowrap" attrs="{'invisible' : [('encode_uom_in_days', '=', True)]}">Sub-tasks Hours Spent</span>
<span class="text-nowrap" attrs="{'invisible' : [('encode_uom_in_days', '=', False)]}">Sub-tasks Days Spent</span>
</button>
<field name="subtask_effective_hours" class="mt-2" widget="timesheet_uom"
attrs="{'invisible' : ['|', ('allow_subtasks', '=', False), ('subtask_effective_hours', '=', 0.0)]}" nolabel="1"/>
<span>
<label class="font-weight-bold" for="total_hours_spent" string="Total Hours"
attrs="{'invisible': ['|', '|', ('allow_subtasks', '=', False), ('subtask_effective_hours', '=', 0.0), ('encode_uom_in_days', '=', True)]}"/>
<label class="font-weight-bold" for="total_hours_spent" string="Total Days"
attrs="{'invisible': ['|', '|', ('allow_subtasks', '=', False), ('subtask_effective_hours', '=', 0.0), ('encode_uom_in_days', '=', False)]}"/>
</span>
<field name="total_hours_spent" widget="timesheet_uom" class="oe_subtotal_footer_separator" nolabel="1"
attrs="{'invisible' : ['|', ('allow_subtasks', '=', False), ('subtask_effective_hours', '=', 0.0)]}" />
<span>
<label class="font-weight-bold" for="remaining_hours" string="Remaining Hours"
attrs="{'invisible': ['|', ('planned_hours', '=', 0.0), ('encode_uom_in_days', '=', True)]}"/>
<label class="font-weight-bold" for="remaining_hours" string="Remaining Days"
attrs="{'invisible': ['|', ('planned_hours', '=', 0.0), ('encode_uom_in_days', '=', False)]}"/>
</span>
<field name="remaining_hours" widget="timesheet_uom" class="oe_subtotal_footer_separator"
attrs="{'invisible' : [('planned_hours', '=', 0.0)]}" nolabel="1"/>
</group>
</group>
</page>
@@ -175,11 +213,11 @@
<field name="arch" type="xml">
<field name="company_id" position="after">
<field name="allow_subtasks" invisible="1"/>
<field name="planned_hours" widget="float_time" sum="Initially Planned Hours" optional="hide"/>
<field name="effective_hours" widget="float_time" sum="Effective Hours" optional="show"/>
<field name="remaining_hours" widget="float_time" sum="Remaining Hours" optional="hide" decoration-danger="progress &gt;= 100" decoration-warning="progress &gt;= 80 and progress &lt; 100"/>
<field name="subtask_effective_hours" widget="float_time" attrs="{'invisible' : [('allow_subtasks', '=', False)]}" optional="hide"/>
<field name="total_hours_spent" widget="float_time" attrs="{'invisible' : [('allow_subtasks', '=', False)]}" optional="hide"/>
<field name="planned_hours" widget="timesheet_uom" sum="Initially Planned Hours" optional="hide"/>
<field name="effective_hours" widget="timesheet_uom" sum="Effective Hours" optional="show"/>
<field name="remaining_hours" widget="timesheet_uom" sum="Remaining Hours" optional="hide" decoration-danger="progress &gt;= 100" decoration-warning="progress &gt;= 80 and progress &lt; 100"/>
<field name="subtask_effective_hours" widget="timesheet_uom" attrs="{'invisible' : [('allow_subtasks', '=', False)]}" optional="hide"/>
<field name="total_hours_spent" widget="timesheet_uom" attrs="{'invisible' : [('allow_subtasks', '=', False)]}" optional="hide"/>
<field name="progress" widget="progressbar" optional="show" groups="hr_timesheet.group_hr_timesheet_user"/>
</field>
</field>
@@ -217,15 +255,18 @@
<field name="remaining_hours" />
<field name="planned_hours" />
<field name="allow_timesheets"/>
<field name="encode_uom_in_days" invisible="1"/>
</templates>
<div class="oe_kanban_bottom_left" position="inside">
<t name="planned_hours" t-if="record.planned_hours.raw_value &gt; 0 and record.allow_timesheets.raw_value">
<t t-set="badge" t-value=""/>
<t t-set="badge" t-value="'badge-warning'" t-if="record.progress.raw_value &gt;= 80 and record.progress.raw_value &lt;= 100"/>
<t t-set="badge" t-value="'badge-danger'" t-if="record.remaining_hours.raw_value &lt; 0"/>
<div t-attf-class="oe_kanban_align badge {{ badge }}" title="Remaining hours">
<field name="remaining_hours" widget="float_time" />
</div>
<t t-set="title" t-value="'Remaining days'" t-if="record.encode_uom_in_days.raw_value"/>
<t t-set="title" t-value="'Remaining hours'" t-else=""/>
<div t-attf-class="oe_kanban_align badge {{ badge }}" t-att-title="title">
<field name="remaining_hours" widget="timesheet_uom" />
</div>
</t>
</div>
</field>
@@ -38,6 +38,7 @@
<div class="content-group">
<div class="mt16">
<field name="timesheet_encode_uom_id" options="{'no_create': True, 'no_open': True}" required="1" class="col-lg-5"/>
<field name="is_encode_uom_days" invisible="1"/>
</div>
</div>
</div>
@@ -72,7 +73,7 @@
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box">
<div class="col-12 col-lg-6 o_setting_box" attrs="{'invisible': [('module_project_timesheet_synchro', '=', False), ('is_encode_uom_days', '=', True)]}">
<div class="o_setting_right_pane">
<strong>Round Timesheets</strong>
<div class="o_row w-30">