1
0
forked from Mapan/odoo17e
odoo17e-kedaikipas58/addons/timesheet_grid/models/analytic.py
2024-12-10 09:04:09 +07:00

799 lines
36 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import ast
import re
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta
from dateutil.rrule import SU
from collections import defaultdict
from odoo import tools, models, fields, api, _
from odoo.addons.resource.models.utils import make_aware
from odoo.addons.resource.models.utils import filter_domain_leaf
from odoo.exceptions import UserError, AccessError
from odoo.osv import expression
from odoo.tools import format_date
class AnalyticLine(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
employee_id = fields.Many2one(group_expand="_group_expand_employee_ids")
# reset amount on copy
amount = fields.Monetary(copy=False)
validated = fields.Boolean("Validated line", group_operator="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.")
is_timesheet = fields.Boolean(
string="Timesheet Line", compute_sudo=True,
compute='_compute_is_timesheet', search='_search_is_timesheet',
help="Set if this analytic line represents a line of timesheet.")
duration_unit_amount = fields.Float(related="unit_amount", readonly=True, string="Timesheet Init Amount")
unit_amount_validate = fields.Float(related="unit_amount", readonly=True, string="Timesheet Unit Time")
display_timer = fields.Boolean(
compute='_compute_display_timer',
help="Technical field used to display the timer if the encoding unit is 'Hours'.")
@api.constrains('unit_amount')
def _check_timesheet_unit_amount(self):
if any(t.unit_amount > 999999 for t in self if t.is_timesheet):
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=None):
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, dummy = make_aware(start_datetime)
to_datetime, dummy = make_aware(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():
return get_unavailable_dates(self.env.company.resource_calendar_id._work_intervals_batch(from_datetime, to_datetime)[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 = 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:
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
else:
unavailability_intervals_per_employee_id[False] = get_company_unavailable_dates()
return unavailability_intervals_per_employee_id
@api.depends('project_id')
def _compute_is_timesheet(self):
for line in self:
line.is_timesheet = bool(line.project_id)
def _search_is_timesheet(self, operator, value):
if (operator, value) in [('=', True), ('!=', False)]:
return [('project_id', '!=', False)]
return [('project_id', '=', False)]
@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.user_has_groups('hr_timesheet.group_timesheet_manager')
is_approver = self.user_has_groups('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.sudo().last_validated_timesheet_date
for employee in self.employee_id
}
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
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.user_has_groups('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
analytic_lines = self.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.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.user_has_groups('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.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.user_has_groups('hr_timesheet.group_timesheet_manager') and not self.env.su:
is_timesheet_approver = self.user_has_groups('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._uid),
'|', ('parent_id.user_id', '=', self._uid),
'|', ('timesheet_manager_id', 'in', [False, self._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.is_timesheet 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)
deleted = _('deleted')
modified = _('modified')
raise AccessError(_('Timesheets before the %s (included) have been validated, and can no longer be %s.', last_validated_timesheet_date_str, deleted if delete else modified))
def _check_can_create(self):
# Check if the user has the correct access to create timesheets
if not (self.user_has_groups('hr_timesheet.group_hr_timesheet_approver') or self.env.su) and any(line.is_timesheet and line.user_id.id != self.env.user.id 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.user_has_groups('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.is_timesheet 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)
@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': '/',
measure_field_name: value,
})
elif len(non_validated_timesheets) == 1:
non_validated_timesheets[measure_field_name] += value
else:
project_id = self._context.get('default_project_id', False)
field_name, model_name = self._get_timesheet_field_and_model_name()
field_value = self._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:
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'message': _("Your timesheet entry is missing a project. Please either group the Grid view by project or enter your timesheets through another view."),
'type': 'danger',
'sticky': False,
}
}
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': '/',
'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.user_has_groups('hr_timesheet.group_hr_timesheet_approver') and self.filtered(
lambda r: r.is_timesheet 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(AnalyticLine, self).unlink()
self.env['timer.timer'].search([
('res_model', '=', self._name),
('res_id', 'in', self.ids)
]).unlink()
return res
@api.model
def _apply_timesheet_label(self, view_arch, view_type='form'):
doc = view_arch
encoding_uom = self.env.company.timesheet_encode_uom_id
# Here, we select only the unit_amount field having no string set to give priority to
# custom inheretied view stored in database. Even if normally, no xpath can be done on
# 'string' attribute.
for node in doc.xpath("//field[@name='unit_amount'][@widget='timesheet_uom' or @widget='timesheet_uom_timer'][not(@string)]"):
if view_type == 'grid':
node.set('string', encoding_uom.name)
else:
node.set('string', _('%s Spent', re.sub(r'[\(\)]', '', encoding_uom.name or '')))
return doc
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 _group_expand_employee_ids(self, employees, domain, order):
""" Group expand by employee_ids in grid view
This group expand allow to add some record by employee, where
the employee has been timesheeted in a task of a project in the
past 7 days.
Example: Filter timesheet from my team this week:
[['project_id', '!=', False],
'|',
['employee_id.timesheet_manager_id', '=', 2],
'|',
['employee_id.parent_id.user_id', '=', 2],
'|',
['project_id.user_id', '=', 2],
['user_id', '=', 2]]
'&',
['date', '>=', '2020-06-01'],
['date', '<=', '2020-06-07']
Becomes:
[('project_id', '!=', False),
('date', '>=', datetime.date(2020, 5, 28)),
('date', '<=', '2020-06-04'),
['project_id', '!=', False],
'|',
['employee_id.timesheet_manager_id', '=', 2],
'|',
['employee_id.parent_id.user_id', '=', 2],
'|',
['project_id.user_id', '=', 2],
['user_id', '=', 2]]
'&',
['date', '>=', '1970-01-01'],
['date', '<=', '2250-01-01']
"""
if not self.env.context.get('group_expand', False):
return employees
grid_anchor, last_week = self._get_last_week()
domain_search = expression.AND([
[('project_id.allow_timesheets', '=', True),
('date', '>=', last_week),
('date', '<=', grid_anchor),
'|',
('task_id.active', '=', True),
('task_id', '=', False),
], filter_domain_leaf(domain, lambda field: field != 'date')
])
group_order = self.env['hr.employee']._order
if order == group_order:
order = 'employee_id'
elif order == tools.reverse_order(group_order):
order = 'employee_id desc'
else:
order = None
return self.search(domain_search, order=order).employee_id
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._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.'))
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):
timesheet = self.create(self._get_new_timesheet_timer_vals())
timesheet.action_timer_start()
else:
super(AnalyticLine, self).action_timer_start()
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.today()),
('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
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))
minutes_spent = self._timer_rounding(minutes_spent, minimum_duration, rounding)
amount = self.unit_amount + minutes_spent * 60 / 3600
if not try_to_match or self.name != '/':
self.write({'unit_amount': amount})
return amount
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 amount
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(AnalyticLine, self).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_data(self, timer=None):
if not timer:
timer = self.user_timer_id
running_seconds = (fields.Datetime.now() - timer.timer_start).total_seconds() + self.unit_amount * 3600
data = {
'id': timer.res_id,
'start': running_seconds,
'project_id': self.project_id.id,
'task_id': self.task_id.id,
'description': self.name,
}
if self.project_id.company_id and self.project_id.company_id not in self.env.companies:
data.update({
'readonly': True,
'project_name': self.project_id.name,
'task_name': self.task_id.name or '',
})
return data
@api.model
def get_running_timer(self):
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 not timer:
return {'step_timer': step_timer}
# sudo as we can have a timesheet related to a company other than the current one.
timer_data = self.sudo().browse(timer.res_id)._get_timesheet_timer_data(timer)
timer_data['step_timer'] = step_timer
return timer_data
@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 = self._timer_rounding(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 _action_open_to_validate_timesheet_view(self, type_view=None):
action = self.env['ir.actions.act_window']._for_xml_id('timesheet_grid.timesheet_grid_to_validate_action')
context = action.get('context', {}) and ast.literal_eval(action['context'])
if (type_view == 'week'):
context['grid_range'] = 'week'
context['grid_anchor'] = fields.Date.today() - relativedelta(weeks=1)
else:
context['grid_range'] = 'month'
if type_view == 'month':
context['grid_anchor'] = fields.Date.today() - relativedelta(months=1)
else:
context['grid_anchor'] = fields.Date.today()
context.pop('search_default_my_team_timesheet', None)
if type_view in ('week', 'month'):
action['view_mode'] = ','.join([
mode
for mode in action['view_mode'].split(",")
if mode != "pivot"
])
action['views'] = [
view
for view in action['views']
if view[1] != "pivot"
]
action['context'] = context
return action
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 = [('is_timesheet', '=', True), ('validated', '=', validated)]
if not validated:
domain = expression.AND([
domain,
[("date", "<=", fields.Date.today())],
])
if not self.user_has_groups('hr_timesheet.group_timesheet_manager'):
return expression.AND([
domain,
[
('user_id', '!=', self._uid),
'|', ('employee_id.timesheet_manager_id', 'in', [False, self._uid]),
'|', ('employee_id', 'in', self.env.user.employee_id.subordinate_ids.ids),
'|', ('employee_id.parent_id.user_id', '=', self._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.is_timesheet 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', 0))
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', 0))
duration = self.unit_amount - (min_duration / 60)
self.update({'unit_amount': duration if duration > 0 else 0 })