Files
enterprise-addons/timesheet_grid/models/account_analytic_line.py
T

734 lines
34 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import timedelta
from dateutil.relativedelta import relativedelta
from dateutil.rrule import SU
from collections import defaultdict
from odoo import models, fields, api, _
from odoo.exceptions import RedirectWarning, UserError, AccessError
from odoo.fields import Domain
from odoo.tools import format_date
from odoo.tools.date_utils import localized
from odoo.addons.timer.utils.timer_utils import round_time_spent
class AccountAnalyticLine(models.Model):
_name = 'account.analytic.line'
_inherit = ['account.analytic.line', 'timer.mixin']
# As this model has his own data merge, avoid to enable the generic data_merge on that model.
_disable_data_merge = True
# reset amount on copy
amount = fields.Monetary(copy=False)
validated = fields.Boolean("Validated line", aggregator="bool_and", store=True, copy=False, readonly=True)
validated_status = fields.Selection([('draft', 'Draft'), ('validated', 'Validated')], required=True,
compute='_compute_validated_status')
user_can_validate = fields.Boolean(compute='_compute_can_validate',
help="Whether or not the current user can validate/reset to draft the record.")
display_timer = fields.Boolean(
"Technical field used to display the timer if the encoding unit is 'Hours'.",
compute='_compute_display_timer', export_string_translation=False)
is_hatched = fields.Boolean(compute='_compute_is_hatched', export_string_translation=False)
@api.constrains('unit_amount')
def _check_timesheet_unit_amount(self):
if any(abs(t.unit_amount) > 999999 for t in self if t.project_id):
raise UserError(_("You can't encode numbers with more than six digits."))
def _is_readonly(self):
return super()._is_readonly() or self.validated
def _should_not_display_timer(self):
self.ensure_one()
return (self.employee_id not in self.env.user.employee_ids) or self.validated
def _compute_display_timer(self):
uom_hour = self.env.ref('uom.product_uom_hour')
is_uom_hour = self.env.company.timesheet_encode_uom_id == uom_hour
for analytic_line in self:
analytic_line.display_timer = is_uom_hour and analytic_line.encoding_uom_id == uom_hour \
and not analytic_line._should_not_display_timer()
@api.model
def grid_unavailability(self, start_date, end_date, groupby='', res_ids=()):
start_datetime = fields.Datetime.from_string(start_date)
end_datetime = fields.Datetime.from_string(end_date) + relativedelta(hour=23, minute=59, second=59)
unavailability_intervals_per_employee_id = {}
# naive datetimes are made explicit in UTC
from_datetime = localized(start_datetime)
to_datetime = localized(end_datetime)
# We need to display in grey the unavailable full days
# We start by getting the availability intervals to avoid false positive with range outside the office hours
def get_unavailable_dates(intervals):
# get the dates where some work can be done in the interval. It returns a list of sets.
available_dates = [{start.date(), end.date()} for start, end, dummy in intervals]
# flatten the list of sets to get a simple list of dates and add it to the pile.
availability_date_list = [date for dates in available_dates for date in dates]
unavailable_days = []
cur_day = from_datetime
while cur_day <= to_datetime:
if cur_day.date() not in availability_date_list:
unavailable_days.append(cur_day.date())
cur_day = cur_day + timedelta(days=1)
return list(set(unavailable_days))
def get_company_unavailable_dates():
company_calendar = self.env.company.resource_calendar_id
if company_calendar.flexible_hours:
return []
return get_unavailable_dates(company_calendar._work_intervals_batch(from_datetime, to_datetime)[False])
def get_current_user_unavailable_dates():
resource = self.env.user.employee_id.resource_id
if resource:
resource_work_intervals, _ = resource._get_valid_work_intervals(
from_datetime, to_datetime
)
if resource.id in resource_work_intervals:
return get_unavailable_dates(resource_work_intervals[resource.id])
return False
if groupby == 'employee_id':
employees = self.env['hr.employee'].browse(set(res_ids))
availability_intervals_per_resource_id, calendar_work_intervals = employees.resource_id._get_valid_work_intervals(from_datetime, to_datetime)
employee_id_per_resource_id = {emp.resource_id.id: emp.id for emp in employees}
if not calendar_work_intervals:
unavailability_intervals_per_employee_id[False] = get_company_unavailable_dates()
return unavailability_intervals_per_employee_id
if self.env.company.resource_calendar_id.id in calendar_work_intervals:
company_unavailable_days = [] if self.env.company.resource_calendar_id.flexible_hours else get_unavailable_dates(calendar_work_intervals[self.env.company.resource_calendar_id.id])
else:
company_unavailable_days = get_company_unavailable_dates()
unavailability_intervals_per_employee_id = {
employee_id:
[]
if self.env['resource.resource'].browse(resource_id)._is_flexible()
else get_unavailable_dates(availability_intervals_per_resource_id[resource_id])
if resource_id in availability_intervals_per_resource_id
else company_unavailable_days
for resource_id, employee_id in employee_id_per_resource_id.items()
}
unavailability_intervals_per_employee_id[False] = company_unavailable_days
elif self.env.context.get('get_current_user_unavailable_dates', False):
unavailability_intervals_per_employee_id[False] = get_current_user_unavailable_dates()
else:
if self.env.user.resource_calendar_id.flexible_hours:
unavailability_intervals_per_employee_id[False] = []
else:
unavailability_intervals_per_employee_id[False] = get_company_unavailable_dates()
return unavailability_intervals_per_employee_id
def _compute_project_id(self):
# override hr_timesheet to allow the check on field validated to only update the project_id on non validated timesheets.
non_validated_timesheets = self.filtered(lambda t: not t.validated and t.task_id.project_id.allow_timesheets)
super(AccountAnalyticLine, non_validated_timesheets)._compute_project_id()
@api.depends('validated')
def _compute_validated_status(self):
for line in self:
if line.validated:
line.validated_status = 'validated'
else:
line.validated_status = 'draft'
@api.depends_context('uid')
def _compute_can_validate(self):
is_manager = self.env.user.has_group('hr_timesheet.group_timesheet_manager')
is_approver = self.env.user.has_group('hr_timesheet.group_hr_timesheet_approver')
for line in self:
if is_manager or (is_approver and (
line.employee_id.timesheet_manager_id.id == self.env.user.id or
line.employee_id.parent_id.user_id.id == self.env.user.id or
line.project_id.user_id.id == self.env.user.id or
line.user_id.id == self.env.user.id)):
line.user_can_validate = True
else:
line.user_can_validate = False
def _update_last_validated_timesheet_date(self):
max_date_per_employee = {
employee: employee.last_validated_timesheet_date
for employee in self.employee_id.sudo()
}
for timesheet in self:
max_date = max_date_per_employee[timesheet.employee_id]
if not max_date or max_date < timesheet.date:
max_date_per_employee[timesheet.employee_id] = timesheet.date
employee_ids_per_date = defaultdict(list)
for employee, max_date in max_date_per_employee.items():
if not employee.last_validated_timesheet_date or (max_date and employee.last_validated_timesheet_date < max_date):
employee_ids_per_date[max_date].append(employee.id)
for date, employee_ids in employee_ids_per_date.items():
self.env['hr.employee'].sudo().browse(employee_ids).write({'last_validated_timesheet_date': date})
@api.model
def _search_last_validated_timesheet_date(self, employee_ids):
EmployeeSudo = self.env['hr.employee'].sudo()
timesheet_read_group = self.env['account.analytic.line']._read_group(
[
('validated', '=', True),
('project_id', '!=', False),
('employee_id', 'in', employee_ids),
],
['employee_id'],
['date:max'],
)
EmployeeSudo.browse(employee_ids).last_validated_timesheet_date = False
for employee, date_max in timesheet_read_group:
employee.sudo().last_validated_timesheet_date = date_max
@api.depends('validated')
def _compute_is_hatched(self):
for line in self:
line.is_hatched = not line.validated
def action_validate_timesheet(self):
notification = {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'message': None,
'type': None, #types: success,warning,danger,info
'sticky': False, #True/False will display for few seconds if false
},
}
if not self.env.user.has_group('hr_timesheet.group_hr_timesheet_approver'):
notification['params'].update({
'message': _("You can only validate the timesheets of employees of whom you are the manager or the timesheet approver."),
'type': 'danger'
})
return notification
# filter in sudo to access employee manager (in domain)
# The rest of the method and calls are anyway sudoing analytic_lines everywhere so no real impact here.
analytic_lines = self.sudo().filtered_domain(self._get_domain_for_validation_timesheets())
if not analytic_lines:
notification['params'].update({
'message': _("You cannot validate the selected timesheets as they either belong to employees who are not part of your team or are not in a state that can be validated. This may be due to the fact that they are dated in the future."),
'type': 'danger',
})
return notification
analytic_lines._stop_all_users_timer()
analytic_lines.sudo().write({'validated': True})
analytic_lines._update_last_validated_timesheet_date()
# Interrupt the timesheet with a timer running that is before the last validated date for each employee
running_analytic_lines = self.env['account.analytic.line'].search([
('employee_id', 'in', analytic_lines.employee_id.ids),
('date', '<', max(analytic_lines.employee_id.sudo().mapped('last_validated_timesheet_date'))),
('is_timer_running', '=', True),
])
running_analytic_lines.filtered(
lambda aal: aal.date < aal.employee_id.sudo().last_validated_timesheet_date)._stop_all_users_timer()
if self.env.context.get('use_notification', True):
notification['params'].update({
'message': _("The timesheets have successfully been validated."),
'type': 'success',
'next': {'type': 'ir.actions.act_window_close'},
})
return notification
return True
def action_invalidate_timesheet(self):
notification = {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'message': None,
'type': None,
'sticky': False,
},
}
if not self.env.user.has_group('hr_timesheet.group_hr_timesheet_approver'):
raise AccessError(_("You can only reset to draft the timesheets of employees of whom you are the manager or the timesheet approver."))
#Use the same domain for validation but change validated = False to validated = True
domain = self._get_domain_for_validation_timesheets(validated=True)
analytic_lines = self.sudo().filtered_domain(domain)
if not analytic_lines:
notification['params'].update({
'message': _('There are no timesheets to reset to draft or they have already been invoiced.'),
'type': 'warning',
})
return notification
analytic_lines.sudo().write({'validated': False})
self.env['account.analytic.line']._search_last_validated_timesheet_date(analytic_lines.employee_id.ids)
if self.env.context.get('use_notification', True):
notification['params'].update({
'message': _("The timesheets have successfully been reset to draft."),
'type': 'success',
'next': {'type': 'ir.actions.act_window_close'},
})
return notification
return True
def check_if_allowed(self, vals=None, delete=False,):
if not self.env.user.has_group('hr_timesheet.group_timesheet_manager') and not self.env.su:
is_timesheet_approver = self.env.user.has_group('hr_timesheet.group_hr_timesheet_approver')
employees = self.env['hr.employee'].with_context(active_test=False).search([
('id', 'in', self.employee_id.ids),
('user_id', '!=', self.env.uid),
'|', ('parent_id.user_id', '=', self.env.uid),
'|', ('timesheet_manager_id', 'in', [False, self.env.uid]),
'|', ('id', 'in', self.env.user.employee_id.subordinate_ids.ids),
'&', ('parent_id', '=', False), ('timesheet_manager_id', '=', False),
])
action = "delete" if delete else "modify" if vals is not None and "date" in vals else "create or edit"
for line in self:
show_access_error = False
employee = line.employee_id
company = line.company_id
last_validated_timesheet_date = employee.sudo().last_validated_timesheet_date
def is_wrong_date(date):
return date != fields.Date.today() and date <= last_validated_timesheet_date
# When an user having this group tries to modify the timesheets of another user in his own team, we shouldn't raise any validation error
if not is_timesheet_approver or employee not in employees:
if line.project_id and last_validated_timesheet_date:
if action == "modify" and is_wrong_date(fields.Date.to_date(str(vals['date']))):
show_access_error = True
elif is_wrong_date(line.date):
show_access_error = True
if show_access_error:
last_validated_timesheet_date_str = format_date(self.env, last_validated_timesheet_date)
if delete:
error_message = _('Timesheets before the %(date)s (included) have been validated, and can no longer be deleted.', date=last_validated_timesheet_date_str)
else:
error_message = _('Timesheets before the %(date)s (included) have been validated, and can no longer be modified.', date=last_validated_timesheet_date_str)
raise AccessError(error_message)
def _check_can_create(self):
# Check if the user has the correct access to create timesheets
if (
not (self.env.user.has_group('hr_timesheet.group_hr_timesheet_approver') or self.env.su)
and any(line.project_id and line.user_id != self.env.user for line in self)
):
raise AccessError(_("You cannot access timesheets that are not yours."))
self.check_if_allowed()
return super()._check_can_create()
def _check_can_write(self, vals):
if not self.env.user.has_group('hr_timesheet.group_hr_timesheet_approver'):
if 'validated' in vals:
raise AccessError(_('You can only validate the timesheets of employees of whom you are the manager or the timesheet approver.'))
elif self.filtered(lambda r: r.project_id and r.validated):
raise AccessError(_('Only a Timesheets Approver or Manager is allowed to modify a validated entry.'))
self.check_if_allowed(vals)
return super()._check_can_write(vals)
def _get_timesheet_action(self, value):
return {
'type': 'ir.actions.act_window',
'name': _('Open Form'),
'res_model': 'account.analytic.line',
'views': [(self.env.ref('timesheet_grid.timesheet_view_form_user_grid').id, 'form')],
'context': {
**self.env.context,
'default_unit_amount': value,
'is_timesheet': True,
},
}
def _update_existing_timers(self, vals):
if 'task_id' in vals:
# Check if there are some running timers linked to the timesheets to also update them.
timers = self.env['timer.timer'].search([('res_id', 'in', self.ids)])
if timers:
timers.write({'parent_res_model': 'project.task' if vals['task_id'] else None, 'parent_res_id': vals['task_id']})
def write(self, vals):
res = super().write(vals)
self._update_existing_timers(vals)
return res
@api.model
def _get_timesheet_field_and_model_name(self):
return 'task_id', 'project.task'
@api.model
def grid_update_cell(self, domain, measure_field_name, value):
if value == 0: # nothing to do
return
timesheets = self.search(domain, limit=2)
# sudo in case of timesheeting a task belonging to a private project
if timesheets.project_id and not all(timesheets.project_id.sudo().mapped("allow_timesheets")):
raise UserError(_("You cannot adjust the time of the timesheet for a project with timesheets disabled."))
non_validated_timesheets = timesheets.filtered(lambda timesheet: not timesheet.validated)
if len(non_validated_timesheets) > 1 or (len(timesheets) == 1 and timesheets.validated):
timesheets[0].copy({
'name': self.env.context.get('default_name', '/'),
measure_field_name: value,
})
elif len(non_validated_timesheets) == 1:
non_validated_timesheets[measure_field_name] += value
else:
project_id = self.env.context.get('default_project_id', False)
field_name, model_name = self._get_timesheet_field_and_model_name()
field_value = self.env.context.get(f'default_{field_name}', False)
if not project_id and field_value:
project_id = self.env[model_name].browse(field_value).project_id.id
if not project_id:
warning_msg = _('Your timesheet entry is missing a project. Please either group the Grid view by project or enter your timesheets by adding a line via the Form view.')
action = self._get_timesheet_action(value)
raise RedirectWarning(warning_msg, action, _('Open Form'))
if not self.env['project.project'].browse(project_id).sudo().allow_timesheets:
raise UserError(_("You cannot adjust the time of the timesheet for a project with timesheets disabled."))
self.create({
'name': self.env.context.get('default_name', '/'),
'project_id': project_id,
field_name: field_value,
measure_field_name: value,
})
@api.ondelete(at_uninstall=False)
def _unlink_if_manager(self):
if not self.env.user.has_group('hr_timesheet.group_hr_timesheet_approver') and self.filtered(
lambda r: r.project_id and r.validated):
raise AccessError(_('You cannot delete a validated entry. Please contact your manager or your timesheet approver.'))
self.check_if_allowed(delete=True)
def unlink(self):
res = super().unlink()
self.env['timer.timer'].search([
('res_model', '=', self._name),
('res_id', 'in', self.ids)
]).unlink()
return res
def _get_project_task_from_domain(self, domain):
project_id = task_id = False
for subdomain in domain:
if subdomain[0] == 'project_id' and subdomain[1] == '=':
project_id = subdomain[2]
elif subdomain[0] == 'task_id' and subdomain[1] == '=':
task_id = subdomain[2]
return project_id, task_id
def _prepare_duplicate_timesheet_line_values(self, column_field, day, cell_field, change):
# Prepares all values that should be set/modified when duplicating the current analytic line
return {
'name': '/',
column_field: day,
cell_field: change,
}
def _get_last_week(self):
today = fields.Date.to_string(fields.Date.today())
grid_anchor = self.env.context.get('grid_anchor', today)
last_week = fields.Datetime.from_string(grid_anchor)
last_week += relativedelta(weekday=SU(-2))
return grid_anchor, last_week.date()
# ----------------------------------------------------
# Timer Methods
# ----------------------------------------------------
@api.model
def action_start_new_timesheet_timer(self, vals):
project = self.env['project.project'].browse(vals.get('project_id', False))
if not project:
task = self.env['project.task'].browse(vals.get('task_id', False))
project = task.project_id or self.env['project.project'].browse(self._get_favorite_project_id())
result = bool(project) and project.check_can_start_timer()
if result is True:
if "default_date" in self.env.context:
self = self.with_context(default_date=fields.Date.today())
timesheet = self.create({
**vals,
'project_id': project.id,
})
timesheet.action_timer_start()
return timesheet._get_timesheet_timer_data()
return result
def _get_new_timesheet_timer_vals(self):
return {
'name': self.name,
'date': fields.Date.today(),
'project_id': self.project_id.id,
'task_id': self.task_id.id,
}
def action_timer_start(self):
""" Action start the timer of current timesheet
* Override method of hr_timesheet module.
"""
if self.validated:
raise UserError(_('You cannot use the timer on validated timesheets.'))
timesheet = self
if self.employee_id.sudo().last_validated_timesheet_date and self.date < self.employee_id.sudo().last_validated_timesheet_date:
timesheet = self.create(self._get_new_timesheet_timer_vals())
timesheet.action_timer_start()
elif not self.user_timer_id.timer_start and self.display_timer:
if self.date != fields.Date.context_today(self) and not self.env.context.get('_from_action_timer'):
timesheet = self.create(self._get_new_timesheet_timer_vals())
timesheet.with_context(_from_action_timer=True).action_timer_start()
else:
super().action_timer_start()
return timesheet._get_timesheet_timer_data()
def _get_last_timesheet_domain(self):
self.ensure_one()
return [
('id', '!=', self.id),
('user_id', '=', self.env.user.id),
('project_id', '=', self.project_id.id),
('task_id', '=', self.task_id.id),
('date', '=', fields.Date.context_today(self)),
('name', '=', '/'),
('validated', '=', False),
]
def _add_timesheet_time(self, minutes_spent, try_to_match=False):
if self.unit_amount == 0 and not minutes_spent:
# Check if unit_amount equals 0,
# if yes, then remove the timesheet
self.unlink()
return 0
minutes_spent = self.get_rounded_time(minutes_spent)
amount = self.unit_amount + minutes_spent
if not try_to_match or self.name != '/':
self.write({'unit_amount': amount})
return minutes_spent
domain = self._get_last_timesheet_domain()
last_timesheet_id = self.search(domain, limit=1)
# If the last timesheet of the day for this project and task has no description,
# we match both together.
if last_timesheet_id:
last_timesheet_id.unit_amount += amount
self.unlink()
else:
self.write({'unit_amount': amount})
return minutes_spent
def action_timer_stop(self, try_to_match=False):
""" Action stop the timer of the current timesheet
try_to_match: if true, we try to match with another timesheet which corresponds to the following criteria:
1. Neither of them has a description
2. The last one is not validated
3. Match user, project task, and must be the same day.
* Override method of timer module.
"""
if self.env.user == self.sudo().user_id:
# sudo as we can have a timesheet related to a company other than the current one.
self = self.sudo()
if self.validated:
raise UserError(_('You cannot use the timer on validated timesheets.'))
amount = 0
if self.user_timer_id.timer_start:
minutes_spent = super().action_timer_stop()
amount = self._add_timesheet_time(minutes_spent, try_to_match)
return amount
def _stop_all_users_timer(self, try_to_match=False):
""" Stop ALL the timers of the timesheets (WHOEVER the timer associated user is)
try_to_match: if true, we try to match with another timesheet which corresponds to the following criteria:
1. Neither of them has a description
2. The last one is not validated
3. Match user, project task, and must be the same day.
"""
if any(self.sudo().mapped('validated')):
raise UserError(_('Sorry, you cannot use a timer for a validated timesheet'))
timers = self.env['timer.timer'].sudo().search([('res_id', 'in', self.ids), ('res_model', '=', self._name)])
for timer in timers:
minutes_spent = timer.action_timer_stop()
self.env["account.analytic.line"].browse(timer.res_id).sudo()._add_timesheet_time(minutes_spent, try_to_match)
timer.unlink()
def action_timer_unlink(self):
""" Action unlink the timer of the current timesheet
"""
if self.env.user == self.sudo().user_id:
# sudo as we can have a timesheet related to a company other than the current one.
self = self.sudo()
self.user_timer_id.unlink()
if not self.unit_amount:
self.unlink()
def _action_interrupt_user_timers(self):
self.action_timer_stop()
def _get_timesheet_timer_field_names(self):
return ['date', 'name', 'company_id', 'project_id', 'task_id', 'unit_amount', 'timer_start']
def _get_timesheet_timer_data(self):
""" Get timesheet timer data needed to correctly run the timer
:return Dictionary containing timer data
:rtype {'field_name': field_value}
"""
self.ensure_one()
timesheet_data = {'other_company': True}
if self.company_id in self.env.companies:
timesheet_data = self._read_format(self._get_timesheet_timer_field_names())[0]
if timesheet_data['name'] == '/':
timesheet_data['name'] = ''
return timesheet_data
@api.model
def get_running_timer(self):
res = {'step_timer': int(self.env['ir.config_parameter'].sudo().get_param('timesheet_grid.timesheet_min_duration', 15))}
timer = self.env['timer.timer'].search([
('user_id', '=', self.env.user.id),
('timer_start', '!=', False),
('timer_pause', '=', False),
('res_model', '=', self._name),
], limit=1)
if timer:
# sudo as we can have a timesheet related to a company other than the current one.
res.update(self.sudo().browse(timer.res_id)._get_timesheet_timer_data())
return res
@api.model
def get_rounded_time(self, timer):
minimum_duration = int(self.env['ir.config_parameter'].sudo().get_param('timesheet_grid.timesheet_min_duration', 0))
rounding = int(self.env['ir.config_parameter'].sudo().get_param('timesheet_grid.timesheet_rounding', 0))
rounded_minutes = round_time_spent(timer, minimum_duration, rounding)
return rounded_minutes / 60
@api.model
def _add_time_to_timesheet_fields(self):
return ['task_id']
def action_add_time_to_timesheet(self, vals):
minutes = int(self.env['ir.config_parameter'].sudo().get_param('timesheet_grid.timesheet_min_duration', 15))
if self:
for field in self._add_time_to_timesheet_fields():
val = vals.get(field, False)
if self[field].id == val and self.project_id.id == vals['project_id']:
self.unit_amount += minutes / 60
return self.id
timesheet = self.create({
**vals,
'unit_amount': minutes / 60,
})
return timesheet.id
def action_add_time_to_timer(self, time):
if self.validated:
raise UserError(_('You cannot use the timer on validated timesheets.'))
if not self.user_id.employee_ids:
raise UserError(_('An employee must be linked to your user to record time.'))
timer = self.user_timer_id
if not timer:
self.action_timer_start()
timer = self.user_timer_id
timer.timer_start = min(timer.timer_start - timedelta(0, time), fields.Datetime.now())
def change_description(self, description):
if not self.exists():
return
if True in self.mapped('validated'):
raise UserError(_('You cannot use the timer on validated timesheets.'))
self.write({'name': description})
def action_change_project_task(self, new_project_id, new_task_id):
if self.validated:
raise UserError(_('You cannot use the timer on validated timesheets.'))
if not self.unit_amount:
self.write({
'project_id': new_project_id,
'task_id': new_task_id,
})
return self.id
new_timesheet = self.create({
'name': self.name,
'project_id': new_project_id,
'task_id': new_task_id,
})
self.user_timer_id.res_id = new_timesheet
return new_timesheet.id
def _get_domain_for_validation_timesheets(self, validated=False):
""" Get the domain to check if the user can validate/invalidate which timesheets
2 access rights give access to validate timesheets:
1. Approver: in this access right, the user can't validate all timesheets,
he can validate the timesheets where he is the manager or timesheet responsible of the
employee who is assigned to this timesheets or the user is the owner of the project.
The user cannot validate his own timesheets.
2. Manager (Administrator): with this access right, the user can validate all timesheets.
"""
domain = Domain('project_id', '!=', False) & Domain('validated', '=', validated)
if not validated:
domain &= Domain("date", "<=", fields.Date.today())
if not self.env.user.has_group('hr_timesheet.group_timesheet_manager'):
return Domain.AND([
domain,
Domain('user_id', '!=', self.env.uid),
[
'|', ('employee_id.timesheet_manager_id', 'in', [False, self.env.uid]),
'|', ('employee_id', 'in', self.env.user.employee_id.subordinate_ids.ids),
'|', ('employee_id.parent_id.user_id', '=', self.env.uid),
'&', ('employee_id.timesheet_manager_id', '=', False), ('employee_id.parent_id', '=', False),
],
])
return domain
def _get_timesheets_to_merge(self):
return self.filtered(lambda l: l.project_id and not l.validated)
def action_merge_timesheets(self):
to_merge = self._get_timesheets_to_merge()
if len(to_merge) <= 1:
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'message': _('There are no timesheets to merge.'),
'type': 'warning',
'sticky': False,
}
}
return {
'name': _('Merge Timesheets'),
'view_mode': 'form',
'res_model': 'hr_timesheet.merge.wizard',
'views': [(self.env.ref('timesheet_grid.timesheet_merge_wizard_view_form').id, 'form')],
'type': 'ir.actions.act_window',
'target': 'new',
'context': dict(self.env.context, active_ids=to_merge.ids),
}
def action_timer_increase(self):
min_duration = int(self.env['ir.config_parameter'].sudo().get_param('timesheet_grid.timesheet_min_duration', 15))
self.update({'unit_amount': self.unit_amount + (min_duration / 60)})
def action_timer_decrease(self):
min_duration = int(self.env['ir.config_parameter'].sudo().get_param('timesheet_grid.timesheet_min_duration', 15))
duration = self.unit_amount - (min_duration / 60)
self.update({'unit_amount': duration if duration > 0 else 0 })
def _get_timer_vals(self):
vals = super()._get_timer_vals()
if self.task_id:
vals['parent_res_model'] = 'project.task'
vals['parent_res_id'] = self.task_id.id
return vals