From 2f20b9437a80d71a6184ab83b9c1db3dd04f9cb3 Mon Sep 17 00:00:00 2001 From: "Xavier Bol (xbo)" Date: Thu, 17 Oct 2019 16:28:25 +0200 Subject: [PATCH 01/14] [IMP] hr_timesheet: Improve access rights TaskID: 2088586 The low level access right is now "See my timesheets". This access disallow to a user to view and manage the timesheets of others users. When the visibility of a project is set on "Invited employees", the users with the access rules : 'See my timesheet' and 'See all timesheets' can show the timesheets, if these users follow the project. --- addons/hr_timesheet/models/hr_timesheet.py | 30 +++++++++++++-- .../security/hr_timesheet_security.xml | 37 +++++++++++++++---- .../hr_timesheet/security/ir.model.access.csv | 4 +- addons/hr_timesheet/tests/test_timesheet.py | 22 +++++++++-- .../hr_timesheet/views/hr_timesheet_views.xml | 3 +- 5 files changed, 81 insertions(+), 15 deletions(-) diff --git a/addons/hr_timesheet/models/hr_timesheet.py b/addons/hr_timesheet/models/hr_timesheet.py index ddd0f05a64e..f7cad827761 100644 --- a/addons/hr_timesheet/models/hr_timesheet.py +++ b/addons/hr_timesheet/models/hr_timesheet.py @@ -5,8 +5,8 @@ from lxml import etree import re from odoo import api, fields, models, _ -from odoo.exceptions import UserError - +from odoo.exceptions import UserError, AccessError +from odoo.osv import expression class AccountAnalyticLine(models.Model): _inherit = 'account.analytic.line' @@ -20,11 +20,29 @@ class AccountAnalyticLine(models.Model): result['employee_id'] = self.env['hr.employee'].search([('user_id', '=', result['user_id'])], limit=1).id return result + def _domain_project_id(self): + domain = [('allow_timesheets', '=', True)] + if not self.user_has_groups('hr_timesheet.group_timesheet_manager'): + return expression.AND([domain, + ['|', ('privacy_visibility', '!=', 'followers'), ('message_partner_ids', 'in', [self.env.user.partner_id.id])] + ]) + return domain + + def _domain_employee_id(self): + if not self.user_has_groups('hr_timesheet.group_hr_timesheet_approver'): + return [('user_id', '=', self.env.user.id)] + return [] + + def _domain_task_id(self): + if not self.user_has_groups('hr_timesheet.group_hr_timesheet_approver'): + return ['|', ('privacy_visibility', '!=', 'followers'), ('message_partner_ids', 'in', [self.env.user.partner_id.id])] + return [] + task_id = fields.Many2one( 'project.task', 'Task', index=True, domain="[('company_id', '=', company_id), ('project_id.allow_timesheets', '=', True), ('project_id', '=?', project_id)]" ) - project_id = fields.Many2one('project.project', 'Project', domain=[('allow_timesheets', '=', True)]) + project_id = fields.Many2one('project.project', 'Project', domain=_domain_project_id) employee_id = fields.Many2one('hr.employee', "Employee", check_company=True) department_id = fields.Many2one('hr.department', "Department", compute='_compute_department_id', store=True, compute_sudo=True) @@ -81,6 +99,12 @@ class AccountAnalyticLine(models.Model): return lines def write(self, values): + # If it's a basic user then check if the timesheet is his own. + for record in self: + if not self.user_has_groups('hr_timesheet.group_hr_timesheet_approver'): + if self.env.user.id != record.user_id.id: + raise AccessError(_("You aren't allowed to access to the timesheet that it's not yours")) + values = self._timesheet_preprocess(values) result = super(AccountAnalyticLine, self).write(values) # applied only for timesheet diff --git a/addons/hr_timesheet/security/hr_timesheet_security.xml b/addons/hr_timesheet/security/hr_timesheet_security.xml index 64033b482ec..ca969ce739d 100644 --- a/addons/hr_timesheet/security/hr_timesheet_security.xml +++ b/addons/hr_timesheet/security/hr_timesheet_security.xml @@ -7,14 +7,14 @@ - See all Timesheets + See own timesheets - + @@ -23,16 +23,24 @@ account.analytic.line.timesheet.user - [('user_id', '=', user.id), ('project_id', '!=', False)] + [ + ('user_id', '=', user.id), + ('project_id', '!=', False), + '|', + ('project_id.privacy_visibility', '!=', 'followers'), + '|', + ('project_id.message_partner_ids', 'in', [user.partner_id.id]), + ('task_id.message_partner_ids', 'in', [user.partner_id.id]) + ] - + - - Team Approver + + See all timesheets @@ -40,7 +48,7 @@ Administrator - + @@ -60,4 +68,19 @@ + + account.analytic.line.timesheet.approver + + [ + ('project_id', '!=', False), + '|', + ('project_id.privacy_visibility', '!=', 'followers'), + ('project_id.message_partner_ids', 'in', [user.partner_id.id]) + ] + + + + + + diff --git a/addons/hr_timesheet/security/ir.model.access.csv b/addons/hr_timesheet/security/ir.model.access.csv index cb11fb119f7..84cb43d62e1 100644 --- a/addons/hr_timesheet/security/ir.model.access.csv +++ b/addons/hr_timesheet/security/ir.model.access.csv @@ -1,5 +1,7 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_account_analytic_line_user,analytic.account.analytic.line.timesheet.user,analytic.model_account_analytic_line,hr_timesheet.group_hr_timesheet_user,1,1,1,1 +access_account_analytic_line_manager,analytic.account.analytic.line.timesheet.manager,analytic.model_account_analytic_line,hr_timesheet.group_timesheet_manager,1,1,1,1 +access_account_analytic_line_approver,analytic.account.analytic.line.timesheet.approver,analytic.model_account_analytic_line,hr_timesheet.group_hr_timesheet_approver,1,1,1,1 +access_account_analytic_line_user,analytic.account.analytic.line.timesheet.user,analytic.model_account_analytic_line,hr_timesheet.group_hr_timesheet_user,1,1,1,1,1 access_account_analytic_user,analytic.account.analytic.timesheet.user,analytic.model_account_analytic_account,hr_timesheet.group_hr_timesheet_user,1,1,0,0 access_uom_uom_hr_timesheet,uom.uom.timesheet.user,uom.model_uom_uom,hr_timesheet.group_hr_timesheet_user,1,0,0,0 access_project_project,project.project.timesheet.user,model_project_project,hr_timesheet.group_hr_timesheet_user,1,0,0,0 diff --git a/addons/hr_timesheet/tests/test_timesheet.py b/addons/hr_timesheet/tests/test_timesheet.py index 2245c40707e..8fd1bde9d0e 100644 --- a/addons/hr_timesheet/tests/test_timesheet.py +++ b/addons/hr_timesheet/tests/test_timesheet.py @@ -16,7 +16,7 @@ class TestCommonTimesheet(TransactionCase): rule = self.env.ref('timesheet_grid.hr_timesheet_rule_approver_update', raise_if_not_found=False) if rule: rule.active = False - + # customer partner self.partner = self.env['res.partner'].create({ 'name': 'Customer Task', @@ -85,6 +85,16 @@ class TestCommonTimesheet(TransactionCase): class TestTimesheet(TestCommonTimesheet): + def setUp(self): + super(TestTimesheet, self).setUp() + + # Crappy hack to disable the rule from timesheet grid, if it exists + # The registry doesn't contain the field timesheet_manager_id. + # but there is an ir.rule about it, crashing during its evaluation + rule = self.env.ref('timesheet_grid.timesheet_line_rule_user_update-unlink', raise_if_not_found=False) + if rule: + rule.active = False + def test_log_timesheet(self): """ Test when log timesheet : check analytic account, user and employee are correctly set. """ Timesheet = self.env['account.analytic.line'] @@ -260,12 +270,18 @@ class TestTimesheet(TestCommonTimesheet): timesheets = timesheet_1 + timesheet_2 # increase unit_amount to trigger amount recomputation - timesheets.sudo().write({ + with self.assertRaises(AccessError): + # because the employee 1 is the sudo and he doesn't have the access right to update timesheet of employee 2 + timesheets.sudo().write({ + 'unit_amount': 2, + }) + + timesheets.with_user(self.user_manager).write({ 'unit_amount': 2, }) # since timesheet costs are different for both employees, we should get different amounts - self.assertRecordValues(timesheets, [{ + self.assertRecordValues(timesheets.with_user(self.user_manager), [{ 'amount': -10.0, }, { 'amount': -12.0, diff --git a/addons/hr_timesheet/views/hr_timesheet_views.xml b/addons/hr_timesheet/views/hr_timesheet_views.xml index eaf5013e498..c97cc5b0cc3 100644 --- a/addons/hr_timesheet/views/hr_timesheet_views.xml +++ b/addons/hr_timesheet/views/hr_timesheet_views.xml @@ -258,7 +258,8 @@ + action="timesheet_action_all" + groups="hr_timesheet.group_hr_timesheet_approver"/> From c9da1571498ad1ded6b16d1483b8cb084d71f9de Mon Sep 17 00:00:00 2001 From: "Xavier Bol (xbo)" Date: Fri, 25 Oct 2019 17:07:26 +0200 Subject: [PATCH 02/14] [IMP] hr_timesheet: add timer into list and kanban views for timesheets TaskID: 2088586 Timer in the list and kanban views : ---------------------------------- In kanban and list views, It's possible to update the 'unit_amount' field with a timer. The user can easily launch the timer in these views for one of his timesheets. One timesheet can be launched at the same time, when the user start a timer for a timesheet and start another timer for another timesheet, the first timer stop and the new timer start. The timer is usable only if the encoding unit is 'Hours' and the timesheet is not validated. To do this : a new computed field 'display_timer' is created in account.analytic.line, this field is set to True is the encoding unit is 'Hours'. With this field, we can easily hide and display the timer in different views. The user can only use the timer for his timesheets. Because the user with the access rights to see all timesheets might launch and/or stop a timer of a timesheet of other user. That is the timer must be accessed only in 'My Timesheets' menu. Add pivot view for 'All timesheets' menu. Creation a timer_mixin : ---------------------- With this timer mixin, we can have a generic timer to use in other models that they need to use a timer like account.analytic.line and project.task models The timer in account.analytic.line model has changed to use the timer mixin. Specific behaviour of the timer : ------------------------------- When the user use the timer and the minutes spent with the timer is less than 1 minute, we don't add in unit_amount of timesheet. If the unit_amount is equals to 0 and minutes spent of timer is less than 0. Then the timesheet created by the timer is deleted because it is useless to have a timesheet with unit_amount set on 0. Misc : ---- Add some styles for the timer button and for the 'unit_amount' field when the timer is running. remove duplicated fields in tree view (record id='timesheet_view_tree_user') --- addons/hr_timesheet/models/__init__.py | 1 + addons/hr_timesheet/models/hr_timesheet.py | 74 +++++++++++- addons/hr_timesheet/models/timer_mixin.py | 67 ++++++++++ .../security/hr_timesheet_security.xml | 60 +++++---- .../hr_timesheet/security/ir.model.access.csv | 4 +- addons/hr_timesheet/static/src/js/Timer.js | 92 ++++++++++++++ .../static/src/js/timer_toggle_button.js | 77 ++++++++++++ .../static/src/js/timesheet_kanban_view.js | 34 ++++++ .../static/src/js/timesheet_list_view.js | 34 ++++++ .../static/src/js/timesheet_uom.js | 114 +++++++++++++++--- .../static/src/scss/timer_button.scss | 26 ++++ addons/hr_timesheet/views/assets.xml | 7 +- .../hr_timesheet/views/hr_timesheet_views.xml | 25 +++- 13 files changed, 548 insertions(+), 67 deletions(-) create mode 100644 addons/hr_timesheet/models/timer_mixin.py create mode 100644 addons/hr_timesheet/static/src/js/Timer.js create mode 100644 addons/hr_timesheet/static/src/js/timer_toggle_button.js create mode 100644 addons/hr_timesheet/static/src/js/timesheet_kanban_view.js create mode 100644 addons/hr_timesheet/static/src/js/timesheet_list_view.js create mode 100644 addons/hr_timesheet/static/src/scss/timer_button.scss diff --git a/addons/hr_timesheet/models/__init__.py b/addons/hr_timesheet/models/__init__.py index ee021539a0f..343cff74edd 100644 --- a/addons/hr_timesheet/models/__init__.py +++ b/addons/hr_timesheet/models/__init__.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from . import timer_mixin from . import hr_employee from . import hr_timesheet from . import ir_http diff --git a/addons/hr_timesheet/models/hr_timesheet.py b/addons/hr_timesheet/models/hr_timesheet.py index f7cad827761..a5995f2ab0a 100644 --- a/addons/hr_timesheet/models/hr_timesheet.py +++ b/addons/hr_timesheet/models/hr_timesheet.py @@ -9,7 +9,8 @@ from odoo.exceptions import UserError, AccessError from odoo.osv import expression class AccountAnalyticLine(models.Model): - _inherit = 'account.analytic.line' + _name = 'account.analytic.line' + _inherit = ['account.analytic.line', 'timer.mixin'] @api.model def default_get(self, field_list): @@ -48,6 +49,12 @@ class AccountAnalyticLine(models.Model): department_id = fields.Many2one('hr.department', "Department", compute='_compute_department_id', store=True, compute_sudo=True) encoding_uom_id = fields.Many2one('uom.uom', compute='_compute_encoding_uom_id') + # Timer Fields + display_timer = fields.Boolean( + compute='_compute_display_timer', + help="used to display the timer if the encoding unit is 'Hours'" + ) + def _compute_encoding_uom_id(self): for analytic_line in self: analytic_line.encoding_uom_id = self.env.company.timesheet_encode_uom_id @@ -100,10 +107,8 @@ class AccountAnalyticLine(models.Model): def write(self, values): # If it's a basic user then check if the timesheet is his own. - for record in self: - if not self.user_has_groups('hr_timesheet.group_hr_timesheet_approver'): - if self.env.user.id != record.user_id.id: - raise AccessError(_("You aren't allowed to access to the timesheet that it's not yours")) + if not self.user_has_groups('hr_timesheet.group_hr_timesheet_approver') and any(self.env.user.id != analytic_line.user_id.id for analytic_line in self): + raise AccessError(_("You cannot access timesheets that are not yours.")) values = self._timesheet_preprocess(values) result = super(AccountAnalyticLine, self).write(values) @@ -201,3 +206,62 @@ class AccountAnalyticLine(models.Model): 'amount': amount_converted, }) return result + + # ---------------------------------------------------- + # Timer Methods + # ---------------------------------------------------- + def _compute_display_timer(self): + """ Check if the encoding unit is 'Hours', + + if yes then the timer button is displayed + """ + for analytic_line in self: + analytic_line.display_timer = analytic_line.encoding_uom_id.id == self.env.ref('uom.product_uom_hour').id + + def action_timer_start(self): + """ Action start the timer of the current timesheet. + + Start timer and search if another timer hasn't been launched. + If yes, then stop the timer before launch this timer. + """ + self.ensure_one() + if not self.timer_start and self.display_timer: + self._stop_running_timers() + super().action_timer_start() + + def _stop_running_timers(self): + """ Search if a timesheet has a timer activated and stop the timer. + + Check if a timer is activated for another timesheet + if yes, then update unit_amount field and stop timer, + otherwise, do nothing. + """ + analytic_line = self.search([('timer_start', '!=', False), ('user_id', '=', self.env.uid)]) + + if analytic_line: + analytic_line.action_timer_stop() + + def action_timer_stop(self): + """ Action stop the timer of the current timesheet. + + When the timer must be stopped, we must calculate the new + unit_amount based on the timer and the previous value of + unit_amount for the current timesheet. + """ + self.ensure_one() + if self.timer_start and self.display_timer: + minutes_spent = self._get_minutes_spent() + + if self.unit_amount == 0 and minutes_spent < 1: + # Check if unit_amount equals 0 and minutes_spent is less than 1 minute, + # if yes, then remove the timesheet + self.unlink() + else: + if minutes_spent < 1: + amount = self.unit_amount + else: + amount = self.unit_amount + minutes_spent * 60 / 3600 + + self.write({'unit_amount': amount}) + + super().action_timer_stop() diff --git a/addons/hr_timesheet/models/timer_mixin.py b/addons/hr_timesheet/models/timer_mixin.py new file mode 100644 index 00000000000..71f916c5dd2 --- /dev/null +++ b/addons/hr_timesheet/models/timer_mixin.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models, fields, api + + +class TimerMixin(models.AbstractModel): + _name = 'timer.mixin' + _description = 'Timer Mixin' + + timer_start = fields.Datetime("Timer Start") + timer_pause = fields.Datetime("Timer Last Pause") + + # is_timer_running field is used with timer_toggle_button widget + # to create a timer button in the view. + # If the timer field is set on False, + # then it displays a button with fa-icon-play icon. + # Otherwise, it displays a button with fa-icon-stop icon + is_timer_running = fields.Boolean(compute="_compute_timer") + + @api.depends('timer_start') + def _compute_timer(self) -> None: + for record in self: + record.is_timer_running = bool(record.timer_start) + + def action_timer_start(self) -> None: + """ Action start the timer. + + Start timer and search if another timer hasn't been launched. + If yes, then stop the timer before launch this timer. + """ + self.ensure_one() + if not self.timer_start: + self.write({'timer_start': fields.Datetime.now()}) + + def action_timer_stop(self): + """ Stop the timer and return the spent minutes since it started + :return minutes_spent if the timer is started, + otherwise return False + """ + self.ensure_one() + if not self.timer_start: + return False + minutes_spent = self._get_minutes_spent() + self.write({'timer_start': False, 'timer_pause': False}) + return minutes_spent + + def _get_minutes_spent(self) -> float: + """ Compute the minutes spent with the timer + + :return minutes spent + """ + start_time = self.timer_start + stop_time = fields.Datetime.now() + + # timer was either running or paused + if self.timer_pause: + start_time += (stop_time - self.timer_pause) + + return (stop_time - start_time).total_seconds() / 60 + + def action_timer_pause(self) -> None: + self.write({'timer_pause': fields.Datetime.now()}) + + def action_timer_resume(self) -> None: + new_start = self.timer_start + (fields.Datetime.now() - self.timer_pause) + self.write({'timer_start': new_start, 'timer_pause': False}) diff --git a/addons/hr_timesheet/security/hr_timesheet_security.xml b/addons/hr_timesheet/security/hr_timesheet_security.xml index ca969ce739d..43bea381021 100644 --- a/addons/hr_timesheet/security/hr_timesheet_security.xml +++ b/addons/hr_timesheet/security/hr_timesheet_security.xml @@ -13,13 +13,23 @@ + + See all timesheets + + + + + + Administrator + + + + + - - - account.analytic.line.timesheet.user @@ -39,22 +49,22 @@ - - See all timesheets - - + + account.analytic.line.timesheet.approver + + [ + ('project_id', '!=', False), + '|', + ('project_id.privacy_visibility', '!=', 'followers'), + ('project_id.message_partner_ids', 'in', [user.partner_id.id]) + ] + + + + + - - Administrator - - - - - - - - account.analytic.line.timesheet.manager @@ -67,20 +77,4 @@ - - - account.analytic.line.timesheet.approver - - [ - ('project_id', '!=', False), - '|', - ('project_id.privacy_visibility', '!=', 'followers'), - ('project_id.message_partner_ids', 'in', [user.partner_id.id]) - ] - - - - - - diff --git a/addons/hr_timesheet/security/ir.model.access.csv b/addons/hr_timesheet/security/ir.model.access.csv index 84cb43d62e1..cb11fb119f7 100644 --- a/addons/hr_timesheet/security/ir.model.access.csv +++ b/addons/hr_timesheet/security/ir.model.access.csv @@ -1,7 +1,5 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_account_analytic_line_manager,analytic.account.analytic.line.timesheet.manager,analytic.model_account_analytic_line,hr_timesheet.group_timesheet_manager,1,1,1,1 -access_account_analytic_line_approver,analytic.account.analytic.line.timesheet.approver,analytic.model_account_analytic_line,hr_timesheet.group_hr_timesheet_approver,1,1,1,1 -access_account_analytic_line_user,analytic.account.analytic.line.timesheet.user,analytic.model_account_analytic_line,hr_timesheet.group_hr_timesheet_user,1,1,1,1,1 +access_account_analytic_line_user,analytic.account.analytic.line.timesheet.user,analytic.model_account_analytic_line,hr_timesheet.group_hr_timesheet_user,1,1,1,1 access_account_analytic_user,analytic.account.analytic.timesheet.user,analytic.model_account_analytic_account,hr_timesheet.group_hr_timesheet_user,1,1,0,0 access_uom_uom_hr_timesheet,uom.uom.timesheet.user,uom.model_uom_uom,hr_timesheet.group_hr_timesheet_user,1,0,0,0 access_project_project,project.project.timesheet.user,model_project_project,hr_timesheet.group_hr_timesheet_user,1,0,0,0 diff --git a/addons/hr_timesheet/static/src/js/Timer.js b/addons/hr_timesheet/static/src/js/Timer.js new file mode 100644 index 00000000000..0c2c6baa7c2 --- /dev/null +++ b/addons/hr_timesheet/static/src/js/Timer.js @@ -0,0 +1,92 @@ +odoo.define('hr_timesheet.Timer', function (require) { +"use strict"; + + /** + * This class allow to facilitate to display the timer + * in unit_amount field of account.analytic.line. + */ + class Timer { + constructor(hours, minutes, seconds) { + this.hours = hours; + this.minutes = minutes; + this.seconds = seconds; + } + + static convertFloatToTime(float) { + if (float === 0) { + return new Timer(0, 0, 0); + } + + let minutes = float % 1; + const hours = float - minutes; + minutes *= 60; + + return new Timer(hours, Math.round(minutes), 0); + } + + addHours(hours) { + this.hours += hours; + } + + addMinutes(minutes) { + minutes += this.minutes; + + this.minutes = minutes % 60; + this.addHours(Math.floor(minutes / 60)); + } + + addSeconds(seconds) { + seconds += this.seconds; + this.seconds = seconds % 60; + this.addMinutes(Math.floor(seconds / 60)); + } + + display2digits(number) { + return number > 9 ? "" + number : "0" + number; + } + + addSecond() { + this.seconds += 1; + if (this.seconds === 60) { + this.minutes += 1; + this.seconds = 0; + + if (this.minutes === 60) { + this.hours += 1; + this.minutes = 0; + } + } + } + + addTime(time) { + if (typeof time == 'string' && time.indexOf(':') !== -1) { + let [hour, minute, second] = time.split(':'); + + hour = parseInt(hour); + minute = parseInt(minute); + second = parseInt(second); + + this.addSeconds(second); + this.addMinutes(minute); + this.addHours(hour); + } + } + + convertToFloat() { + return (this.hours * 60 + this.minutes) * 60 / 3600; + } + + toString() { + const time = { + hours: this.display2digits(this.hours), + minutes: this.display2digits(this.minutes), + seconds: this.display2digits(this.seconds) + }; + + return `${time.hours}:${time.minutes}:${time.seconds}`; + } + } + + return Timer; + +}); diff --git a/addons/hr_timesheet/static/src/js/timer_toggle_button.js b/addons/hr_timesheet/static/src/js/timer_toggle_button.js new file mode 100644 index 00000000000..3a0fabd28c9 --- /dev/null +++ b/addons/hr_timesheet/static/src/js/timer_toggle_button.js @@ -0,0 +1,77 @@ +odoo.define('hr_timesheet.timer_toggle_button', function (require) { +"use strict"; + +const fieldRegistry = require('web.field_registry'); +const { FieldToggleBoolean } = require("web.basic_fields"); + +/** + * The TimerToggleButton is used to display correctly the button + * to start or stop a timer for a timesheet in kanban, list and grid + * views. + */ +const TimerToggleButton = FieldToggleBoolean.extend({ + /** + * @override + * @private + */ + _render: function () { + // When the is_timer_running field is false, then the button is used to start the timer + const title = this.value ? 'stop' : 'play'; + const name = this.value ? 'action_timer_stop' : 'action_timer_start'; + const label = this.value ? 'stop' : 'start'; + + this.$('i') + .addClass('fa') + .toggleClass('fa-stop-circle o-timer-stop-button', this.value) + .toggleClass('fa-play-circle o-timer-play-button', !this.value) + .attr('title', title); + + this.$el.addClass('o-timer-button'); + this.$el.attr('title', title); + this.$el.attr('name', name); + this.$el.attr('aria-label', label); + this.$el.attr('aria-pressed', this.value); + this.$el.attr('type', 'button'); + this.$el.attr('role', 'button'); + }, + /** + * Toggle the button + * + * When the user click on this button, + * - the action "action_timer_start" is called + * into the account.analytic.line model, + * if the value of is_timer_running field is set on false. + * - the action "action_timer_stop" is called + * into the account.analytic.line model, + * if the value of is_timer_running field is set on true. + * Then we change the value of the is_timer_running. + * @override + * @private + * @param {MouseEvent} event + */ + _onToggleButton: async function (event) { + event.stopPropagation(); + + const result = await this._rpc({ + model: 'account.analytic.line', + method: this._getActionButton(), + args: [this.res_id] + }); + + this.trigger_up('timer_changed', { + id: this.res_id, + is_timer_running: !this.value + }); + + this._setValue(!this.value); + }, + _getActionButton: function () { + return this.value ? 'action_timer_stop' : 'action_timer_start'; + } +}); + +fieldRegistry.add('timer_toggle_button', TimerToggleButton); + +return TimerToggleButton; + +}); diff --git a/addons/hr_timesheet/static/src/js/timesheet_kanban_view.js b/addons/hr_timesheet/static/src/js/timesheet_kanban_view.js new file mode 100644 index 00000000000..9a577a9895d --- /dev/null +++ b/addons/hr_timesheet/static/src/js/timesheet_kanban_view.js @@ -0,0 +1,34 @@ +odoo.define('hr_timesheet.timesheet_kanban_view', function (require) { +"use strict"; + +const KanbanController = require('web.KanbanController'); +const KanbanView = require('web.KanbanView'); +const viewRegistry = require('web.view_registry'); + +/** + * @override the KanbanController to add a trigger when a timer is launched or stopped + */ +const TimesheetKanbanController = KanbanController.extend({ + custom_events: _.extend({}, KanbanController.prototype.custom_events, { + 'timer_changed': '_onTimerChanged', + }), + /** + * When a timer is launched or stopped, we reload the view to see the updating. + * @param {Object} event + */ + _onTimerChanged: function (event) { + this.reload(); + } +}); + +const TimesheetKanbanView = KanbanView.extend({ + config: _.extend({}, KanbanView.prototype.config, { + Controller: TimesheetKanbanController + }) +}); + +viewRegistry.add('timesheet_kanban_view', TimesheetKanbanView); + +return { TimesheetKanbanController, TimesheetKanbanView }; + +}); diff --git a/addons/hr_timesheet/static/src/js/timesheet_list_view.js b/addons/hr_timesheet/static/src/js/timesheet_list_view.js new file mode 100644 index 00000000000..903352a35de --- /dev/null +++ b/addons/hr_timesheet/static/src/js/timesheet_list_view.js @@ -0,0 +1,34 @@ +odoo.define('hr_timesheet.timesheet_list_view', function (require) { +"use strict"; + +const ListController = require('web.ListController'); +var ListView = require('web.ListView'); +var viewRegistry = require('web.view_registry'); + +/** + * @override the ListController to add a trigger when the timer is launched or stopped + */ +const TimesheetListController = ListController.extend({ + custom_events: _.extend({}, ListController.prototype.custom_events, { + 'timer_changed': '_onTimerChanged', + }), + /** + * When a timer is launched or stopped, we reload the view to see the updating. + * @param {Object} event + */ + _onTimerChanged: function (event) { + this.reload(); + } +}); + +const TimesheetListView = ListView.extend({ + config: _.extend({}, ListView.prototype.config, { + Controller: TimesheetListController + }) +}); + +viewRegistry.add('timesheet_tree', TimesheetListView); + +return { TimesheetListController, TimesheetListView }; + +}); diff --git a/addons/hr_timesheet/static/src/js/timesheet_uom.js b/addons/hr_timesheet/static/src/js/timesheet_uom.js index 89945ef164a..3e9e192b8f1 100644 --- a/addons/hr_timesheet/static/src/js/timesheet_uom.js +++ b/addons/hr_timesheet/static/src/js/timesheet_uom.js @@ -1,24 +1,25 @@ odoo.define('hr_timesheet.timesheet_uom', function (require) { 'use strict'; -var AbstractField = require('web.AbstractField'); -var basicFields = require('web.basic_fields'); -var fieldUtils = require('web.field_utils'); +const basicFields = require('web.basic_fields'); +const fieldUtils = require('web.field_utils'); -var fieldRegistry = require('web.field_registry'); +const fieldRegistry = require('web.field_registry'); + +const Timer = require('hr_timesheet.Timer'); // We need the field registry to be populated, as we bind the // timesheet_uom widget on existing field widgets. require('web._field_registry'); -var session = require('web.session'); +const session = require('web.session'); /** * Extend the float factor widget to set default value for timesheet * use case. The 'factor' is forced to be the UoM timesheet * conversion from the session info. **/ -var FieldTimesheetFactor = basicFields.FieldFloatFactor.extend({ +const FieldTimesheetFactor = basicFields.FieldFloatFactor.extend({ formatType: 'float_factor', /** * Override init to tweak options depending on the session_info @@ -44,7 +45,7 @@ var FieldTimesheetFactor = basicFields.FieldFloatFactor.extend({ * native widget, and the 'factor' is forced to be the UoM timesheet * conversion. **/ -var FieldTimesheetToggle = basicFields.FieldFloatToggle.extend({ +const FieldTimesheetToggle = basicFields.FieldFloatToggle.extend({ formatType: 'float_factor', /** * Override init to tweak options depending on the session_info @@ -72,6 +73,68 @@ var FieldTimesheetToggle = basicFields.FieldFloatToggle.extend({ }); +/** + * Extend float time widget to add the using of a timer for duration + * (unit_amount) field. + */ +const FieldTimesheetTime = basicFields.FieldFloatTime.extend({ + init: function () { + this._super.apply(this, arguments); + + if (session.timesheet_uom_factor) { + this.nodeOptions.factor = session.timesheet_uom_factor; + this.parseOptions.factor = session.timesheet_uom_factor; + } + }, + _render: function () { + this._super.apply(this, arguments); + + // Check if the timer_start exists and it's not false + // In other word, when user clicks on play button, this button + // launches the "action_timer_start". + if (this.recordData.timer_start) { + this.time = this._createTimer(this.recordData.unit_amount, this.recordData.timer_start); + return this._startTimeCounter(); + } else if (this.mode === "edit") { + return this._renderEdit(); + } else if (this.mode === 'readonly') { + return this._renderReadonly(); + } + }, + /** + * @override + */ + destroy: function () { + this._super.apply(this, arguments); + clearTimeout(this.timer); + }, + _createTimer: function (unit_amount, timer_start) { + const timer = Timer.convertFloatToTime(unit_amount); + + timer.addTime( + moment.utc( + moment() + .diff(moment(timer_start)) + ).format("HH:mm:ss") + ); + + return timer; + }, + _startTimeCounter: function () { + if (this.time) { + this.timer = setInterval(() => { + this.time.addSecond(); + this.$el.text(this.time.toString()); + this.$el.addClass('font-weight-bold text-danger'); + }, 1000); + } else { + clearTimeout(this.timer); + this.$el.removeClass('font-weight-bold text-danger'); + } + }, +}); + + /** * Binding depending on Company Preference * @@ -80,22 +143,27 @@ var FieldTimesheetToggle = basicFields.FieldFloatToggle.extend({ * implementation (float_time, float_toggle, ...). The default * value will be 'float_factor'. **/ -var widgetName = 'timesheet_uom' in session ? +const widgetName = 'timesheet_uom' in session ? session.timesheet_uom.timesheet_widget : 'float_factor'; -var FieldTimesheetUom = widgetName === 'float_toggle' ? FieldTimesheetToggle - : ( - ( - fieldRegistry.get(widgetName) && - fieldRegistry.get(widgetName).extend({}) - ) || - FieldTimesheetFactor - ); + +let FieldTimesheetUom = null; + +if (widgetName === 'float_toggle') { + FieldTimesheetUom = FieldTimesheetToggle; +} else if (widgetName === 'float_time') { + FieldTimesheetUom = FieldTimesheetTime; +} else { + FieldTimesheetUom = ( + fieldRegistry.get(widgetName) && + fieldRegistry.get(widgetName).extend({}) + ) || FieldTimesheetFactor; +} fieldRegistry.add('timesheet_uom', FieldTimesheetUom); // bind the formatter and parser method, and tweak the options -var _tweak_options = function(options) { +const _tweak_options = function(options) { if (!_.contains(options, 'factor')) { options.factor = session.timesheet_uom_factor; } @@ -104,15 +172,21 @@ var _tweak_options = function(options) { fieldUtils.format.timesheet_uom = function(value, field, options) { options = _tweak_options(options || {}); - var formatter = fieldUtils.format[FieldTimesheetUom.prototype.formatType]; + const formatter = fieldUtils.format[FieldTimesheetUom.prototype.formatType]; return formatter(value, field, options); }; fieldUtils.parse.timesheet_uom = function(value, field, options) { options = _tweak_options(options || {}); - var parser = fieldUtils.parse[FieldTimesheetUom.prototype.formatType]; + const parser = fieldUtils.parse[FieldTimesheetUom.prototype.formatType]; return parser(value, field, options); }; -return FieldTimesheetUom; +return { + FieldTimesheetUom, + FieldTimesheetFactor, + FieldTimesheetTime, + FieldTimesheetToggle +}; + }); diff --git a/addons/hr_timesheet/static/src/scss/timer_button.scss b/addons/hr_timesheet/static/src/scss/timer_button.scss new file mode 100644 index 00000000000..8d68d4eb9ea --- /dev/null +++ b/addons/hr_timesheet/static/src/scss/timer_button.scss @@ -0,0 +1,26 @@ +.o-timer-button { + &:focus { + border: none !important; + outline: 0 !important; + } + + & > i { + transition: 0.3s; + + &.o-timer-play-button { + color: $link-color !important; + } + + &.o-timer-stop-button { + color: $red !important; + } + } + + &:hover > i.o-timer-play-button { + color: $link-hover-color !important; + } + + &:hover > i.o-timer-stop-button { + color: darken($red, 15%) !important; + } +} \ No newline at end of file diff --git a/addons/hr_timesheet/views/assets.xml b/addons/hr_timesheet/views/assets.xml index 54a2b0af002..e45bab81a91 100644 --- a/addons/hr_timesheet/views/assets.xml +++ b/addons/hr_timesheet/views/assets.xml @@ -2,8 +2,13 @@