diff --git a/addons/hr_timesheet/controllers/portal.py b/addons/hr_timesheet/controllers/portal.py index aceb00e3232..f103e80970a 100644 --- a/addons/hr_timesheet/controllers/portal.py +++ b/addons/hr_timesheet/controllers/portal.py @@ -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) diff --git a/addons/hr_timesheet/controllers/project.py b/addons/hr_timesheet/controllers/project.py index dd377160d6b..31fbe2f326c 100644 --- a/addons/hr_timesheet/controllers/project.py +++ b/addons/hr_timesheet/controllers/project.py @@ -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 diff --git a/addons/hr_timesheet/models/hr_timesheet.py b/addons/hr_timesheet/models/hr_timesheet.py index 69ed16ed4b2..24458df59a5 100644 --- a/addons/hr_timesheet/models/hr_timesheet.py +++ b/addons/hr_timesheet/models/hr_timesheet.py @@ -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) diff --git a/addons/hr_timesheet/models/project.py b/addons/hr_timesheet/models/project.py index 4fbe868b2de..fdd3b0210ec 100644 --- a/addons/hr_timesheet/models/project.py +++ b/addons/hr_timesheet/models/project.py @@ -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) diff --git a/addons/hr_timesheet/models/res_config_settings.py b/addons/hr_timesheet/models/res_config_settings.py index f8e8f00621c..68ed5bd2df1 100644 --- a/addons/hr_timesheet/models/res_config_settings.py +++ b/addons/hr_timesheet/models/res_config_settings.py @@ -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 diff --git a/addons/hr_timesheet/report/report_timesheet_templates.xml b/addons/hr_timesheet/report/report_timesheet_templates.xml index 38fc2f4e82e..7dac6e7430d 100644 --- a/addons/hr_timesheet/report/report_timesheet_templates.xml +++ b/addons/hr_timesheet/report/report_timesheet_templates.xml @@ -20,15 +20,19 @@
+ - - - - - - + + + + + + @@ -49,16 +53,24 @@ - - + + + +
DateResponsibleDescriptionProjectTaskTimeDateResponsibleDescriptionProjectTask + Time Spent (Days) + Time Spent (Hours) +
- + +
- - - - Total + + Total (Days) + + + + Total (Hours) + + +
diff --git a/addons/hr_timesheet/static/src/js/timesheet_factor.js b/addons/hr_timesheet/static/src/js/timesheet_factor.js new file mode 100644 index 00000000000..def165e03fd --- /dev/null +++ b/addons/hr_timesheet/static/src/js/timesheet_factor.js @@ -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); +}; + +}); diff --git a/addons/hr_timesheet/static/src/scss/timesheets_task_form.scss b/addons/hr_timesheet/static/src/scss/timesheets_task_form.scss new file mode 100644 index 00000000000..8a7e3b78477 --- /dev/null +++ b/addons/hr_timesheet/static/src/scss/timesheets_task_form.scss @@ -0,0 +1,4 @@ +.o_form_subtask_button { + border: none; + padding-left: 0px; +} diff --git a/addons/hr_timesheet/views/assets.xml b/addons/hr_timesheet/views/assets.xml index 816f36f5c53..bb997241062 100644 --- a/addons/hr_timesheet/views/assets.xml +++ b/addons/hr_timesheet/views/assets.xml @@ -2,8 +2,10 @@