[MOV] timer: Move to Enterprise
taskId: 2217930
This commit is contained in:
@@ -11,37 +11,28 @@ from odoo import api, fields, SUPERUSER_ID, _
|
||||
|
||||
def create_internal_project(cr, registry):
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
companies_with_internal = env['project.project'].read_group([
|
||||
('name', '=', _('Internal')), ('allow_timesheets', '=', True)
|
||||
], ['company_id'], ['company_id'])
|
||||
company_ids_with_internal = [c['company_id'][0] for c in companies_with_internal]
|
||||
for company in env['res.company'].search([('id', 'not in', company_ids_with_internal)]):
|
||||
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)
|
||||
internal_project = company.env['project.project'].create({
|
||||
project_vals += [{
|
||||
'name': _('Internal'),
|
||||
'allow_timesheets': True,
|
||||
'company_id': company.id,
|
||||
})
|
||||
admin = env.ref('base.user_admin', raise_if_not_found=False)
|
||||
if not admin:
|
||||
continue
|
||||
tasks = company.env['project.task'].create([{
|
||||
'name': _('Training'),
|
||||
'project_id': internal_project.id,
|
||||
'company_id': company.id,
|
||||
}, {
|
||||
'name': _('Meeting'),
|
||||
'project_id': internal_project.id,
|
||||
'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)
|
||||
|
||||
if not admin.employee_ids.filtered(lambda e: e.company_id == company):
|
||||
continue
|
||||
company.env['account.analytic.line'].create([{
|
||||
'name': _("Analysis"),
|
||||
'user_id': admin.id,
|
||||
'date': fields.datetime.today(),
|
||||
'unit_amount': 0,
|
||||
'project_id': internal_project.id,
|
||||
'task_id': task.id,
|
||||
} for task in tasks])
|
||||
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)])
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,48 +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()
|
||||
|
||||
def unlink(self):
|
||||
res = super(AccountAnalyticLine, self).unlink()
|
||||
self.env['timer.timer'].search([
|
||||
('res_model', '=', self._name),
|
||||
('res_id', 'in', self.ids)
|
||||
]).unlink()
|
||||
return res
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 > 24"/>
|
||||
<field name="unit_amount" optional="show" widget="timesheet_uom" sum="Total" decoration-danger="unit_amount > 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 > 24"/>
|
||||
<field name="unit_amount" widget="timesheet_uom" decoration-danger="unit_amount > 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 && 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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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')]}"/>
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
@@ -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,
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import timer_test
|
||||
@@ -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,4 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import test_timer
|
||||
@@ -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()
|
||||
@@ -1,4 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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 ""
|
||||
@@ -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
|
||||
@@ -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,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>
|
||||
@@ -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);
|
||||
|
||||
});
|
||||
@@ -1,119 +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(
|
||||
Math.abs(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 = (second === undefined) ? 0 : parseInt(second);
|
||||
|
||||
this.addSeconds(second);
|
||||
this.addMinutes(minute);
|
||||
this.addHours(hour);
|
||||
}
|
||||
}
|
||||
|
||||
convertToFloat() {
|
||||
return (this.hours * 60 + this.minutes) * 60 / 3600;
|
||||
}
|
||||
|
||||
convertToSeconds() {
|
||||
return (this.hours * 60 + this.minutes) * 60 + this.seconds;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
});
|
||||
@@ -52,12 +52,11 @@ const TimerToggleButton = FieldToggleBoolean.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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user