[MOV] hr_timesheet: move timer from timesheet_grid to hr_timesheet

TaskID: 2088586

The timer use in project > task can be usable in community and not only in enterprise.

move timer constraint from sale_timesheet_enterprise to hr_timesheet
This commit is contained in:
Xavier Bol (xbo)
2019-12-19 10:20:27 +01:00
committed by Yannick Tivisse
parent 2ace7893f0
commit 15e41f59c7
12 changed files with 339 additions and 22 deletions
+1
View File
@@ -4,3 +4,4 @@
from . import controllers
from . import models
from . import report
from . import wizard
+1
View File
@@ -34,6 +34,7 @@ up a management by affair.
'report/report_timesheet_templates.xml',
'views/hr_views.xml',
'data/hr_timesheet_data.xml',
'wizard/project_task_create_timesheet_views.xml',
],
'demo': [
'data/hr_timesheet_demo.xml',
+22 -19
View File
@@ -1,26 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- For a perfect onboarding, we want the tour of project to be consumed
only by admin when installing hr_timesheet, to avoid confusing -->
<record id="web_tour_project_consumed_by_admin" model="web_tour.tour">
<field name="name">project_tour</field>
<field name="user_id" ref='base.user_admin'/>
</record>
<!-- For a perfect onboarding, we want the tour of project to be consumed
only by admin when installing hr_timesheet, to avoid confusing -->
<record id="web_tour_project_consumed_by_admin" model="web_tour.tour">
<field name="name">project_tour</field>
<field name="user_id" ref='base.user_admin'/>
</record>
<!-- Set the JS widget -->
<record id="uom.product_uom_day" model="uom.uom">
<field name="timesheet_widget">float_toggle</field>
</record>
<!-- Set the JS widget -->
<record id="uom.product_uom_day" model="uom.uom">
<field name="timesheet_widget">float_toggle</field>
</record>
<record id="uom.product_uom_hour" model="uom.uom">
<field name="timesheet_widget">float_time</field>
</record>
<record id="uom.product_uom_hour" model="uom.uom">
<field name="timesheet_widget">float_time</field>
</record>
<!-- Force Analytic account creation for projects allowing timesheet (default is True) -->
<function
model="project.project"
name="_init_data_analytic_account"
eval="[]"/>
<!-- Force Analytic account creation for projects allowing timesheet (default is True) -->
<function
model="project.project"
name="_init_data_analytic_account"
eval="[]"/>
<function model="ir.config_parameter" name="set_param" eval="('hr_timesheet.timesheet_rounding', '15')"/>
<function model="ir.config_parameter" name="set_param" eval="('hr_timesheet.timesheet_min_duration', '15')"/>
</data>
</odoo>
+78 -2
View File
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from math import ceil
from odoo import models, fields, api, _
from odoo.exceptions import UserError, ValidationError
@@ -16,6 +17,18 @@ class Project(models.Model):
('partner_id', '=?', partner_id),
]"""
)
allow_timesheet_timer = fields.Boolean('Timesheet Timer', default=False, help="Use a timer to record timesheets on tasks")
_sql_constraints = [
('timer_only_when_timesheet', "CHECK((allow_timesheets = 'f' AND allow_timesheet_timer = 'f') OR (allow_timesheets = 't'))", 'The timesheet timer can only be activated on project allowing timesheet.'),
]
@api.onchange('partner_id')
def _onchange_partner_id(self):
domain = []
if self.partner_id:
domain = [('partner_id', '=', self.partner_id.id)]
return {'domain': {'analytic_account_id': domain}}
@api.onchange('analytic_account_id')
def _onchange_analytic_account(self):
@@ -28,6 +41,11 @@ class Project(models.Model):
if project.allow_timesheets and not project.analytic_account_id:
raise ValidationError(_('To allow timesheet, your project %s should have an analytic account set.' % (project.name,)))
@api.onchange('allow_timesheets')
def _onchange_allow_timesheets(self):
if not self.allow_timesheets:
self.allow_timesheet_timer = False
@api.model
def name_create(self, name):
""" Create a project with name_create should generate analytic account creation """
@@ -55,6 +73,11 @@ class Project(models.Model):
if not project.analytic_account_id and not values.get('analytic_account_id'):
project._create_analytic_account()
result = super(Project, self).write(values)
if 'allow_timesheet_timer' in values and not values.get('allow_timesheet_timer'):
self.env['project.task'].with_context(active_test=False).search([('project_id', 'in', self.ids)]).write({
'timer_start': False,
'timer_pause': False,
})
return result
# ---------------------------------------------------
@@ -67,7 +90,8 @@ class Project(models.Model):
class Task(models.Model):
_inherit = "project.task"
_name = "project.task"
_inherit = ["project.task", "timer.mixin"]
analytic_account_active = fields.Boolean("Analytic Account", related='project_id.analytic_account_id.active', readonly=True)
allow_timesheets = fields.Boolean("Allow timesheets", related='project_id.allow_timesheets', help="Timesheets can be logged on this task.", readonly=True)
@@ -78,6 +102,12 @@ class Task(models.Model):
subtask_effective_hours = fields.Float("Sub-tasks Hours Spent", compute='_compute_subtask_effective_hours', store=True, help="Sum of actually spent hours on the subtask(s)")
timesheet_ids = fields.One2many('account.analytic.line', 'task_id', 'Timesheets')
timer_start = fields.Datetime("Timesheet Timer Start", default=None)
timer_pause = fields.Datetime("Timesheet Timer Last Pause")
timesheet_timer_first_start = fields.Datetime("Timesheet Timer First Use", readonly=True)
timesheet_timer_last_stop = fields.Datetime("Timesheet Timer Last Use", readonly=True)
display_timesheet_timer = fields.Boolean("Display Timesheet Time", compute='_compute_display_timesheet_timer')
@api.depends('timesheet_ids.unit_amount')
def _compute_effective_hours(self):
for task in self:
@@ -110,6 +140,11 @@ class Task(models.Model):
for task in self:
task.subtask_effective_hours = sum(child_task.effective_hours + child_task.subtask_effective_hours for child_task in task.child_ids)
@api.depends('allow_timesheets', 'project_id.allow_timesheet_timer', 'analytic_account_active')
def _compute_display_timesheet_timer(self):
for task in self:
task.display_timesheet_timer = task.allow_timesheets and task.project_id.allow_timesheet_timer and task.analytic_account_active
# ---------------------------------------------------------
# ORM
# ---------------------------------------------------------
@@ -117,7 +152,7 @@ class Task(models.Model):
def write(self, values):
# a timesheet must have an analytic account (and a project)
if 'project_id' in values and self and not values.get('project_id'):
raise UserError(_('This task must be part of a project because there are some timesheets linked to it.'))
raise UserError(_('This task must be part of a project because there are some timesheets linked to it.'))
return super(Task, self).write(values)
def name_get(self):
@@ -137,3 +172,44 @@ class Task(models.Model):
result = super(Task, self)._fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu)
result['arch'] = self.env['account.analytic.line']._apply_timesheet_label(result['arch'])
return result
# ---------------------------------------------------------
# Timer Methods
# ---------------------------------------------------------
def action_timer_start(self):
self.ensure_one()
if not self.timesheet_timer_first_start:
self.write({'timesheet_timer_first_start': fields.Datetime.now()})
super(Task, self).action_timer_start()
def action_timer_stop(self):
self.ensure_one()
if self.timer_start: # timer was either running or paused
minutes_spent = self._get_minutes_spent()
minutes_spent = self._timer_rounding(minutes_spent)
return self._action_create_timesheet(minutes_spent * 60 / 3600)
return False
def _timer_rounding(self, minutes_spent):
minimum_duration = int(self.env['ir.config_parameter'].sudo().get_param('hr_timesheet.timesheet_min_duration', 0))
rounding = int(self.env['ir.config_parameter'].sudo().get_param('hr_timesheet.timesheet_rounding', 0))
minutes_spent = max(minimum_duration, minutes_spent)
if rounding and ceil(minutes_spent % rounding) != 0:
minutes_spent = ceil(minutes_spent / rounding) * rounding
return minutes_spent
def _action_create_timesheet(self, time_spent):
return {
"name": _("Confirm Time Spent"),
"type": 'ir.actions.act_window',
"res_model": 'project.task.create.timesheet',
"views": [[False, "form"]],
"target": 'new',
"context": {
**self.env.context,
'active_id': self.id,
'active_model': 'project.task',
'default_time_spent': time_spent,
},
}
@@ -18,3 +18,5 @@ class ResConfigSettings(models.TransientModel):
related='company_id.timesheet_encode_uom_id', readonly=False,
help="""This will set the unit of measure used to encode timesheet. This will simply provide tools
and widgets to help the encoding. All reporting will still be expressed in hours (default value).""")
timesheet_min_duration = fields.Integer('Minimal duration', default=15, config_parameter='hr_timesheet.timesheet_min_duration')
timesheet_rounding = fields.Integer('Rounding up', default=15, config_parameter='hr_timesheet.timesheet_rounding')
@@ -0,0 +1,62 @@
odoo.define('hr_timesheet.timer', function (require) {
"use strict";
var fieldRegistry = require('web.field_registry');
var AbstractField = require('web.AbstractField');
var TimerFieldWidget = AbstractField.extend({
/**
* @override
* @private
*/
isSet: function () {
return true;
},
/**
* @private
*/
_getDuration: function (dateStart, datePause) {
if (datePause && dateStart) {
return moment(datePause).diff(moment(dateStart));
}
if (dateStart) {
return moment().diff(moment(dateStart));
}
else return 0;
},
/**
* @override
* @private
*/
_render: function () {
this._startTimeCounter();
},
/**
* @override
*/
destroy: function () {
this._super.apply(this, arguments);
clearTimeout(this.timer);
},
/**
* @private
*/
_startTimeCounter: function () {
var self = this;
clearTimeout(this.timer);
if (self.record.data.timer_start) {
this.timer = setTimeout(function () {
self._startTimeCounter();
}, 1000);
this.$el.text(moment.utc(self._getDuration(self.record.data.timer_start, self.record.data.timer_pause)).format("HH:mm:ss"));
} else if (!self.record.data.timer_pause){
clearTimeout(this.timer);
}
},
});
fieldRegistry.add('timesheet_timer', TimerFieldWidget);
});
+1
View File
@@ -4,6 +4,7 @@
<xpath expr="." position="inside">
<link type="text/scss" href="/hr_timesheet/static/src/scss/timer_button.scss" rel="stylesheet"/>
<script type="text/javascript" src="/hr_timesheet/static/src/js/task_with_hours.js"></script>
<script type="text/javascript" src="/hr_timesheet/static/src/js/timer.js"/>
<script type="text/javascript" src="/hr_timesheet/static/src/js/Timer.js"/>
<script type="text/javascript" src="/hr_timesheet/static/src/js/timesheet_uom.js"/>
<script type="text/javascript" src="/hr_timesheet/static/src/js/timer_toggle_button.js"/>
+82 -1
View File
@@ -59,7 +59,40 @@
</field>
</record>
<record model="ir.ui.view" id="view_task_form2_inherited_hr_timesheet">
<record id="project_view_form_inherit" model="ir.ui.view">
<field name="name">project.view.form.inherit</field>
<field name="model">project.project</field>
<field name="inherit_id" ref="hr_timesheet.project_invoice_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@id='timesheet_settings']" position="after">
<div class="col-lg-6 o_setting_box" attrs="{'invisible': [('allow_timesheets', '=', False)]}">
<div class="o_setting_left_pane">
<field name="allow_timesheet_timer"/>
</div>
<div class="o_setting_right_pane">
<label for="allow_timesheet_timer"/>
<div class="text-muted">
Track your time using a timer
</div>
</div>
</div>
</xpath>
</field>
</record>
<record id="project_project_view_form_simplified_inherit" model="ir.ui.view">
<field name="name">project.project.view.form.simplified.inherit</field>
<field name="model">project.project</field>
<field name="inherit_id" ref="project.project_project_view_form_simplified"/>
<field name="priority">32</field>
<field name="arch" type="xml">
<xpath expr="//field[@name='allow_timesheets']" position="after">
<field name="allow_timesheet_timer" invisible="1"/>
</xpath>
</field>
</record>
<record model="ir.ui.view" id="view_task_form2_inherited">
<field name="name">project.task.form.inherited</field>
<field name="model">project.task</field>
<field name="inherit_id" ref="project.view_task_form2" />
@@ -221,5 +254,53 @@
</field>
</record>
<record id="project_task_view_form" model="ir.ui.view">
<field name="name">project.task.view.form.inherit.sale.timesheet.enterprise</field>
<field name="model">project.task</field>
<field name="inherit_id" ref="project.view_task_form2"/>
<field name="arch" type="xml">
<xpath expr="//header" position='inside'>
<field name="company_id" invisible="1"/>
<field name="display_timesheet_timer" invisible="1"/>
<field name="timer_pause" invisible="1" />
<button class="btn-primary" name="action_timer_start" type="object" string="Start"
attrs="{'invisible': ['|', '|', ('total_hours_spent', '!=', 0), ('timer_start', '!=', False), ('display_timesheet_timer', '=', False)]}" groups="hr_timesheet.group_hr_timesheet_user"/>
<button class="btn-secondary" name="action_timer_start" type="object" string="Start"
attrs="{'invisible': ['|', '|', ('total_hours_spent', '=', 0), ('timer_start', '!=', False), ('display_timesheet_timer', '=', False)]}" groups="hr_timesheet.group_hr_timesheet_user"/>
<button class="btn-primary btn-danger o_fsm_stop" name="action_timer_stop" type="object" string="Stop"
attrs="{'invisible': ['|', ('timer_start', '=', False), ('display_timesheet_timer', '=', False)]}" groups="hr_timesheet.group_hr_timesheet_user"/>
<button class="btn-primary" name="action_timer_pause" type="object" string="Pause"
attrs="{'invisible': ['|', '|', ('timer_pause', '!=', False), ('timer_start', '=', False), ('display_timesheet_timer', '=', False)]}" groups="hr_timesheet.group_hr_timesheet_user"/>
<button class="btn-primary btn-info" name="action_timer_resume" type="object" string="Resume"
attrs="{'invisible': ['|', '|', ('timer_pause', '=', False), ('timer_start', '=', False), ('display_timesheet_timer', '=', False)]}" groups="hr_timesheet.group_hr_timesheet_user"/>
</xpath>
<xpath expr="//field[@name='stage_id']" position="attributes">
<attribute name="class">ml-2</attribute>
</xpath>
<xpath expr="//field[@name='stage_id']" position="before">
<field name="timer_start" widget="timesheet_timer" class="text-danger ml-auto h2 ml-4 font-weight-bold" groups="hr_timesheet.group_hr_timesheet_user"/>
</xpath>
</field>
</record>
<!-- Improve the task kanban in Project -->
<record id="project_task_view_kanban" model="ir.ui.view">
<field name="name">project.task.kanban.timer</field>
<field name="model">project.task</field>
<field name="inherit_id" ref="project.view_task_kanban"/>
<field name="arch" type="xml">
<xpath expr="//span[@name='date']" position="before">
<field name="display_timesheet_timer" invisible="1"/>
<field name="timer_start" invisible="1"/>
<field name="timer_pause" invisible="1"/>
<t t-if="record.display_timesheet_timer &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>
@@ -72,6 +72,17 @@
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_right_pane">
<strong>Round timesheets</strong>
<div class="o_row w-30">
<span class="o_light_label"><label for="timesheet_min_duration"/><field name="timesheet_min_duration" class="col-lg-2"/> minutes</span>
</div>
<div class="o_row">
<span class="o_light_label"><label for="timesheet_rounding"/><field name="timesheet_rounding" class="col-lg-2"/> minutes</span>
</div>
</div>
</div>
</div>
<div name="section_leaves" groups="base.group_no_one">
<h2>Time Off</h2>
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import project_task_create_timesheet
@@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from datetime import datetime
from odoo.exceptions import UserError
class ProjectTaskCreateTimesheet(models.TransientModel):
_name = 'project.task.create.timesheet'
_description = "Create Timesheet from task"
_sql_constraints = [('time_positive', 'CHECK(time_spent > 0)', 'The timesheet\'s time must be positive' )]
@api.model
def default_get(self, fields):
result = super(ProjectTaskCreateTimesheet, self).default_get(fields)
active_model = self._context.get('active_model')
if active_model != 'project.task':
raise UserError(_("You can only apply this action from a task."))
active_id = self._context.get('active_id')
if 'task_id' in fields and active_id:
task_id = self.env['project.task'].browse(active_id)
result['task_id'] = active_id
result['description'] = task_id.name
return result
time_spent = fields.Float('Time', precision_digits=2)
description = fields.Char('Description')
task_id = fields.Many2one('project.task', "Task", help="Task for which we are creating a sales order", required=True)
def save_timesheet(self):
values = {
'task_id': self.task_id.id,
'project_id': self.task_id.project_id.id,
'date': datetime.now(),
'name': self.description,
'user_id': self.env.uid,
'unit_amount': self.time_spent,
}
self.task_id.write({
'timer_start': False,
'timer_pause': False,
'timesheet_timer_last_stop': fields.datetime.now(),
})
return self.env['account.analytic.line'].create(values)
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="project_task_create_timesheet_view_form" model="ir.ui.view">
<field name="name">project.task.create.timesheet.wizard.form</field>
<field name="model">project.task.create.timesheet</field>
<field name="arch" type="xml">
<form string="Save time">
<group>
<group>
<field name="task_id" invisible="True"/>
<label for="time_spent"/>
<div class="o_row" >
<field name="time_spent" widget="float_time" required="True"/>
hours
</div>
<field name="description" widget="text" required="True"/>
</group>
</group>
<footer>
<button string="Save" type="object" name="save_timesheet" class="btn btn-primary"/>
<button string="Cancel" special="cancel" type="object" class="btn btn-secondary"/>
</footer>
</form>
</field>
</record>
</odoo>