From 15e41f59c7ad0ebeff1ca08cdd0c8c4a57e29394 Mon Sep 17 00:00:00 2001 From: "Xavier Bol (xbo)" Date: Fri, 6 Dec 2019 10:47:16 +0100 Subject: [PATCH] [MOV] hr_timesheet: move timer from timesheet_grid to hr_timesheet TaskID: 2088586 The timer use in project > task can be usable in community and not only in enterprise. move timer constraint from sale_timesheet_enterprise to hr_timesheet --- addons/hr_timesheet/__init__.py | 1 + addons/hr_timesheet/__manifest__.py | 1 + .../hr_timesheet/data/hr_timesheet_data.xml | 41 ++++----- addons/hr_timesheet/models/project.py | 80 +++++++++++++++++- .../models/res_config_settings.py | 2 + addons/hr_timesheet/static/src/js/timer.js | 62 ++++++++++++++ addons/hr_timesheet/views/assets.xml | 1 + addons/hr_timesheet/views/project_views.xml | 83 ++++++++++++++++++- .../views/res_config_settings_views.xml | 11 +++ addons/hr_timesheet/wizard/__init__.py | 4 + .../wizard/project_task_create_timesheet.py | 47 +++++++++++ .../project_task_create_timesheet_views.xml | 28 +++++++ 12 files changed, 339 insertions(+), 22 deletions(-) create mode 100644 addons/hr_timesheet/static/src/js/timer.js create mode 100644 addons/hr_timesheet/wizard/__init__.py create mode 100644 addons/hr_timesheet/wizard/project_task_create_timesheet.py create mode 100644 addons/hr_timesheet/wizard/project_task_create_timesheet_views.xml diff --git a/addons/hr_timesheet/__init__.py b/addons/hr_timesheet/__init__.py index 5905766d867..4e9b019bd7a 100644 --- a/addons/hr_timesheet/__init__.py +++ b/addons/hr_timesheet/__init__.py @@ -4,3 +4,4 @@ from . import controllers from . import models from . import report +from . import wizard diff --git a/addons/hr_timesheet/__manifest__.py b/addons/hr_timesheet/__manifest__.py index d80c3769250..c9e8db62f0f 100644 --- a/addons/hr_timesheet/__manifest__.py +++ b/addons/hr_timesheet/__manifest__.py @@ -34,6 +34,7 @@ up a management by affair. 'report/report_timesheet_templates.xml', 'views/hr_views.xml', 'data/hr_timesheet_data.xml', + 'wizard/project_task_create_timesheet_views.xml', ], 'demo': [ 'data/hr_timesheet_demo.xml', diff --git a/addons/hr_timesheet/data/hr_timesheet_data.xml b/addons/hr_timesheet/data/hr_timesheet_data.xml index 2ae8070b787..ed572593da6 100644 --- a/addons/hr_timesheet/data/hr_timesheet_data.xml +++ b/addons/hr_timesheet/data/hr_timesheet_data.xml @@ -1,26 +1,29 @@ + + + + project_tour + + - - - project_tour - - + + + float_toggle + - - - float_toggle - + + float_time + - - float_time - - - - + + + + + diff --git a/addons/hr_timesheet/models/project.py b/addons/hr_timesheet/models/project.py index e4ebc4d3389..d50a9571882 100644 --- a/addons/hr_timesheet/models/project.py +++ b/addons/hr_timesheet/models/project.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from math import ceil from odoo import models, fields, api, _ from odoo.exceptions import UserError, ValidationError @@ -16,6 +17,18 @@ class Project(models.Model): ('partner_id', '=?', partner_id), ]""" ) + allow_timesheet_timer = fields.Boolean('Timesheet Timer', default=False, help="Use a timer to record timesheets on tasks") + + _sql_constraints = [ + ('timer_only_when_timesheet', "CHECK((allow_timesheets = 'f' AND allow_timesheet_timer = 'f') OR (allow_timesheets = 't'))", 'The timesheet timer can only be activated on project allowing timesheet.'), + ] + + @api.onchange('partner_id') + def _onchange_partner_id(self): + domain = [] + if self.partner_id: + domain = [('partner_id', '=', self.partner_id.id)] + return {'domain': {'analytic_account_id': domain}} @api.onchange('analytic_account_id') def _onchange_analytic_account(self): @@ -28,6 +41,11 @@ class Project(models.Model): if project.allow_timesheets and not project.analytic_account_id: raise ValidationError(_('To allow timesheet, your project %s should have an analytic account set.' % (project.name,))) + @api.onchange('allow_timesheets') + def _onchange_allow_timesheets(self): + if not self.allow_timesheets: + self.allow_timesheet_timer = False + @api.model def name_create(self, name): """ Create a project with name_create should generate analytic account creation """ @@ -55,6 +73,11 @@ class Project(models.Model): if not project.analytic_account_id and not values.get('analytic_account_id'): project._create_analytic_account() result = super(Project, self).write(values) + if 'allow_timesheet_timer' in values and not values.get('allow_timesheet_timer'): + self.env['project.task'].with_context(active_test=False).search([('project_id', 'in', self.ids)]).write({ + 'timer_start': False, + 'timer_pause': False, + }) return result # --------------------------------------------------- @@ -67,7 +90,8 @@ class Project(models.Model): class Task(models.Model): - _inherit = "project.task" + _name = "project.task" + _inherit = ["project.task", "timer.mixin"] analytic_account_active = fields.Boolean("Analytic Account", related='project_id.analytic_account_id.active', readonly=True) allow_timesheets = fields.Boolean("Allow timesheets", related='project_id.allow_timesheets', help="Timesheets can be logged on this task.", readonly=True) @@ -78,6 +102,12 @@ class Task(models.Model): subtask_effective_hours = fields.Float("Sub-tasks Hours Spent", compute='_compute_subtask_effective_hours', store=True, help="Sum of actually spent hours on the subtask(s)") timesheet_ids = fields.One2many('account.analytic.line', 'task_id', 'Timesheets') + timer_start = fields.Datetime("Timesheet Timer Start", default=None) + timer_pause = fields.Datetime("Timesheet Timer Last Pause") + timesheet_timer_first_start = fields.Datetime("Timesheet Timer First Use", readonly=True) + timesheet_timer_last_stop = fields.Datetime("Timesheet Timer Last Use", readonly=True) + display_timesheet_timer = fields.Boolean("Display Timesheet Time", compute='_compute_display_timesheet_timer') + @api.depends('timesheet_ids.unit_amount') def _compute_effective_hours(self): for task in self: @@ -110,6 +140,11 @@ class Task(models.Model): for task in self: task.subtask_effective_hours = sum(child_task.effective_hours + child_task.subtask_effective_hours for child_task in task.child_ids) + @api.depends('allow_timesheets', 'project_id.allow_timesheet_timer', 'analytic_account_active') + def _compute_display_timesheet_timer(self): + for task in self: + task.display_timesheet_timer = task.allow_timesheets and task.project_id.allow_timesheet_timer and task.analytic_account_active + # --------------------------------------------------------- # ORM # --------------------------------------------------------- @@ -117,7 +152,7 @@ class Task(models.Model): def write(self, values): # a timesheet must have an analytic account (and a project) if 'project_id' in values and self and not values.get('project_id'): - raise UserError(_('This task must be part of a project because there are some timesheets linked to it.')) + raise UserError(_('This task must be part of a project because there are some timesheets linked to it.')) return super(Task, self).write(values) def name_get(self): @@ -137,3 +172,44 @@ class Task(models.Model): 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']) return result + + # --------------------------------------------------------- + # Timer Methods + # --------------------------------------------------------- + + def action_timer_start(self): + self.ensure_one() + if not self.timesheet_timer_first_start: + self.write({'timesheet_timer_first_start': fields.Datetime.now()}) + super(Task, self).action_timer_start() + + def action_timer_stop(self): + self.ensure_one() + if self.timer_start: # timer was either running or paused + minutes_spent = self._get_minutes_spent() + minutes_spent = self._timer_rounding(minutes_spent) + return self._action_create_timesheet(minutes_spent * 60 / 3600) + return False + + def _timer_rounding(self, minutes_spent): + minimum_duration = int(self.env['ir.config_parameter'].sudo().get_param('hr_timesheet.timesheet_min_duration', 0)) + rounding = int(self.env['ir.config_parameter'].sudo().get_param('hr_timesheet.timesheet_rounding', 0)) + minutes_spent = max(minimum_duration, minutes_spent) + if rounding and ceil(minutes_spent % rounding) != 0: + minutes_spent = ceil(minutes_spent / rounding) * rounding + return minutes_spent + + def _action_create_timesheet(self, time_spent): + return { + "name": _("Confirm Time Spent"), + "type": 'ir.actions.act_window', + "res_model": 'project.task.create.timesheet', + "views": [[False, "form"]], + "target": 'new', + "context": { + **self.env.context, + 'active_id': self.id, + 'active_model': 'project.task', + 'default_time_spent': time_spent, + }, + } diff --git a/addons/hr_timesheet/models/res_config_settings.py b/addons/hr_timesheet/models/res_config_settings.py index 2dc6c852589..f8e8f00621c 100644 --- a/addons/hr_timesheet/models/res_config_settings.py +++ b/addons/hr_timesheet/models/res_config_settings.py @@ -18,3 +18,5 @@ class ResConfigSettings(models.TransientModel): related='company_id.timesheet_encode_uom_id', readonly=False, help="""This will set the unit of measure used to encode timesheet. This will simply provide tools 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') 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..e7330755822 --- /dev/null +++ b/addons/hr_timesheet/static/src/js/timer.js @@ -0,0 +1,62 @@ +odoo.define('hr_timesheet.timer', function (require) { +"use strict"; + +var fieldRegistry = require('web.field_registry'); +var AbstractField = require('web.AbstractField'); + +var TimerFieldWidget = AbstractField.extend({ + + /** + * @override + * @private + */ + isSet: function () { + return true; + }, + /** + * @private + */ + _getDuration: function (dateStart, datePause) { + if (datePause && dateStart) { + return moment(datePause).diff(moment(dateStart)); + } + if (dateStart) { + return moment().diff(moment(dateStart)); + } + else return 0; + }, + /** + * @override + * @private + */ + _render: function () { + this._startTimeCounter(); + + }, + /** + * @override + */ + destroy: function () { + this._super.apply(this, arguments); + clearTimeout(this.timer); + }, + /** + * @private + */ + _startTimeCounter: function () { + var self = this; + clearTimeout(this.timer); + if (self.record.data.timer_start) { + this.timer = setTimeout(function () { + self._startTimeCounter(); + }, 1000); + this.$el.text(moment.utc(self._getDuration(self.record.data.timer_start, self.record.data.timer_pause)).format("HH:mm:ss")); + } else if (!self.record.data.timer_pause){ + clearTimeout(this.timer); + } + }, +}); + +fieldRegistry.add('timesheet_timer', TimerFieldWidget); + +}); diff --git a/addons/hr_timesheet/views/assets.xml b/addons/hr_timesheet/views/assets.xml index e45bab81a91..b55907ac4ed 100644 --- a/addons/hr_timesheet/views/assets.xml +++ b/addons/hr_timesheet/views/assets.xml @@ -4,6 +4,7 @@ +