[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:
committed by
Yannick Tivisse
parent
2ace7893f0
commit
15e41f59c7
@@ -4,3 +4,4 @@
|
||||
from . import controllers
|
||||
from . import models
|
||||
from . import report
|
||||
from . import wizard
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
});
|
||||
@@ -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"/>
|
||||
|
||||
@@ -59,7 +59,40 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="view_task_form2_inherited_hr_timesheet">
|
||||
<record id="project_view_form_inherit" model="ir.ui.view">
|
||||
<field name="name">project.view.form.inherit</field>
|
||||
<field name="model">project.project</field>
|
||||
<field name="inherit_id" ref="hr_timesheet.project_invoice_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@id='timesheet_settings']" position="after">
|
||||
<div class="col-lg-6 o_setting_box" attrs="{'invisible': [('allow_timesheets', '=', False)]}">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="allow_timesheet_timer"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="allow_timesheet_timer"/>
|
||||
<div class="text-muted">
|
||||
Track your time using a timer
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="project_project_view_form_simplified_inherit" model="ir.ui.view">
|
||||
<field name="name">project.project.view.form.simplified.inherit</field>
|
||||
<field name="model">project.project</field>
|
||||
<field name="inherit_id" ref="project.project_project_view_form_simplified"/>
|
||||
<field name="priority">32</field>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='allow_timesheets']" position="after">
|
||||
<field name="allow_timesheet_timer" invisible="1"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="view_task_form2_inherited">
|
||||
<field name="name">project.task.form.inherited</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="inherit_id" ref="project.view_task_form2" />
|
||||
@@ -221,5 +254,53 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="project_task_view_form" model="ir.ui.view">
|
||||
<field name="name">project.task.view.form.inherit.sale.timesheet.enterprise</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="inherit_id" ref="project.view_task_form2"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//header" position='inside'>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="display_timesheet_timer" invisible="1"/>
|
||||
<field name="timer_pause" invisible="1" />
|
||||
<button class="btn-primary" name="action_timer_start" type="object" string="Start"
|
||||
attrs="{'invisible': ['|', '|', ('total_hours_spent', '!=', 0), ('timer_start', '!=', False), ('display_timesheet_timer', '=', False)]}" groups="hr_timesheet.group_hr_timesheet_user"/>
|
||||
<button class="btn-secondary" name="action_timer_start" type="object" string="Start"
|
||||
attrs="{'invisible': ['|', '|', ('total_hours_spent', '=', 0), ('timer_start', '!=', False), ('display_timesheet_timer', '=', False)]}" groups="hr_timesheet.group_hr_timesheet_user"/>
|
||||
<button class="btn-primary btn-danger o_fsm_stop" name="action_timer_stop" type="object" string="Stop"
|
||||
attrs="{'invisible': ['|', ('timer_start', '=', False), ('display_timesheet_timer', '=', False)]}" groups="hr_timesheet.group_hr_timesheet_user"/>
|
||||
<button class="btn-primary" name="action_timer_pause" type="object" string="Pause"
|
||||
attrs="{'invisible': ['|', '|', ('timer_pause', '!=', False), ('timer_start', '=', False), ('display_timesheet_timer', '=', False)]}" groups="hr_timesheet.group_hr_timesheet_user"/>
|
||||
<button class="btn-primary btn-info" name="action_timer_resume" type="object" string="Resume"
|
||||
attrs="{'invisible': ['|', '|', ('timer_pause', '=', False), ('timer_start', '=', False), ('display_timesheet_timer', '=', False)]}" groups="hr_timesheet.group_hr_timesheet_user"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='stage_id']" position="attributes">
|
||||
<attribute name="class">ml-2</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='stage_id']" position="before">
|
||||
<field name="timer_start" widget="timesheet_timer" class="text-danger ml-auto h2 ml-4 font-weight-bold" groups="hr_timesheet.group_hr_timesheet_user"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Improve the task kanban in Project -->
|
||||
<record id="project_task_view_kanban" model="ir.ui.view">
|
||||
<field name="name">project.task.kanban.timer</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="inherit_id" ref="project.view_task_kanban"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//span[@name='date']" position="before">
|
||||
<field name="display_timesheet_timer" invisible="1"/>
|
||||
<field name="timer_start" invisible="1"/>
|
||||
<field name="timer_pause" invisible="1"/>
|
||||
<t t-if="record.display_timesheet_timer && record.timer_pause.raw_value">
|
||||
<i class="fa fa-pause text-warning" title="Timer is Paused"></i>
|
||||
</t>
|
||||
<t t-elif="record.display_timesheet_timer && record.timer_start.raw_value">
|
||||
<i class="fa fa-play text-success" title="Timer is Running"></i>
|
||||
</t>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user