[IMP] Timesheets: Back2basics

Add internal project to ease the onboarding
Move timer to enterprise

taskID: 2217930

--
I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr

closes odoo/odoo#52585

Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
This commit is contained in:
Odoo's Mergebot
2020-06-15 18:29:17 +02:00
committed by GitHub
38 changed files with 111 additions and 1253 deletions
+31
View File
@@ -5,3 +5,34 @@ from . import controllers
from . import models
from . import report
from . import wizard
from odoo import api, fields, SUPERUSER_ID, _
def create_internal_project(cr, registry):
env = api.Environment(cr, SUPERUSER_ID, {})
admin = env.ref('base.user_admin', raise_if_not_found=False)
if not admin:
return
project_vals = []
for company in env['res.company'].search([]):
company = company.with_company(company)
project_vals += [{
'name': _('Internal'),
'allow_timesheets': True,
'company_id': company.id,
'task_ids': [(0, 0, {
'name': name,
'company_id': company.id,
}) for name in [_('Training'), _('Meeting')]]
}]
project_ids = env['project.project'].create(project_vals)
env['account.analytic.line'].create([{
'name': _("Analysis"),
'user_id': admin.id,
'date': fields.datetime.today(),
'unit_amount': 0,
'project_id': task.project_id.id,
'task_id': task.id,
} for task in project_ids.task_ids.filtered(lambda t: t.company_id in admin.employee_ids.company_id)])
+2 -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', 'timer'],
'depends': ['hr', 'analytic', 'project', 'uom'],
'data': [
'security/hr_timesheet_security.xml',
'security/ir.model.access.csv',
@@ -42,4 +42,5 @@ up a management by affair.
'installable': True,
'application': False,
'auto_install': False,
'post_init_hook': 'create_internal_project',
}
+1 -42
View File
@@ -9,8 +9,7 @@ from odoo.exceptions import UserError, AccessError
from odoo.osv import expression
class AccountAnalyticLine(models.Model):
_name = 'account.analytic.line'
_inherit = ['account.analytic.line', 'timer.mixin']
_inherit = 'account.analytic.line'
@api.model
def default_get(self, field_list):
@@ -48,9 +47,6 @@ class AccountAnalyticLine(models.Model):
employee_id = fields.Many2one('hr.employee', "Employee", check_company=True, domain=_domain_employee_id)
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')
display_timer = fields.Boolean(
compute='_compute_display_timer',
help="Technical field used to display the timer if the encoding unit is 'Hours'.")
def _compute_encoding_uom_id(self):
for analytic_line in self:
@@ -198,40 +194,3 @@ class AccountAnalyticLine(models.Model):
'amount': amount_converted,
})
return result
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
def action_timer_start(self):
""" Start a timer if it isn't already started and the
timesheets allow to track time
"""
if not self.user_timer_id.timer_start and self.display_timer:
super().action_timer_start()
def action_timer_stop(self):
""" Stop the current timer
"""
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 and not self._context.get('prevent_deletion', False):
# 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:
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)
amount = self.unit_amount + minutes_spent * 60 / 3600
self.write({'unit_amount': amount})
def _action_interrupt_user_timers(self):
self.action_timer_stop()
+2 -89
View File
@@ -16,12 +16,6 @@ class Project(models.Model):
('partner_id', '=?', partner_id),
]"""
)
allow_timesheet_timer = fields.Boolean(
'Timesheet Timer',
compute='_compute_allow_timesheet_timer',
readonly=False,
store=True,
help="Use a timer to record timesheets on tasks")
timesheet_ids = fields.One2many('account.analytic.line', 'project_id', 'Associated Timesheets')
timesheet_encode_uom_id = fields.Many2one('uom.uom', related='company_id.timesheet_encode_uom_id')
@@ -29,10 +23,6 @@ class Project(models.Model):
compute='_compute_total_timesheet_time',
help="Total number of time (in the proper UoM) recorded in the project, rounded to the unit.")
_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 timesheets.'),
]
@api.onchange('analytic_account_id')
def _onchange_analytic_account(self):
if not self.analytic_account_id and self._origin:
@@ -44,11 +34,6 @@ 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.depends('allow_timesheets')
def _compute_allow_timesheet_timer(self):
for project in self:
project.allow_timesheet_timer = project.allow_timesheets
@api.depends('timesheet_ids')
def _compute_total_timesheet_time(self):
for project in self:
@@ -87,13 +72,7 @@ class Project(models.Model):
for project in self:
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.with_context(active_test=False).mapped('task_ids').write({
'timer_start': False,
'timer_pause': False,
})
return result
return super(Project, self).write(values)
@api.model
def _init_data_analytic_account(self):
@@ -120,7 +99,7 @@ class Project(models.Model):
class Task(models.Model):
_name = "project.task"
_inherit = ["project.task", "timer.mixin"]
_inherit = "project.task"
analytic_account_active = fields.Boolean("Active Analytic Account", compute='_compute_analytic_account_active')
allow_timesheets = fields.Boolean("Allow timesheets", related='project_id.allow_timesheets', help="Timesheets can be logged on this task.", readonly=True)
@@ -132,38 +111,6 @@ class Task(models.Model):
subtask_effective_hours = fields.Float("Sub-tasks Hours Spent", compute='_compute_subtask_effective_hours', store=True, help="Time spent on the sub-tasks (and their own sub-tasks) of this task.")
timesheet_ids = fields.One2many('account.analytic.line', 'task_id', 'Timesheets')
# 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_secondary = 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:
if not task.display_timesheet_timer:
task.update({
'display_timer_start_primary': False,
'display_timer_start_secondary': False,
'display_timer_stop': False,
'display_timer_pause': False,
'display_timer_resume': False,
})
else:
super(Task, task)._compute_display_timer_buttons()
task.display_timer_start_secondary = task.display_timer_start_primary
if not task.timer_start:
task.update({
'display_timer_stop': False,
'display_timer_pause': False,
'display_timer_resume': False,
})
if not task.total_hours_spent:
task.display_timer_start_secondary = False
else:
task.display_timer_start_primary = False
@api.depends('project_id.analytic_account_id.active')
def _compute_analytic_account_active(self):
""" Overridden in sale_timesheet """
@@ -204,11 +151,6 @@ 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
def action_view_subtask_timesheet(self):
self.ensure_one()
tasks = self._get_all_subtasks()
@@ -256,35 +198,6 @@ class Task(models.Model):
result['arch'] = self.env['account.analytic.line']._apply_timesheet_label(result['arch'])
return result
def action_timer_start(self):
if not self.user_timer_id.timer_start and self.display_timesheet_timer:
super(Task, self).action_timer_start()
def action_timer_stop(self):
# 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 _action_create_timesheet(self, time_spent):
return {
"name": _("Validate Spent Time"),
"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': self._name,
'default_time_spent': time_spent,
},
}
def unlink(self):
"""
If some tasks to unlink have some timesheets entries, these
+32 -1
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
from odoo import api, fields, models, _
class ResCompany(models.Model):
@@ -36,3 +36,34 @@ class ResCompany(models.Model):
default=_default_timesheet_encode_uom_id, domain=lambda self: [('category_id', '=', self.env.ref('uom.uom_categ_wtime').id)],
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).""")
@api.model_create_multi
def create(self, values):
company = super(ResCompany, self).create(values)
# use sudo as the user could have the right to create a company
# but not to create a project. On the other hand, when the company
# is created, it is not in the allowed_company_ids on the env
company.sudo()._create_internal_project_task()
return company
def _create_internal_project_task(self):
results = []
for company in self:
company = company.with_company(company)
internal_project = company.env['project.project'].sudo().create({
'name': _('Internal'),
'allow_timesheets': True,
'company_id': company.id,
})
company.env['project.task'].sudo().create([{
'name': _('Training'),
'project_id': internal_project.id,
'company_id': company.id,
}, {
'name': _('Meeting'),
'project_id': internal_project.id,
'company_id': company.id,
}])
results.append(internal_project)
return results
@@ -1,34 +0,0 @@
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 };
});
@@ -1,34 +0,0 @@
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 };
});
@@ -6,8 +6,6 @@ const fieldUtils = require('web.field_utils');
const fieldRegistry = require('web.field_registry');
const Timer = require('timer.Timer');
// We need the field registry to be populated, as we bind the
// timesheet_uom widget on existing field widgets.
require('web._field_registry');
@@ -74,8 +72,7 @@ const FieldTimesheetToggle = basicFields.FieldFloatToggle.extend({
/**
* Extend float time widget to add the using of a timer for duration
* (unit_amount) field.
* Extend float time widget
*/
const FieldTimesheetTime = basicFields.FieldFloatTime.extend({
init: function () {
@@ -85,54 +82,7 @@ const FieldTimesheetTime = basicFields.FieldFloatTime.extend({
this.nodeOptions.factor = session.timesheet_uom_factor;
this.parseOptions.factor = session.timesheet_uom_factor;
}
},
willstart() {
const timePromise = this._rpc({
model: 'timer.timer',
method: 'get_server_time',
args: []
}).then((time) => {
this.serverTime = time;
});
return Promise.all([
this._super(...arguments),
timePromise,
]);
},
_render: async function () {
await 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.recordData.timer_pause) {
this.time = Timer.createTimer(this.recordData.unit_amount, this.recordData.timer_start, this.serverTime);
this._startTimeCounter();
}
},
/**
* @override
*/
destroy: function () {
clearTimeout(this.timer);
this._super.apply(this, arguments);
},
_startTimeCounter: function () {
if (this.time) {
this.timer = setInterval(() => {
this.time.addSecond();
if (this.$el.children().length) {
this.$el.contents()[1].replaceWith(this.time.toString());
} else {
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');
}
},
}
});
@@ -48,12 +48,11 @@ const FieldTimesheetTimeTimer = TimesheetUom.FieldTimesheetTime.extend({
_onToggleButton: async function (event) {
const context = this.record.getContext();
const prevent_deletion = this.attrs.options && this.attrs.options.prevent_deletion || false;
event.stopPropagation();
const result = await this._rpc({
model: this.model,
method: this._getActionButton(),
context: $.extend({}, context, {prevent_deletion: prevent_deletion}),
context: context,
args: [this.res_id]
});
@@ -1,26 +0,0 @@
.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;
}
}
@@ -4,8 +4,6 @@
from odoo.tests.common import TransactionCase
from odoo.exceptions import AccessError, UserError
from datetime import datetime, timedelta
class TestCommonTimesheet(TransactionCase):
@@ -37,7 +35,6 @@ 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',
@@ -172,7 +169,6 @@ class TestTimesheet(TestCommonTimesheet):
non_tracked_project = self.env['project.project'].create({
'name': 'Project without timesheet',
'allow_timesheets': False,
'allow_timesheet_timer': False,
'partner_id': self.partner.id,
})
self.assertFalse(non_tracked_project.analytic_account_id, "A non time-tracked project shouldn't generate an analytic account")
@@ -289,77 +285,3 @@ 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')
def test_allow_timesheets_and_timer(self):
"""
Check that a modification of the 'allow_timesheets' field updates correctly the
'allow_timesheet_timer' field.
"""
Project = self.env['project.project']
# case 1: create a project with allow_timesheets set to FALSE
project_1 = Project.create({
'name': 'Project 1',
'allow_timesheets': False,
'partner_id': self.partner.id
})
self.assertFalse(
project_1.allow_timesheet_timer,
"On project creation with 'allow_timesheets' set to FALSE, 'allow_timesheet_timer' shall be set to FALSE")
# case 2: create a project with allow_timesheets set to TRUE
project_2 = Project.create({
'name': 'Project 2',
'allow_timesheets': True,
'partner_id': self.partner.id,
'analytic_account_id': self.analytic_account.id
})
self.assertTrue(
project_2.allow_timesheet_timer,
"On project creation with 'allow_timesheets' set to TRUE, 'allow_timesheet_timer' shall be set to TRUE")
# case 3: change 'allow_timesheets' from FALSE to TRUE
project_1.write({
'allow_timesheets': True
})
self.assertTrue(
project_1.allow_timesheet_timer,
"On 'allow_timesheets' change to TRUE, 'allow_timesheet_timer' shall be set to TRUE")
# case 4: change 'allow_timesheets' from TRUE to FALSE
project_2.write({
'allow_timesheets': False
})
self.assertFalse(
project_2.allow_timesheet_timer,
"On 'allow_timesheets' change to FALSE, 'allow_timesheet_timer' shall be set to FALSE")
-4
View File
@@ -2,12 +2,8 @@
<odoo>
<template id="assets_backend" name="timesheet assets" inherit_id="web.assets_backend">
<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/timesheet_uom.js"/>
<script type="text/javascript" src="/hr_timesheet/static/src/js/timesheet_uom_timer.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>
</template>
</odoo>
@@ -21,16 +21,13 @@
<field name="name">account.analytic.line.tree.hr_timesheet</field>
<field name="model">account.analytic.line</field>
<field name="arch" type="xml">
<tree editable="top" string="Timesheet Activities" js_class="timesheet_tree">
<tree editable="top" string="Timesheet Activities">
<field name="date"/>
<field name="project_id" required="1" context="{'form_view_ref': 'project.project_project_view_form_simplified',}"/>
<field name="task_id" optional="show" widget="task_with_hours" context="{'default_project_id': project_id}" domain="[('project_id', '=', project_id)]"/>
<field name="project_id" required="1" options="{'no_create_edit': True}" context="{'form_view_ref': 'project.project_project_view_form_simplified',}"/>
<field name="task_id" optional="show" options="{'no_create_edit': True}" widget="task_with_hours" context="{'default_project_id': project_id}" domain="[('project_id', '=', project_id)]"/>
<field name="name" optional="show" required="0"/>
<field name="is_timer_running" invisible="1"/>
<field name="unit_amount" optional="show" widget="timesheet_uom_timer" sum="Total" attrs="{'readonly': [('id', '!=', False), ('timer_start', '!=', False)]}" decoration-danger="unit_amount &gt; 24"/>
<field name="unit_amount" optional="show" widget="timesheet_uom" sum="Total" decoration-danger="unit_amount &gt; 24"/>
<field name="company_id" invisible="1"/>
<field name="timer_start" invisible="1"/>
<field name="display_timer" invisible="1"/>
<field name="user_id" invisible="1"/>
</tree>
</field>
@@ -93,9 +90,8 @@
</group>
<group>
<field name="amount"/>
<field name="unit_amount" widget="timesheet_uom" attrs="{'readonly': [('id', '!=', False), ('timer_start', '!=', False)]}" decoration-danger="unit_amount &gt; 24"/>
<field name="unit_amount" widget="timesheet_uom" decoration-danger="unit_amount &gt; 24"/>
<field name="currency_id" invisible="1"/>
<field name="timer_start" invisible="1"/>
</group>
</group>
</sheet>
@@ -160,15 +156,13 @@
<field name="name">account.analytic.line.kanban</field>
<field name="model">account.analytic.line</field>
<field name="arch" type="xml">
<kanban class="o_kanban_mobile" js_class="timesheet_kanban_view">
<kanban class="o_kanban_mobile">
<field name="date"/>
<field name="user_id"/>
<field name="name"/>
<field name="project_id"/>
<field name="task_id" context="{'default_project_id': project_id}" domain="[('project_id', '=', project_id)]"/>
<field name="unit_amount" widget="timesheet_uom"/>
<field name="timer_start" invisible="1"/>
<field name="display_timer" invisible="1"/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_global_click">
@@ -184,9 +178,7 @@
<span>
<t t-esc="record.name.value"/>
</span>
<div class="float-right">
<field name="is_timer_running" widget="timer_toggle_button" options="{'prevent_deletion': True}" attrs="{ 'invisible': [('display_timer', '=', False)] }" />
</div>
<div class="float-right" id="bottom_right"/>
</div>
</div>
</div>
@@ -214,7 +206,6 @@
<field name="view_mode">tree,form</field>
<field name="domain">[('project_id', '!=', False), ('user_id', '=', uid)]</field>
<field name="context">{
"my_timesheet_display_timer": True,
"search_default_week":1,
}</field>
<field name="search_view_id" ref="hr_timesheet_line_my_timesheet_search"/>
@@ -71,39 +71,6 @@
</field>
</record>
<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)]}" id="timer_setting">
<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>
@@ -263,40 +230,6 @@
</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" />
<field name="display_timer_start_primary" invisible="1"/>
<field name="display_timer_start_secondary" invisible="1"/>
<field name="display_timer_stop" invisible="1"/>
<field name="display_timer_pause" invisible="1"/>
<field name="display_timer_resume" invisible="1"/>
<button class="btn-primary" name="action_timer_start" type="object" string="Start"
attrs="{'invisible': [('display_timer_start_primary', '=', False)]}" groups="hr_timesheet.group_hr_timesheet_user"/>
<button class="btn-secondary" name="action_timer_start" type="object" string="Start"
attrs="{'invisible': [('display_timer_start_secondary', '=', 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': [('display_timer_stop', '=', False)]}" groups="hr_timesheet.group_hr_timesheet_user"/>
<button class="btn-primary" name="action_timer_pause" type="object" string="Pause"
attrs="{'invisible': [('display_timer_pause', '=', False)]}" groups="hr_timesheet.group_hr_timesheet_user"/>
<button class="btn-primary btn-info" name="action_timer_resume" type="object" string="Resume"
attrs="{'invisible': [('display_timer_resume', '=', 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="timer_timer" class="text-danger ml-auto h2 ml-4 font-weight-bold" groups="hr_timesheet.group_hr_timesheet_user"/>
</xpath>
</field>
</record>
<record id="project_task_view_search" model="ir.ui.view">
<field name="name">project.task.view.search.inherit.sale.timesheet.enterprise</field>
<field name="model">project.task</field>
@@ -308,24 +241,5 @@
</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="//div[@name='date_deadline']" 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 &amp;&amp; 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 &amp;&amp; record.timer_start.raw_value">
<i class="fa fa-play text-success" title="Timer is Running"></i>
</t>
</xpath>
</field>
</record>
</data>
</odoo>
@@ -36,9 +36,4 @@ class ProjectTaskCreateTimesheet(models.TransientModel):
'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)
@@ -23,27 +23,39 @@ class Company(models.Model):
raise ValidationError(_('The Internal Project of a company should be in that company.'))
def init(self):
self.search([('leave_timesheet_project_id', '=', False)])._create_leave_project_task()
@api.model_create_multi
def create(self, values):
company = super(Company, self).create(values)
# use sudo as the user could have the right to create a company
# but not to create a project. On the other hand, when the company
# is created, it is not in the allowed_company_ids on the env
company.sudo()._create_leave_project_task()
return company
def _create_leave_project_task(self):
for company in self:
for company in self.search([('leave_timesheet_project_id', '=', False)]):
company = company.with_company(company)
if not company.leave_timesheet_project_id:
project = company.env['project.project'].sudo().create({
'name': _('Internal Project'),
project = company.env['project.project'].search([
('name', '=', _('Internal')),
('allow_timesheets', '=', True),
('company_id', '=', company.id),
], limit=1)
if not project:
project = company.env['project.project'].create({
'name': _('Internal'),
'allow_timesheets': True,
'company_id': company.id,
})
company.write({
'leave_timesheet_project_id': project.id,
})
if not company.leave_timesheet_task_id:
task = company.env['project.task'].create({
'name': _('Time Off'),
'project_id': company.leave_timesheet_project_id.id,
'active': False,
'company_id': company.id,
})
company.write({
'leave_timesheet_task_id': task.id,
})
def _create_internal_project_task(self):
projects = super()._create_internal_project_task()
for project in projects:
company = project.company_id
company = company.with_company(company)
if not company.leave_timesheet_project_id:
company.write({
'leave_timesheet_project_id': project.id,
})
+2 -3
View File
@@ -176,14 +176,13 @@ class ProjectTask(models.Model):
display_create_order = fields.Boolean(compute='_compute_display_create_order')
@api.depends(
'allow_billable', 'allow_timesheets', 'sale_order_id', 'display_timesheet_timer',
'timer_start')
'allow_billable', 'allow_timesheets', 'sale_order_id')
def _compute_display_create_order(self):
for task in self:
show = True
if not task.allow_billable or not task.allow_timesheets or \
task.billable_type == 'employee_rate' or not task.partner_id or \
task.sale_order_id or (task.display_timesheet_timer and task.timer_start):
task.sale_order_id:
show = False
task.display_create_order = show
+1 -1
View File
@@ -114,7 +114,7 @@ class SaleOrderLine(models.Model):
def _timesheet_create_project(self):
project = super()._timesheet_create_project()
project.write({'allow_timesheets': True, 'allow_timesheet_timer': True})
project.write({'allow_timesheets': True})
return project
def _recompute_qty_to_invoice(self, date):
@@ -52,9 +52,9 @@
<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="hr_timesheet.project_project_view_form_simplified_inherit"/>
<field name="inherit_id" ref="hr_timesheet.project_project_view_form_simplified_inherit_timesheet"/>
<field name="arch" type="xml">
<field name="allow_timesheet_timer" position="after">
<field name="allow_timesheets" position="after">
<field name="billable_type" invisible="1"/>
<field name="company_id" invisible="1"/>
<field name="allow_billable" attrs="{'invisible': [('billable_type', '=', 'employee_rate')]}"/>
-4
View File
@@ -1,4 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
-20
View File
@@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
{
'name': 'Timer Tests',
'version': '1.0',
'category': 'Hidden/Tests',
'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
@@ -1,4 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import timer_test
-25
View File
@@ -1,25 +0,0 @@
# -*- 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()
@@ -1,3 +0,0 @@
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
@@ -1,4 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_timer
-141
View File
@@ -1,141 +0,0 @@
# -*- 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
@@ -1,4 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
-27
View File
@@ -1,27 +0,0 @@
# -*- 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',
'category': 'Services/Timesheets',
'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,
}
-127
View File
@@ -1,127 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * timer
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server saas~13.2\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-02-13 07:24+0000\n"
"PO-Revision-Date: 2020-02-13 07:24+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: timer
#: model:ir.model.fields,field_description:timer.field_timer_timer__create_uid
msgid "Created by"
msgstr ""
#. module: timer
#: model:ir.model.fields,field_description:timer.field_timer_timer__create_date
msgid "Created on"
msgstr ""
#. module: timer
#: model:ir.model.fields,field_description:timer.field_timer_mixin__display_name
#: model:ir.model.fields,field_description:timer.field_timer_timer__display_name
msgid "Display Name"
msgstr ""
#. module: timer
#: model:ir.model.fields,field_description:timer.field_timer_mixin__display_timer_pause
msgid "Display Timer Pause"
msgstr ""
#. module: timer
#: model:ir.model.fields,field_description:timer.field_timer_mixin__display_timer_resume
msgid "Display Timer Resume"
msgstr ""
#. module: timer
#: model:ir.model.fields,field_description:timer.field_timer_mixin__display_timer_start_primary
msgid "Display Timer Start Primary"
msgstr ""
#. module: timer
#: model:ir.model.fields,field_description:timer.field_timer_mixin__display_timer_stop
msgid "Display Timer Stop"
msgstr ""
#. module: timer
#: model:ir.model.fields,field_description:timer.field_timer_mixin__id
#: model:ir.model.fields,field_description:timer.field_timer_timer__id
msgid "ID"
msgstr ""
#. module: timer
#: model:ir.model.fields,field_description:timer.field_timer_mixin__is_timer_running
#: model:ir.model.fields,field_description:timer.field_timer_timer__is_timer_running
msgid "Is Timer Running"
msgstr ""
#. module: timer
#: model:ir.model.fields,field_description:timer.field_timer_mixin____last_update
#: model:ir.model.fields,field_description:timer.field_timer_timer____last_update
msgid "Last Modified on"
msgstr ""
#. module: timer
#: model:ir.model.fields,field_description:timer.field_timer_timer__write_uid
msgid "Last Updated by"
msgstr ""
#. module: timer
#: model:ir.model.fields,field_description:timer.field_timer_timer__write_date
msgid "Last Updated on"
msgstr ""
#. module: timer
#: model:ir.model.constraint,message:timer.constraint_timer_timer_unique_timer
msgid "Only one timer occurrence by model, record and user"
msgstr ""
#. module: timer
#: model:ir.model.fields,field_description:timer.field_timer_timer__res_id
msgid "Res"
msgstr ""
#. module: timer
#: model:ir.model.fields,field_description:timer.field_timer_timer__res_model
msgid "Res Model"
msgstr ""
#. module: timer
#: model:ir.model.fields,field_description:timer.field_timer_mixin__timer_pause
#: model:ir.model.fields,field_description:timer.field_timer_timer__timer_pause
msgid "Timer Last Pause"
msgstr ""
#. module: timer
#: model:ir.model,name:timer.model_timer_mixin
msgid "Timer Mixin"
msgstr ""
#. module: timer
#: model:ir.model,name:timer.model_timer_timer
msgid "Timer Module"
msgstr ""
#. module: timer
#: model:ir.model.fields,field_description:timer.field_timer_mixin__timer_start
#: model:ir.model.fields,field_description:timer.field_timer_timer__timer_start
msgid "Timer Start"
msgstr ""
#. module: timer
#: model:ir.model.fields,field_description:timer.field_timer_timer__user_id
msgid "User"
msgstr ""
#. module: timer
#: model:ir.model.fields,field_description:timer.field_timer_mixin__user_timer_id
msgid "User Timer"
msgstr ""
-5
View File
@@ -1,5 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import timer
from . import timer_mixin
-70
View File
@@ -1,70 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields, api
from odoo.exceptions import ValidationError
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_is_timer_running")
res_model = fields.Char(required=True)
res_id = fields.Integer(required=True)
user_id = fields.Many2one('res.users')
_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 = 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):
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
"""
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):
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):
self.write({'timer_pause': fields.Datetime.now()})
def action_timer_resume(self):
new_start = self.timer_start + (fields.Datetime.now() - self.timer_pause)
self.write({'timer_start': new_start, 'timer_pause': False})
@api.model
def get_server_time(self):
""" Returns the server time.
The timer widget needs the server time instead of the client time
to avoid time desynchronization issues like the timer beginning at 0:00
and not 23:59 and so on.
"""
return fields.Datetime.now()
@@ -1,2 +0,0 @@
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
-30
View File
@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo noupdate="1">
<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>
</odoo>
-63
View File
@@ -1,63 +0,0 @@
odoo.define('timer.timer', function (require) {
"use strict";
var fieldRegistry = require('web.field_registry');
var AbstractField = require('web.AbstractField');
var Timer = require('timer.Timer');
var TimerFieldWidget = AbstractField.extend({
/**
* @override
* @private
*/
isSet: function () {
return true;
},
/**
* @override
* @private
*/
_render: function () {
this._super.apply(this, arguments);
this._startTimeCounter();
},
/**
* @override
*/
destroy: function () {
this._super.apply(this, arguments);
clearInterval(this.timer);
},
/**
* @private
*/
_startTimeCounter: async function () {
if (this.record.data.timer_start) {
const serverTime = this.record.data.timer_pause || await this._getServerTime();
this.time = Timer.createTimer(0, this.record.data.timer_start, serverTime);
this.$el.text(this.time.toString());
this.timer = setInterval(() => {
if (this.record.data.timer_pause) {
clearInterval(this.timer);
} else {
this.time.addSecond();
this.$el.text(this.time.toString());
}
}, 1000);
} else if (!this.record.data.timer_pause){
clearInterval(this.timer);
}
},
_getServerTime: function () {
return this._rpc({
model: 'timer.timer',
method: 'get_server_time',
args: []
});
}
});
fieldRegistry.add('timer_timer', TimerFieldWidget);
});
-115
View File
@@ -1,115 +0,0 @@
odoo.define('timer.Timer', function (require) {
"use strict";
/**
* This class improves the timer display using the
* unit_amount field on account.analytic.line.
*/
class Timer {
constructor(hours, minutes, seconds) {
this.hours = hours;
this.minutes = minutes;
this.seconds = seconds;
}
/**
* Convert float to time
* @param {number} float
*/
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);
}
/**
* Create timer
* @param {number} unit_amount
* @param {String} timer_start
* @param {String} serverTime
*/
static createTimer(unit_amount, timer_start, serverTime) {
const timer = this.convertFloatToTime(unit_amount);
timer.addTime(
moment.utc(
moment.utc(serverTime)
.diff(moment.utc(timer_start))
).format("HH:mm:ss")
);
return timer;
}
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;
});
@@ -1,80 +0,0 @@
odoo.define('timer.timer_toggle_button', function (require) {
"use strict";
const fieldRegistry = require('web.field_registry');
const { FieldToggleBoolean } = require("web.basic_fields");
const { _lt } = require('web.core');
/**
* 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 ? _lt('Stop') : _lt('Play');
const name = this.value ? 'action_timer_stop' : 'action_timer_start';
const label = this.value ? _lt('Stop') : _lt('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) {
const context = this.record.getContext();
const prevent_deletion = this.attrs.options && this.attrs.options.prevent_deletion || false;
event.stopPropagation();
const result = await this._rpc({
model: this.model,
method: this._getActionButton(),
context: $.extend({}, context, {prevent_deletion: prevent_deletion}),
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;
});
@@ -1,26 +0,0 @@
.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
@@ -1,11 +0,0 @@
<?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>