mirror of
https://github.com/bringout/oca-ocb-project.git
synced 2026-04-22 17:22:01 +02:00
19.0 vanilla
This commit is contained in:
parent
a2f74aefd8
commit
4a4d12c333
844 changed files with 212348 additions and 270090 deletions
|
|
@ -1,9 +1,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import res_company # has to be before hr_holidays to create needed columns on res.company
|
||||
from . import account_analytic
|
||||
from . import hr_holidays
|
||||
from . import hr_leave
|
||||
from . import project_task
|
||||
from . import res_config_settings
|
||||
from . import resource_calendar_leaves
|
||||
|
|
|
|||
|
|
@ -1,43 +1,57 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.osv import expression
|
||||
from odoo.exceptions import RedirectWarning, UserError
|
||||
from odoo.fields import Domain
|
||||
|
||||
|
||||
class AccountAnalyticLine(models.Model):
|
||||
_inherit = 'account.analytic.line'
|
||||
|
||||
holiday_id = fields.Many2one("hr.leave", string='Leave Request', copy=False)
|
||||
global_leave_id = fields.Many2one("resource.calendar.leaves", string="Global Time Off", ondelete='cascade')
|
||||
task_id = fields.Many2one(domain="[('allow_timesheets', '=', True),"
|
||||
"('project_id', '=?', project_id), ('is_timeoff_task', '=', False)]")
|
||||
holiday_id = fields.Many2one("hr.leave", string='Time Off Request', copy=False, index='btree_not_null', export_string_translation=False)
|
||||
global_leave_id = fields.Many2one("resource.calendar.leaves", string="Global Time Off", index='btree_not_null', ondelete='cascade', export_string_translation=False)
|
||||
task_id = fields.Many2one(domain="[('allow_timesheets', '=', True), ('project_id', '=?', project_id), ('has_template_ancestor', '=', False), ('is_timeoff_task', '=', False)]")
|
||||
|
||||
_timeoff_timesheet_idx = models.Index('(task_id) WHERE (global_leave_id IS NOT NULL OR holiday_id IS NOT NULL) AND project_id IS NOT NULL')
|
||||
|
||||
def _get_redirect_action(self):
|
||||
leave_form_view_id = self.env.ref('hr_holidays.hr_leave_view_form').id
|
||||
action_data = {
|
||||
'name': _('Time Off'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'hr.leave',
|
||||
'views': [(self.env.ref('hr_holidays.hr_leave_view_tree_my').id, 'list'), (leave_form_view_id, 'form')],
|
||||
'domain': [('id', 'in', self.holiday_id.ids)],
|
||||
}
|
||||
if len(self.holiday_id) == 1:
|
||||
action_data['views'] = [(leave_form_view_id, 'form')]
|
||||
action_data['res_id'] = self.holiday_id.id
|
||||
return action_data
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def _unlink_except_linked_leave(self):
|
||||
if any(line.holiday_id for line in self):
|
||||
raise UserError(_('You cannot delete timesheets that are linked to time off requests. Please cancel your time off request from the Time Off application instead.'))
|
||||
if any(line.global_leave_id for line in self):
|
||||
raise UserError(_('You cannot delete timesheets that are linked to global time off.'))
|
||||
elif any(line.holiday_id for line in self):
|
||||
error_message = _('You cannot delete timesheets that are linked to time off requests. Please cancel your time off request from the Time Off application instead.')
|
||||
if not self.env.user.has_group('hr_holidays.group_hr_holidays_user') and self.env.user not in self.holiday_id.sudo().user_id:
|
||||
raise UserError(error_message)
|
||||
action = self._get_redirect_action()
|
||||
raise RedirectWarning(error_message, action, _('View Time Off'))
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
if not self.env.su:
|
||||
task_ids = [vals['task_id'] for vals in vals_list if vals.get('task_id')]
|
||||
has_timeoff_task = self.env['project.task'].search_count([('id', 'in', task_ids), ('is_timeoff_task', '=', True)], limit=1) > 0
|
||||
if has_timeoff_task:
|
||||
raise UserError(_('You cannot create timesheets for a task that is linked to a time off type. Please use the Time Off application to request new time off instead.'))
|
||||
return super().create(vals_list)
|
||||
|
||||
def _check_can_update_timesheet(self):
|
||||
return self.env.su or not self.filtered('holiday_id')
|
||||
|
||||
def write(self, vals):
|
||||
if not self._check_can_update_timesheet():
|
||||
def _check_can_write(self, values):
|
||||
if not self.env.su and self.holiday_id:
|
||||
raise UserError(_('You cannot modify timesheets that are linked to time off requests. Please use the Time Off application to modify your time off requests instead.'))
|
||||
return super().write(vals)
|
||||
return super()._check_can_write(values)
|
||||
|
||||
def _check_can_create(self):
|
||||
if not self.env.su and any(task.is_timeoff_task for task in self.task_id):
|
||||
raise UserError(_('You cannot create timesheets for a task that is linked to a time off type. Please use the Time Off application to request new time off instead.'))
|
||||
return super()._check_can_create()
|
||||
|
||||
def _get_favorite_project_id_domain(self, employee_id=False):
|
||||
return expression.AND([
|
||||
return Domain.AND([
|
||||
super()._get_favorite_project_id_domain(employee_id),
|
||||
[('holiday_id', '=', False), ('global_leave_id', '=', False)],
|
||||
Domain('holiday_id', '=', False),
|
||||
Domain('global_leave_id', '=', False),
|
||||
])
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
class Employee(models.Model):
|
||||
class HrEmployee(models.Model):
|
||||
_inherit = 'hr.employee'
|
||||
|
||||
@api.model_create_multi
|
||||
|
|
@ -20,36 +21,46 @@ class Employee(models.Model):
|
|||
return employees
|
||||
|
||||
def write(self, vals):
|
||||
result = super(Employee, self).write(vals)
|
||||
if vals.get('active'):
|
||||
inactive_emp = self.filtered(lambda e: not e.active)
|
||||
result = super().write(vals)
|
||||
self_company = self.with_context(allowed_company_ids=self.company_id.ids)
|
||||
if 'active' in vals:
|
||||
if vals.get('active'):
|
||||
# Create future holiday timesheets
|
||||
self_company._create_future_public_holidays_timesheets(self)
|
||||
inactive_emp = inactive_emp.with_env(self_company.env)
|
||||
inactive_emp._create_future_public_holidays_timesheets(inactive_emp)
|
||||
else:
|
||||
# Delete future holiday timesheets
|
||||
self_company._delete_future_public_holidays_timesheets()
|
||||
elif 'resource_calendar_id' in vals:
|
||||
# Update future holiday timesheets
|
||||
self_company._delete_future_public_holidays_timesheets()
|
||||
self_company._create_future_public_holidays_timesheets(self)
|
||||
self_company._create_future_public_holidays_timesheets(self_company)
|
||||
return result
|
||||
|
||||
def _delete_future_public_holidays_timesheets(self):
|
||||
future_timesheets = self.env['account.analytic.line'].sudo().search([('global_leave_id', '!=', False), ('date', '>=', fields.date.today()), ('employee_id', 'in', self.ids)])
|
||||
future_timesheets = self.env['account.analytic.line'].sudo().search([('global_leave_id', '!=', False), ('date', '>=', fields.Date.today()), ('employee_id', 'in', self.ids)])
|
||||
future_timesheets.write({'global_leave_id': False})
|
||||
future_timesheets.unlink()
|
||||
|
||||
def _create_future_public_holidays_timesheets(self, employees):
|
||||
lines_vals = []
|
||||
today = fields.Datetime.today()
|
||||
global_leaves_wo_calendar = defaultdict(lambda: self.env["resource.calendar.leaves"])
|
||||
global_leaves_wo_calendar.update(dict(self.env['resource.calendar.leaves']._read_group(
|
||||
[('calendar_id', '=', False), ('resource_id', '=', False), ('date_from', '>=', today)],
|
||||
groupby=['company_id'],
|
||||
aggregates=['id:recordset'],
|
||||
)))
|
||||
for employee in employees:
|
||||
if not employee.active:
|
||||
continue
|
||||
# First we look for the global time off that are already planned after today
|
||||
global_leaves = employee.resource_calendar_id.global_leave_ids.filtered(lambda l: l.date_from >= fields.Datetime.today())
|
||||
global_leaves = employee.resource_calendar_id.global_leave_ids.filtered(lambda l: l.date_from >= today) + global_leaves_wo_calendar[employee.company_id]
|
||||
work_hours_data = global_leaves._work_time_per_day()
|
||||
for global_time_off in global_leaves:
|
||||
for index, (day_date, work_hours_count) in enumerate(work_hours_data[global_time_off.id]):
|
||||
for index, (day_date, work_hours_count) in enumerate(work_hours_data[employee.resource_calendar_id.id][global_time_off.id]):
|
||||
lines_vals.append(
|
||||
global_time_off._timesheet_prepare_line_values(
|
||||
index,
|
||||
|
|
|
|||
|
|
@ -1,142 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class HolidaysType(models.Model):
|
||||
_inherit = "hr.leave.type"
|
||||
|
||||
def _default_project_id(self):
|
||||
company = self.company_id if self.company_id else self.env.company
|
||||
return company.internal_project_id.id
|
||||
|
||||
def _default_task_id(self):
|
||||
company = self.company_id if self.company_id else self.env.company
|
||||
return company.leave_timesheet_task_id.id
|
||||
|
||||
timesheet_generate = fields.Boolean(
|
||||
'Generate Timesheet', compute='_compute_timesheet_generate', store=True, readonly=False,
|
||||
help="If checked, when validating a time off, timesheet will be generated in the Vacation Project of the company.")
|
||||
timesheet_project_id = fields.Many2one('project.project', string="Project", default=_default_project_id, domain="[('company_id', '=', company_id)]")
|
||||
timesheet_task_id = fields.Many2one(
|
||||
'project.task', string="Task", compute='_compute_timesheet_task_id',
|
||||
store=True, readonly=False, default=_default_task_id,
|
||||
domain="[('project_id', '=', timesheet_project_id),"
|
||||
"('project_id', '!=', False),"
|
||||
"('company_id', '=', company_id)]")
|
||||
|
||||
@api.depends('timesheet_task_id', 'timesheet_project_id')
|
||||
def _compute_timesheet_generate(self):
|
||||
for leave_type in self:
|
||||
leave_type.timesheet_generate = leave_type.timesheet_task_id and leave_type.timesheet_project_id
|
||||
|
||||
@api.depends('timesheet_project_id')
|
||||
def _compute_timesheet_task_id(self):
|
||||
for leave_type in self:
|
||||
company = leave_type.company_id if leave_type.company_id else self.env.company
|
||||
default_task_id = company.leave_timesheet_task_id
|
||||
|
||||
if default_task_id and default_task_id.project_id == leave_type.timesheet_project_id:
|
||||
leave_type.timesheet_task_id = default_task_id
|
||||
else:
|
||||
leave_type.timesheet_task_id = False
|
||||
|
||||
@api.constrains('timesheet_generate', 'timesheet_project_id', 'timesheet_task_id')
|
||||
def _check_timesheet_generate(self):
|
||||
for holiday_status in self:
|
||||
if holiday_status.timesheet_generate:
|
||||
if not holiday_status.timesheet_project_id or not holiday_status.timesheet_task_id:
|
||||
raise ValidationError(_("Both the internal project and task are required to "
|
||||
"generate a timesheet for the time off %s. If you don't want a timesheet, you should "
|
||||
"leave the internal project and task empty.") % (holiday_status.name))
|
||||
|
||||
|
||||
class Holidays(models.Model):
|
||||
_inherit = "hr.leave"
|
||||
|
||||
timesheet_ids = fields.One2many('account.analytic.line', 'holiday_id', string="Analytic Lines")
|
||||
|
||||
def _validate_leave_request(self):
|
||||
""" Timesheet will be generated on leave validation only if a timesheet_project_id and a
|
||||
timesheet_task_id are set on the corresponding leave type. The generated timesheet will
|
||||
be attached to this project/task.
|
||||
"""
|
||||
holidays = self.filtered(
|
||||
lambda l: l.holiday_type == 'employee' and
|
||||
l.holiday_status_id.timesheet_project_id and
|
||||
l.holiday_status_id.timesheet_task_id and
|
||||
l.holiday_status_id.timesheet_project_id.sudo().company_id == (l.holiday_status_id.company_id or self.env.company))
|
||||
|
||||
# Unlink previous timesheets do avoid doublon (shouldn't happen on the interface but meh)
|
||||
old_timesheets = holidays.sudo().timesheet_ids
|
||||
if old_timesheets:
|
||||
old_timesheets.holiday_id = False
|
||||
old_timesheets.unlink()
|
||||
|
||||
# create the timesheet on the vacation project
|
||||
holidays._timesheet_create_lines()
|
||||
|
||||
return super()._validate_leave_request()
|
||||
|
||||
def _timesheet_create_lines(self):
|
||||
vals_list = []
|
||||
for leave in self:
|
||||
if not leave.employee_id:
|
||||
continue
|
||||
work_hours_data = leave.employee_id.list_work_time_per_day(
|
||||
leave.date_from,
|
||||
leave.date_to)
|
||||
for index, (day_date, work_hours_count) in enumerate(work_hours_data):
|
||||
vals_list.append(leave._timesheet_prepare_line_values(index, work_hours_data, day_date, work_hours_count))
|
||||
return self.env['account.analytic.line'].sudo().create(vals_list)
|
||||
|
||||
def _timesheet_prepare_line_values(self, index, work_hours_data, day_date, work_hours_count):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'name': _("Time Off (%s/%s)", index + 1, len(work_hours_data)),
|
||||
'project_id': self.holiday_status_id.timesheet_project_id.id,
|
||||
'task_id': self.holiday_status_id.timesheet_task_id.id,
|
||||
'account_id': self.holiday_status_id.timesheet_project_id.analytic_account_id.id,
|
||||
'unit_amount': work_hours_count,
|
||||
'user_id': self.employee_id.user_id.id,
|
||||
'date': day_date,
|
||||
'holiday_id': self.id,
|
||||
'employee_id': self.employee_id.id,
|
||||
'company_id': self.holiday_status_id.timesheet_task_id.company_id.id or self.holiday_status_id.timesheet_project_id.company_id.id,
|
||||
}
|
||||
|
||||
def _check_missing_global_leave_timesheets(self):
|
||||
if not self:
|
||||
return
|
||||
min_date = min([leave.date_from for leave in self])
|
||||
max_date = max([leave.date_to for leave in self])
|
||||
|
||||
global_leaves = self.env['resource.calendar.leaves'].search([
|
||||
("resource_id", "=", False),
|
||||
("date_to", ">=", min_date),
|
||||
("date_from", "<=", max_date),
|
||||
("calendar_id", "!=", False),
|
||||
("company_id.internal_project_id", "!=", False),
|
||||
("company_id.leave_timesheet_task_id", "!=", False),
|
||||
])
|
||||
if global_leaves:
|
||||
global_leaves._generate_public_time_off_timesheets(self.sudo().employee_ids)
|
||||
|
||||
def action_refuse(self):
|
||||
""" Remove the timesheets linked to the refused holidays """
|
||||
result = super(Holidays, self).action_refuse()
|
||||
timesheets = self.sudo().mapped('timesheet_ids')
|
||||
timesheets.write({'holiday_id': False})
|
||||
timesheets.unlink()
|
||||
self._check_missing_global_leave_timesheets()
|
||||
return result
|
||||
|
||||
def _action_user_cancel(self, reason):
|
||||
res = super()._action_user_cancel(reason)
|
||||
timesheets = self.sudo().timesheet_ids
|
||||
timesheets.write({'holiday_id': False})
|
||||
timesheets.unlink()
|
||||
self._check_missing_global_leave_timesheets()
|
||||
return res
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models, _
|
||||
|
||||
import pytz
|
||||
|
||||
|
||||
class HrLeave(models.Model):
|
||||
_inherit = "hr.leave"
|
||||
|
||||
timesheet_ids = fields.One2many('account.analytic.line', 'holiday_id', string="Analytic Lines")
|
||||
|
||||
def _validate_leave_request(self):
|
||||
self._generate_timesheets()
|
||||
return super()._validate_leave_request()
|
||||
|
||||
def _generate_timesheets(self, ignored_resource_calendar_leaves=None):
|
||||
""" Timesheet will be generated on leave validation
|
||||
internal_project_id and leave_timesheet_task_id are used.
|
||||
The generated timesheet will be attached to this project/task.
|
||||
"""
|
||||
vals_list = []
|
||||
leave_ids = []
|
||||
calendar_leaves_data = self.env['resource.calendar.leaves']._read_group([('holiday_id', 'in', self.ids)], ['holiday_id'], ['id:array_agg'])
|
||||
mapped_calendar_leaves = {leave: calendar_leave_ids[0] for leave, calendar_leave_ids in calendar_leaves_data}
|
||||
for leave in self:
|
||||
project, task = leave.employee_id.company_id.internal_project_id, leave.employee_id.company_id.leave_timesheet_task_id
|
||||
|
||||
if not project or not task or leave.holiday_status_id.time_type == 'other':
|
||||
continue
|
||||
|
||||
leave_ids.append(leave.id)
|
||||
if not leave.employee_id:
|
||||
continue
|
||||
|
||||
calendar = leave.employee_id.resource_calendar_id
|
||||
calendar_timezone = pytz.timezone((calendar or leave.employee_id).tz)
|
||||
|
||||
if calendar.flexible_hours and (leave.request_unit_hours or leave.request_unit_half or leave.date_from.date() == leave.date_to.date()):
|
||||
leave_date = leave.date_from.astimezone(calendar_timezone).date()
|
||||
if leave.request_unit_hours:
|
||||
hours = leave.request_hour_to - leave.request_hour_from
|
||||
elif leave.request_unit_half:
|
||||
hours = calendar.hours_per_day / 2
|
||||
else: # Single-day leave
|
||||
hours = calendar.hours_per_day
|
||||
work_hours_data = [(leave_date, hours)]
|
||||
else:
|
||||
ignored_resource_calendar_leaves = ignored_resource_calendar_leaves or []
|
||||
if leave in mapped_calendar_leaves:
|
||||
ignored_resource_calendar_leaves.append(mapped_calendar_leaves[leave])
|
||||
work_hours_data = leave.employee_id._list_work_time_per_day(
|
||||
leave.date_from,
|
||||
leave.date_to,
|
||||
domain=[('id', 'not in', ignored_resource_calendar_leaves)] if ignored_resource_calendar_leaves else None)[leave.employee_id.id]
|
||||
|
||||
for index, (day_date, work_hours_count) in enumerate(work_hours_data):
|
||||
vals_list.append(leave._timesheet_prepare_line_values(index, work_hours_data, day_date, work_hours_count, project, task))
|
||||
|
||||
# Unlink previous timesheets to avoid doublon (shouldn't happen on the interface but meh). Necessary when the function is called to regenerate timesheets.
|
||||
old_timesheets = self.env["account.analytic.line"].sudo().search([('project_id', '!=', False), ('holiday_id', 'in', leave_ids)])
|
||||
if old_timesheets:
|
||||
old_timesheets.holiday_id = False
|
||||
old_timesheets.unlink()
|
||||
|
||||
self.env['account.analytic.line'].sudo().create(vals_list)
|
||||
|
||||
def _timesheet_prepare_line_values(self, index, work_hours_data, day_date, work_hours_count, project, task):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'name': _("Time Off (%(index)s/%(total)s)", index=index + 1, total=len(work_hours_data)),
|
||||
'project_id': project.id,
|
||||
'task_id': task.id,
|
||||
'account_id': project.sudo().account_id.id,
|
||||
'unit_amount': work_hours_count,
|
||||
'user_id': self.employee_id.user_id.id,
|
||||
'date': day_date,
|
||||
'holiday_id': self.id,
|
||||
'employee_id': self.employee_id.id,
|
||||
'company_id': task.sudo().company_id.id or project.sudo().company_id.id,
|
||||
}
|
||||
|
||||
def _check_missing_global_leave_timesheets(self):
|
||||
if not self:
|
||||
return
|
||||
min_date = min(self.mapped('date_from'))
|
||||
max_date = max(self.mapped('date_to'))
|
||||
|
||||
global_leaves = self.env['resource.calendar.leaves'].search([
|
||||
("resource_id", "=", False),
|
||||
("date_to", ">=", min_date),
|
||||
("date_from", "<=", max_date),
|
||||
("company_id.internal_project_id", "!=", False),
|
||||
("company_id.leave_timesheet_task_id", "!=", False),
|
||||
])
|
||||
if global_leaves:
|
||||
global_leaves._generate_public_time_off_timesheets(self.employee_id)
|
||||
|
||||
def action_refuse(self):
|
||||
""" Remove the timesheets linked to the refused holidays """
|
||||
result = super().action_refuse()
|
||||
timesheets = self.sudo().mapped('timesheet_ids')
|
||||
timesheets.write({'holiday_id': False})
|
||||
timesheets.unlink()
|
||||
self._check_missing_global_leave_timesheets()
|
||||
return result
|
||||
|
||||
def _action_user_cancel(self, reason=None):
|
||||
res = super()._action_user_cancel(reason)
|
||||
timesheets = self.sudo().timesheet_ids
|
||||
timesheets.write({'holiday_id': False})
|
||||
timesheets.unlink()
|
||||
self._check_missing_global_leave_timesheets()
|
||||
return res
|
||||
|
||||
def _force_cancel(self, *args, **kwargs):
|
||||
super()._force_cancel(*args, **kwargs)
|
||||
# override this method to reevaluate timesheets after the leaves are updated via force cancel
|
||||
timesheets = self.sudo().timesheet_ids
|
||||
timesheets.holiday_id = False
|
||||
timesheets.unlink()
|
||||
|
||||
def write(self, vals):
|
||||
res = super().write(vals)
|
||||
# reevaluate timesheets after the leaves are wrote in order to remove empty timesheets
|
||||
timesheet_ids_to_remove = []
|
||||
for leave in self:
|
||||
if leave.number_of_days == 0 and leave.sudo().timesheet_ids:
|
||||
leave.sudo().timesheet_ids.holiday_id = False
|
||||
timesheet_ids_to_remove.extend(leave.timesheet_ids)
|
||||
self.env['account.analytic.line'].browse(set(timesheet_ids_to_remove)).sudo().unlink()
|
||||
return res
|
||||
|
|
@ -1,23 +1,25 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models, _
|
||||
from odoo import fields, models
|
||||
from odoo.fields import Domain
|
||||
from odoo.tools import OrderedSet
|
||||
|
||||
class Task(models.Model):
|
||||
|
||||
class ProjectTask(models.Model):
|
||||
_inherit = 'project.task'
|
||||
|
||||
leave_types_count = fields.Integer(compute='_compute_leave_types_count')
|
||||
is_timeoff_task = fields.Boolean("Is Time off Task", compute="_compute_is_timeoff_task", search="_search_is_timeoff_task")
|
||||
leave_types_count = fields.Integer(compute='_compute_leave_types_count', string="Time Off Types Count")
|
||||
is_timeoff_task = fields.Boolean("Is Time off Task", compute="_compute_is_timeoff_task", search="_search_is_timeoff_task", export_string_translation=False, groups="hr_timesheet.group_hr_timesheet_user")
|
||||
|
||||
def _compute_leave_types_count(self):
|
||||
time_off_type_read_group = self.env['hr.leave.type']._read_group(
|
||||
[('timesheet_task_id', 'in', self.ids)],
|
||||
['timesheet_task_id'],
|
||||
['timesheet_task_id'],
|
||||
timesheet_read_group = self.env['account.analytic.line']._read_group(
|
||||
[('task_id', 'in', self.ids), '|', ('holiday_id', '!=', False), ('global_leave_id', '!=', False)],
|
||||
['task_id'],
|
||||
['__count'],
|
||||
)
|
||||
time_off_type_count_per_task = {res['timesheet_task_id'][0]: res['timesheet_task_id_count'] for res in time_off_type_read_group}
|
||||
timesheet_count_per_task = {timesheet_task.id: count for timesheet_task, count in timesheet_read_group}
|
||||
for task in self:
|
||||
task.leave_types_count = time_off_type_count_per_task.get(task.id, 0)
|
||||
task.leave_types_count = timesheet_count_per_task.get(task.id, 0)
|
||||
|
||||
def _compute_is_timeoff_task(self):
|
||||
timeoff_tasks = self.filtered(lambda task: task.leave_types_count or task.company_id.leave_timesheet_task_id == task)
|
||||
|
|
@ -25,16 +27,16 @@ class Task(models.Model):
|
|||
(self - timeoff_tasks).is_timeoff_task = False
|
||||
|
||||
def _search_is_timeoff_task(self, operator, value):
|
||||
if operator not in ['=', '!='] or not isinstance(value, bool):
|
||||
raise NotImplementedError(_('Operation not supported'))
|
||||
leave_type_read_group = self.env['hr.leave.type']._read_group(
|
||||
[('timesheet_task_id', '!=', False)],
|
||||
['timesheet_task_ids:array_agg(timesheet_task_id)'],
|
||||
[],
|
||||
)
|
||||
timeoff_task_ids = leave_type_read_group[0]['timesheet_task_ids'] if leave_type_read_group[0]['timesheet_task_ids'] else []
|
||||
if operator != 'in':
|
||||
return NotImplemented
|
||||
|
||||
timeoff_tasks_ids = {row[0] for row in self.env.execute_query(
|
||||
self.env['account.analytic.line']._search(
|
||||
[('task_id', '!=', False), '|', ('holiday_id', '!=', False), ('global_leave_id', '!=', False)],
|
||||
).select('DISTINCT task_id')
|
||||
)}
|
||||
|
||||
if self.env.company.leave_timesheet_task_id:
|
||||
timeoff_task_ids.append(self.env.company.leave_timesheet_task_id.id)
|
||||
if operator == '!=':
|
||||
value = not value
|
||||
return [('id', 'in' if value else 'not in', timeoff_task_ids)]
|
||||
timeoff_tasks_ids.add(self.env.company.leave_timesheet_task_id.id)
|
||||
|
||||
return Domain('id', 'in', tuple(timeoff_tasks_ids))
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
from odoo import fields, models, _
|
||||
|
||||
|
||||
class Company(models.Model):
|
||||
class ResCompany(models.Model):
|
||||
_inherit = 'res.company'
|
||||
|
||||
leave_timesheet_task_id = fields.Many2one(
|
||||
|
|
|
|||
|
|
@ -9,12 +9,12 @@ class ResConfigSettings(models.TransientModel):
|
|||
|
||||
internal_project_id = fields.Many2one(
|
||||
related='company_id.internal_project_id', required=True, string="Internal Project",
|
||||
domain="[('company_id', '=', company_id)]", readonly=False,
|
||||
domain="[('company_id', '=', company_id), ('is_template', '=', False)]", readonly=False,
|
||||
help="The default project used when automatically generating timesheets via time off requests."
|
||||
" You can specify another project on each time off type individually.")
|
||||
leave_timesheet_task_id = fields.Many2one(
|
||||
related='company_id.leave_timesheet_task_id', string="Time Off Task", readonly=False,
|
||||
domain="[('company_id', '=', company_id), ('project_id', '=?', internal_project_id)]",
|
||||
domain="[('company_id', '=', company_id), ('project_id', '=?', internal_project_id), ('has_template_ancestor', '=', False)]",
|
||||
help="The default task used when automatically generating timesheets via time off requests."
|
||||
" You can specify another task on each time off type individually.")
|
||||
|
||||
|
|
|
|||
|
|
@ -10,9 +10,19 @@ from odoo import api, fields, models, _
|
|||
class ResourceCalendarLeaves(models.Model):
|
||||
_inherit = "resource.calendar.leaves"
|
||||
|
||||
timesheet_ids = fields.One2many('account.analytic.line', 'global_leave_id', string="Analytic Lines")
|
||||
timesheet_ids = fields.One2many('account.analytic.line', 'global_leave_id', string="Analytic Lines", export_string_translation=False)
|
||||
|
||||
def _work_time_per_day(self):
|
||||
def _get_resource_calendars(self):
|
||||
leaves_with_calendar = self.filtered('calendar_id')
|
||||
calendars = leaves_with_calendar.calendar_id
|
||||
leaves_wo_calendar = self - leaves_with_calendar
|
||||
if leaves_wo_calendar:
|
||||
calendars += self.env['resource.calendar'].search([
|
||||
('company_id', 'in', leaves_wo_calendar.company_id.ids + [False]),
|
||||
])
|
||||
return calendars
|
||||
|
||||
def _work_time_per_day(self, resource_calendars=False):
|
||||
""" Get work time per day based on the calendar and its attendances
|
||||
|
||||
1) Gets all calendars with their characteristics (i.e.
|
||||
|
|
@ -20,7 +30,7 @@ class ResourceCalendarLeaves(models.Model):
|
|||
(b) the resources which have a leave,
|
||||
(c) the oldest and
|
||||
(d) the latest leave dates
|
||||
) for leaves in self.
|
||||
) for leaves in self (first for calendar's leaves, then for company's global leaves)
|
||||
2) Search the attendances based on the characteristics retrieved for each calendar.
|
||||
The attendances found are the ones between the date_from of the oldest leave
|
||||
and the date_to of the most recent leave.
|
||||
|
|
@ -32,28 +42,58 @@ class ResourceCalendarLeaves(models.Model):
|
|||
}
|
||||
}
|
||||
"""
|
||||
resource_calendars = resource_calendars or self._get_resource_calendars()
|
||||
# to easily find the calendar with its id.
|
||||
calendars_dict = {calendar.id: calendar for calendar in resource_calendars}
|
||||
|
||||
leaves_read_group = self.env['resource.calendar.leaves']._read_group(
|
||||
[('id', 'in', self.ids)],
|
||||
['calendar_id', 'ids:array_agg(id)', 'resource_ids:array_agg(resource_id)', 'min_date_from:min(date_from)', 'max_date_to:max(date_to)'],
|
||||
[('id', 'in', self.ids), ('calendar_id', '!=', False)],
|
||||
['calendar_id'],
|
||||
['id:recordset', 'resource_id:recordset', 'date_from:min', 'date_to:max'],
|
||||
)
|
||||
# dict of keys: calendar_id
|
||||
# and values : { 'date_from': datetime, 'date_to': datetime, resources: self.env['resource.resource'] }
|
||||
cal_attendance_intervals_dict = {
|
||||
res['calendar_id'][0]: {
|
||||
'date_from': utc.localize(res['min_date_from']),
|
||||
'date_to': utc.localize(res['max_date_to']),
|
||||
'resources': self.env['resource.resource'].browse(res['resource_ids'] if res['resource_ids'] and res['resource_ids'][0] else []),
|
||||
'leaves': self.env['resource.calendar.leaves'].browse(res['ids']),
|
||||
} for res in leaves_read_group
|
||||
}
|
||||
# to easily find the calendar with its id.
|
||||
calendars_dict = {calendar.id: calendar for calendar in self.calendar_id}
|
||||
cal_attendance_intervals_dict = {}
|
||||
for calendar, leaves, resources, date_from_min, date_to_max in leaves_read_group:
|
||||
calendar_data = {
|
||||
'date_from': utc.localize(date_from_min),
|
||||
'date_to': utc.localize(date_to_max),
|
||||
'resources': resources,
|
||||
'leaves': leaves,
|
||||
}
|
||||
cal_attendance_intervals_dict[calendar.id] = calendar_data
|
||||
|
||||
# dict of keys: leave.id
|
||||
# and values: a dict of keys: date
|
||||
# and values: number of days
|
||||
results = defaultdict(lambda: defaultdict(float))
|
||||
comp_leaves_read_group = self.env['resource.calendar.leaves']._read_group(
|
||||
[('id', 'in', self.ids), ('calendar_id', '=', False)],
|
||||
['company_id'],
|
||||
['id:recordset', 'resource_id:recordset', 'date_from:min', 'date_to:max'],
|
||||
)
|
||||
for company, leaves, resources, date_from_min, date_to_max in comp_leaves_read_group:
|
||||
for calendar_id in resource_calendars.ids:
|
||||
if (calendar_company := calendars_dict[calendar_id].company_id) and calendar_company != company:
|
||||
continue # only consider global leaves of the same company as the calendar
|
||||
calendar_data = cal_attendance_intervals_dict.get(calendar_id)
|
||||
if calendar_data is None:
|
||||
calendar_data = {
|
||||
'date_from': utc.localize(date_from_min),
|
||||
'date_to': utc.localize(date_to_max),
|
||||
'resources': resources,
|
||||
'leaves': leaves,
|
||||
}
|
||||
cal_attendance_intervals_dict[calendar_id] = calendar_data
|
||||
else:
|
||||
calendar_data.update(
|
||||
date_from=min(utc.localize(date_from_min), calendar_data['date_from']),
|
||||
date_to=max(utc.localize(date_to_max), calendar_data['date_to']),
|
||||
resources=resources | calendar_data['resources'],
|
||||
leaves=leaves | calendar_data['leaves'],
|
||||
)
|
||||
|
||||
# dict of keys: calendar_id
|
||||
# and values: a dict of keys: leave.id
|
||||
# and values: a dict of keys: date
|
||||
# and values: number of days
|
||||
results = defaultdict(lambda: defaultdict(lambda: defaultdict(float)))
|
||||
for calendar_id, cal_attendance_intervals_params_entry in cal_attendance_intervals_dict.items():
|
||||
calendar = calendars_dict[calendar_id]
|
||||
work_hours_intervals = calendar._attendance_intervals_batch(
|
||||
|
|
@ -65,12 +105,12 @@ class ResourceCalendarLeaves(models.Model):
|
|||
for leave in cal_attendance_intervals_params_entry['leaves']:
|
||||
work_hours_data = work_hours_intervals[leave.resource_id.id]
|
||||
|
||||
for date_from, date_to, dummy in work_hours_data:
|
||||
for date_from, date_to, _dummy in work_hours_data:
|
||||
if date_to > utc.localize(leave.date_from) and date_from < utc.localize(leave.date_to):
|
||||
tmp_start = max(date_from, utc.localize(leave.date_from))
|
||||
tmp_end = min(date_to, utc.localize(leave.date_to))
|
||||
results[leave.id][tmp_start.date()] += (tmp_end - tmp_start).total_seconds() / 3600
|
||||
results[leave.id] = sorted(results[leave.id].items())
|
||||
results[calendar_id][leave.id][tmp_start.date()] += (tmp_end - tmp_start).total_seconds() / 3600
|
||||
results[calendar_id][leave.id] = sorted(results[calendar_id][leave.id].items())
|
||||
return results
|
||||
|
||||
def _timesheet_create_lines(self):
|
||||
|
|
@ -78,43 +118,44 @@ class ResourceCalendarLeaves(models.Model):
|
|||
|
||||
If the employee has already a time off in the same day then no timesheet should be created.
|
||||
"""
|
||||
work_hours_data = self._work_time_per_day()
|
||||
resource_calendars = self._get_resource_calendars()
|
||||
work_hours_data = self._work_time_per_day(resource_calendars)
|
||||
employees_groups = self.env['hr.employee']._read_group(
|
||||
[('resource_calendar_id', 'in', self.calendar_id.ids), ('company_id', 'in', self.env.companies.ids)],
|
||||
['resource_calendar_id', 'ids:array_agg(id)'],
|
||||
['resource_calendar_id'])
|
||||
[('resource_calendar_id', 'in', resource_calendars.ids), ('company_id', 'in', self.env.companies.ids)],
|
||||
['resource_calendar_id'],
|
||||
['id:recordset'])
|
||||
mapped_employee = {
|
||||
employee['resource_calendar_id'][0]: self.env['hr.employee'].browse(employee['ids'])
|
||||
for employee in employees_groups
|
||||
resource_calendar.id: employees
|
||||
for resource_calendar, employees in employees_groups
|
||||
}
|
||||
employee_ids_set = set()
|
||||
employee_ids_set.update(*[line['ids'] for line in employees_groups])
|
||||
employee_ids_all = [_id for __, employees in employees_groups for _id in employees._ids]
|
||||
min_date = max_date = None
|
||||
for values in work_hours_data.values():
|
||||
for d, dummy in values:
|
||||
if not min_date and not max_date:
|
||||
min_date = max_date = d
|
||||
elif d < min_date:
|
||||
min_date = d
|
||||
elif d > max_date:
|
||||
max_date = d
|
||||
for vals in values.values():
|
||||
for d, _dummy in vals:
|
||||
if not min_date and not max_date:
|
||||
min_date = max_date = d
|
||||
elif d < min_date:
|
||||
min_date = d
|
||||
elif d > max_date:
|
||||
max_date = d
|
||||
|
||||
holidays_read_group = self.env['hr.leave']._read_group([
|
||||
('employee_id', 'in', list(employee_ids_set)),
|
||||
('employee_id', 'in', employee_ids_all),
|
||||
('date_from', '<=', max_date),
|
||||
('date_to', '>=', min_date),
|
||||
('state', '=', 'validate'),
|
||||
], ['date_from_list:array_agg(date_from)', 'date_to_list:array_agg(date_to)', 'employee_id'], ['employee_id'])
|
||||
], ['employee_id'], ['date_from:array_agg', 'date_to:array_agg'])
|
||||
holidays_by_employee = {
|
||||
line['employee_id'][0]: [
|
||||
(date_from.date(), date_to.date()) for date_from, date_to in zip(line['date_from_list'], line['date_to_list'])
|
||||
] for line in holidays_read_group
|
||||
employee.id: [
|
||||
(date_from.date(), date_to.date()) for date_from, date_to in zip(date_from_list, date_to_list)
|
||||
] for employee, date_from_list, date_to_list in holidays_read_group
|
||||
}
|
||||
vals_list = []
|
||||
for leave in self:
|
||||
for employee in mapped_employee.get(leave.calendar_id.id, self.env['hr.employee']):
|
||||
|
||||
def get_timesheets_data(employees, work_hours_list, vals_list):
|
||||
for employee in employees:
|
||||
holidays = holidays_by_employee.get(employee.id)
|
||||
work_hours_list = work_hours_data[leave.id]
|
||||
for index, (day_date, work_hours_count) in enumerate(work_hours_list):
|
||||
if not holidays or all(not (date_from <= day_date and date_to >= day_date) for date_from, date_to in holidays):
|
||||
vals_list.append(
|
||||
|
|
@ -126,15 +167,27 @@ class ResourceCalendarLeaves(models.Model):
|
|||
work_hours_count
|
||||
)
|
||||
)
|
||||
return vals_list
|
||||
|
||||
for leave in self:
|
||||
if not leave.calendar_id:
|
||||
for calendar_id, calendar_employees in mapped_employee.items():
|
||||
work_hours_list = work_hours_data[calendar_id][leave.id]
|
||||
vals_list = get_timesheets_data(calendar_employees, work_hours_list, vals_list)
|
||||
else:
|
||||
employees = mapped_employee.get(leave.calendar_id.id, self.env['hr.employee'])
|
||||
work_hours_list = work_hours_data[leave.calendar_id.id][leave.id]
|
||||
vals_list = get_timesheets_data(employees, work_hours_list, vals_list)
|
||||
|
||||
return self.env['account.analytic.line'].sudo().create(vals_list)
|
||||
|
||||
def _timesheet_prepare_line_values(self, index, employee_id, work_hours_data, day_date, work_hours_count):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'name': _("Time Off (%s/%s)", index + 1, len(work_hours_data)),
|
||||
'name': _("Time Off (%(index)s/%(total)s)", index=index + 1, total=len(work_hours_data)),
|
||||
'project_id': employee_id.company_id.internal_project_id.id,
|
||||
'task_id': employee_id.company_id.leave_timesheet_task_id.id,
|
||||
'account_id': employee_id.company_id.internal_project_id.analytic_account_id.id,
|
||||
'account_id': employee_id.company_id.internal_project_id.account_id.id,
|
||||
'unit_amount': work_hours_count,
|
||||
'user_id': employee_id.user_id.id,
|
||||
'date': day_date,
|
||||
|
|
@ -143,23 +196,30 @@ class ResourceCalendarLeaves(models.Model):
|
|||
'company_id': employee_id.company_id.id,
|
||||
}
|
||||
|
||||
def _generate_timesheeets(self):
|
||||
results_with_leave_timesheet = self.filtered(lambda r: not r.resource_id and r.company_id.internal_project_id and r.company_id.leave_timesheet_task_id)
|
||||
if results_with_leave_timesheet:
|
||||
results_with_leave_timesheet._timesheet_create_lines()
|
||||
|
||||
def _generate_public_time_off_timesheets(self, employees):
|
||||
timesheet_vals_list = []
|
||||
work_hours_data = self._work_time_per_day()
|
||||
resource_calendars = self._get_resource_calendars()
|
||||
work_hours_data = self._work_time_per_day(resource_calendars)
|
||||
timesheet_read_group = self.env['account.analytic.line']._read_group(
|
||||
[('global_leave_id', 'in', self.ids), ('employee_id', 'in', employees.ids)],
|
||||
['date:array_agg'],
|
||||
['employee_id']
|
||||
['employee_id'],
|
||||
['date:array_agg']
|
||||
)
|
||||
timesheet_dates_per_employee_id = {
|
||||
res['employee_id'][0]: res['date']
|
||||
for res in timesheet_read_group
|
||||
employee.id: date
|
||||
for employee, date in timesheet_read_group
|
||||
}
|
||||
for leave in self:
|
||||
for employee in employees:
|
||||
if employee.resource_calendar_id != leave.calendar_id:
|
||||
if leave.calendar_id and employee.resource_calendar_id != leave.calendar_id:
|
||||
continue
|
||||
work_hours_list = work_hours_data[leave.id]
|
||||
calendar = leave.calendar_id or employee.resource_calendar_id
|
||||
work_hours_list = work_hours_data[calendar.id][leave.id]
|
||||
timesheet_dates = timesheet_dates_per_employee_id.get(employee.id, [])
|
||||
for index, (day_date, work_hours_count) in enumerate(work_hours_list):
|
||||
generate_timesheet = day_date not in timesheet_dates
|
||||
|
|
@ -175,24 +235,51 @@ class ResourceCalendarLeaves(models.Model):
|
|||
timesheet_vals_list.append(timesheet_vals)
|
||||
return self.env['account.analytic.line'].sudo().create(timesheet_vals_list)
|
||||
|
||||
def _get_overlapping_hr_leaves(self, domain=None):
|
||||
"""Find leaves with potentially missing timesheets."""
|
||||
self.ensure_one()
|
||||
leave_domain = domain or []
|
||||
leave_domain += [
|
||||
('company_id', '=', self.company_id.id),
|
||||
('date_from', '<=', self.date_to),
|
||||
('date_to', '>=', self.date_from),
|
||||
]
|
||||
if self.calendar_id:
|
||||
leave_domain += [('resource_calendar_id', 'in', [False, self.calendar_id.id])]
|
||||
return self.env['hr.leave'].search(leave_domain)
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
results = super(ResourceCalendarLeaves, self).create(vals_list)
|
||||
results_with_leave_timesheet = results.filtered(lambda r: not r.resource_id.id and r.calendar_id.company_id.internal_project_id and r.calendar_id.company_id.leave_timesheet_task_id)
|
||||
results_with_leave_timesheet and results_with_leave_timesheet._timesheet_create_lines()
|
||||
results = super().create(vals_list)
|
||||
results._generate_timesheeets()
|
||||
return results
|
||||
|
||||
def write(self, vals):
|
||||
date_from, date_to, calendar_id = vals.get('date_from'), vals.get('date_to'), vals.get('calendar_id')
|
||||
global_time_off_updated = self.env['resource.calendar.leaves']
|
||||
overlapping_leaves = self.env['hr.leave']
|
||||
if date_from or date_to or 'calendar_id' in vals:
|
||||
global_time_off_updated = self.filtered(lambda r: (date_from is not None and r.date_from != date_from) or (date_to is not None and r.date_to != date_to) or (calendar_id is not None and r.calendar_id.id != calendar_id))
|
||||
global_time_off_updated = self.filtered(lambda r: (date_from is not None and r.date_from != date_from) or (date_to is not None and r.date_to != date_to) or (calendar_id is None or r.calendar_id.id != calendar_id))
|
||||
timesheets = global_time_off_updated.sudo().timesheet_ids
|
||||
if timesheets:
|
||||
timesheets.write({'global_leave_id': False})
|
||||
timesheets.unlink()
|
||||
result = super(ResourceCalendarLeaves, self).write(vals)
|
||||
if global_time_off_updated:
|
||||
global_time_offs_with_leave_timesheet = global_time_off_updated.filtered(lambda r: not r.resource_id and r.calendar_id.company_id.internal_project_id and r.calendar_id.company_id.leave_timesheet_task_id)
|
||||
global_time_offs_with_leave_timesheet.sudo()._timesheet_create_lines()
|
||||
if calendar_id:
|
||||
for gto in global_time_off_updated:
|
||||
domain = [] if gto.calendar_id else [('resource_calendar_id', '!=', calendar_id)]
|
||||
overlapping_leaves += gto._get_overlapping_hr_leaves(domain)
|
||||
result = super().write(vals)
|
||||
global_time_off_updated and global_time_off_updated.sudo()._generate_timesheeets()
|
||||
if overlapping_leaves:
|
||||
overlapping_leaves.sudo()._generate_timesheets()
|
||||
return result
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def _regenerate_hr_leave_timesheets_on_gto_unlinked(self):
|
||||
overlapping_leaves = self.env['hr.leave']
|
||||
global_leaves = self.filtered(lambda l: not l.resource_id)
|
||||
for global_leave in global_leaves:
|
||||
overlapping_leaves += global_leave._get_overlapping_hr_leaves()
|
||||
if overlapping_leaves:
|
||||
# we need to ignore the global time off since it hasn't been deleted yet
|
||||
overlapping_leaves.sudo()._generate_timesheets(ignored_resource_calendar_leaves=global_leaves.ids)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue