diff --git a/addons/account/security/account_security.xml b/addons/account/security/account_security.xml
index 7b67561fc6d..986c3563b21 100644
--- a/addons/account/security/account_security.xml
+++ b/addons/account/security/account_security.xml
@@ -65,10 +65,6 @@
[(1, '=', 1)]
-
-
-
-
diff --git a/addons/hr_timesheet/data/hr_timesheet_data.xml b/addons/hr_timesheet/data/hr_timesheet_data.xml
index ed572593da6..8fb4eb76cf6 100644
--- a/addons/hr_timesheet/data/hr_timesheet_data.xml
+++ b/addons/hr_timesheet/data/hr_timesheet_data.xml
@@ -1,29 +1,26 @@
-
-
-
- project_tour
-
-
+
+
+ float_toggle
+
-
-
- float_toggle
-
+
+ float_time
+
-
- float_time
-
+
+
-
-
-
-
-
-
+
+ hr_timesheet.timesheet_rounding
+ 15
+
+
+ hr_timesheet.timesheet_min_duration
+ 15
+
diff --git a/addons/hr_timesheet/models/hr_timesheet.py b/addons/hr_timesheet/models/hr_timesheet.py
index f258d49c5de..4e4fa8e20a1 100644
--- a/addons/hr_timesheet/models/hr_timesheet.py
+++ b/addons/hr_timesheet/models/hr_timesheet.py
@@ -48,12 +48,9 @@ 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')
-
- # Timer Fields
display_timer = fields.Boolean(
compute='_compute_display_timer',
- help="used to display the timer if the encoding unit is 'Hours'"
- )
+ 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:
@@ -82,10 +79,6 @@ class AccountAnalyticLine(models.Model):
for line in self:
line.department_id = line.employee_id.department_id
- # ----------------------------------------------------
- # ORM overrides
- # ----------------------------------------------------
-
@api.model_create_multi
def create(self, vals_list):
default_user_id = self._default_user()
@@ -134,10 +127,6 @@ class AccountAnalyticLine(models.Model):
node.set('string', _('Duration (%s)') % (re.sub(r'[\(\)]', '', encoding_uom.name or '')))
return etree.tostring(doc, encoding='unicode')
- # ----------------------------------------------------
- # Business Methods
- # ----------------------------------------------------
-
def _timesheet_get_portal_domain(self):
return ['|', '&',
('task_id.project_id.privacy_visibility', '=', 'portal'),
@@ -207,51 +196,37 @@ class AccountAnalyticLine(models.Model):
})
return result
- # ----------------------------------------------------
- # Timer Methods
- # ----------------------------------------------------
def _compute_display_timer(self):
- """ Check if the encoding unit is 'Hours',
-
- if yes then the timer button is displayed
- """
+ uom_hour = self.env.ref('uom.product_uom_hour')
for analytic_line in self:
- analytic_line.display_timer = analytic_line.encoding_uom_id.id == self.env.ref('uom.product_uom_hour').id
+ analytic_line.display_timer = analytic_line.encoding_uom_id == uom_hour
def action_timer_start(self):
- """ Action start the timer of the current timesheet.
-
- Start timer and search if another timer hasn't been launched.
+ """ Start timer and search if another timer hasn't been launched.
If yes, then stop the timer before launch this timer.
"""
- self.ensure_one()
if not self.timer_start and self.display_timer:
self._stop_running_timers()
super().action_timer_start()
def _stop_running_timers(self):
""" Search if a timesheet has a timer activated and stop the timer.
-
Check if a timer is activated for another timesheet
if yes, then update unit_amount field and stop timer,
otherwise, do nothing.
"""
analytic_line = self.search([('timer_start', '!=', False), ('user_id', '=', self.env.uid)])
-
if analytic_line:
analytic_line.action_timer_stop()
def action_timer_stop(self):
""" Action stop the timer of the current timesheet.
-
When the timer must be stopped, we must calculate the new
unit_amount based on the timer and the previous value of
unit_amount for the current timesheet.
"""
- self.ensure_one()
if self.timer_start and self.display_timer:
minutes_spent = self._get_minutes_spent()
-
if self.unit_amount == 0 and minutes_spent < 1:
# Check if unit_amount equals 0 and minutes_spent is less than 1 minute,
# if yes, then remove the timesheet
@@ -261,7 +236,5 @@ class AccountAnalyticLine(models.Model):
amount = self.unit_amount
else:
amount = self.unit_amount + minutes_spent * 60 / 3600
-
self.write({'unit_amount': amount})
-
super().action_timer_stop()
diff --git a/addons/hr_timesheet/models/project.py b/addons/hr_timesheet/models/project.py
index d50a9571882..a475536aae7 100644
--- a/addons/hr_timesheet/models/project.py
+++ b/addons/hr_timesheet/models/project.py
@@ -20,15 +20,12 @@ class Project(models.Model):
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.'),
+ ('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('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}}
+ return {'domain': {'analytic_account_id': [('partner_id', '=', self.partner_id.id)] if self.partner_id else []}}
@api.onchange('analytic_account_id')
def _onchange_analytic_account(self):
@@ -74,16 +71,12 @@ class Project(models.Model):
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({
+ self.with_context(active_test=False).mapped('task_ids').write({
'timer_start': False,
'timer_pause': False,
})
return result
- # ---------------------------------------------------
- # Business Methods
- # ---------------------------------------------------
-
@api.model
def _init_data_analytic_account(self):
self.search([('analytic_account_id', '=', False), ('allow_timesheets', '=', True)])._create_analytic_account()
@@ -102,8 +95,9 @@ 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_start = fields.Datetime("Timesheet Timer Start")
timer_pause = fields.Datetime("Timesheet Timer Last Pause")
+ # YTI FIXME: Those field seems quite useless
timesheet_timer_first_start = fields.Datetime("Timesheet Timer First Use", readonly=True)
timesheet_timer_last_stop = fields.Datetime("Timesheet Timer Last Use", readonly=True)
display_timesheet_timer = fields.Boolean("Display Timesheet Time", compute='_compute_display_timesheet_timer')
@@ -145,10 +139,6 @@ class Task(models.Model):
for task in self:
task.display_timesheet_timer = task.allow_timesheets and task.project_id.allow_timesheet_timer and task.analytic_account_active
- # ---------------------------------------------------------
- # ORM
- # ---------------------------------------------------------
-
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'):
@@ -173,10 +163,6 @@ class Task(models.Model):
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:
@@ -201,7 +187,7 @@ class Task(models.Model):
def _action_create_timesheet(self, time_spent):
return {
- "name": _("Confirm Time Spent"),
+ "name": _("Validate Spent Time"),
"type": 'ir.actions.act_window',
"res_model": 'project.task.create.timesheet',
"views": [[False, "form"]],
diff --git a/addons/hr_timesheet/models/timer_mixin.py b/addons/hr_timesheet/models/timer_mixin.py
index 319d020a66e..e4070d9b0a3 100644
--- a/addons/hr_timesheet/models/timer_mixin.py
+++ b/addons/hr_timesheet/models/timer_mixin.py
@@ -7,25 +7,20 @@ from odoo import models, fields, api
class TimerMixin(models.AbstractModel):
_name = 'timer.mixin'
_description = 'Timer Mixin'
+ # YTI Note: This mixin is supposed to be extended to
+ # any models
timer_start = fields.Datetime("Timer Start")
timer_pause = fields.Datetime("Timer Last Pause")
-
- # is_timer_running field is used with timer_toggle_button widget
- # to create a timer button in the view.
- # If the timer field is set on False,
- # then it displays a button with fa-icon-play icon.
- # Otherwise, it displays a button with fa-icon-stop icon
is_timer_running = fields.Boolean(compute="_compute_timer")
@api.depends('timer_start')
- def _compute_timer(self) -> None:
+ def _compute_timer(self):
for record in self:
record.is_timer_running = bool(record.timer_start)
- def action_timer_start(self) -> None:
+ def action_timer_start(self):
""" Action start the timer.
-
Start timer and search if another timer hasn't been launched.
If yes, then stop the timer before launch this timer.
"""
@@ -45,32 +40,26 @@ class TimerMixin(models.AbstractModel):
self.write({'timer_start': False, 'timer_pause': False})
return minutes_spent
- def _get_minutes_spent(self) -> float:
- """ Compute the minutes spent with the timer
-
- :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) -> None:
+ def action_timer_pause(self):
self.write({'timer_pause': fields.Datetime.now()})
- def action_timer_resume(self) -> None:
+ 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):
- """ Get the time of the server
-
- The problem with the timer, it's the time can be different between server side and client side.
- We need to have the time of the server and don't use the local time. Then, we have a timer beginning at 0:00
- and not 23:59 or something else.
+ """ 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()
diff --git a/addons/hr_timesheet/security/hr_timesheet_security.xml b/addons/hr_timesheet/security/hr_timesheet_security.xml
index 43bea381021..0b7970610f7 100644
--- a/addons/hr_timesheet/security/hr_timesheet_security.xml
+++ b/addons/hr_timesheet/security/hr_timesheet_security.xml
@@ -36,17 +36,12 @@
[
('user_id', '=', user.id),
('project_id', '!=', False),
- '|',
+ '|', '|',
('project_id.privacy_visibility', '!=', 'followers'),
- '|',
- ('project_id.message_partner_ids', 'in', [user.partner_id.id]),
- ('task_id.message_partner_ids', 'in', [user.partner_id.id])
+ ('project_id.message_partner_ids', 'in', [user.partner_id.id]),
+ ('task_id.message_partner_ids', 'in', [user.partner_id.id])
]
-
-
-
-
@@ -59,10 +54,6 @@
('project_id.message_partner_ids', 'in', [user.partner_id.id])
]
-
-
-
-
@@ -70,10 +61,6 @@
[('project_id', '!=', False)]
-
-
-
-
diff --git a/addons/hr_timesheet/static/src/js/Timer.js b/addons/hr_timesheet/static/src/js/Timer.js
index c2cbabc5834..2a2c993dd91 100644
--- a/addons/hr_timesheet/static/src/js/Timer.js
+++ b/addons/hr_timesheet/static/src/js/Timer.js
@@ -2,8 +2,8 @@ odoo.define('hr_timesheet.Timer', function (require) {
"use strict";
/**
- * This class allow to facilitate to display the timer
- * in unit_amount field of account.analytic.line.
+ * This class improves the timer display using the
+ * unit_amount field on account.analytic.line.
*/
class Timer {
constructor(hours, minutes, seconds) {
diff --git a/addons/hr_timesheet/static/src/js/timer.js b/addons/hr_timesheet/static/src/js/timer.js
index f1ef8edd53e..5e7e4a1b2fb 100644
--- a/addons/hr_timesheet/static/src/js/timer.js
+++ b/addons/hr_timesheet/static/src/js/timer.js
@@ -19,6 +19,7 @@ var TimerFieldWidget = AbstractField.extend({
* @private
*/
_render: function () {
+ this._super.apply(this, arguments);
this._startTimeCounter();
},
/**
@@ -52,7 +53,7 @@ var TimerFieldWidget = AbstractField.extend({
return this._rpc({
model: 'account.analytic.line',
method: 'get_server_time',
- args: [false]
+ args: []
});
}
});
diff --git a/addons/hr_timesheet/static/src/js/timesheet_uom.js b/addons/hr_timesheet/static/src/js/timesheet_uom.js
index 9b74f686c4e..a462b1d463b 100644
--- a/addons/hr_timesheet/static/src/js/timesheet_uom.js
+++ b/addons/hr_timesheet/static/src/js/timesheet_uom.js
@@ -96,7 +96,7 @@ const FieldTimesheetTime = basicFields.FieldFloatTime.extend({
const time = await this._rpc({
model: this.model,
method: 'get_server_time',
- args: [false]
+ args: []
});
this.time = Timer.createTimer(this.recordData.unit_amount, this.recordData.timer_start, time);
return this._startTimeCounter();
diff --git a/addons/hr_timesheet/static/src/scss/timer_button.scss b/addons/hr_timesheet/static/src/scss/timer_button.scss
index 8d68d4eb9ea..8b269fa8b20 100644
--- a/addons/hr_timesheet/static/src/scss/timer_button.scss
+++ b/addons/hr_timesheet/static/src/scss/timer_button.scss
@@ -23,4 +23,4 @@
&:hover > i.o-timer-stop-button {
color: darken($red, 15%) !important;
}
-}
\ No newline at end of file
+}
diff --git a/addons/hr_timesheet/views/hr_timesheet_views.xml b/addons/hr_timesheet/views/hr_timesheet_views.xml
index 06b400d845e..d81fee4d186 100644
--- a/addons/hr_timesheet/views/hr_timesheet_views.xml
+++ b/addons/hr_timesheet/views/hr_timesheet_views.xml
@@ -167,7 +167,7 @@
-
+
diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py
index 889bf98dc44..5fce56f0fa1 100644
--- a/addons/sale/models/sale.py
+++ b/addons/sale/models/sale.py
@@ -572,7 +572,7 @@ class SaleOrder(models.Model):
action['context'] = context
return action
- def _create_invoices(self, grouped=False, final=False):
+ def _create_invoices(self, grouped=False, final=False, date=None):
"""
Create the invoice associated to the SO.
:param grouped: if True, invoices are grouped by SO id. If False, invoices are grouped by
diff --git a/addons/sale_timesheet/models/account_move.py b/addons/sale_timesheet/models/account_move.py
index 69d8ee4277f..e98e479576b 100644
--- a/addons/sale_timesheet/models/account_move.py
+++ b/addons/sale_timesheet/models/account_move.py
@@ -59,7 +59,7 @@ class AccountMove(models.Model):
domain = line._timesheet_domain_get_invoiced_lines(sale_line_delivery)
if date:
domain = expression.AND([domain, [('date', '<=', date)]])
- timesheets = self.env['account.analytic.line'].search(domain).sudo()
+ timesheets = self.env['account.analytic.line'].sudo().search(domain)
timesheets.write({'timesheet_invoice_id': line.move_id.id})
@@ -73,9 +73,7 @@ class AccountMoveLine(models.Model):
:return a normalized domain
"""
return [
- '&',
('so_line', 'in', sale_line_delivery.ids),
- '&',
('timesheet_invoice_id', '=', False),
('project_id', '!=', False)
]
diff --git a/addons/sale_timesheet/models/project_overview.py b/addons/sale_timesheet/models/project_overview.py
index 7dbb0f192ec..c5953a556f4 100644
--- a/addons/sale_timesheet/models/project_overview.py
+++ b/addons/sale_timesheet/models/project_overview.py
@@ -62,7 +62,7 @@ class Project(models.Model):
# hours (from timesheet) and rates (by billable type)
dashboard_domain = [('project_id', 'in', self.ids), ('timesheet_invoice_type', '!=', False), '|', ('so_line', '=', False), ('so_line.state', '!=', 'cancel')] # force billable type
- dashboard_data = self.env['account.analytic.line'].sudo().read_group(dashboard_domain, ['unit_amount', 'timesheet_invoice_type'], ['timesheet_invoice_type'])
+ dashboard_data = self.env['account.analytic.line'].read_group(dashboard_domain, ['unit_amount', 'timesheet_invoice_type'], ['timesheet_invoice_type'])
dashboard_total_hours = sum([data['unit_amount'] for data in dashboard_data]) + total_canceled_hours
for data in dashboard_data:
billable_type = data['timesheet_invoice_type']
diff --git a/addons/sale_timesheet/models/sale_order.py b/addons/sale_timesheet/models/sale_order.py
index cd4a056e719..bf3c405cbd2 100644
--- a/addons/sale_timesheet/models/sale_order.py
+++ b/addons/sale_timesheet/models/sale_order.py
@@ -65,8 +65,16 @@ class SaleOrder(models.Model):
@api.depends('timesheet_ids', 'company_id.timesheet_encode_uom_id')
def _compute_timesheet_total_duration(self):
for sale_order in self:
+ duration_list = []
timesheets = sale_order.timesheet_ids if self.user_has_groups('hr_timesheet.group_hr_timesheet_approver') else sale_order.timesheet_ids.filtered(lambda t: t.user_id.id == self.env.uid)
- sale_order.timesheet_total_duration = sum([timesheet.unit_amount for timesheet in timesheets])
+ for timesheet in timesheets:
+ timesheet_uom = timesheet.product_uom_id or timesheet.company_id.project_time_mode_id
+ if timesheet_uom != sale_order.timesheet_encode_uom_id and timesheet_uom.category_id == sale_order.timesheet_encode_uom_id.category_id:
+ duration_list.append(timesheet_uom._compute_quantity(timesheet.unit_amount, sale_order.timesheet_encode_uom_id))
+ else:
+ duration_list.append(timesheet.unit_amount)
+ sale_order.timesheet_total_duration = sum(duration_list)
+
@api.onchange('project_id')
def _onchange_project_id(self):
@@ -144,18 +152,14 @@ class SaleOrder(models.Model):
def _create_invoices(self, grouped=False, final=False, date=None):
""" Override the _create_invoice method in sale.order model in sale module
-
Add new parameter in this method, to invoice sale.order with a date. This date is used in sale_make_invoice_advance_inv into this module.
-
:param date
-
:return {account.move}: the invoices created
"""
moves = super(SaleOrder, self)._create_invoices(grouped, final)
moves._link_timesheets_to_invoice(date)
return moves
-
class SaleOrderLine(models.Model):
_inherit = "sale.order.line"
diff --git a/addons/sale_timesheet/wizard/sale_make_invoice_advance.py b/addons/sale_timesheet/wizard/sale_make_invoice_advance.py
index ab4e866dd21..4c74fa9c0a5 100644
--- a/addons/sale_timesheet/wizard/sale_make_invoice_advance.py
+++ b/addons/sale_timesheet/wizard/sale_make_invoice_advance.py
@@ -6,18 +6,12 @@ class SaleAdvancePaymentInv(models.TransientModel):
@api.model
def _default_invoicing_timesheet_enabled(self):
- product_ids = None
- if self._context.get('active_id', False):
- sale_order = self.env['sale.order'].browse(self._context.get('active_id'))
- product_ids = sale_order.order_line.filtered(lambda sol: sol.invoice_status == 'to invoice').mapped('product_id')
- elif self._context.get('active_ids', []):
- sale_orders = self.env['sale.order'].browse(self._context.get('active_ids'))
- order_lines = sale_orders.mapped('order_line').filtered(lambda sol: sol.invoice_status == 'to invoice')
- product_ids = order_lines.mapped('product_id')
-
- if product_ids:
- return any(product._is_delivered_timesheet() for product in product_ids)
- return False
+ if 'active_id' not in self._context and 'active_ids' not in self._context:
+ return False
+ sale_orders = self.env['sale.order'].browse(self._context.get('active_id') or self._context.get('active_ids'))
+ order_lines = sale_orders.mapped('order_line').filtered(lambda sol: sol.invoice_status == 'to invoice')
+ product_ids = order_lines.mapped('product_id').filtered(lambda p: p._is_delivered_timesheet())
+ return bool(product_ids)
date_invoice_timesheet = fields.Date(
string='Invoice Timesheets Up To This Date',