diff options
| author | Xavier Bol (xbo) <xbo@odoo.com> | 2019-12-06 10:47:16 +0100 |
|---|---|---|
| committer | Yannick Tivisse <yti@odoo.com> | 2019-12-19 10:20:27 +0100 |
| commit | 15e41f59c7ad0ebeff1ca08cdd0c8c4a57e29394 (patch) | |
| tree | a4c91a4570481433c8153f06d8186bd75862e9ab | |
| parent | 2ace7893f079af17127f6279283a7df353ecbf4d (diff) | |
[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
| -rw-r--r-- | addons/hr_timesheet/__init__.py | 1 | ||||
| -rw-r--r-- | addons/hr_timesheet/__manifest__.py | 1 | ||||
| -rw-r--r-- | addons/hr_timesheet/data/hr_timesheet_data.xml | 41 | ||||
| -rw-r--r-- | addons/hr_timesheet/models/project.py | 80 | ||||
| -rw-r--r-- | addons/hr_timesheet/models/res_config_settings.py | 2 | ||||
| -rw-r--r-- | addons/hr_timesheet/static/src/js/timer.js | 62 | ||||
| -rw-r--r-- | addons/hr_timesheet/views/assets.xml | 1 | ||||
| -rw-r--r-- | addons/hr_timesheet/views/project_views.xml | 83 | ||||
| -rw-r--r-- | addons/hr_timesheet/views/res_config_settings_views.xml | 11 | ||||
| -rw-r--r-- | addons/hr_timesheet/wizard/__init__.py | 4 | ||||
| -rw-r--r-- | addons/hr_timesheet/wizard/project_task_create_timesheet.py | 47 | ||||
| -rw-r--r-- | addons/hr_timesheet/wizard/project_task_create_timesheet_views.xml | 28 |
12 files changed, 339 insertions, 22 deletions
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 @@ <?xml version="1.0" encoding="utf-8"?> <odoo> + <data> + <!-- For a perfect onboarding, we want the tour of project to be consumed + only by admin when installing hr_timesheet, to avoid confusing --> + <record id="web_tour_project_consumed_by_admin" model="web_tour.tour"> + <field name="name">project_tour</field> + <field name="user_id" ref='base.user_admin'/> + </record> - <!-- For a perfect onboarding, we want the tour of project to be consumed - only by admin when installing hr_timesheet, to avoid confusing --> - <record id="web_tour_project_consumed_by_admin" model="web_tour.tour"> - <field name="name">project_tour</field> - <field name="user_id" ref='base.user_admin'/> - </record> + <!-- Set the JS widget --> + <record id="uom.product_uom_day" model="uom.uom"> + <field name="timesheet_widget">float_toggle</field> + </record> - <!-- Set the JS widget --> - <record id="uom.product_uom_day" model="uom.uom"> - <field name="timesheet_widget">float_toggle</field> - </record> + <record id="uom.product_uom_hour" model="uom.uom"> + <field name="timesheet_widget">float_time</field> + </record> - <record id="uom.product_uom_hour" model="uom.uom"> - <field name="timesheet_widget">float_time</field> - </record> - - <!-- Force Analytic account creation for projects allowing timesheet (default is True) --> - <function - model="project.project" - name="_init_data_analytic_account" - eval="[]"/> + <!-- Force Analytic account creation for projects allowing timesheet (default is True) --> + <function + model="project.project" + name="_init_data_analytic_account" + eval="[]"/> + <function model="ir.config_parameter" name="set_param" eval="('hr_timesheet.timesheet_rounding', '15')"/> + <function model="ir.config_parameter" name="set_param" eval="('hr_timesheet.timesheet_min_duration', '15')"/> + </data> </odoo> 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 @@ <xpath expr="." position="inside"> <link type="text/scss" href="/hr_timesheet/static/src/scss/timer_button.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/timer.js"/> <script type="text/javascript" src="/hr_timesheet/static/src/js/Timer.js"/> <script type="text/javascript" src="/hr_timesheet/static/src/js/timesheet_uom.js"/> <script type="text/javascript" src="/hr_timesheet/static/src/js/timer_toggle_button.js"/> diff --git a/addons/hr_timesheet/views/project_views.xml b/addons/hr_timesheet/views/project_views.xml index 0d970bb2785..4d533577f81 100644 --- a/addons/hr_timesheet/views/project_views.xml +++ b/addons/hr_timesheet/views/project_views.xml @@ -59,7 +59,40 @@ </field> </record> - <record model="ir.ui.view" id="view_task_form2_inherited_hr_timesheet"> + <record id="project_view_form_inherit" model="ir.ui.view"> + <field name="name">project.view.form.inherit</field> + <field name="model">project.project</field> + <field name="inherit_id" ref="hr_timesheet.project_invoice_form"/> + <field name="arch" type="xml"> + <xpath expr="//div[@id='timesheet_settings']" position="after"> + <div class="col-lg-6 o_setting_box" attrs="{'invisible': [('allow_timesheets', '=', False)]}"> + <div class="o_setting_left_pane"> + <field name="allow_timesheet_timer"/> + </div> + <div class="o_setting_right_pane"> + <label for="allow_timesheet_timer"/> + <div class="text-muted"> + Track your time using a timer + </div> + </div> + </div> + </xpath> + </field> + </record> + + <record id="project_project_view_form_simplified_inherit" model="ir.ui.view"> + <field name="name">project.project.view.form.simplified.inherit</field> + <field name="model">project.project</field> + <field name="inherit_id" ref="project.project_project_view_form_simplified"/> + <field name="priority">32</field> + <field name="arch" type="xml"> + <xpath expr="//field[@name='allow_timesheets']" position="after"> + <field name="allow_timesheet_timer" invisible="1"/> + </xpath> + </field> + </record> + + <record model="ir.ui.view" id="view_task_form2_inherited"> <field name="name">project.task.form.inherited</field> <field name="model">project.task</field> <field name="inherit_id" ref="project.view_task_form2" /> @@ -221,5 +254,53 @@ </field> </record> + <record id="project_task_view_form" model="ir.ui.view"> + <field name="name">project.task.view.form.inherit.sale.timesheet.enterprise</field> + <field name="model">project.task</field> + <field name="inherit_id" ref="project.view_task_form2"/> + <field name="arch" type="xml"> + <xpath expr="//header" position='inside'> + <field name="company_id" invisible="1"/> + <field name="display_timesheet_timer" invisible="1"/> + <field name="timer_pause" invisible="1" /> + <button class="btn-primary" name="action_timer_start" type="object" string="Start" + attrs="{'invisible': ['|', '|', ('total_hours_spent', '!=', 0), ('timer_start', '!=', False), ('display_timesheet_timer', '=', False)]}" groups="hr_timesheet.group_hr_timesheet_user"/> + <button class="btn-secondary" name="action_timer_start" type="object" string="Start" + attrs="{'invisible': ['|', '|', ('total_hours_spent', '=', 0), ('timer_start', '!=', False), ('display_timesheet_timer', '=', False)]}" groups="hr_timesheet.group_hr_timesheet_user"/> + <button class="btn-primary btn-danger o_fsm_stop" name="action_timer_stop" type="object" string="Stop" + attrs="{'invisible': ['|', ('timer_start', '=', False), ('display_timesheet_timer', '=', False)]}" groups="hr_timesheet.group_hr_timesheet_user"/> + <button class="btn-primary" name="action_timer_pause" type="object" string="Pause" + attrs="{'invisible': ['|', '|', ('timer_pause', '!=', False), ('timer_start', '=', False), ('display_timesheet_timer', '=', False)]}" groups="hr_timesheet.group_hr_timesheet_user"/> + <button class="btn-primary btn-info" name="action_timer_resume" type="object" string="Resume" + attrs="{'invisible': ['|', '|', ('timer_pause', '=', False), ('timer_start', '=', False), ('display_timesheet_timer', '=', False)]}" groups="hr_timesheet.group_hr_timesheet_user"/> + </xpath> + <xpath expr="//field[@name='stage_id']" position="attributes"> + <attribute name="class">ml-2</attribute> + </xpath> + <xpath expr="//field[@name='stage_id']" position="before"> + <field name="timer_start" widget="timesheet_timer" class="text-danger ml-auto h2 ml-4 font-weight-bold" groups="hr_timesheet.group_hr_timesheet_user"/> + </xpath> + </field> + </record> + + <!-- Improve the task kanban in Project --> + <record id="project_task_view_kanban" model="ir.ui.view"> + <field name="name">project.task.kanban.timer</field> + <field name="model">project.task</field> + <field name="inherit_id" ref="project.view_task_kanban"/> + <field name="arch" type="xml"> + <xpath expr="//span[@name='date']" position="before"> + <field name="display_timesheet_timer" invisible="1"/> + <field name="timer_start" invisible="1"/> + <field name="timer_pause" invisible="1"/> + <t t-if="record.display_timesheet_timer && record.timer_pause.raw_value"> + <i class="fa fa-pause text-warning" title="Timer is Paused"></i> + </t> + <t t-elif="record.display_timesheet_timer && record.timer_start.raw_value"> + <i class="fa fa-play text-success" title="Timer is Running"></i> + </t> + </xpath> + </field> + </record> </data> </odoo> diff --git a/addons/hr_timesheet/views/res_config_settings_views.xml b/addons/hr_timesheet/views/res_config_settings_views.xml index 791e7601548..8a983b90570 100644 --- a/addons/hr_timesheet/views/res_config_settings_views.xml +++ b/addons/hr_timesheet/views/res_config_settings_views.xml @@ -72,6 +72,17 @@ </div> </div> </div> + <div class="col-12 col-lg-6 o_setting_box"> + <div class="o_setting_right_pane"> + <strong>Round timesheets</strong> + <div class="o_row w-30"> + <span class="o_light_label"><label for="timesheet_min_duration"/><field name="timesheet_min_duration" class="col-lg-2"/> minutes</span> + </div> + <div class="o_row"> + <span class="o_light_label"><label for="timesheet_rounding"/><field name="timesheet_rounding" class="col-lg-2"/> minutes</span> + </div> + </div> + </div> </div> <div name="section_leaves" groups="base.group_no_one"> <h2>Time Off</h2> diff --git a/addons/hr_timesheet/wizard/__init__.py b/addons/hr_timesheet/wizard/__init__.py new file mode 100644 index 00000000000..9030e15e6e0 --- /dev/null +++ b/addons/hr_timesheet/wizard/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import project_task_create_timesheet diff --git a/addons/hr_timesheet/wizard/project_task_create_timesheet.py b/addons/hr_timesheet/wizard/project_task_create_timesheet.py new file mode 100644 index 00000000000..dd461c29ea9 --- /dev/null +++ b/addons/hr_timesheet/wizard/project_task_create_timesheet.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models, _ +from datetime import datetime +from odoo.exceptions import UserError + + +class ProjectTaskCreateTimesheet(models.TransientModel): + _name = 'project.task.create.timesheet' + _description = "Create Timesheet from task" + + _sql_constraints = [('time_positive', 'CHECK(time_spent > 0)', 'The timesheet\'s time must be positive' )] + + @api.model + def default_get(self, fields): + result = super(ProjectTaskCreateTimesheet, self).default_get(fields) + active_model = self._context.get('active_model') + if active_model != 'project.task': + raise UserError(_("You can only apply this action from a task.")) + + active_id = self._context.get('active_id') + if 'task_id' in fields and active_id: + task_id = self.env['project.task'].browse(active_id) + result['task_id'] = active_id + result['description'] = task_id.name + return result + + time_spent = fields.Float('Time', precision_digits=2) + description = fields.Char('Description') + task_id = fields.Many2one('project.task', "Task", help="Task for which we are creating a sales order", required=True) + + def save_timesheet(self): + values = { + 'task_id': self.task_id.id, + 'project_id': self.task_id.project_id.id, + 'date': datetime.now(), + 'name': self.description, + 'user_id': self.env.uid, + 'unit_amount': self.time_spent, + } + self.task_id.write({ + 'timer_start': False, + 'timer_pause': False, + 'timesheet_timer_last_stop': fields.datetime.now(), + }) + return self.env['account.analytic.line'].create(values) diff --git a/addons/hr_timesheet/wizard/project_task_create_timesheet_views.xml b/addons/hr_timesheet/wizard/project_task_create_timesheet_views.xml new file mode 100644 index 00000000000..2b5deaf854a --- /dev/null +++ b/addons/hr_timesheet/wizard/project_task_create_timesheet_views.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> +<odoo> + + <record id="project_task_create_timesheet_view_form" model="ir.ui.view"> + <field name="name">project.task.create.timesheet.wizard.form</field> + <field name="model">project.task.create.timesheet</field> + <field name="arch" type="xml"> + <form string="Save time"> + <group> + <group> + <field name="task_id" invisible="True"/> + <label for="time_spent"/> + <div class="o_row" > + <field name="time_spent" widget="float_time" required="True"/> + hours + </div> + <field name="description" widget="text" required="True"/> + </group> + </group> + <footer> + <button string="Save" type="object" name="save_timesheet" class="btn btn-primary"/> + <button string="Cancel" special="cancel" type="object" class="btn btn-secondary"/> + </footer> + </form> + </field> + </record> + +</odoo> |
