[MERGE] timer: Introduce a new mixin to record time

1/ timer: New mixin to record time on models

    Purpose

    The timer mechanism is already existing on timesheet.

    We would like to introduce it on helpdesk.ticket too, and
    probably on other models afterward.

    On the other hand, only one timer can be launched on a single
    timesheet currently, shared amongst the different users.

    This commit introduced a new mixin on which
    - Each user can launch a timer on each record of the models
      inheriting the mixin.
    - Launching a new timer on a record pauses the timer of the
      user that could be already running.
    - The mixin brings methods to start/stop/pause/resume the
      timer, that could be overridden to implement a specific
      behavior for the related model, for example create a
      timesheet entry.

2/ test_timer: Add test module for the timer mixin

    Purpose
    As testing the mixin requires to create a new model,
    we prefered to create another module.

3/ hr_timesheet: Adapt timer with new timer mixin

    Purpose
    Now that we have a new timer mixin that offers more possibilities
    than what was currently implemented, we can reuse it in the timesheet
    management module.

4/ timesheet_grid: Adapt timer buttons to new mixin

    Purpose
    The timesheets now uses an improved version of the timer,
    using a new mixin.
    Adapt the code accordingly.

5/ helpdesk_timesheet: Use timer mixin on helpdesk tickets

    Purpose
    Now that we have a new timer mixin that offers more possibilities
    than what was currently implemented, we can reuse it in the helpdesk
    management module.

closes odoo/odoo#43778

Taskid: 2149450
Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
This commit is contained in:
Odoo's Mergebot
2020-02-05 18:40:06 +01:00
committed by GitHub
28 changed files with 539 additions and 101 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ It is completely integrated with the cost accounting module. It allows you to se
up a management by affair.
""",
'website': 'https://www.odoo.com/page/timesheet-mobile-app',
'depends': ['hr', 'analytic', 'project', 'uom'],
'depends': ['hr', 'analytic', 'project', 'uom', 'timer'],
'data': [
'security/hr_timesheet_security.xml',
'security/ir.model.access.csv',
-1
View File
@@ -1,7 +1,6 @@
# -*- 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
+22 -31
View File
@@ -199,42 +199,33 @@ class AccountAnalyticLine(models.Model):
def _compute_display_timer(self):
uom_hour = self.env.ref('uom.product_uom_hour')
for analytic_line in self:
analytic_line.display_timer = analytic_line.encoding_uom_id == uom_hour
analytic_line.display_timer = analytic_line.encoding_uom_id == uom_hour and analytic_line.project_id.allow_timesheet_timer
def action_timer_start(self):
""" Start timer and search if another timer hasn't been launched.
If yes, then stop the timer before launch this timer.
""" Start a timer if it isn't already started and the
timesheets allow to track time
"""
if not self.timer_start and self.display_timer:
self._stop_running_timers()
if not self.user_timer_id.timer_start and self.display_timer:
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.
""" Stop the current timer
"""
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()
if self.user_timer_id.timer_start and self.display_timer:
minutes_spent = super().action_timer_stop()
self._add_timesheet_time(minutes_spent)
def _add_timesheet_time(self, 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:
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()
amount = self.unit_amount + minutes_spent * 60 / 3600
self.write({'unit_amount': amount})
def _action_interrupt_user_timers(self):
self.action_timer_stop()
+13 -33
View File
@@ -1,6 +1,5 @@
# -*- 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
@@ -79,7 +78,6 @@ class Project(models.Model):
def _init_data_analytic_account(self):
self.search([('analytic_account_id', '=', False), ('allow_timesheets', '=', True)])._create_analytic_account()
class Task(models.Model):
_name = "project.task"
_inherit = ["project.task", "timer.mixin"]
@@ -94,32 +92,22 @@ 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")
timer_pause = fields.Datetime("Timesheet Timer Last Pause")
# YTI FIXME: Those field seems quite useless
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')
display_timer_start_primary = fields.Boolean(compute='_compute_display_timer_buttons')
display_timer_start_secondary = fields.Boolean(compute='_compute_display_timer_buttons')
display_timer_stop = fields.Boolean(compute='_compute_display_timer_buttons')
display_timer_pause = fields.Boolean(compute='_compute_display_timer_buttons')
display_timer_resume = fields.Boolean(compute='_compute_display_timer_buttons')
@api.depends('display_timesheet_timer', 'timer_start', 'timer_pause', 'total_hours_spent')
def _compute_display_timer_buttons(self):
for task in self:
start_p, start_s, stop, pause, resume = True, True, True, True, True
displays = super()._compute_display_timer_buttons()
start_p, start_s, stop, pause, resume = displays['start_p'], displays['start_p'], displays['stop'], displays['pause'], displays['resume']
if not task.display_timesheet_timer:
start_p, start_s, stop, pause, resume = False, False, False, False, False
else:
if task.timer_start:
start_p, start_s = False, False
if task.timer_pause:
pause = False
else:
resume = False
else:
if not task.timer_start:
stop, pause, resume = False, False, False
if not task.total_hours_spent:
start_s = False
@@ -213,27 +201,19 @@ class Task(models.Model):
return result
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()
if not self.user_timer_id.timer_start and self.display_timesheet_timer:
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)
# timer was either running or paused
if self.user_timer_id.timer_start and self.display_timesheet_timer:
minutes_spent = super().action_timer_stop()
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 = self._timer_rounding(minutes_spent, minimum_duration, rounding)
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": _("Validate Spent Time"),
@@ -244,7 +224,7 @@ class Task(models.Model):
"context": {
**self.env.context,
'active_id': self.id,
'active_model': 'project.task',
'active_model': self._name,
'default_time_spent': time_spent,
},
}
@@ -6,7 +6,7 @@ const fieldUtils = require('web.field_utils');
const fieldRegistry = require('web.field_registry');
const Timer = require('hr_timesheet.Timer');
const Timer = require('timer.Timer');
// We need the field registry to be populated, as we bind the
// timesheet_uom widget on existing field widgets.
@@ -92,9 +92,9 @@ const FieldTimesheetTime = basicFields.FieldFloatTime.extend({
// 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) {
if (this.recordData.timer_start && !this.recordData.timer_pause) {
const time = await this._rpc({
model: this.model,
model: 'timer.timer',
method: 'get_server_time',
args: []
});
+29 -1
View File
@@ -4,6 +4,8 @@
from odoo.tests.common import TransactionCase
from odoo.exceptions import AccessError, UserError
from datetime import datetime, timedelta
class TestCommonTimesheet(TransactionCase):
@@ -35,6 +37,7 @@ class TestCommonTimesheet(TransactionCase):
'allow_timesheets': True,
'partner_id': self.partner.id,
'analytic_account_id': self.analytic_account.id,
'allow_timesheet_timer': True
})
self.task1 = self.env['project.task'].create({
'name': 'Task One',
@@ -82,7 +85,6 @@ class TestCommonTimesheet(TransactionCase):
'user_id': self.user_manager.id,
})
class TestTimesheet(TestCommonTimesheet):
def setUp(self):
@@ -287,3 +289,29 @@ class TestTimesheet(TestCommonTimesheet):
}, {
'amount': -12.0,
}])
def test_minutes_computing_after_timer_stop(self):
""" Test if unit_amount is updated after stoping a timer """
Timesheet = self.env['account.analytic.line']
timesheet_1 = Timesheet.with_user(self.user_employee).create({
'project_id': self.project_customer.id,
'task_id': self.task1.id,
'name': '/',
'unit_amount': 1,
})
# When the timer is less than 1 minute
now = datetime.now()
timesheet_1.with_user(self.user_employee).action_timer_start()
timesheet_1.with_user(self.user_employee).user_timer_id.timer_start = now - timedelta(seconds=28)
timesheet_1.with_user(self.user_employee).action_timer_stop()
self.assertEqual(timesheet_1.unit_amount, 1, 'unit_amount still should have the same value')
# When the timer is greater than 1 minute
now = datetime.now()
timesheet_1.with_user(self.user_employee).action_timer_start()
timesheet_1.with_user(self.user_employee).user_timer_id.timer_start = now - timedelta(minutes=1, seconds=28)
timesheet_1.with_user(self.user_employee).action_timer_stop()
self.assertGreater(timesheet_1.unit_amount, 1, 'unit_amount should be greated than his last value')
-3
View File
@@ -4,10 +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_mixin.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"/>
<script type="text/javascript" src="/hr_timesheet/static/src/js/timesheet_list_view.js"/>
<script type="text/javascript" src="/hr_timesheet/static/src/js/timesheet_kanban_view.js"/>
</xpath>
+1 -1
View File
@@ -282,7 +282,7 @@
<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"/>
<field name="timer_start" widget="timer_timer" class="text-danger ml-auto h2 ml-4 font-weight-bold" groups="hr_timesheet.group_hr_timesheet_user"/>
</xpath>
</field>
</record>
@@ -15,9 +15,6 @@ class ProjectTaskCreateTimesheet(models.TransientModel):
@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:
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
+20
View File
@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
{
'name': 'Timer Tests',
'version': '1.0',
'category': 'Hidden',
'sequence': 8765,
'summary': 'Timer Tests: feature and performance tests for timer',
'description': """This module contains tests related to timer. Those are
contained in a separate module as it contains models used only to perform
tests independently to functional aspects of other models. """,
'depends': ['timer'],
'data': [
'security/ir.model.access.csv',
],
'demo': [
],
'installable': True,
'application': False,
}
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import timer_test
+25
View File
@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class TimerTest(models.Model):
""" A very simple model only inheriting from timer.mixin to test
timer features """
_description = 'Timer Model'
_name = 'timer.test'
_inherit = ['timer.mixin']
name = fields.Char(required=1)
class OverrideInterruptionTimerTest(models.Model):
""" A very simple model inheriting from timer.mixin and
overriding _action_interrupt_user_timers() """
_description = 'Interruption Timer Model'
_name = 'interruption.timer.test'
_inherit = ['timer.mixin']
def _action_interrupt_user_timers(self):
self.action_timer_stop()
@@ -0,0 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_timer_test_user,timer.test.user,model_timer_test,base.group_user,1,1,1,1
access_interruption_timer_test_user,timer.interruption.test.user,model_interruption_timer_test,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_timer_test_user timer.test.user model_timer_test base.group_user 1 1 1 1
3 access_interruption_timer_test_user timer.interruption.test.user model_interruption_timer_test base.group_user 1 1 1 1
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_timer
+141
View File
@@ -0,0 +1,141 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.exceptions import AccessError
from odoo.tests.common import SavepointCase
class TestTimer(SavepointCase):
@classmethod
def setUpClass(cls):
super(TestTimer, cls).setUpClass()
# Setup mixin
cls.test_timer = cls.env['timer.test'].create({'name': 'Timer 1'})
cls.test_timer_bis = cls.env['timer.test'].create({'name': 'Timer 2'})
# Setup users
cls.usr1 = cls.env['res.users'].create({
'name': 'Usr1',
'login': 'Usr1',
'email': 'usr1@test.com',
'groups_id': [(6, 0, [cls.env.ref('base.group_user').id])],
})
cls.usr2 = cls.env['res.users'].create({
'name': 'Usr2',
'login': 'Usr2',
'email': 'usr2@test.com',
'groups_id': [(6, 0, [cls.env.ref('base.group_user').id])],
})
def test_timer_from_self_user(self):
# Start and stop
self.test_timer.action_timer_start()
self.assertEqual(len(self.test_timer.user_timer_id), 1, 'It should have created one timer')
self.test_timer.action_timer_stop()
self.assertEqual(len(self.test_timer.user_timer_id), 0, 'It should have delete the timer')
# Start the first timer then the second one
self.test_timer.action_timer_start()
self.test_timer_bis.action_timer_start()
self.assertEqual(len(self.env['timer.timer'].search([])), 2, 'It should have created 2 timers for the same user')
self.assertFalse(self.test_timer.is_timer_running, "The first timer should be in pause because the second one interrupt it")
self.assertTrue(self.test_timer_bis.is_timer_running, 'The second timer should be running')
# Resume the first one
self.test_timer.action_timer_resume()
self.assertTrue(self.test_timer.is_timer_running, 'The first timer should be running after being resumed')
self.assertFalse(self.test_timer_bis.is_timer_running, 'The second timer should be in pause when another timer has been started')
# Start a new test timer with interruption override
override_test_timer = self.env['interruption.timer.test'].create({})
override_test_timer.action_timer_start()
self.assertEqual(len(self.env['timer.timer'].search([])), 3, 'A third timer should be created')
self.assertFalse(self.test_timer.is_timer_running, 'The first timer has been interrupted and should be in pause')
self.assertFalse(self.test_timer_bis.is_timer_running, 'The second timer has been interrupted and should be in pause')
self.assertTrue(override_test_timer.is_timer_running, 'The third timer should be running')
# Resume another timer to interrupt the new one
self.test_timer_bis.action_timer_resume()
self.assertEqual(len(self.env['timer.timer'].search([])), 2, 'It should remains only 2 timers')
self.assertEqual(len(override_test_timer.user_timer_id), 0, 'The third timer should be deleted because of his override method')
def test_timer_with_many_users(self):
# 2 users, 1 record = 2 timers
self.test_timer.with_user(self.usr1).action_timer_start()
self.test_timer.with_user(self.usr2).action_timer_start()
self.assertEqual(len(self.env['timer.timer'].search([])), 2, 'It should have created two timers')
self.assertEqual(len(self.test_timer.with_user(self.usr1).user_timer_id), 1, 'It should exist only one timer for this user, model and record')
self.assertEqual(len(self.test_timer.with_user(self.usr2).user_timer_id), 1, 'It should exist only one timer for this user, model and record')
# Stop one of them
self.test_timer.with_user(self.usr2).action_timer_stop()
self.assertEqual(len(self.env['timer.timer'].search([])), 1, 'It should have deleted one timer')
self.assertEqual(len(self.test_timer.with_user(self.usr1).user_timer_id), 1, 'It should exist only one timer for this user, model and record')
self.assertEqual(len(self.test_timer.with_user(self.usr2).user_timer_id), 0, 'It shouldn\'t exit one timer for this user, model and record')
def test_timer_rounding(self):
minutes_spent, minimum, rounding = 4.5,10,5
result = self.test_timer._timer_rounding(minutes_spent, minimum, rounding)
self.assertEqual(result, 10, 'It should have been round to the minimum amount')
minutes_spent = 12.4
result = self.test_timer._timer_rounding(minutes_spent, minimum, rounding)
self.assertEqual(result, 15, 'It should have been round to the next multiple of 15')
def test_timer_access_security(self):
# Create usr1's timer1
timer1 = self.env['timer.timer'].with_user(self.usr1).create({
'timer_start' : False,
'timer_pause' : False,
'is_timer_running' : False,
'res_model' : self.test_timer._name,
'res_id' : self.test_timer.id,
'user_id' : self.usr1.id,
})
# Create usr1's timer2
timer2 = self.env['timer.timer'].with_user(self.usr1).create({
'timer_start' : False,
'timer_pause' : False,
'is_timer_running' : False,
'res_model' : self.test_timer_bis._name,
'res_id' : self.test_timer_bis.id,
'user_id' : self.usr1.id,
})
# Start timer2
timer2.action_timer_start()
with self.assertRaises(AccessError):
# Try to create a timer with usr1 for usr2 (Create)
self.env['timer.timer'].with_user(self.usr1).create({
'timer_start' : False,
'timer_pause' : False,
'is_timer_running' : False,
'res_model' : self.test_timer._name,
'res_id' : self.test_timer.id,
'user_id' : self.usr2.id,
})
# Try to start the timer1 with another usr2 (Write)
timer1.with_user(self.usr2).action_timer_start()
# Try to stop the timer2 with usr2 (Unlink)
timer2.with_user(self.usr2).action_timer_stop()
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
+26
View File
@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Timer',
'version': '1.0',
'sequence': 24,
'summary': 'Record time',
'description': """
This module implements a timer.
==========================================
It adds a timer to a view for time recording purpose
""",
'depends': ['web', 'mail'],
'data': [
'security/timer_security.xml',
'security/ir.model.access.csv',
'views/assets.xml',
],
'demo': [],
'installable': True,
'application': False,
'auto_install': False,
}
+5
View File
@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import timer
from . import timer_mixin
@@ -2,29 +2,35 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields, api
from odoo.exceptions import ValidationError
class TimerMixin(models.AbstractModel):
_name = 'timer.mixin'
_description = 'Timer Mixin'
# YTI Note: This mixin is supposed to be extended to
# any models
class TimerTimer(models.Model):
_name = 'timer.timer'
_description = 'Timer Module'
timer_start = fields.Datetime("Timer Start")
timer_pause = fields.Datetime("Timer Last Pause")
is_timer_running = fields.Boolean(compute="_compute_timer")
is_timer_running = fields.Boolean(compute="_compute_is_timer_running")
res_model = fields.Char(required=True)
res_id = fields.Char(required=True)
user_id = fields.Many2one('res.users')
@api.depends('timer_start')
def _compute_timer(self):
_sql_constraints = [(
'unique_timer', 'UNIQUE(res_model, res_id, user_id)',
'Only one timer occurrence by model, record and user')]
@api.depends('timer_start', 'timer_pause')
def _compute_is_timer_running(self):
for record in self:
record.is_timer_running = bool(record.timer_start)
record.is_timer_running = record.timer_start and not record.timer_pause
@api.model
def create(self, vals):
# Reset the user_timer_id to force the recomputation
self.env[vals['res_model']].invalidate_cache(fnames=['user_timer_id'], ids=[vals['res_id']])
return super().create(vals)
def action_timer_start(self):
""" 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()})
@@ -33,7 +39,6 @@ class TimerMixin(models.AbstractModel):
: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()
+135
View File
@@ -0,0 +1,135 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields, api
from math import ceil
class TimerMixin(models.AbstractModel):
_name = 'timer.mixin'
_description = 'Timer Mixin'
timer_start = fields.Datetime(related='user_timer_id.timer_start')
timer_pause = fields.Datetime(related='user_timer_id.timer_pause')
is_timer_running = fields.Boolean(related='user_timer_id.is_timer_running')
user_timer_id = fields.One2many('timer.timer', compute='_compute_user_timer_id', search='_search_user_timer_id')
display_timer_start_primary = fields.Boolean(compute='_compute_display_timer_buttons')
display_timer_stop = fields.Boolean(compute='_compute_display_timer_buttons')
display_timer_pause = fields.Boolean(compute='_compute_display_timer_buttons')
display_timer_resume = fields.Boolean(compute='_compute_display_timer_buttons')
def _search_user_timer_id(self, operator, value):
timers = self.env['timer.timer'].search([
('id', operator, value),
('user_id', '=', self.env.user.id),
])
return [('id', 'in', timers.mapped('res_id'))]
@api.depends_context('uid')
def _compute_user_timer_id(self):
""" Get the timers according these conditions
:user_id is is the current user
:res_id is the current record
:res_model is the current model
limit=1 by security but the search should never have more than one record
"""
for record in self:
record.user_timer_id = self.env['timer.timer'].search([
('user_id', '=', record.env.user.id),
('res_id', '=', record.id),
('res_model', '=', record._name)
], limit=1)
@api.model
def _get_user_timers(self):
# Return user's timers. Can have multiple timers if some are in pause
return self.env['timer.timer'].search([('user_id', '=', self.env.user.id)])
def action_timer_start(self):
""" Start the timer of the current record
First, if a timer is running, stop or pause it
If there isn't a timer for the current record, create one then start it
Otherwise, resume or start it
"""
self.ensure_one()
self._stop_timer_in_progress()
timer = self.user_timer_id
if not timer:
timer = self.env['timer.timer'].create({
'timer_start' : False,
'timer_pause' : False,
'is_timer_running' : False,
'res_model' : self._name,
'res_id' : self.id,
'user_id' : self.env.user.id,
})
timer.action_timer_start()
else:
# Check if it is in pause then resume it or start it
if timer.timer_pause:
timer.action_timer_resume()
else:
timer.action_timer_start()
def action_timer_stop(self):
""" Stop the timer of the current record
Unlink the timer, it's useless to keep the stopped timer.
A new timer can be create if needed
Return the amount of minutes spent
"""
self.ensure_one()
timer = self.user_timer_id
minutes_spent = timer.action_timer_stop()
timer.unlink()
return minutes_spent
def action_timer_pause(self):
self.ensure_one()
timer = self.user_timer_id
timer.action_timer_pause()
def action_timer_resume(self):
self.ensure_one()
self._stop_timer_in_progress()
timer = self.user_timer_id
timer.action_timer_resume()
def _action_interrupt_user_timers(self):
# Interruption is the action called when the timer is stoped by the start of another one
self.action_timer_pause()
def _stop_timer_in_progress(self):
"""
Cancel the timer in progress if there is one
Each model can interrupt the running timer in a specific way
By setting it in pause or stop by example
"""
timer = self._get_user_timers().filtered(lambda t: t.is_timer_running)
if timer:
model = self.env[timer.res_model].browse(timer.res_id)
model._action_interrupt_user_timers()
@api.depends('timer_start', 'timer_pause')
def _compute_display_timer_buttons(self):
for record in self:
start_p, stop, pause, resume = True, True, True, True
if record.timer_start:
start_p = False
if record.timer_pause:
pause = False
else:
resume = False
record.write({
'display_timer_start_primary': start_p,
'display_timer_stop': stop,
'display_timer_pause': pause,
'display_timer_resume': resume,
})
return {'start_p': start_p, 'stop': stop, 'pause': pause, 'resume': resume}
@api.model
def _timer_rounding(self, minutes_spent, minimum, rounding):
minutes_spent = max(minimum, minutes_spent)
if rounding and ceil(minutes_spent % rounding) != 0:
minutes_spent = ceil(minutes_spent / rounding) * rounding
return minutes_spent
@@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_timer_user,timer.timer.user,timer.model_timer_timer,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_timer_user timer.timer.user timer.model_timer_timer base.group_user 1 1 1 1
+32
View File
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<data noupdate="0">
<record id="timer_user_rule_create-write-delete" model="ir.rule">
<field name="name">timer.user.rule.create-write-delete</field>
<field name="model_id" ref="timer.model_timer_timer"/>
<field name="domain_force">[
('user_id', '=', user.id)
]</field>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
<field name="perm_create" eval="1"/>
<field name="perm_write" eval="1"/>
<field name="perm_unlink" eval="1"/>
<field name="perm_read" eval="1"/>
</record>
<record id="timer_not_user_rule_create-write-delete" model="ir.rule">
<field name="name">timer.not.user.rule.create-write-delete</field>
<field name="model_id" ref="timer.model_timer_timer"/>
<field name="domain_force">[
('user_id', '!=', user.id)
]</field>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
<field name="perm_create" eval="0"/>
<field name="perm_write" eval="0"/>
<field name="perm_unlink" eval="0"/>
<field name="perm_read" eval="1"/>
</record>
</data>
</odoo>
@@ -1,9 +1,9 @@
odoo.define('hr_timesheet.timer', function (require) {
odoo.define('timer.timer', function (require) {
"use strict";
var fieldRegistry = require('web.field_registry');
var AbstractField = require('web.AbstractField');
var Timer = require('hr_timesheet.Timer');
var Timer = require('timer.Timer');
var TimerFieldWidget = AbstractField.extend({
@@ -51,13 +51,13 @@ var TimerFieldWidget = AbstractField.extend({
},
_getServerTime: function () {
return this._rpc({
model: 'account.analytic.line',
model: 'timer.timer',
method: 'get_server_time',
args: []
});
}
});
fieldRegistry.add('timesheet_timer', TimerFieldWidget);
fieldRegistry.add('timer_timer', TimerFieldWidget);
});
@@ -1,4 +1,4 @@
odoo.define('hr_timesheet.Timer', function (require) {
odoo.define('timer.Timer', function (require) {
"use strict";
/**
@@ -1,4 +1,4 @@
odoo.define('hr_timesheet.timer_toggle_button', function (require) {
odoo.define('timer.timer_toggle_button', function (require) {
"use strict";
const fieldRegistry = require('web.field_registry');
@@ -51,9 +51,8 @@ const TimerToggleButton = FieldToggleBoolean.extend({
*/
_onToggleButton: async function (event) {
event.stopPropagation();
const result = await this._rpc({
model: 'account.analytic.line',
model: this.model,
method: this._getActionButton(),
args: [this.res_id]
});
@@ -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;
}
}
+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0"?>
<odoo>
<template id="assets_backend" name="timer assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<link type="text/scss" href="/timer/static/src/scss/timer_button.scss" rel="stylesheet"/>
<script type="text/javascript" src="/timer/static/src/js/timer_mixin.js"/>
<script type="text/javascript" src="/timer/static/src/js/timer.js"/>
<script type="text/javascript" src="/timer/static/src/js/timer_toggle_button.js"/>
</xpath>
</template>
</odoo>