mirror of
https://github.com/bringout/oca-ocb-hr.git
synced 2026-04-26 21:12:05 +02:00
Initial commit: Hr packages
This commit is contained in:
commit
62531cd146
2820 changed files with 1432848 additions and 0 deletions
|
|
@ -0,0 +1,15 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import resource
|
||||
from . import hr_employee
|
||||
from . import hr_department
|
||||
from . import hr_leave
|
||||
from . import hr_leave_allocation
|
||||
from . import hr_leave_type
|
||||
from . import hr_leave_accrual_plan_level
|
||||
from . import hr_leave_accrual_plan
|
||||
from . import hr_leave_stress_day
|
||||
from . import mail_message_subtype
|
||||
from . import res_partner
|
||||
from . import res_users
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,76 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.osv import expression
|
||||
import ast
|
||||
|
||||
|
||||
class Department(models.Model):
|
||||
|
||||
_inherit = 'hr.department'
|
||||
|
||||
absence_of_today = fields.Integer(
|
||||
compute='_compute_leave_count', string='Absence by Today')
|
||||
leave_to_approve_count = fields.Integer(
|
||||
compute='_compute_leave_count', string='Time Off to Approve')
|
||||
allocation_to_approve_count = fields.Integer(
|
||||
compute='_compute_leave_count', string='Allocation to Approve')
|
||||
|
||||
def _compute_leave_count(self):
|
||||
Requests = self.env['hr.leave']
|
||||
Allocations = self.env['hr.leave.allocation']
|
||||
today_date = datetime.datetime.utcnow().date()
|
||||
today_start = fields.Datetime.to_string(today_date) # get the midnight of the current utc day
|
||||
today_end = fields.Datetime.to_string(today_date + relativedelta(hours=23, minutes=59, seconds=59))
|
||||
|
||||
leave_data = Requests._read_group(
|
||||
[('department_id', 'in', self.ids),
|
||||
('state', '=', 'confirm')],
|
||||
['department_id'], ['department_id'])
|
||||
allocation_data = Allocations._read_group(
|
||||
[('department_id', 'in', self.ids),
|
||||
('state', '=', 'confirm')],
|
||||
['department_id'], ['department_id'])
|
||||
absence_data = Requests._read_group(
|
||||
[('department_id', 'in', self.ids), ('state', 'not in', ['cancel', 'refuse']),
|
||||
('date_from', '<=', today_end), ('date_to', '>=', today_start)],
|
||||
['department_id'], ['department_id'])
|
||||
|
||||
res_leave = dict((data['department_id'][0], data['department_id_count']) for data in leave_data)
|
||||
res_allocation = dict((data['department_id'][0], data['department_id_count']) for data in allocation_data)
|
||||
res_absence = dict((data['department_id'][0], data['department_id_count']) for data in absence_data)
|
||||
|
||||
for department in self:
|
||||
department.leave_to_approve_count = res_leave.get(department.id, 0)
|
||||
department.allocation_to_approve_count = res_allocation.get(department.id, 0)
|
||||
department.absence_of_today = res_absence.get(department.id, 0)
|
||||
|
||||
def _get_action_context(self):
|
||||
return {
|
||||
'search_default_approve': 1,
|
||||
'search_default_active_employee': 2,
|
||||
'search_default_department_id': self.id,
|
||||
'default_department_id': self.id,
|
||||
'searchpanel_default_department_id': self.id,
|
||||
}
|
||||
|
||||
def action_open_leave_department(self):
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("hr_holidays.hr_leave_action_action_approve_department")
|
||||
action['context'] = {
|
||||
**self._get_action_context(),
|
||||
'search_default_active_time_off': 3,
|
||||
'hide_employee_name': 1,
|
||||
'holiday_status_name_get': False
|
||||
}
|
||||
return action
|
||||
|
||||
def action_open_allocation_department(self):
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("hr_holidays.hr_leave_allocation_action_approve_department")
|
||||
action['context'] = self._get_action_context()
|
||||
action['context']['search_default_second_approval'] = 3
|
||||
action['domain'] = expression.AND([ast.literal_eval(action['domain']), [('state', '=', 'confirm')]])
|
||||
return action
|
||||
|
|
@ -0,0 +1,365 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools.float_utils import float_round
|
||||
from odoo.addons.resource.models.resource import HOURS_PER_DAY
|
||||
import pytz
|
||||
|
||||
class HrEmployeeBase(models.AbstractModel):
|
||||
_inherit = "hr.employee.base"
|
||||
|
||||
leave_manager_id = fields.Many2one(
|
||||
'res.users', string='Time Off',
|
||||
compute='_compute_leave_manager', store=True, readonly=False,
|
||||
domain="[('share', '=', False), ('company_ids', 'in', company_id)]",
|
||||
help='Select the user responsible for approving "Time Off" of this employee.\n'
|
||||
'If empty, the approval is done by an Administrator or Approver (determined in settings/users).')
|
||||
remaining_leaves = fields.Float(
|
||||
compute='_compute_remaining_leaves', string='Remaining Paid Time Off',
|
||||
help='Total number of paid time off allocated to this employee, change this value to create allocation/time off request. '
|
||||
'Total based on all the time off types without overriding limit.')
|
||||
current_leave_state = fields.Selection(compute='_compute_leave_status', string="Current Time Off Status",
|
||||
selection=[
|
||||
('draft', 'New'),
|
||||
('confirm', 'Waiting Approval'),
|
||||
('refuse', 'Refused'),
|
||||
('validate1', 'Waiting Second Approval'),
|
||||
('validate', 'Approved'),
|
||||
('cancel', 'Cancelled')
|
||||
])
|
||||
leave_date_from = fields.Date('From Date', compute='_compute_leave_status')
|
||||
leave_date_to = fields.Date('To Date', compute='_compute_leave_status')
|
||||
leaves_count = fields.Float('Number of Time Off', compute='_compute_remaining_leaves')
|
||||
allocation_count = fields.Float('Total number of days allocated.', compute='_compute_allocation_count')
|
||||
allocations_count = fields.Integer('Total number of allocations', compute="_compute_allocation_count")
|
||||
show_leaves = fields.Boolean('Able to see Remaining Time Off', compute='_compute_show_leaves')
|
||||
is_absent = fields.Boolean('Absent Today', compute='_compute_leave_status', search='_search_absent_employee')
|
||||
allocation_display = fields.Char(compute='_compute_allocation_count')
|
||||
allocation_remaining_display = fields.Char(compute='_compute_allocation_remaining_display')
|
||||
hr_icon_display = fields.Selection(selection_add=[('presence_holiday_absent', 'On leave'),
|
||||
('presence_holiday_present', 'Present but on leave')])
|
||||
|
||||
def _get_remaining_leaves(self):
|
||||
""" Helper to compute the remaining leaves for the current employees
|
||||
:returns dict where the key is the employee id, and the value is the remain leaves
|
||||
"""
|
||||
self._cr.execute("""
|
||||
SELECT
|
||||
sum(h.number_of_days) AS days,
|
||||
h.employee_id
|
||||
FROM
|
||||
(
|
||||
SELECT holiday_status_id, number_of_days,
|
||||
state, employee_id
|
||||
FROM hr_leave_allocation
|
||||
UNION ALL
|
||||
SELECT holiday_status_id, (number_of_days * -1) as number_of_days,
|
||||
state, employee_id
|
||||
FROM hr_leave
|
||||
) h
|
||||
join hr_leave_type s ON (s.id=h.holiday_status_id)
|
||||
WHERE
|
||||
s.active = true AND h.state='validate' AND
|
||||
s.requires_allocation='yes' AND
|
||||
h.employee_id in %s
|
||||
GROUP BY h.employee_id""", (tuple(self.ids),))
|
||||
return dict((row['employee_id'], row['days']) for row in self._cr.dictfetchall())
|
||||
|
||||
def _compute_remaining_leaves(self):
|
||||
remaining = {}
|
||||
if self.ids:
|
||||
remaining = self._get_remaining_leaves()
|
||||
for employee in self:
|
||||
value = float_round(remaining.get(employee.id, 0.0), precision_digits=2)
|
||||
employee.leaves_count = value
|
||||
employee.remaining_leaves = value
|
||||
|
||||
def _compute_allocation_count(self):
|
||||
# Don't get allocations that are expired
|
||||
current_date = datetime.date.today()
|
||||
data = self.env['hr.leave.allocation']._read_group([
|
||||
('employee_id', 'in', self.ids),
|
||||
('holiday_status_id.active', '=', True),
|
||||
('holiday_status_id.requires_allocation', '=', 'yes'),
|
||||
('state', '=', 'validate'),
|
||||
('date_from', '<=', current_date),
|
||||
'|',
|
||||
('date_to', '=', False),
|
||||
('date_to', '>=', current_date),
|
||||
], ['number_of_days:sum', 'employee_id'], ['employee_id'])
|
||||
rg_results = dict((d['employee_id'][0], {"employee_id_count": d['employee_id_count'], "number_of_days": d['number_of_days']}) for d in data)
|
||||
for employee in self:
|
||||
result = rg_results.get(employee.id)
|
||||
employee.allocation_count = float_round(result['number_of_days'], precision_digits=2) if result else 0.0
|
||||
employee.allocation_display = "%g" % employee.allocation_count
|
||||
employee.allocations_count = result['employee_id_count'] if result else 0.0
|
||||
|
||||
def _compute_allocation_remaining_display(self):
|
||||
allocations = self.env['hr.leave.allocation'].search([('employee_id', 'in', self.ids)])
|
||||
leaves_taken = allocations.holiday_status_id._get_employees_days_per_allocation(self.ids)
|
||||
for employee in self:
|
||||
employee_remaining_leaves = 0
|
||||
for leave_type in leaves_taken[employee.id]:
|
||||
if leave_type.requires_allocation == 'no':
|
||||
continue
|
||||
for allocation in leaves_taken[employee.id][leave_type]:
|
||||
if allocation:
|
||||
virtual_remaining_leaves = leaves_taken[employee.id][leave_type][allocation]['virtual_remaining_leaves']
|
||||
employee_remaining_leaves += virtual_remaining_leaves\
|
||||
if leave_type.request_unit in ['day', 'half_day']\
|
||||
else virtual_remaining_leaves / (employee.resource_calendar_id.hours_per_day or HOURS_PER_DAY)
|
||||
employee.allocation_remaining_display = "%g" % float_round(employee_remaining_leaves, precision_digits=2)
|
||||
|
||||
def _compute_presence_state(self):
|
||||
super()._compute_presence_state()
|
||||
employees = self.filtered(lambda employee: employee.hr_presence_state != 'present' and employee.is_absent)
|
||||
employees.update({'hr_presence_state': 'absent'})
|
||||
|
||||
def _compute_presence_icon(self):
|
||||
super()._compute_presence_icon()
|
||||
employees_absent = self.filtered(lambda employee:
|
||||
employee.hr_presence_state != 'present'
|
||||
and employee.is_absent)
|
||||
employees_absent.update({'hr_icon_display': 'presence_holiday_absent'})
|
||||
employees_present = self.filtered(lambda employee:
|
||||
employee.hr_presence_state == 'present'
|
||||
and employee.is_absent)
|
||||
employees_present.update({'hr_icon_display': 'presence_holiday_present'})
|
||||
|
||||
def _compute_leave_status(self):
|
||||
# Used SUPERUSER_ID to forcefully get status of other user's leave, to bypass record rule
|
||||
holidays = self.env['hr.leave'].sudo().search([
|
||||
('employee_id', 'in', self.ids),
|
||||
('date_from', '<=', fields.Datetime.now()),
|
||||
('date_to', '>=', fields.Datetime.now()),
|
||||
('state', '=', 'validate'),
|
||||
])
|
||||
leave_data = {}
|
||||
for holiday in holidays:
|
||||
leave_data[holiday.employee_id.id] = {}
|
||||
leave_data[holiday.employee_id.id]['leave_date_from'] = holiday.date_from.date()
|
||||
leave_data[holiday.employee_id.id]['leave_date_to'] = holiday.date_to.date()
|
||||
leave_data[holiday.employee_id.id]['current_leave_state'] = holiday.state
|
||||
|
||||
for employee in self:
|
||||
employee.leave_date_from = leave_data.get(employee.id, {}).get('leave_date_from')
|
||||
employee.leave_date_to = leave_data.get(employee.id, {}).get('leave_date_to')
|
||||
employee.current_leave_state = leave_data.get(employee.id, {}).get('current_leave_state')
|
||||
employee.is_absent = leave_data.get(employee.id) and leave_data.get(employee.id, {}).get('current_leave_state') in ['validate']
|
||||
|
||||
@api.depends('parent_id')
|
||||
def _compute_leave_manager(self):
|
||||
for employee in self:
|
||||
previous_manager = employee._origin.parent_id.user_id
|
||||
manager = employee.parent_id.user_id
|
||||
if manager and employee.leave_manager_id == previous_manager or not employee.leave_manager_id:
|
||||
employee.leave_manager_id = manager
|
||||
elif not employee.leave_manager_id:
|
||||
employee.leave_manager_id = False
|
||||
|
||||
def _compute_show_leaves(self):
|
||||
show_leaves = self.env['res.users'].has_group('hr_holidays.group_hr_holidays_user')
|
||||
for employee in self:
|
||||
if show_leaves or employee.user_id == self.env.user:
|
||||
employee.show_leaves = True
|
||||
else:
|
||||
employee.show_leaves = False
|
||||
|
||||
def _search_absent_employee(self, operator, value):
|
||||
if operator not in ('=', '!=') or not isinstance(value, bool):
|
||||
raise UserError(_('Operation not supported'))
|
||||
# This search is only used for the 'Absent Today' filter however
|
||||
# this only returns employees that are absent right now.
|
||||
today_date = datetime.datetime.utcnow().date()
|
||||
today_start = fields.Datetime.to_string(today_date)
|
||||
today_end = fields.Datetime.to_string(today_date + relativedelta(hours=23, minutes=59, seconds=59))
|
||||
holidays = self.env['hr.leave'].sudo().search([
|
||||
('employee_id', '!=', False),
|
||||
('state', '=', 'validate'),
|
||||
('date_from', '<=', today_end),
|
||||
('date_to', '>=', today_start),
|
||||
])
|
||||
operator = ['in', 'not in'][(operator == '=') != value]
|
||||
return [('id', operator, holidays.mapped('employee_id').ids)]
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
if self.env.context.get('salary_simulation'):
|
||||
return super().create(vals_list)
|
||||
approver_group = self.env.ref('hr_holidays.group_hr_holidays_responsible', raise_if_not_found=False)
|
||||
group_updates = []
|
||||
for vals in vals_list:
|
||||
if 'parent_id' in vals:
|
||||
manager = self.env['hr.employee'].browse(vals['parent_id']).user_id
|
||||
vals['leave_manager_id'] = vals.get('leave_manager_id', manager.id)
|
||||
if approver_group and vals.get('leave_manager_id'):
|
||||
group_updates.append((4, vals['leave_manager_id']))
|
||||
if group_updates:
|
||||
approver_group.sudo().write({'users': group_updates})
|
||||
return super().create(vals_list)
|
||||
|
||||
def write(self, values):
|
||||
if 'parent_id' in values:
|
||||
manager = self.env['hr.employee'].browse(values['parent_id']).user_id
|
||||
if manager:
|
||||
to_change = self.filtered(lambda e: e.leave_manager_id == e.parent_id.user_id or not e.leave_manager_id)
|
||||
to_change.write({'leave_manager_id': values.get('leave_manager_id', manager.id)})
|
||||
|
||||
old_managers = self.env['res.users']
|
||||
if 'leave_manager_id' in values:
|
||||
old_managers = self.mapped('leave_manager_id')
|
||||
if values['leave_manager_id']:
|
||||
leave_manager = self.env['res.users'].browse(values['leave_manager_id'])
|
||||
old_managers -= leave_manager
|
||||
approver_group = self.env.ref('hr_holidays.group_hr_holidays_responsible', raise_if_not_found=False)
|
||||
if approver_group and not leave_manager.has_group('hr_holidays.group_hr_holidays_responsible'):
|
||||
leave_manager.sudo().write({'groups_id': [(4, approver_group.id)]})
|
||||
|
||||
res = super(HrEmployeeBase, self).write(values)
|
||||
# remove users from the Responsible group if they are no longer leave managers
|
||||
old_managers.sudo()._clean_leave_responsible_users()
|
||||
|
||||
if 'parent_id' in values or 'department_id' in values:
|
||||
today_date = fields.Datetime.now()
|
||||
hr_vals = {}
|
||||
if values.get('parent_id') is not None:
|
||||
hr_vals['manager_id'] = values['parent_id']
|
||||
if values.get('department_id') is not None:
|
||||
hr_vals['department_id'] = values['department_id']
|
||||
holidays = self.env['hr.leave'].sudo().search(['|', ('state', 'in', ['draft', 'confirm']), ('date_from', '>', today_date), ('employee_id', 'in', self.ids)])
|
||||
holidays.write(hr_vals)
|
||||
allocations = self.env['hr.leave.allocation'].sudo().search([('state', 'in', ['draft', 'confirm']), ('employee_id', 'in', self.ids)])
|
||||
allocations.write(hr_vals)
|
||||
return res
|
||||
|
||||
class HrEmployee(models.Model):
|
||||
_inherit = 'hr.employee'
|
||||
|
||||
current_leave_id = fields.Many2one('hr.leave.type', compute='_compute_current_leave', string="Current Time Off Type",
|
||||
groups="hr.group_hr_user")
|
||||
|
||||
def _compute_current_leave(self):
|
||||
self.current_leave_id = False
|
||||
|
||||
holidays = self.env['hr.leave'].sudo().search([
|
||||
('employee_id', 'in', self.ids),
|
||||
('date_from', '<=', fields.Datetime.now()),
|
||||
('date_to', '>=', fields.Datetime.now()),
|
||||
('state', '=', 'validate'),
|
||||
])
|
||||
for holiday in holidays:
|
||||
employee = self.filtered(lambda e: e.id == holiday.employee_id.id)
|
||||
employee.current_leave_id = holiday.holiday_status_id.id
|
||||
|
||||
def _get_user_m2o_to_empty_on_archived_employees(self):
|
||||
return super()._get_user_m2o_to_empty_on_archived_employees() + ['leave_manager_id']
|
||||
|
||||
def action_time_off_dashboard(self):
|
||||
return {
|
||||
'name': _('Time Off Dashboard'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'hr.leave',
|
||||
'views': [[self.env.ref('hr_holidays.hr_leave_employee_view_dashboard').id, 'calendar']],
|
||||
'domain': [('employee_id', 'in', self.ids)],
|
||||
'context': {
|
||||
'employee_id': self.ids,
|
||||
},
|
||||
}
|
||||
|
||||
def _get_contextual_employee(self):
|
||||
if self.env.context.get('employee_id'):
|
||||
return self.browse(self.env.context['employee_id'])
|
||||
return self.env.user.employee_id
|
||||
|
||||
def _is_leave_user(self):
|
||||
return self == self.env.user.employee_id and self.user_has_groups('hr_holidays.group_hr_holidays_user')
|
||||
|
||||
def get_stress_days(self, start_date, end_date):
|
||||
all_days = {}
|
||||
|
||||
self = self or self.env.user.employee_id
|
||||
|
||||
stress_days = self._get_stress_days(start_date, end_date)
|
||||
for stress_day in stress_days:
|
||||
num_days = (stress_day.end_date - stress_day.start_date).days
|
||||
for d in range(num_days + 1):
|
||||
all_days[str(stress_day.start_date + relativedelta(days=d))] = stress_day.color
|
||||
|
||||
return all_days
|
||||
|
||||
@api.model
|
||||
def get_special_days_data(self, date_start, date_end):
|
||||
return {
|
||||
'stressDays': self.get_stress_days_data(date_start, date_end),
|
||||
'bankHolidays': self.get_public_holidays_data(date_start, date_end),
|
||||
}
|
||||
|
||||
@api.model
|
||||
def get_public_holidays_data(self, date_start, date_end):
|
||||
self = self._get_contextual_employee()
|
||||
employee_tz = pytz.timezone(self._get_tz() if self else self.env.user.tz or 'utc')
|
||||
public_holidays = self._get_public_holidays(date_start, date_end).sorted('date_from')
|
||||
return list(map(lambda bh: {
|
||||
'id': -bh.id,
|
||||
'colorIndex': 0,
|
||||
'end': datetime.datetime.combine(bh.date_to.astimezone(employee_tz), datetime.datetime.max.time()).isoformat(),
|
||||
'endType': "datetime",
|
||||
'isAllDay': True,
|
||||
'start': datetime.datetime.combine(bh.date_from.astimezone(employee_tz), datetime.datetime.min.time()).isoformat(),
|
||||
'startType': "datetime",
|
||||
'title': bh.name,
|
||||
}, public_holidays))
|
||||
|
||||
def _get_public_holidays(self, date_start, date_end):
|
||||
domain = [
|
||||
('resource_id', '=', False),
|
||||
('company_id', 'in', self.env.companies.ids),
|
||||
('date_from', '<=', date_end),
|
||||
('date_to', '>=', date_start),
|
||||
'|',
|
||||
('calendar_id', '=', False),
|
||||
('calendar_id', '=', self.resource_calendar_id.id),
|
||||
]
|
||||
|
||||
return self.env['resource.calendar.leaves'].search(domain)
|
||||
|
||||
@api.model
|
||||
def get_stress_days_data(self, date_start, date_end):
|
||||
self = self._get_contextual_employee()
|
||||
stress_days = self._get_stress_days(date_start, date_end).sorted('start_date')
|
||||
return list(map(lambda sd: {
|
||||
'id': -sd.id,
|
||||
'colorIndex': sd.color,
|
||||
'end': datetime.datetime.combine(sd.end_date, datetime.datetime.max.time()).isoformat(),
|
||||
'endType': "datetime",
|
||||
'isAllDay': True,
|
||||
'start': datetime.datetime.combine(sd.start_date, datetime.datetime.min.time()).isoformat(),
|
||||
'startType': "datetime",
|
||||
'title': sd.name,
|
||||
}, stress_days))
|
||||
|
||||
def _get_stress_days(self, start_date, end_date):
|
||||
domain = [
|
||||
('start_date', '<=', end_date),
|
||||
('end_date', '>=', start_date),
|
||||
('company_id', 'in', self.env.companies.ids),
|
||||
'|',
|
||||
('resource_calendar_id', '=', False),
|
||||
('resource_calendar_id', '=', self.resource_calendar_id.id),
|
||||
]
|
||||
|
||||
if self.department_id:
|
||||
domain += [
|
||||
'|',
|
||||
('department_ids', '=', False),
|
||||
('department_ids', 'parent_of', self.department_id.id),
|
||||
]
|
||||
else:
|
||||
domain += [('department_ids', '=', False)]
|
||||
|
||||
return self.env['hr.leave.stress.day'].search(domain)
|
||||
1716
odoo-bringout-oca-ocb-hr_holidays/hr_holidays/models/hr_leave.py
Normal file
1716
odoo-bringout-oca-ocb-hr_holidays/hr_holidays/models/hr_leave.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,83 @@
|
|||
# -*- 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 AccrualPlan(models.Model):
|
||||
_name = "hr.leave.accrual.plan"
|
||||
_description = "Accrual Plan"
|
||||
|
||||
name = fields.Char('Name', required=True)
|
||||
time_off_type_id = fields.Many2one('hr.leave.type', string="Time Off Type",
|
||||
help="""Specify if this accrual plan can only be used with this Time Off Type.
|
||||
Leave empty if this accrual plan can be used with any Time Off Type.""")
|
||||
employees_count = fields.Integer("Employees", compute='_compute_employee_count')
|
||||
level_ids = fields.One2many('hr.leave.accrual.level', 'accrual_plan_id', copy=True)
|
||||
allocation_ids = fields.One2many('hr.leave.allocation', 'accrual_plan_id')
|
||||
transition_mode = fields.Selection([
|
||||
('immediately', 'Immediately'),
|
||||
('end_of_accrual', "After this accrual's period")],
|
||||
string="Level Transition", default="immediately", required=True,
|
||||
help="""Immediately: When the date corresponds to the new level, your accrual is automatically computed, granted and you switch to new level
|
||||
After this accrual's period: When the accrual is complete (a week, a month), and granted, you switch to next level if allocation date corresponds""")
|
||||
show_transition_mode = fields.Boolean(compute='_compute_show_transition_mode')
|
||||
|
||||
@api.depends('level_ids')
|
||||
def _compute_show_transition_mode(self):
|
||||
for plan in self:
|
||||
plan.show_transition_mode = len(plan.level_ids) > 1
|
||||
|
||||
level_count = fields.Integer('Levels', compute='_compute_level_count')
|
||||
|
||||
@api.depends('level_ids')
|
||||
def _compute_level_count(self):
|
||||
level_read_group = self.env['hr.leave.accrual.level']._read_group(
|
||||
[('accrual_plan_id', 'in', self.ids)],
|
||||
fields=['accrual_plan_id'],
|
||||
groupby=['accrual_plan_id'],
|
||||
)
|
||||
mapped_count = {group['accrual_plan_id'][0]: group['accrual_plan_id_count'] for group in level_read_group}
|
||||
for plan in self:
|
||||
plan.level_count = mapped_count.get(plan.id, 0)
|
||||
|
||||
@api.depends('allocation_ids')
|
||||
def _compute_employee_count(self):
|
||||
allocations_read_group = self.env['hr.leave.allocation']._read_group(
|
||||
[('accrual_plan_id', 'in', self.ids)],
|
||||
['accrual_plan_id', 'employee_count:count_distinct(employee_id)'],
|
||||
['accrual_plan_id'],
|
||||
)
|
||||
allocations_dict = {res['accrual_plan_id'][0]: res['employee_count'] for res in allocations_read_group}
|
||||
for plan in self:
|
||||
plan.employees_count = allocations_dict.get(plan.id, 0)
|
||||
|
||||
def action_open_accrual_plan_employees(self):
|
||||
self.ensure_one()
|
||||
|
||||
return {
|
||||
'name': _("Accrual Plan's Employees"),
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_mode': 'kanban,tree,form',
|
||||
'res_model': 'hr.employee',
|
||||
'domain': [('id', 'in', self.allocation_ids.employee_id.ids)],
|
||||
}
|
||||
|
||||
@api.returns('self', lambda value: value.id)
|
||||
def copy(self, default=None):
|
||||
default = dict(default or {},
|
||||
name=_("%s (copy)", self.name))
|
||||
return super().copy(default=default)
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def _prevent_used_plan_unlink(self):
|
||||
domain = [
|
||||
('allocation_type', '=', 'accrual'),
|
||||
('accrual_plan_id', 'in', self.ids),
|
||||
('state', 'not in', ('cancel', 'refuse')),
|
||||
]
|
||||
if self.env['hr.leave.allocation'].search_count(domain):
|
||||
raise ValidationError(_(
|
||||
"Some of the accrual plans you're trying to delete are linked to an existing allocation. Delete or cancel them first."
|
||||
))
|
||||
|
|
@ -0,0 +1,290 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import datetime
|
||||
import calendar
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.tools.date_utils import get_timedelta
|
||||
|
||||
|
||||
DAYS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']
|
||||
MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']
|
||||
# Used for displaying the days and reversing selection -> integer
|
||||
DAY_SELECT_VALUES = [str(i) for i in range(1, 29)] + ['last']
|
||||
DAY_SELECT_SELECTION_NO_LAST = tuple(zip(DAY_SELECT_VALUES, (str(i) for i in range(1, 29))))
|
||||
|
||||
def _get_selection_days(self):
|
||||
return DAY_SELECT_SELECTION_NO_LAST + (("last", _("last day")),)
|
||||
|
||||
class AccrualPlanLevel(models.Model):
|
||||
_name = "hr.leave.accrual.level"
|
||||
_description = "Accrual Plan Level"
|
||||
_order = 'sequence asc'
|
||||
|
||||
sequence = fields.Integer(
|
||||
string='sequence', compute='_compute_sequence', store=True,
|
||||
help='Sequence is generated automatically by start time delta.')
|
||||
accrual_plan_id = fields.Many2one('hr.leave.accrual.plan', "Accrual Plan", required=True)
|
||||
start_count = fields.Integer(
|
||||
"Start after",
|
||||
help="The accrual starts after a defined period from the allocation start date. This field defines the number of days, months or years after which accrual is used.", default="1")
|
||||
start_type = fields.Selection(
|
||||
[('day', 'day(s)'),
|
||||
('month', 'month(s)'),
|
||||
('year', 'year(s)')],
|
||||
default='day', string=" ", required=True,
|
||||
help="This field defines the unit of time after which the accrual starts.")
|
||||
is_based_on_worked_time = fields.Boolean("Based on worked time",
|
||||
help="If checked, the rate will be prorated on time off type where type is set on Working Time in the configuration.")
|
||||
|
||||
# Accrue of
|
||||
added_value = fields.Float(
|
||||
"Rate", digits=(16, 5), required=True,
|
||||
help="The number of hours/days that will be incremented in the specified Time Off Type for every period")
|
||||
added_value_type = fields.Selection(
|
||||
[('days', 'Days'),
|
||||
('hours', 'Hours')],
|
||||
default='days', required=True)
|
||||
frequency = fields.Selection([
|
||||
('daily', 'Daily'),
|
||||
('weekly', 'Weekly'),
|
||||
('bimonthly', 'Twice a month'),
|
||||
('monthly', 'Monthly'),
|
||||
('biyearly', 'Twice a year'),
|
||||
('yearly', 'Yearly'),
|
||||
], default='daily', required=True, string="Frequency")
|
||||
week_day = fields.Selection([
|
||||
('mon', 'Monday'),
|
||||
('tue', 'Tuesday'),
|
||||
('wed', 'Wednesday'),
|
||||
('thu', 'Thursday'),
|
||||
('fri', 'Friday'),
|
||||
('sat', 'Saturday'),
|
||||
('sun', 'Sunday'),
|
||||
], default='mon', required=True, string="Allocation on")
|
||||
first_day = fields.Integer(default=1)
|
||||
first_day_display = fields.Selection(
|
||||
_get_selection_days, compute='_compute_days_display', inverse='_inverse_first_day_display')
|
||||
second_day = fields.Integer(default=15)
|
||||
second_day_display = fields.Selection(
|
||||
_get_selection_days, compute='_compute_days_display', inverse='_inverse_second_day_display')
|
||||
first_month_day = fields.Integer(default=1)
|
||||
first_month_day_display = fields.Selection(
|
||||
_get_selection_days, compute='_compute_days_display', inverse='_inverse_first_month_day_display')
|
||||
first_month = fields.Selection([
|
||||
('jan', 'January'),
|
||||
('feb', 'February'),
|
||||
('mar', 'March'),
|
||||
('apr', 'April'),
|
||||
('may', 'May'),
|
||||
('jun', 'June'),
|
||||
], default="jan")
|
||||
second_month_day = fields.Integer(default=1)
|
||||
second_month_day_display = fields.Selection(
|
||||
_get_selection_days, compute='_compute_days_display', inverse='_inverse_second_month_day_display')
|
||||
second_month = fields.Selection([
|
||||
('jul', 'July'),
|
||||
('aug', 'August'),
|
||||
('sep', 'September'),
|
||||
('oct', 'October'),
|
||||
('nov', 'November'),
|
||||
('dec', 'December')
|
||||
], default="jul")
|
||||
yearly_month = fields.Selection([
|
||||
('jan', 'January'),
|
||||
('feb', 'February'),
|
||||
('mar', 'March'),
|
||||
('apr', 'April'),
|
||||
('may', 'May'),
|
||||
('jun', 'June'),
|
||||
('jul', 'July'),
|
||||
('aug', 'August'),
|
||||
('sep', 'September'),
|
||||
('oct', 'October'),
|
||||
('nov', 'November'),
|
||||
('dec', 'December')
|
||||
], default="jan")
|
||||
yearly_day = fields.Integer(default=1)
|
||||
yearly_day_display = fields.Selection(
|
||||
_get_selection_days, compute='_compute_days_display', inverse='_inverse_yearly_day_display')
|
||||
maximum_leave = fields.Float(
|
||||
'Limit to', required=False, default=100,
|
||||
help="Choose a cap for this accrual. 0 means no cap.")
|
||||
parent_id = fields.Many2one(
|
||||
'hr.leave.accrual.level', string="Previous Level",
|
||||
help="If this field is empty, this level is the first one.")
|
||||
action_with_unused_accruals = fields.Selection(
|
||||
[('postponed', 'Transferred to the next year'),
|
||||
('lost', 'Lost')],
|
||||
string="At the end of the calendar year, unused accruals will be",
|
||||
default='postponed', required='True')
|
||||
postpone_max_days = fields.Integer("Maximum amount of accruals to transfer",
|
||||
help="Set a maximum of days an allocation keeps at the end of the year. 0 for no limit.")
|
||||
|
||||
_sql_constraints = [
|
||||
('check_dates',
|
||||
"CHECK( (frequency = 'daily') or"
|
||||
"(week_day IS NOT NULL AND frequency = 'weekly') or "
|
||||
"(first_day > 0 AND second_day > first_day AND first_day <= 31 AND second_day <= 31 AND frequency = 'bimonthly') or "
|
||||
"(first_day > 0 AND first_day <= 31 AND frequency = 'monthly')or "
|
||||
"(first_month_day > 0 AND first_month_day <= 31 AND second_month_day > 0 AND second_month_day <= 31 AND frequency = 'biyearly') or "
|
||||
"(yearly_day > 0 AND yearly_day <= 31 AND frequency = 'yearly'))",
|
||||
"The dates you've set up aren't correct. Please check them."),
|
||||
('start_count_check', "CHECK( start_count >= 0 )", "You can not start an accrual in the past."),
|
||||
('added_value_greater_than_zero', 'CHECK(added_value > 0)', 'You must give a rate greater than 0 in accrual plan levels.')
|
||||
]
|
||||
|
||||
@api.depends('start_count', 'start_type')
|
||||
def _compute_sequence(self):
|
||||
# Not 100% accurate because of odd months/years, but good enough
|
||||
start_type_multipliers = {
|
||||
'day': 1,
|
||||
'month': 30,
|
||||
'year': 365,
|
||||
}
|
||||
for level in self:
|
||||
level.sequence = level.start_count * start_type_multipliers[level.start_type]
|
||||
|
||||
@api.depends('first_day', 'second_day', 'first_month_day', 'second_month_day', 'yearly_day')
|
||||
def _compute_days_display(self):
|
||||
days_select = _get_selection_days(self)
|
||||
for level in self:
|
||||
level.first_day_display = days_select[min(level.first_day - 1, 28)][0]
|
||||
level.second_day_display = days_select[min(level.second_day - 1, 28)][0]
|
||||
level.first_month_day_display = days_select[min(level.first_month_day - 1, 28)][0]
|
||||
level.second_month_day_display = days_select[min(level.second_month_day - 1, 28)][0]
|
||||
level.yearly_day_display = days_select[min(level.yearly_day - 1, 28)][0]
|
||||
|
||||
def _inverse_first_day_display(self):
|
||||
for level in self:
|
||||
if level.first_day_display == 'last':
|
||||
level.first_day = 31
|
||||
else:
|
||||
level.first_day = DAY_SELECT_VALUES.index(level.first_day_display) + 1
|
||||
|
||||
def _inverse_second_day_display(self):
|
||||
for level in self:
|
||||
if level.second_day_display == 'last':
|
||||
level.second_day = 31
|
||||
else:
|
||||
level.second_day = DAY_SELECT_VALUES.index(level.second_day_display) + 1
|
||||
|
||||
def _inverse_first_month_day_display(self):
|
||||
for level in self:
|
||||
if level.first_month_day_display == 'last':
|
||||
level.first_month_day = 31
|
||||
else:
|
||||
level.first_month_day = DAY_SELECT_VALUES.index(level.first_month_day_display) + 1
|
||||
|
||||
def _inverse_second_month_day_display(self):
|
||||
for level in self:
|
||||
if level.second_month_day_display == 'last':
|
||||
level.second_month_day = 31
|
||||
else:
|
||||
level.second_month_day = DAY_SELECT_VALUES.index(level.second_month_day_display) + 1
|
||||
|
||||
def _inverse_yearly_day_display(self):
|
||||
for level in self:
|
||||
if level.yearly_day_display == 'last':
|
||||
level.yearly_day = 31
|
||||
else:
|
||||
level.yearly_day = DAY_SELECT_VALUES.index(level.yearly_day_display) + 1
|
||||
|
||||
def _get_next_date(self, last_call):
|
||||
"""
|
||||
Returns the next date with the given last call
|
||||
"""
|
||||
self.ensure_one()
|
||||
if self.frequency == 'daily':
|
||||
return last_call + relativedelta(days=1)
|
||||
elif self.frequency == 'weekly':
|
||||
daynames = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
|
||||
weekday = daynames.index(self.week_day)
|
||||
return last_call + relativedelta(days=1, weekday=weekday)
|
||||
elif self.frequency == 'bimonthly':
|
||||
first_date = last_call + relativedelta(day=self.first_day)
|
||||
second_date = last_call + relativedelta(day=self.second_day)
|
||||
if last_call < first_date:
|
||||
return first_date
|
||||
elif last_call < second_date:
|
||||
return second_date
|
||||
else:
|
||||
return last_call + relativedelta(months=1, day=self.first_day)
|
||||
elif self.frequency == 'monthly':
|
||||
date = last_call + relativedelta(day=self.first_day)
|
||||
if last_call < date:
|
||||
return date
|
||||
else:
|
||||
return last_call + relativedelta(months=1, day=self.first_day)
|
||||
elif self.frequency == 'biyearly':
|
||||
first_month = MONTHS.index(self.first_month) + 1
|
||||
second_month = MONTHS.index(self.second_month) + 1
|
||||
first_date = last_call + relativedelta(month=first_month, day=self.first_month_day)
|
||||
second_date = last_call + relativedelta(month=second_month, day=self.second_month_day)
|
||||
if last_call < first_date:
|
||||
return first_date
|
||||
elif last_call < second_date:
|
||||
return second_date
|
||||
else:
|
||||
return last_call + relativedelta(years=1, month=first_month, day=self.first_month_day)
|
||||
elif self.frequency == 'yearly':
|
||||
month = MONTHS.index(self.yearly_month) + 1
|
||||
date = last_call + relativedelta(month=month, day=self.yearly_day)
|
||||
if last_call < date:
|
||||
return date
|
||||
else:
|
||||
return last_call + relativedelta(years=1, month=month, day=self.yearly_day)
|
||||
else:
|
||||
return False
|
||||
|
||||
def _get_previous_date(self, last_call):
|
||||
"""
|
||||
Returns the date a potential previous call would have been at
|
||||
For example if you have a monthly level giving 16/02 would return 01/02
|
||||
Contrary to `_get_next_date` this function will return the 01/02 if that date is given
|
||||
"""
|
||||
self.ensure_one()
|
||||
if self.frequency == 'daily':
|
||||
return last_call
|
||||
elif self.frequency == 'weekly':
|
||||
daynames = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
|
||||
weekday = daynames.index(self.week_day)
|
||||
return last_call + relativedelta(days=-6, weekday=weekday)
|
||||
elif self.frequency == 'bimonthly':
|
||||
second_date = last_call + relativedelta(day=self.second_day)
|
||||
first_date = last_call + relativedelta(day=self.first_day)
|
||||
if last_call >= second_date:
|
||||
return second_date
|
||||
elif last_call >= first_date:
|
||||
return first_date
|
||||
else:
|
||||
return last_call + relativedelta(months=-1, day=self.second_day)
|
||||
elif self.frequency == 'monthly':
|
||||
date = last_call + relativedelta(day=self.first_day)
|
||||
if last_call >= date:
|
||||
return date
|
||||
else:
|
||||
return last_call + relativedelta(months=-1, day=self.first_day)
|
||||
elif self.frequency == 'biyearly':
|
||||
first_month = MONTHS.index(self.first_month) + 1
|
||||
second_month = MONTHS.index(self.second_month) + 1
|
||||
first_date = last_call + relativedelta(month=first_month, day=self.first_month_day)
|
||||
second_date = last_call + relativedelta(month=second_month, day=self.second_month_day)
|
||||
if last_call >= second_date:
|
||||
return second_date
|
||||
elif last_call >= first_date:
|
||||
return first_date
|
||||
else:
|
||||
return last_call + relativedelta(years=-1, month=second_month, day=self.second_month_day)
|
||||
elif self.frequency == 'yearly':
|
||||
month = MONTHS.index(self.yearly_month) + 1
|
||||
year_date = last_call + relativedelta(month=month, day=self.yearly_day)
|
||||
if last_call >= year_date:
|
||||
return year_date
|
||||
else:
|
||||
return last_call + relativedelta(years=-1, month=month, day=self.yearly_day)
|
||||
else:
|
||||
return False
|
||||
|
|
@ -0,0 +1,840 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
# Copyright (c) 2005-2006 Axelor SARL. (http://www.axelor.com)
|
||||
|
||||
from collections import defaultdict
|
||||
import logging
|
||||
|
||||
from datetime import datetime, time
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.addons.resource.models.resource import HOURS_PER_DAY
|
||||
from odoo.addons.hr_holidays.models.hr_leave import get_employee_from_context
|
||||
from odoo.exceptions import AccessError, UserError, ValidationError
|
||||
from odoo.tools.translate import _
|
||||
from odoo.tools.float_utils import float_round
|
||||
from odoo.tools.date_utils import get_timedelta
|
||||
from odoo.osv import expression
|
||||
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class HolidaysAllocation(models.Model):
|
||||
""" Allocation Requests Access specifications: similar to leave requests """
|
||||
_name = "hr.leave.allocation"
|
||||
_description = "Time Off Allocation"
|
||||
_order = "create_date desc"
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_mail_post_access = 'read'
|
||||
|
||||
def _default_holiday_status_id(self):
|
||||
if self.user_has_groups('hr_holidays.group_hr_holidays_user'):
|
||||
domain = [('has_valid_allocation', '=', True), ('requires_allocation', '=', 'yes')]
|
||||
else:
|
||||
domain = [('has_valid_allocation', '=', True), ('requires_allocation', '=', 'yes'), ('employee_requests', '=', 'yes')]
|
||||
return self.env['hr.leave.type'].search(domain, limit=1)
|
||||
|
||||
def _domain_holiday_status_id(self):
|
||||
if self.user_has_groups('hr_holidays.group_hr_holidays_user'):
|
||||
return [('requires_allocation', '=', 'yes')]
|
||||
return [('employee_requests', '=', 'yes')]
|
||||
|
||||
name = fields.Char('Description', compute='_compute_description', inverse='_inverse_description', search='_search_description', compute_sudo=False)
|
||||
name_validity = fields.Char('Description with validity', compute='_compute_description_validity')
|
||||
active = fields.Boolean(default=True)
|
||||
private_name = fields.Char('Allocation Description', groups='hr_holidays.group_hr_holidays_user')
|
||||
state = fields.Selection([
|
||||
('draft', 'To Submit'),
|
||||
('cancel', 'Cancelled'),
|
||||
('confirm', 'To Approve'),
|
||||
('refuse', 'Refused'),
|
||||
('validate', 'Approved')
|
||||
], string='Status', readonly=True, tracking=True, copy=False, default='draft',
|
||||
help="The status is set to 'To Submit', when an allocation request is created." +
|
||||
"\nThe status is 'To Approve', when an allocation request is confirmed by user." +
|
||||
"\nThe status is 'Refused', when an allocation request is refused by manager." +
|
||||
"\nThe status is 'Approved', when an allocation request is approved by manager.")
|
||||
date_from = fields.Date('Start Date', index=True, copy=False, default=fields.Date.context_today,
|
||||
states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, tracking=True, required=True)
|
||||
date_to = fields.Date('End Date', copy=False, tracking=True,
|
||||
states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate1': [('readonly', True)], 'validate': [('readonly', True)]})
|
||||
holiday_status_id = fields.Many2one(
|
||||
"hr.leave.type", compute='_compute_holiday_status_id', store=True, string="Time Off Type", required=True, readonly=False,
|
||||
states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate1': [('readonly', True)], 'validate': [('readonly', True)]},
|
||||
domain=_domain_holiday_status_id,
|
||||
default=_default_holiday_status_id)
|
||||
employee_id = fields.Many2one(
|
||||
'hr.employee', compute='_compute_from_employee_ids', store=True, string='Employee', index=True, readonly=False, ondelete="restrict", tracking=True,
|
||||
states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate': [('readonly', True)]})
|
||||
employee_company_id = fields.Many2one(related='employee_id.company_id', readonly=True, store=True)
|
||||
active_employee = fields.Boolean('Active Employee', related='employee_id.active', readonly=True)
|
||||
manager_id = fields.Many2one('hr.employee', compute='_compute_manager_id', store=True, string='Manager')
|
||||
notes = fields.Text('Reasons', readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]})
|
||||
# duration
|
||||
number_of_days = fields.Float(
|
||||
'Number of Days', compute='_compute_from_holiday_status_id', store=True, readonly=False, tracking=True, default=1,
|
||||
help='Duration in days. Reference field to use when necessary.')
|
||||
number_of_days_display = fields.Float(
|
||||
'Duration (days)', compute='_compute_number_of_days_display',
|
||||
states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]},
|
||||
help="If Accrual Allocation: Days given by the accrual system.")
|
||||
number_of_hours_display = fields.Float(
|
||||
'Duration (hours)', compute='_compute_number_of_hours_display',
|
||||
help="If Accrual Allocation: Number of hours allocated in addition to the ones you will get via the accrual' system.")
|
||||
duration_display = fields.Char('Allocated (Days/Hours)', compute='_compute_duration_display',
|
||||
help="Field allowing to see the allocation duration in days or hours depending on the type_request_unit")
|
||||
# details
|
||||
parent_id = fields.Many2one('hr.leave.allocation', string='Parent')
|
||||
linked_request_ids = fields.One2many('hr.leave.allocation', 'parent_id', string='Linked Requests')
|
||||
approver_id = fields.Many2one(
|
||||
'hr.employee', string='First Approval', readonly=True, copy=False,
|
||||
help='This area is automatically filled by the user who validates the allocation')
|
||||
validation_type = fields.Selection(string='Validation Type', related='holiday_status_id.allocation_validation_type', readonly=True)
|
||||
can_reset = fields.Boolean('Can reset', compute='_compute_can_reset')
|
||||
can_approve = fields.Boolean('Can Approve', compute='_compute_can_approve')
|
||||
type_request_unit = fields.Selection(related='holiday_status_id.request_unit', readonly=True)
|
||||
# mode
|
||||
holiday_type = fields.Selection([
|
||||
('employee', 'By Employee'),
|
||||
('company', 'By Company'),
|
||||
('department', 'By Department'),
|
||||
('category', 'By Employee Tag')],
|
||||
string='Allocation Mode', readonly=True, required=True, default='employee',
|
||||
states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]},
|
||||
help="Allow to create requests in batchs:\n- By Employee: for a specific employee"
|
||||
"\n- By Company: all employees of the specified company"
|
||||
"\n- By Department: all employees of the specified department"
|
||||
"\n- By Employee Tag: all employees of the specific employee group category")
|
||||
employee_ids = fields.Many2many(
|
||||
'hr.employee', compute='_compute_from_holiday_type', store=True, string='Employees', readonly=False,
|
||||
states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate': [('readonly', True)]})
|
||||
multi_employee = fields.Boolean(
|
||||
compute='_compute_from_employee_ids', store=True,
|
||||
help='Holds whether this allocation concerns more than 1 employee')
|
||||
mode_company_id = fields.Many2one(
|
||||
'res.company', compute='_compute_from_holiday_type', store=True, string='Company Mode', readonly=False,
|
||||
states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate': [('readonly', True)]})
|
||||
department_id = fields.Many2one(
|
||||
'hr.department', compute='_compute_department_id', store=True, string='Department',
|
||||
states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]})
|
||||
category_id = fields.Many2one(
|
||||
'hr.employee.category', compute='_compute_from_holiday_type', store=True, string='Employee Tag', readonly=False,
|
||||
states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate': [('readonly', True)]})
|
||||
# accrual configuration
|
||||
lastcall = fields.Date("Date of the last accrual allocation", readonly=True, default=fields.Date.context_today)
|
||||
nextcall = fields.Date("Date of the next accrual allocation", default=False, readonly=True)
|
||||
allocation_type = fields.Selection(
|
||||
[
|
||||
('regular', 'Regular Allocation'),
|
||||
('accrual', 'Accrual Allocation')
|
||||
], string="Allocation Type", default="regular", required=True, readonly=True,
|
||||
states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]})
|
||||
is_officer = fields.Boolean(compute='_compute_is_officer')
|
||||
accrual_plan_id = fields.Many2one('hr.leave.accrual.plan', compute="_compute_from_holiday_status_id", store=True, readonly=False, domain="['|', ('time_off_type_id', '=', False), ('time_off_type_id', '=', holiday_status_id)]", tracking=True)
|
||||
max_leaves = fields.Float(compute='_compute_leaves')
|
||||
leaves_taken = fields.Float(compute='_compute_leaves')
|
||||
taken_leave_ids = fields.One2many('hr.leave', 'holiday_allocation_id', domain="[('state', 'in', ['confirm', 'validate1', 'validate'])]")
|
||||
|
||||
_sql_constraints = [
|
||||
('type_value',
|
||||
"CHECK( (holiday_type='employee' AND (employee_id IS NOT NULL OR multi_employee IS TRUE)) or "
|
||||
"(holiday_type='category' AND category_id IS NOT NULL) or "
|
||||
"(holiday_type='department' AND department_id IS NOT NULL) or "
|
||||
"(holiday_type='company' AND mode_company_id IS NOT NULL))",
|
||||
"The employee, department, company or employee category of this request is missing. Please make sure that your user login is linked to an employee."),
|
||||
('duration_check', "CHECK( ( number_of_days > 0 AND allocation_type='regular') or (allocation_type != 'regular'))", "The duration must be greater than 0."),
|
||||
]
|
||||
|
||||
@api.constrains('date_from', 'date_to')
|
||||
def _check_date_from_date_to(self):
|
||||
if any(allocation.date_to and allocation.date_from > allocation.date_to for allocation in self):
|
||||
raise UserError(_("The Start Date of the Validity Period must be anterior to the End Date."))
|
||||
|
||||
# The compute does not get triggered without a depends on record creation
|
||||
# aka keep the 'useless' depends
|
||||
@api.depends_context('uid')
|
||||
@api.depends('allocation_type')
|
||||
def _compute_is_officer(self):
|
||||
self.is_officer = self.env.user.has_group("hr_holidays.group_hr_holidays_user")
|
||||
|
||||
@api.depends_context('uid')
|
||||
def _compute_description(self):
|
||||
self.check_access_rights('read')
|
||||
self.check_access_rule('read')
|
||||
|
||||
is_officer = self.env.user.has_group('hr_holidays.group_hr_holidays_user')
|
||||
|
||||
for allocation in self:
|
||||
if is_officer or allocation.employee_id.user_id == self.env.user or allocation.employee_id.leave_manager_id == self.env.user:
|
||||
allocation.name = allocation.sudo().private_name
|
||||
else:
|
||||
allocation.name = '*****'
|
||||
|
||||
def _inverse_description(self):
|
||||
is_officer = self.env.user.has_group('hr_holidays.group_hr_holidays_user')
|
||||
for allocation in self:
|
||||
if is_officer or allocation.employee_id.user_id == self.env.user or allocation.employee_id.leave_manager_id == self.env.user:
|
||||
allocation.sudo().private_name = allocation.name
|
||||
|
||||
def _search_description(self, operator, value):
|
||||
is_officer = self.env.user.has_group('hr_holidays.group_hr_holidays_user')
|
||||
domain = [('private_name', operator, value)]
|
||||
|
||||
if not is_officer:
|
||||
domain = expression.AND([domain, [('employee_id.user_id', '=', self.env.user.id)]])
|
||||
|
||||
allocations = self.sudo().search(domain)
|
||||
return [('id', 'in', allocations.ids)]
|
||||
|
||||
@api.depends('name', 'date_from', 'date_to')
|
||||
def _compute_description_validity(self):
|
||||
for allocation in self:
|
||||
if allocation.date_to:
|
||||
name_validity = _("%s (from %s to %s)", allocation.name, allocation.date_from.strftime("%b %d %Y"), allocation.date_to.strftime("%b %d %Y"))
|
||||
else:
|
||||
name_validity = _("%s (from %s to No Limit)", allocation.name, allocation.date_from.strftime("%b %d %Y"))
|
||||
allocation.name_validity = name_validity
|
||||
|
||||
@api.depends('employee_id', 'holiday_status_id', 'taken_leave_ids.number_of_days', 'taken_leave_ids.state')
|
||||
def _compute_leaves(self):
|
||||
employee_days_per_allocation = self.holiday_status_id.with_context(ignore_future=True)._get_employees_days_per_allocation(self.employee_id.ids)
|
||||
for allocation in self:
|
||||
allocation.max_leaves = allocation.number_of_hours_display if allocation.type_request_unit == 'hour' else allocation.number_of_days
|
||||
allocation.leaves_taken = employee_days_per_allocation[allocation.employee_id.id][allocation.holiday_status_id][allocation]['leaves_taken']
|
||||
|
||||
@api.depends('number_of_days')
|
||||
def _compute_number_of_days_display(self):
|
||||
for allocation in self:
|
||||
allocation.number_of_days_display = allocation.number_of_days
|
||||
|
||||
@api.depends('number_of_days', 'holiday_status_id', 'employee_id', 'holiday_type')
|
||||
def _compute_number_of_hours_display(self):
|
||||
for allocation in self:
|
||||
allocation_calendar = allocation.holiday_status_id.company_id.resource_calendar_id
|
||||
if allocation.holiday_type == 'employee' and allocation.employee_id:
|
||||
allocation_calendar = allocation.employee_id.sudo().resource_calendar_id
|
||||
|
||||
allocation.number_of_hours_display = allocation.number_of_days * (allocation_calendar.hours_per_day or HOURS_PER_DAY)
|
||||
|
||||
|
||||
@api.depends('number_of_hours_display', 'number_of_days_display')
|
||||
def _compute_duration_display(self):
|
||||
for allocation in self:
|
||||
allocation.duration_display = '%g %s' % (
|
||||
(float_round(allocation.number_of_hours_display, precision_digits=2)
|
||||
if allocation.type_request_unit == 'hour'
|
||||
else float_round(allocation.number_of_days_display, precision_digits=2)),
|
||||
_('hours') if allocation.type_request_unit == 'hour' else _('days'))
|
||||
|
||||
@api.depends('state', 'employee_id', 'department_id')
|
||||
def _compute_can_reset(self):
|
||||
for allocation in self:
|
||||
try:
|
||||
allocation._check_approval_update('draft')
|
||||
except (AccessError, UserError):
|
||||
allocation.can_reset = False
|
||||
else:
|
||||
allocation.can_reset = True
|
||||
|
||||
@api.depends('state', 'employee_id', 'department_id')
|
||||
def _compute_can_approve(self):
|
||||
for allocation in self:
|
||||
try:
|
||||
if allocation.state == 'confirm' and allocation.validation_type != 'no':
|
||||
allocation._check_approval_update('validate')
|
||||
except (AccessError, UserError):
|
||||
allocation.can_approve = False
|
||||
else:
|
||||
allocation.can_approve = True
|
||||
|
||||
@api.depends('employee_ids')
|
||||
def _compute_from_employee_ids(self):
|
||||
for allocation in self:
|
||||
if len(allocation.employee_ids) == 1:
|
||||
allocation.employee_id = allocation.employee_ids[0]._origin
|
||||
else:
|
||||
allocation.employee_id = False
|
||||
allocation.multi_employee = (len(allocation.employee_ids) > 1)
|
||||
|
||||
@api.depends('holiday_type')
|
||||
def _compute_from_holiday_type(self):
|
||||
default_employee_ids = self.env['hr.employee'].browse(self.env.context.get('default_employee_id')) or self.env.user.employee_id
|
||||
for allocation in self:
|
||||
if allocation.holiday_type == 'employee':
|
||||
if not allocation.employee_ids:
|
||||
allocation.employee_ids = self.env.user.employee_id
|
||||
allocation.mode_company_id = False
|
||||
allocation.category_id = False
|
||||
elif allocation.holiday_type == 'company':
|
||||
allocation.employee_ids = False
|
||||
if not allocation.mode_company_id:
|
||||
allocation.mode_company_id = self.env.company
|
||||
allocation.category_id = False
|
||||
elif allocation.holiday_type == 'department':
|
||||
allocation.employee_ids = False
|
||||
allocation.mode_company_id = False
|
||||
allocation.category_id = False
|
||||
elif allocation.holiday_type == 'category':
|
||||
allocation.employee_ids = False
|
||||
allocation.mode_company_id = False
|
||||
else:
|
||||
allocation.employee_ids = default_employee_ids
|
||||
|
||||
@api.depends('holiday_type', 'employee_id')
|
||||
def _compute_department_id(self):
|
||||
for allocation in self:
|
||||
if allocation.holiday_type == 'employee':
|
||||
allocation.department_id = allocation.employee_id.department_id
|
||||
elif allocation.holiday_type == 'department':
|
||||
if not allocation.department_id:
|
||||
allocation.department_id = self.env.user.employee_id.department_id
|
||||
elif allocation.holiday_type == 'category':
|
||||
allocation.department_id = False
|
||||
|
||||
@api.depends('employee_id')
|
||||
def _compute_manager_id(self):
|
||||
for allocation in self:
|
||||
allocation.manager_id = allocation.employee_id and allocation.employee_id.parent_id
|
||||
|
||||
@api.depends('accrual_plan_id')
|
||||
def _compute_holiday_status_id(self):
|
||||
default_holiday_status_id = None
|
||||
for holiday in self:
|
||||
if not holiday.holiday_status_id:
|
||||
if holiday.accrual_plan_id:
|
||||
holiday.holiday_status_id = holiday.accrual_plan_id.time_off_type_id
|
||||
else:
|
||||
if not default_holiday_status_id: # fetch when we need it
|
||||
default_holiday_status_id = self._default_holiday_status_id()
|
||||
holiday.holiday_status_id = default_holiday_status_id
|
||||
|
||||
@api.depends('holiday_status_id', 'allocation_type', 'number_of_hours_display', 'number_of_days_display', 'date_to')
|
||||
def _compute_from_holiday_status_id(self):
|
||||
accrual_allocations = self.filtered(lambda alloc: alloc.allocation_type == 'accrual' and not alloc.accrual_plan_id and alloc.holiday_status_id)
|
||||
accruals_dict = {}
|
||||
if accrual_allocations:
|
||||
accruals_read_group = self.env['hr.leave.accrual.plan'].read_group(
|
||||
[('time_off_type_id', 'in', accrual_allocations.holiday_status_id.ids)],
|
||||
['time_off_type_id', 'ids:array_agg(id)'],
|
||||
['time_off_type_id'],
|
||||
)
|
||||
accruals_dict = {res['time_off_type_id'][0]: res['ids'] for res in accruals_read_group}
|
||||
for allocation in self:
|
||||
allocation.number_of_days = allocation.number_of_days_display
|
||||
if allocation.type_request_unit == 'hour':
|
||||
allocation.number_of_days = allocation.number_of_hours_display / \
|
||||
(allocation.employee_id.sudo().resource_calendar_id.hours_per_day \
|
||||
or allocation.holiday_status_id.company_id.resource_calendar_id.hours_per_day \
|
||||
or HOURS_PER_DAY)
|
||||
if allocation.accrual_plan_id.time_off_type_id.id not in (False, allocation.holiday_status_id.id):
|
||||
allocation.accrual_plan_id = False
|
||||
if allocation.allocation_type == 'accrual' and not allocation.accrual_plan_id:
|
||||
if allocation.holiday_status_id:
|
||||
allocation.accrual_plan_id = accruals_dict.get(allocation.holiday_status_id.id, [False])[0]
|
||||
|
||||
def _end_of_year_accrual(self):
|
||||
# to override in payroll
|
||||
today = fields.Date.today()
|
||||
last_day_last_year = today + relativedelta(years=-1, month=12, day=31)
|
||||
first_day_this_year = today + relativedelta(month=1, day=1)
|
||||
for allocation in self:
|
||||
current_level = allocation._get_current_accrual_plan_level_id(first_day_this_year)[0]
|
||||
if not current_level:
|
||||
continue
|
||||
# lastcall has two cases:
|
||||
# 1. The period was fully ran until the last day of last year
|
||||
# 2. The period was not fully ran until the last day of last year
|
||||
# For case 2, we need to prorata the number of days so need to check if the lastcall within the current level period
|
||||
lastcall = current_level._get_previous_date(last_day_last_year) if allocation.lastcall < current_level._get_previous_date(last_day_last_year) else allocation.lastcall
|
||||
nextcall = current_level._get_next_date(last_day_last_year)
|
||||
if current_level.action_with_unused_accruals == 'lost':
|
||||
# Allocations are lost but number_of_days should not be lower than leaves_taken
|
||||
# `lastcall` and `nextcall` must be those of the last period in order
|
||||
# to receive the full period allocation during the next call of the current year.
|
||||
allocation.write({'number_of_days': allocation.leaves_taken, 'lastcall': lastcall, 'nextcall': nextcall})
|
||||
elif current_level.action_with_unused_accruals == 'postponed' and current_level.postpone_max_days:
|
||||
# Make sure the period was ran until the last day of last year
|
||||
if allocation.nextcall:
|
||||
allocation.nextcall = first_day_this_year
|
||||
# date_to should be first day of this year so the prorata amount is computed correctly
|
||||
allocation._process_accrual_plans(first_day_this_year, True)
|
||||
|
||||
def _get_current_accrual_plan_level_id(self, date, level_ids=False):
|
||||
"""
|
||||
Returns a pair (accrual_plan_level, idx) where accrual_plan_level is the level for the given date
|
||||
and idx is the index for the plan in the ordered set of levels
|
||||
"""
|
||||
self.ensure_one()
|
||||
if not self.accrual_plan_id.level_ids:
|
||||
return (False, False)
|
||||
# Sort by sequence which should be equivalent to the level
|
||||
if not level_ids:
|
||||
level_ids = self.accrual_plan_id.level_ids.sorted('sequence')
|
||||
current_level = False
|
||||
current_level_idx = -1
|
||||
for idx, level in enumerate(level_ids):
|
||||
if date > self.date_from + get_timedelta(level.start_count, level.start_type):
|
||||
current_level = level
|
||||
current_level_idx = idx
|
||||
# If transition_mode is set to `immediately` or we are currently on the first level
|
||||
# the current_level is simply the first level in the list.
|
||||
if current_level_idx <= 0 or self.accrual_plan_id.transition_mode == "immediately":
|
||||
return (current_level, current_level_idx)
|
||||
# In this case we have to verify that the 'previous level' is not the current one due to `end_of_accrual`
|
||||
level_start_date = self.date_from + get_timedelta(current_level.start_count, current_level.start_type)
|
||||
previous_level = level_ids[current_level_idx - 1]
|
||||
# If the next date from the current level's start date is before the last call of the previous level
|
||||
# return the previous level
|
||||
if current_level._get_next_date(level_start_date) < previous_level._get_next_date(level_start_date):
|
||||
return (previous_level, current_level_idx - 1)
|
||||
return (current_level, current_level_idx)
|
||||
|
||||
def _process_accrual_plan_level(self, level, start_period, start_date, end_period, end_date):
|
||||
"""
|
||||
Returns the added days for that level
|
||||
"""
|
||||
self.ensure_one()
|
||||
if level.is_based_on_worked_time:
|
||||
start_dt = datetime.combine(start_date, datetime.min.time())
|
||||
end_dt = datetime.combine(end_date, datetime.min.time())
|
||||
worked = self.employee_id._get_work_days_data_batch(start_dt, end_dt, calendar=self.employee_id.resource_calendar_id)\
|
||||
[self.employee_id.id]['hours']
|
||||
if start_period != start_date or end_period != end_date:
|
||||
start_dt = datetime.combine(start_period, datetime.min.time())
|
||||
end_dt = datetime.combine(end_period, datetime.min.time())
|
||||
planned_worked = self.employee_id._get_work_days_data_batch(start_dt, end_dt, calendar=self.employee_id.resource_calendar_id)\
|
||||
[self.employee_id.id]['hours']
|
||||
else:
|
||||
planned_worked = worked
|
||||
left = self.employee_id.sudo()._get_leave_days_data_batch(start_dt, end_dt,
|
||||
domain=[('time_type', '=', 'leave')])[self.employee_id.id]['hours']
|
||||
work_entry_prorata = worked / (left + planned_worked) if (left + planned_worked) else 0
|
||||
added_value = work_entry_prorata * level.added_value
|
||||
else:
|
||||
added_value = level.added_value
|
||||
# Convert time in hours to time in days in case the level is encoded in hours
|
||||
if level.added_value_type == 'hours':
|
||||
added_value = added_value / (self.employee_id.sudo().resource_id.calendar_id.hours_per_day or HOURS_PER_DAY)
|
||||
period_prorata = 1
|
||||
if (start_period != start_date or end_period != end_date) and not level.is_based_on_worked_time:
|
||||
period_days = (end_period - start_period)
|
||||
call_days = (end_date - start_date)
|
||||
period_prorata = min(1, call_days / period_days) if period_days else 1
|
||||
return added_value * period_prorata
|
||||
|
||||
def _process_accrual_plans(self, date_to=False, force_period=False):
|
||||
"""
|
||||
This method is part of the cron's process.
|
||||
The goal of this method is to retroactively apply accrual plan levels and progress from nextcall to date_to or today.
|
||||
If force_period is set, the accrual will run until date_to in a prorated way (used for end of year accrual actions).
|
||||
"""
|
||||
date_to = date_to or fields.Date.today()
|
||||
first_allocation = _("""This allocation have already ran once, any modification won't be effective to the days allocated to the employee. If you need to change the configuration of the allocation, cancel and create a new one.""")
|
||||
for allocation in self:
|
||||
level_ids = allocation.accrual_plan_id.level_ids.sorted('sequence')
|
||||
if not level_ids:
|
||||
continue
|
||||
if not allocation.nextcall:
|
||||
first_level = level_ids[0]
|
||||
first_level_start_date = allocation.date_from + get_timedelta(first_level.start_count, first_level.start_type)
|
||||
if date_to < first_level_start_date:
|
||||
# Accrual plan is not configured properly or has not started
|
||||
continue
|
||||
allocation.lastcall = max(allocation.lastcall, first_level_start_date)
|
||||
allocation.nextcall = first_level._get_next_date(allocation.lastcall)
|
||||
if len(level_ids) > 1:
|
||||
second_level_start_date = allocation.date_from + get_timedelta(level_ids[1].start_count, level_ids[1].start_type)
|
||||
allocation.nextcall = min(second_level_start_date, allocation.nextcall)
|
||||
allocation._message_log(body=first_allocation)
|
||||
days_added_per_level = defaultdict(lambda: 0)
|
||||
while allocation.nextcall <= date_to:
|
||||
(current_level, current_level_idx) = allocation._get_current_accrual_plan_level_id(allocation.nextcall)
|
||||
if not current_level:
|
||||
break
|
||||
current_level_maximum_leave = current_level.maximum_leave if current_level.added_value_type == "days" else current_level.maximum_leave / (allocation.employee_id.sudo().resource_id.calendar_id.hours_per_day or HOURS_PER_DAY)
|
||||
nextcall = current_level._get_next_date(allocation.nextcall)
|
||||
# Since _get_previous_date returns the given date if it corresponds to a call date
|
||||
# this will always return lastcall except possibly on the first call
|
||||
# this is used to prorate the first number of days given to the employee
|
||||
period_start = current_level._get_previous_date(allocation.lastcall)
|
||||
period_end = current_level._get_next_date(allocation.lastcall)
|
||||
# Also prorate this accrual in the event that we are passing from one level to another
|
||||
if current_level_idx < (len(level_ids) - 1) and allocation.accrual_plan_id.transition_mode == 'immediately':
|
||||
next_level = level_ids[current_level_idx + 1]
|
||||
current_level_last_date = allocation.date_from + get_timedelta(next_level.start_count, next_level.start_type)
|
||||
if allocation.nextcall != current_level_last_date:
|
||||
nextcall = min(nextcall, current_level_last_date)
|
||||
# We have to check for end of year actions if it is within our period
|
||||
# since we can create retroactive allocations.
|
||||
if allocation.lastcall.year < allocation.nextcall.year and\
|
||||
current_level.action_with_unused_accruals == 'postponed' and\
|
||||
current_level.postpone_max_days > 0:
|
||||
# Compute number of days kept
|
||||
allocation_days = allocation.number_of_days - allocation.leaves_taken
|
||||
allowed_to_keep = max(0, current_level.postpone_max_days - allocation_days)
|
||||
number_of_days = min(allocation_days, current_level.postpone_max_days)
|
||||
allocation.number_of_days = number_of_days + allocation.leaves_taken
|
||||
total_gained_days = sum(days_added_per_level.values())
|
||||
days_added_per_level.clear()
|
||||
days_added_per_level[current_level] = min(total_gained_days, allowed_to_keep)
|
||||
gained_days = allocation._process_accrual_plan_level(
|
||||
current_level, period_start, allocation.lastcall, period_end, allocation.nextcall)
|
||||
days_added_per_level[current_level] += gained_days
|
||||
if current_level_maximum_leave > 0 and sum(days_added_per_level.values()) > current_level_maximum_leave:
|
||||
days_added_per_level[current_level] -= sum(days_added_per_level.values()) - current_level_maximum_leave
|
||||
|
||||
allocation.lastcall = allocation.nextcall
|
||||
allocation.nextcall = nextcall
|
||||
if force_period and allocation.nextcall > date_to:
|
||||
allocation.nextcall = date_to
|
||||
force_period = False
|
||||
|
||||
if days_added_per_level:
|
||||
number_of_days_to_add = allocation.number_of_days + sum(days_added_per_level.values())
|
||||
max_allocation_days = current_level_maximum_leave + (allocation.leaves_taken if allocation.type_request_unit != "hour" else allocation.leaves_taken / (allocation.employee_id.sudo().resource_id.calendar_id.hours_per_day or HOURS_PER_DAY))
|
||||
# Let's assume the limit of the last level is the correct one
|
||||
allocation.number_of_days = min(number_of_days_to_add, max_allocation_days) if current_level_maximum_leave > 0 else number_of_days_to_add
|
||||
|
||||
@api.model
|
||||
def _update_accrual(self):
|
||||
"""
|
||||
Method called by the cron task in order to increment the number_of_days when
|
||||
necessary.
|
||||
"""
|
||||
# Get the current date to determine the start and end of the accrual period
|
||||
today = datetime.combine(fields.Date.today(), time(0, 0, 0))
|
||||
this_year_first_day = (today + relativedelta(day=1, month=1)).date()
|
||||
end_of_year_allocations = self.search(
|
||||
[('allocation_type', '=', 'accrual'), ('state', '=', 'validate'), ('accrual_plan_id', '!=', False), ('employee_id', '!=', False),
|
||||
'|', ('date_to', '=', False), ('date_to', '>', fields.Datetime.now()), ('lastcall', '<', this_year_first_day)])
|
||||
end_of_year_allocations._end_of_year_accrual()
|
||||
end_of_year_allocations.flush_model()
|
||||
allocations = self.search(
|
||||
[('allocation_type', '=', 'accrual'), ('state', '=', 'validate'), ('accrual_plan_id', '!=', False), ('employee_id', '!=', False),
|
||||
'|', ('date_to', '=', False), ('date_to', '>', fields.Datetime.now()),
|
||||
'|', ('nextcall', '=', False), ('nextcall', '<=', today)])
|
||||
allocations._process_accrual_plans()
|
||||
|
||||
####################################################
|
||||
# ORM Overrides methods
|
||||
####################################################
|
||||
|
||||
def onchange(self, values, field_name, field_onchange):
|
||||
# Try to force the leave_type name_get when creating new records
|
||||
# This is called right after pressing create and returns the name_get for
|
||||
# most fields in the view.
|
||||
if field_onchange.get('employee_id') and 'employee_id' not in self._context and values:
|
||||
employee_id = get_employee_from_context(values, self._context, self.env.user.employee_id.id)
|
||||
self = self.with_context(employee_id=employee_id)
|
||||
return super().onchange(values, field_name, field_onchange)
|
||||
|
||||
def name_get(self):
|
||||
res = []
|
||||
for allocation in self:
|
||||
if allocation.holiday_type == 'company':
|
||||
target = allocation.mode_company_id.name
|
||||
elif allocation.holiday_type == 'department':
|
||||
target = allocation.department_id.name
|
||||
elif allocation.holiday_type == 'category':
|
||||
target = allocation.category_id.name
|
||||
elif allocation.employee_id:
|
||||
target = allocation.employee_id.name
|
||||
else:
|
||||
target = ', '.join(allocation.employee_ids.sudo().mapped('name'))
|
||||
|
||||
res.append(
|
||||
(allocation.id,
|
||||
_("Allocation of %(allocation_name)s : %(duration).2f %(duration_type)s to %(person)s",
|
||||
allocation_name=allocation.holiday_status_id.sudo().name,
|
||||
duration=allocation.number_of_hours_display if allocation.type_request_unit == 'hour' else allocation.number_of_days,
|
||||
duration_type=_('hours') if allocation.type_request_unit == 'hour' else _('days'),
|
||||
person=target
|
||||
))
|
||||
)
|
||||
return res
|
||||
|
||||
def add_follower(self, employee_id):
|
||||
employee = self.env['hr.employee'].browse(employee_id)
|
||||
if employee.user_id:
|
||||
self.message_subscribe(partner_ids=employee.user_id.partner_id.ids)
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
""" Override to avoid automatic logging of creation """
|
||||
for values in vals_list:
|
||||
if 'state' in values and values['state'] not in ('draft', 'confirm'):
|
||||
raise UserError(_('Incorrect state for new allocation'))
|
||||
employee_id = values.get('employee_id', False)
|
||||
if not values.get('department_id'):
|
||||
values.update({'department_id': self.env['hr.employee'].browse(employee_id).department_id.id})
|
||||
# default `lastcall` to `nextcall`
|
||||
if 'date_from' in values and 'lastcall' not in values:
|
||||
values['lastcall'] = values['date_from']
|
||||
holidays = super(HolidaysAllocation, self.with_context(mail_create_nosubscribe=True)).create(vals_list)
|
||||
for holiday in holidays:
|
||||
partners_to_subscribe = set()
|
||||
if holiday.employee_id.user_id:
|
||||
partners_to_subscribe.add(holiday.employee_id.user_id.partner_id.id)
|
||||
if holiday.validation_type == 'officer':
|
||||
partners_to_subscribe.add(holiday.employee_id.parent_id.user_id.partner_id.id)
|
||||
partners_to_subscribe.add(holiday.employee_id.leave_manager_id.partner_id.id)
|
||||
holiday.message_subscribe(partner_ids=tuple(partners_to_subscribe))
|
||||
if not self._context.get('import_file'):
|
||||
holiday.activity_update()
|
||||
if holiday.validation_type == 'no' and holiday.state == 'draft':
|
||||
holiday.action_confirm()
|
||||
return holidays
|
||||
|
||||
def write(self, values):
|
||||
if not self.env.context.get('toggle_active') and not bool(values.get('active', True)):
|
||||
if any(allocation.state not in ['draft', 'cancel', 'refuse'] for allocation in self):
|
||||
raise UserError(_('You cannot archive an allocation which is in confirm or validate state.'))
|
||||
employee_id = values.get('employee_id', False)
|
||||
if values.get('state'):
|
||||
self._check_approval_update(values['state'])
|
||||
result = super(HolidaysAllocation, self).write(values)
|
||||
self.add_follower(employee_id)
|
||||
return result
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def _unlink_if_correct_states(self):
|
||||
state_description_values = {elem[0]: elem[1] for elem in self._fields['state']._description_selection(self.env)}
|
||||
for holiday in self.filtered(lambda holiday: holiday.state not in ['draft', 'cancel', 'confirm']):
|
||||
raise UserError(_('You cannot delete an allocation request which is in %s state.') % (state_description_values.get(holiday.state),))
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def _unlink_if_no_leaves(self):
|
||||
if any(allocation.holiday_status_id.requires_allocation == 'yes' and allocation.leaves_taken > 0 for allocation in self):
|
||||
raise UserError(_('You cannot delete an allocation request which has some validated leaves.'))
|
||||
|
||||
def _get_mail_redirect_suggested_company(self):
|
||||
return self.holiday_status_id.company_id
|
||||
|
||||
####################################################
|
||||
# Business methods
|
||||
####################################################
|
||||
|
||||
def _prepare_holiday_values(self, employees):
|
||||
self.ensure_one()
|
||||
return [{
|
||||
'name': self.name,
|
||||
'holiday_type': 'employee',
|
||||
'holiday_status_id': self.holiday_status_id.id,
|
||||
'notes': self.notes,
|
||||
'number_of_days': self.number_of_days,
|
||||
'parent_id': self.id,
|
||||
'employee_id': employee.id,
|
||||
'employee_ids': [(6, 0, [employee.id])],
|
||||
'state': 'confirm',
|
||||
'allocation_type': self.allocation_type,
|
||||
'date_from': self.date_from,
|
||||
'date_to': self.date_to,
|
||||
'accrual_plan_id': self.accrual_plan_id.id,
|
||||
} for employee in employees if (not employee.resource_calendar_id) or employee.resource_calendar_id.hours_per_day]
|
||||
|
||||
def action_draft(self):
|
||||
if any(holiday.state not in ['confirm', 'refuse'] for holiday in self):
|
||||
raise UserError(_('Allocation request state must be "Refused" or "To Approve" in order to be reset to Draft.'))
|
||||
self.write({
|
||||
'state': 'draft',
|
||||
'approver_id': False,
|
||||
})
|
||||
linked_requests = self.mapped('linked_request_ids')
|
||||
if linked_requests:
|
||||
linked_requests.action_draft()
|
||||
linked_requests.unlink()
|
||||
self.activity_update()
|
||||
return True
|
||||
|
||||
def action_confirm(self):
|
||||
if self.filtered(lambda holiday: holiday.state != 'draft' and holiday.validation_type != 'no'):
|
||||
raise UserError(_('Allocation request must be in Draft state ("To Submit") in order to confirm it.'))
|
||||
validated_holidays = self.filtered(lambda holiday: holiday.state == 'validate')
|
||||
res = (self - validated_holidays).write({'state': 'confirm'})
|
||||
self.activity_update()
|
||||
no_employee_requests = [holiday.id for holiday in self.sudo() if holiday.holiday_status_id.employee_requests == 'no']
|
||||
self.filtered(lambda holiday: (holiday.id in no_employee_requests or holiday.validation_type == 'no') and holiday.state != 'validate').action_validate()
|
||||
return res
|
||||
|
||||
def action_validate(self):
|
||||
current_employee = self.env.user.employee_id
|
||||
no_employee_requests = [holiday.id for holiday in self.sudo() if holiday.holiday_status_id.employee_requests == 'no']
|
||||
if any((holiday.state != 'confirm' and holiday.id not in no_employee_requests and holiday.validation_type != 'no') for holiday in self):
|
||||
raise UserError(_('Allocation request must be confirmed in order to approve it.'))
|
||||
|
||||
self.write({
|
||||
'state': 'validate',
|
||||
'approver_id': current_employee.id
|
||||
})
|
||||
|
||||
for holiday in self:
|
||||
holiday._action_validate_create_childs()
|
||||
self.activity_update()
|
||||
return True
|
||||
|
||||
def _action_validate_create_childs(self):
|
||||
childs = self.env['hr.leave.allocation']
|
||||
# In the case we are in holiday_type `employee` and there is only one employee we can keep the same allocation
|
||||
# Otherwise we do need to create an allocation for all employees to have a behaviour that is in line
|
||||
# with the other holiday_type
|
||||
if self.state == 'validate' and (self.holiday_type in ['category', 'department', 'company'] or
|
||||
(self.holiday_type == 'employee' and len(self.employee_ids) > 1)):
|
||||
if self.holiday_type == 'employee':
|
||||
employees = self.employee_ids
|
||||
elif self.holiday_type == 'category':
|
||||
employees = self.category_id.employee_ids
|
||||
elif self.holiday_type == 'department':
|
||||
employees = self.department_id.member_ids
|
||||
else:
|
||||
employees = self.env['hr.employee'].search([('company_id', '=', self.mode_company_id.id)])
|
||||
|
||||
allocation_create_vals = self._prepare_holiday_values(employees)
|
||||
childs += self.with_context(
|
||||
mail_notify_force_send=False,
|
||||
mail_activity_automation_skip=True
|
||||
).create(allocation_create_vals)
|
||||
if childs:
|
||||
childs.action_validate()
|
||||
return childs
|
||||
|
||||
def action_refuse(self):
|
||||
current_employee = self.env.user.employee_id
|
||||
if any(holiday.state not in ['confirm', 'validate', 'validate1'] for holiday in self):
|
||||
raise UserError(_('Allocation request must be confirmed or validated in order to refuse it.'))
|
||||
|
||||
self.write({'state': 'refuse', 'approver_id': current_employee.id})
|
||||
# If a category that created several holidays, cancel all related
|
||||
linked_requests = self.mapped('linked_request_ids')
|
||||
if linked_requests:
|
||||
linked_requests.action_refuse()
|
||||
self.activity_update()
|
||||
return True
|
||||
|
||||
def _check_approval_update(self, state):
|
||||
""" Check if target state is achievable. """
|
||||
if self.env.is_superuser():
|
||||
return
|
||||
current_employee = self.env.user.employee_id
|
||||
if not current_employee:
|
||||
return
|
||||
is_officer = self.env.user.has_group('hr_holidays.group_hr_holidays_user')
|
||||
is_manager = self.env.user.has_group('hr_holidays.group_hr_holidays_manager')
|
||||
for holiday in self:
|
||||
val_type = holiday.holiday_status_id.sudo().allocation_validation_type
|
||||
if state == 'confirm':
|
||||
continue
|
||||
|
||||
if state == 'draft':
|
||||
if holiday.employee_id != current_employee and not is_manager:
|
||||
raise UserError(_('Only a time off Manager can reset other people allocation.'))
|
||||
continue
|
||||
|
||||
if not is_officer and self.env.user != holiday.employee_id.leave_manager_id and not val_type == 'no':
|
||||
raise UserError(_('Only a time off Officer/Responsible or Manager can approve or refuse time off requests.'))
|
||||
|
||||
if is_officer or self.env.user == holiday.employee_id.leave_manager_id:
|
||||
# use ir.rule based first access check: department, members, ... (see security.xml)
|
||||
holiday.check_access_rule('write')
|
||||
|
||||
if holiday.employee_id == current_employee and not is_manager and not val_type == 'no':
|
||||
raise UserError(_('Only a time off Manager can approve its own requests.'))
|
||||
|
||||
@api.onchange('allocation_type')
|
||||
def _onchange_allocation_type(self):
|
||||
if self.allocation_type == 'accrual':
|
||||
self.number_of_days = 0.0
|
||||
elif not self.number_of_days_display:
|
||||
self.number_of_days = 1.0
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Activity methods
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def _get_responsible_for_approval(self):
|
||||
self.ensure_one()
|
||||
responsible = self.env.user
|
||||
|
||||
if self.validation_type == 'officer' or self.validation_type == 'set':
|
||||
if self.holiday_status_id.responsible_id:
|
||||
responsible = self.holiday_status_id.responsible_id
|
||||
|
||||
return responsible
|
||||
|
||||
def activity_update(self):
|
||||
to_clean, to_do = self.env['hr.leave.allocation'], self.env['hr.leave.allocation']
|
||||
for allocation in self:
|
||||
if allocation.validation_type != 'no':
|
||||
note = _(
|
||||
'New Allocation Request created by %(user)s: %(count)s Days of %(allocation_type)s',
|
||||
user=allocation.create_uid.name,
|
||||
count=allocation.number_of_days,
|
||||
allocation_type=allocation.holiday_status_id.name
|
||||
)
|
||||
if allocation.state == 'draft':
|
||||
to_clean |= allocation
|
||||
elif allocation.state == 'confirm':
|
||||
allocation.activity_schedule(
|
||||
'hr_holidays.mail_act_leave_allocation_approval',
|
||||
note=note,
|
||||
user_id=allocation.sudo()._get_responsible_for_approval().id or self.env.user.id)
|
||||
elif allocation.state == 'validate1':
|
||||
allocation.activity_feedback(['hr_holidays.mail_act_leave_allocation_approval'])
|
||||
allocation.activity_schedule(
|
||||
'hr_holidays.mail_act_leave_allocation_second_approval',
|
||||
note=note,
|
||||
user_id=allocation.sudo()._get_responsible_for_approval().id or self.env.user.id)
|
||||
elif allocation.state == 'validate':
|
||||
to_do |= allocation
|
||||
elif allocation.state == 'refuse':
|
||||
to_clean |= allocation
|
||||
if to_clean:
|
||||
to_clean.activity_unlink(['hr_holidays.mail_act_leave_allocation_approval', 'hr_holidays.mail_act_leave_allocation_second_approval'])
|
||||
if to_do:
|
||||
to_do.activity_feedback(['hr_holidays.mail_act_leave_allocation_approval', 'hr_holidays.mail_act_leave_allocation_second_approval'])
|
||||
|
||||
####################################################
|
||||
# Messaging methods
|
||||
####################################################
|
||||
|
||||
def _track_subtype(self, init_values):
|
||||
if 'state' in init_values and self.state == 'validate':
|
||||
allocation_notif_subtype_id = self.holiday_status_id.allocation_notif_subtype_id
|
||||
return allocation_notif_subtype_id or self.env.ref('hr_holidays.mt_leave_allocation')
|
||||
return super(HolidaysAllocation, self)._track_subtype(init_values)
|
||||
|
||||
def _notify_get_recipients_groups(self, msg_vals=None):
|
||||
""" Handle HR users and officers recipients that can validate or refuse holidays
|
||||
directly from email. """
|
||||
groups = super(HolidaysAllocation, self)._notify_get_recipients_groups(msg_vals=msg_vals)
|
||||
if not self:
|
||||
return groups
|
||||
|
||||
local_msg_vals = dict(msg_vals or {})
|
||||
|
||||
self.ensure_one()
|
||||
hr_actions = []
|
||||
if self.state == 'confirm':
|
||||
app_action = self._notify_get_action_link('controller', controller='/allocation/validate', **local_msg_vals)
|
||||
hr_actions += [{'url': app_action, 'title': _('Approve')}]
|
||||
if self.state in ['confirm', 'validate', 'validate1']:
|
||||
ref_action = self._notify_get_action_link('controller', controller='/allocation/refuse', **local_msg_vals)
|
||||
hr_actions += [{'url': ref_action, 'title': _('Refuse')}]
|
||||
|
||||
holiday_user_group_id = self.env.ref('hr_holidays.group_hr_holidays_user').id
|
||||
new_group = (
|
||||
'group_hr_holidays_user',
|
||||
lambda pdata: pdata['type'] == 'user' and holiday_user_group_id in pdata['groups'],
|
||||
{'actions': hr_actions}
|
||||
)
|
||||
|
||||
return [new_group] + groups
|
||||
|
||||
def message_subscribe(self, partner_ids=None, subtype_ids=None):
|
||||
# due to record rule can not allow to add follower and mention on validated leave so subscribe through sudo
|
||||
if self.state in ['validate', 'validate1']:
|
||||
self.check_access_rights('read')
|
||||
self.check_access_rule('read')
|
||||
return super(HolidaysAllocation, self.sudo()).message_subscribe(partner_ids=partner_ids, subtype_ids=subtype_ids)
|
||||
return super(HolidaysAllocation, self).message_subscribe(partner_ids=partner_ids, subtype_ids=subtype_ids)
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from random import randint
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class StressDay(models.Model):
|
||||
_name = 'hr.leave.stress.day'
|
||||
_description = 'Stress Day'
|
||||
_order = 'start_date desc, end_date desc'
|
||||
|
||||
name = fields.Char(required=True)
|
||||
company_id = fields.Many2one('res.company', default=lambda self: self.env.company, required=True)
|
||||
start_date = fields.Date(required=True)
|
||||
end_date = fields.Date(required=True)
|
||||
color = fields.Integer(default=lambda dummy: randint(1, 11))
|
||||
resource_calendar_id = fields.Many2one(
|
||||
'resource.calendar', 'Working Hours',
|
||||
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
|
||||
department_ids = fields.Many2many('hr.department', string="Departments")
|
||||
|
||||
_sql_constraints = [
|
||||
('date_from_after_day_to', 'CHECK(start_date <= end_date)', 'The start date must be anterior than the end date.')
|
||||
]
|
||||
|
|
@ -0,0 +1,662 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
# Copyright (c) 2005-2006 Axelor SARL. (http://www.axelor.com)
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
from collections import defaultdict
|
||||
from datetime import time, timedelta
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.osv import expression
|
||||
from odoo.tools import format_date
|
||||
from odoo.tools.translate import _
|
||||
from odoo.tools.float_utils import float_round
|
||||
from odoo.addons.resource.models.resource import Intervals
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HolidaysType(models.Model):
|
||||
_name = "hr.leave.type"
|
||||
_description = "Time Off Type"
|
||||
_order = 'sequence'
|
||||
|
||||
@api.model
|
||||
def _model_sorting_key(self, leave_type):
|
||||
remaining = leave_type.virtual_remaining_leaves > 0
|
||||
taken = leave_type.leaves_taken > 0
|
||||
return -1*leave_type.sequence, leave_type.employee_requests == 'no' and remaining, leave_type.employee_requests == 'yes' and remaining, taken
|
||||
|
||||
name = fields.Char('Time Off Type', required=True, translate=True)
|
||||
sequence = fields.Integer(default=100,
|
||||
help='The type with the smallest sequence is the default value in time off request')
|
||||
create_calendar_meeting = fields.Boolean(string="Display Time Off in Calendar", default=True)
|
||||
color_name = fields.Selection([
|
||||
('red', 'Red'),
|
||||
('blue', 'Blue'),
|
||||
('lightgreen', 'Light Green'),
|
||||
('lightblue', 'Light Blue'),
|
||||
('lightyellow', 'Light Yellow'),
|
||||
('magenta', 'Magenta'),
|
||||
('lightcyan', 'Light Cyan'),
|
||||
('black', 'Black'),
|
||||
('lightpink', 'Light Pink'),
|
||||
('brown', 'Brown'),
|
||||
('violet', 'Violet'),
|
||||
('lightcoral', 'Light Coral'),
|
||||
('lightsalmon', 'Light Salmon'),
|
||||
('lavender', 'Lavender'),
|
||||
('wheat', 'Wheat'),
|
||||
('ivory', 'Ivory')], string='Color in Report', required=True, default='red',
|
||||
help='This color will be used in the time off summary located in Reporting > Time off by Department.')
|
||||
color = fields.Integer(string='Color', help="The color selected here will be used in every screen with the time off type.")
|
||||
icon_id = fields.Many2one('ir.attachment', string='Cover Image', domain="[('res_model', '=', 'hr.leave.type'), ('res_field', '=', 'icon_id')]")
|
||||
active = fields.Boolean('Active', default=True,
|
||||
help="If the active field is set to false, it will allow you to hide the time off type without removing it.")
|
||||
max_leaves = fields.Float(compute='_compute_leaves', string='Maximum Allowed', search='_search_max_leaves',
|
||||
help='This value is given by the sum of all time off requests with a positive value.')
|
||||
leaves_taken = fields.Float(
|
||||
compute='_compute_leaves', string='Time off Already Taken',
|
||||
help='This value is given by the sum of all time off requests with a negative value.')
|
||||
remaining_leaves = fields.Float(
|
||||
compute='_compute_leaves', string='Remaining Time Off',
|
||||
help='Maximum Time Off Allowed - Time Off Already Taken')
|
||||
virtual_remaining_leaves = fields.Float(
|
||||
compute='_compute_leaves', search='_search_virtual_remaining_leaves', string='Virtual Remaining Time Off',
|
||||
help='Maximum Time Off Allowed - Time Off Already Taken - Time Off Waiting Approval')
|
||||
virtual_leaves_taken = fields.Float(
|
||||
compute='_compute_leaves', string='Virtual Time Off Already Taken',
|
||||
help='Sum of validated and non validated time off requests.')
|
||||
closest_allocation_to_expire = fields.Many2one('hr.leave.allocation', 'Allocation', compute='_compute_leaves')
|
||||
allocation_count = fields.Integer(
|
||||
compute='_compute_allocation_count', string='Allocations')
|
||||
group_days_leave = fields.Float(
|
||||
compute='_compute_group_days_leave', string='Group Time Off')
|
||||
company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.company)
|
||||
responsible_id = fields.Many2one(
|
||||
'res.users', 'Responsible Time Off Officer',
|
||||
domain=lambda self: [('groups_id', 'in', self.env.ref('hr_holidays.group_hr_holidays_user').id),
|
||||
('share', '=', False)],
|
||||
help="Choose the Time Off Officer who will be notified to approve allocation or Time Off request")
|
||||
leave_validation_type = fields.Selection([
|
||||
('no_validation', 'No Validation'),
|
||||
('hr', 'By Time Off Officer'),
|
||||
('manager', "By Employee's Approver"),
|
||||
('both', "By Employee's Approver and Time Off Officer")], default='hr', string='Leave Validation')
|
||||
requires_allocation = fields.Selection([
|
||||
('yes', 'Yes'),
|
||||
('no', 'No Limit')], default="yes", required=True, string='Requires allocation',
|
||||
help="""Yes: Time off requests need to have a valid allocation.\n
|
||||
No Limit: Time Off requests can be taken without any prior allocation.""")
|
||||
employee_requests = fields.Selection([
|
||||
('yes', 'Extra Days Requests Allowed'),
|
||||
('no', 'Not Allowed')], default="no", required=True, string="Employee Requests",
|
||||
help="""Extra Days Requests Allowed: User can request an allocation for himself.\n
|
||||
Not Allowed: User cannot request an allocation.""")
|
||||
allocation_validation_type = fields.Selection([
|
||||
('officer', 'Approved by Time Off Officer'),
|
||||
('no', 'No validation needed')], default='no', string='Approval',
|
||||
compute='_compute_allocation_validation_type', store=True, readonly=False,
|
||||
help="""Select the level of approval needed in case of request by employee
|
||||
- No validation needed: The employee's request is automatically approved.
|
||||
- Approved by Time Off Officer: The employee's request need to be manually approved by the Time Off Officer.""")
|
||||
has_valid_allocation = fields.Boolean(compute='_compute_valid', search='_search_valid', help='This indicates if it is still possible to use this type of leave')
|
||||
time_type = fields.Selection([('other', 'Worked Time'), ('leave', 'Absence')], default='leave', string="Kind of Time Off",
|
||||
help="The distinction between working time (ex. Attendance) and absence (ex. Training) will be used in the computation of Accrual's plan rate.")
|
||||
request_unit = fields.Selection([
|
||||
('day', 'Day'),
|
||||
('half_day', 'Half Day'),
|
||||
('hour', 'Hours')], default='day', string='Take Time Off in', required=True)
|
||||
unpaid = fields.Boolean('Is Unpaid', default=False)
|
||||
leave_notif_subtype_id = fields.Many2one('mail.message.subtype', string='Time Off Notification Subtype', default=lambda self: self.env.ref('hr_holidays.mt_leave', raise_if_not_found=False))
|
||||
allocation_notif_subtype_id = fields.Many2one('mail.message.subtype', string='Allocation Notification Subtype', default=lambda self: self.env.ref('hr_holidays.mt_leave_allocation', raise_if_not_found=False))
|
||||
support_document = fields.Boolean(string='Supporting Document')
|
||||
accruals_ids = fields.One2many('hr.leave.accrual.plan', 'time_off_type_id')
|
||||
accrual_count = fields.Float(compute="_compute_accrual_count", string="Accruals count")
|
||||
|
||||
|
||||
@api.model
|
||||
def _search_valid(self, operator, value):
|
||||
""" Returns leave_type ids for which a valid allocation exists
|
||||
or that don't need an allocation
|
||||
return [('id', domain_operator, [x['id'] for x in res])]
|
||||
"""
|
||||
|
||||
if {'default_date_from', 'default_date_to', 'tz'} <= set(self._context):
|
||||
default_date_from_dt = fields.Datetime.to_datetime(self._context.get('default_date_from'))
|
||||
default_date_to_dt = fields.Datetime.to_datetime(self._context.get('default_date_to'))
|
||||
|
||||
# Cast: Datetime -> Date using user's tz
|
||||
date_to = fields.Date.context_today(self, default_date_from_dt)
|
||||
date_from = fields.Date.context_today(self, default_date_to_dt)
|
||||
|
||||
else:
|
||||
date_to = fields.Date.today().strftime('%Y-1-1')
|
||||
date_from = fields.Date.today().strftime('%Y-12-31')
|
||||
|
||||
employee_id = self._context.get('default_employee_id', self._context.get('employee_id')) or self.env.user.employee_id.id
|
||||
|
||||
if not isinstance(value, bool):
|
||||
raise ValueError('Invalid value: %s' % (value))
|
||||
if operator not in ['=', '!=']:
|
||||
raise ValueError('Invalid operator: %s' % (operator))
|
||||
new_operator = 'in' if operator == '=' else 'not in'
|
||||
|
||||
query = '''
|
||||
SELECT
|
||||
holiday_status_id
|
||||
FROM
|
||||
hr_leave_allocation alloc
|
||||
WHERE
|
||||
alloc.employee_id = %s AND
|
||||
alloc.active = True AND alloc.state = 'validate' AND
|
||||
(alloc.date_to >= %s OR alloc.date_to IS NULL) AND
|
||||
alloc.date_from <= %s
|
||||
'''
|
||||
|
||||
self._cr.execute(query, (employee_id or None, date_to, date_from))
|
||||
|
||||
return [('id', new_operator, [x['holiday_status_id'] for x in self._cr.dictfetchall()])]
|
||||
|
||||
|
||||
@api.depends('requires_allocation')
|
||||
def _compute_valid(self):
|
||||
date_to = self._context.get('default_date_to', fields.Datetime.today())
|
||||
date_from = self._context.get('default_date_from', fields.Datetime.today())
|
||||
employee_id = self._context.get('default_employee_id', self._context.get('employee_id', self.env.user.employee_id.id))
|
||||
for holiday_type in self:
|
||||
if holiday_type.requires_allocation == 'yes':
|
||||
allocation = self.env['hr.leave.allocation'].search([
|
||||
('holiday_status_id', '=', holiday_type.id),
|
||||
('employee_id', '=', employee_id),
|
||||
'|',
|
||||
('date_to', '>=', date_to),
|
||||
'&',
|
||||
('date_to', '=', False),
|
||||
('date_from', '<=', date_from)])
|
||||
holiday_type.has_valid_allocation = bool(allocation)
|
||||
else:
|
||||
holiday_type.has_valid_allocation = True
|
||||
|
||||
def _load_records_write(self, values):
|
||||
if 'requires_allocation' in values and self.requires_allocation == values['requires_allocation']:
|
||||
values.pop('requires_allocation')
|
||||
return super()._load_records_write(values)
|
||||
|
||||
@api.constrains('requires_allocation')
|
||||
def check_allocation_requirement_edit_validity(self):
|
||||
if self.env['hr.leave'].search_count([('holiday_status_id', 'in', self.ids)], limit=1):
|
||||
raise UserError(_("The allocation requirement of a time off type cannot be changed once leaves of that type have been taken. You should create a new time off type instead."))
|
||||
|
||||
def _search_max_leaves(self, operator, value):
|
||||
value = float(value)
|
||||
employee_id = self._get_contextual_employee_id()
|
||||
leaves = defaultdict(int)
|
||||
|
||||
if employee_id:
|
||||
allocations = self.env['hr.leave.allocation'].search([
|
||||
('employee_id', '=', employee_id),
|
||||
('state', '=', 'validate')
|
||||
])
|
||||
for allocation in allocations:
|
||||
leaves[allocation.holiday_status_id.id] += allocation.number_of_days
|
||||
valid_leave = []
|
||||
for leave in leaves:
|
||||
if operator == '>':
|
||||
if leaves[leave] > value:
|
||||
valid_leave.append(leave)
|
||||
elif operator == '<':
|
||||
if leaves[leave] < value:
|
||||
valid_leave.append(leave)
|
||||
elif operator == '=':
|
||||
if leaves[leave] == value:
|
||||
valid_leave.append(leave)
|
||||
elif operator == '!=':
|
||||
if leaves[leave] != value:
|
||||
valid_leave.append(leave)
|
||||
|
||||
return [('id', 'in', valid_leave)]
|
||||
|
||||
def _search_virtual_remaining_leaves(self, operator, value):
|
||||
value = float(value)
|
||||
leave_types = self.env['hr.leave.type'].search([])
|
||||
valid_leave_types = self.env['hr.leave.type']
|
||||
|
||||
for leave_type in leave_types:
|
||||
if leave_type.requires_allocation == "yes":
|
||||
if operator == '>' and leave_type.virtual_remaining_leaves > value:
|
||||
valid_leave_types |= leave_type
|
||||
elif operator == '<' and leave_type.virtual_remaining_leaves < value:
|
||||
valid_leave_types |= leave_type
|
||||
elif operator == '>=' and leave_type.virtual_remaining_leaves >= value:
|
||||
valid_leave_types |= leave_type
|
||||
elif operator == '<=' and leave_type.virtual_remaining_leaves <= value:
|
||||
valid_leave_types |= leave_type
|
||||
elif operator == '=' and leave_type.virtual_remaining_leaves == value:
|
||||
valid_leave_types |= leave_type
|
||||
elif operator == '!=' and leave_type.virtual_remaining_leaves != value:
|
||||
valid_leave_types |= leave_type
|
||||
else:
|
||||
valid_leave_types |= leave_type
|
||||
|
||||
return [('id', 'in', valid_leave_types.ids)]
|
||||
|
||||
def _get_employees_days_per_allocation(self, employee_ids, date=None):
|
||||
if not date:
|
||||
date = fields.Date.to_date(self.env.context.get('default_date_from')) or fields.Date.context_today(self)
|
||||
|
||||
leaves_domain = [
|
||||
('employee_id', 'in', employee_ids),
|
||||
('state', 'in', ['confirm', 'validate1', 'validate']),
|
||||
('holiday_status_id', 'in', self.ids)
|
||||
]
|
||||
if self.env.context.get("ignore_future"):
|
||||
leaves_domain.append(('date_from', '<=', date))
|
||||
leaves = self.env['hr.leave'].search(leaves_domain)
|
||||
|
||||
allocations = self.env['hr.leave.allocation'].with_context(active_test=False).search([
|
||||
('employee_id', 'in', employee_ids),
|
||||
('state', 'in', ['validate']),
|
||||
('holiday_status_id', 'in', self.ids),
|
||||
])
|
||||
|
||||
# The allocation_employees dictionary groups the allocations based on the employee and the holiday type
|
||||
# The structure is the following:
|
||||
# - KEYS:
|
||||
# allocation_employees
|
||||
# |--employee_id
|
||||
# |--holiday_status_id
|
||||
# - VALUES:
|
||||
# Intervals with the start and end date of each allocation and associated allocations within this interval
|
||||
allocation_employees = defaultdict(lambda: defaultdict(list))
|
||||
|
||||
### Creation of the allocation intervals ###
|
||||
for holiday_status_id in allocations.holiday_status_id:
|
||||
for employee_id in employee_ids:
|
||||
allocation_intervals = Intervals([(
|
||||
fields.datetime.combine(allocation.date_from, time.min),
|
||||
fields.datetime.combine(allocation.date_to or datetime.date.max, time.max),
|
||||
allocation)
|
||||
for allocation in allocations.filtered(lambda allocation: allocation.employee_id.id == employee_id and allocation.holiday_status_id == holiday_status_id)])
|
||||
|
||||
allocation_employees[employee_id][holiday_status_id] = allocation_intervals
|
||||
|
||||
# The leave_employees dictionary groups the leavess based on the employee and the holiday type
|
||||
# The structure is the following:
|
||||
# - KEYS:
|
||||
# leave_employees
|
||||
# |--employee_id
|
||||
# |--holiday_status_id
|
||||
# - VALUES:
|
||||
# Intervals with the start and end date of each leave and associated leave within this interval
|
||||
leaves_employees = defaultdict(lambda: defaultdict(list))
|
||||
leave_intervals = []
|
||||
|
||||
### Creation of the leave intervals ###
|
||||
if leaves:
|
||||
for holiday_status_id in leaves.holiday_status_id:
|
||||
for employee_id in employee_ids:
|
||||
leave_intervals = Intervals([(
|
||||
fields.datetime.combine(leave.date_from, time.min),
|
||||
fields.datetime.combine(leave.date_to, time.max),
|
||||
leave)
|
||||
for leave in leaves.filtered(lambda leave: leave.employee_id.id == employee_id and leave.holiday_status_id == holiday_status_id)])
|
||||
|
||||
leaves_employees[employee_id][holiday_status_id] = leave_intervals
|
||||
|
||||
# allocation_days_consumed is a dictionary to map the number of days/hours of leaves taken per allocation
|
||||
# The structure is the following:
|
||||
# - KEYS:
|
||||
# allocation_days_consumed
|
||||
# |--employee_id
|
||||
# |--holiday_status_id
|
||||
# |--allocation
|
||||
# |--virtual_leaves_taken
|
||||
# |--leaves_taken
|
||||
# |--virtual_remaining_leaves
|
||||
# |--remaining_leaves
|
||||
# |--max_leaves
|
||||
# |--closest_allocation_to_expire
|
||||
# - VALUES:
|
||||
# Integer representing the number of (virtual) remaining leaves, (virtual) leaves taken or max leaves for each allocation.
|
||||
# The unit is in hour or days depending on the leave type request unit
|
||||
allocations_days_consumed = defaultdict(lambda: defaultdict(lambda: defaultdict(lambda: defaultdict(lambda: 0))))
|
||||
|
||||
company_domain = [('company_id', 'in', list(set(self.env.company.ids + self.env.context.get('allowed_company_ids', []))))]
|
||||
|
||||
### Existing leaves assigned to allocations ###
|
||||
if leaves_employees:
|
||||
for employee_id, leaves_interval_by_status in leaves_employees.items():
|
||||
for holiday_status_id in leaves_interval_by_status:
|
||||
days_consumed = allocations_days_consumed[employee_id][holiday_status_id]
|
||||
if allocation_employees[employee_id][holiday_status_id]:
|
||||
allocations = allocation_employees[employee_id][holiday_status_id] & leaves_interval_by_status[holiday_status_id]
|
||||
available_allocations = self.env['hr.leave.allocation']
|
||||
for allocation_interval in allocations._items:
|
||||
available_allocations |= allocation_interval[2]
|
||||
# Consume the allocations that are close to expiration first
|
||||
sorted_available_allocations = available_allocations.filtered('date_to').sorted(key='date_to')
|
||||
sorted_available_allocations += available_allocations.filtered(lambda allocation: not allocation.date_to)
|
||||
leave_intervals = leaves_interval_by_status[holiday_status_id]._items
|
||||
sorted_allocations_with_remaining_leaves = self.env['hr.leave.allocation']
|
||||
for leave_interval in leave_intervals:
|
||||
leaves = leave_interval[2]
|
||||
for leave in leaves:
|
||||
if leave.leave_type_request_unit in ['day', 'half_day']:
|
||||
leave_duration = leave.number_of_days
|
||||
leave_unit = 'days'
|
||||
else:
|
||||
leave_duration = leave.number_of_hours_display
|
||||
leave_unit = 'hours'
|
||||
if holiday_status_id.requires_allocation != 'no':
|
||||
for available_allocation in sorted_available_allocations:
|
||||
if (available_allocation.date_to and available_allocation.date_to < leave.date_from.date()) \
|
||||
or (available_allocation.date_from > leave.date_to.date()):
|
||||
continue
|
||||
virtual_remaining_leaves = (available_allocation.number_of_days if leave_unit == 'days' else available_allocation.number_of_hours_display) - allocations_days_consumed[employee_id][holiday_status_id][available_allocation]['virtual_leaves_taken']
|
||||
max_leaves = min(virtual_remaining_leaves, leave_duration)
|
||||
days_consumed[available_allocation]['virtual_leaves_taken'] += max_leaves
|
||||
if leave.state == 'validate':
|
||||
days_consumed[available_allocation]['leaves_taken'] += max_leaves
|
||||
leave_duration -= max_leaves
|
||||
# Check valid allocations with still availabe leaves on it
|
||||
if days_consumed[available_allocation]['virtual_remaining_leaves'] > 0 and available_allocation.date_to and available_allocation.date_to > date:
|
||||
sorted_allocations_with_remaining_leaves |= available_allocation
|
||||
if leave_duration > 0:
|
||||
# There are not enough allocation for the number of leaves
|
||||
days_consumed[False]['virtual_remaining_leaves'] -= leave_duration
|
||||
else:
|
||||
days_consumed[False]['virtual_leaves_taken'] += leave_duration
|
||||
if leave.state == 'validate':
|
||||
days_consumed[False]['leaves_taken'] += leave_duration
|
||||
# no need to sort the allocations again
|
||||
allocations_days_consumed[employee_id][holiday_status_id][False]['closest_allocation_to_expire'] = sorted_allocations_with_remaining_leaves[0] if sorted_allocations_with_remaining_leaves else False
|
||||
|
||||
# Future available leaves
|
||||
future_allocations_date_from = fields.datetime.combine(date, time.min)
|
||||
future_allocations_date_to = fields.datetime.combine(date, time.max) + timedelta(days=5*365)
|
||||
for employee_id, allocation_intervals_by_status in allocation_employees.items():
|
||||
employee = self.env['hr.employee'].browse(employee_id)
|
||||
for holiday_status_id, intervals in allocation_intervals_by_status.items():
|
||||
if not intervals:
|
||||
continue
|
||||
future_allocation_intervals = intervals & Intervals([(
|
||||
future_allocations_date_from,
|
||||
future_allocations_date_to,
|
||||
self.env['hr.leave'])])
|
||||
search_date = date
|
||||
closest_allocations = self.env['hr.leave.allocation']
|
||||
for interval in intervals._items:
|
||||
closest_allocations |= interval[2]
|
||||
allocations_with_remaining_leaves = self.env['hr.leave.allocation']
|
||||
for interval_from, interval_to, interval_allocations in future_allocation_intervals._items:
|
||||
if interval_from.date() > search_date:
|
||||
continue
|
||||
interval_allocations = interval_allocations.filtered('active')
|
||||
if not interval_allocations:
|
||||
continue
|
||||
# If no end date to the allocation, consider the number of days remaining as infinite
|
||||
employee_quantity_available = (
|
||||
employee._get_work_days_data_batch(interval_from, interval_to, compute_leaves=False, domain=company_domain)[employee_id]
|
||||
if interval_to != future_allocations_date_to
|
||||
else {'days': float('inf'), 'hours': float('inf')}
|
||||
)
|
||||
reached_remaining_days_limit = False
|
||||
for allocation in interval_allocations:
|
||||
if allocation.date_from > search_date:
|
||||
continue
|
||||
days_consumed = allocations_days_consumed[employee_id][holiday_status_id][allocation]
|
||||
if allocation.type_request_unit in ['day', 'half_day']:
|
||||
quantity_available = employee_quantity_available['days']
|
||||
remaining_days_allocation = (allocation.number_of_days - days_consumed['virtual_leaves_taken'])
|
||||
else:
|
||||
quantity_available = employee_quantity_available['hours']
|
||||
remaining_days_allocation = (allocation.number_of_hours_display - days_consumed['virtual_leaves_taken'])
|
||||
if quantity_available <= remaining_days_allocation:
|
||||
search_date = interval_to.date() + timedelta(days=1)
|
||||
days_consumed['max_leaves'] = allocation.number_of_days if allocation.type_request_unit in ['day', 'half_day'] else allocation.number_of_hours_display
|
||||
if not reached_remaining_days_limit:
|
||||
days_consumed['virtual_remaining_leaves'] += min(quantity_available, remaining_days_allocation)
|
||||
days_consumed['remaining_leaves'] = days_consumed['max_leaves'] - days_consumed['leaves_taken']
|
||||
if remaining_days_allocation >= quantity_available:
|
||||
reached_remaining_days_limit = True
|
||||
# Check valid allocations with still availabe leaves on it
|
||||
if days_consumed['virtual_remaining_leaves'] > 0 and allocation.date_to and allocation.date_to > date:
|
||||
allocations_with_remaining_leaves |= allocation
|
||||
allocations_sorted = sorted(allocations_with_remaining_leaves, key=lambda a: a.date_to)
|
||||
allocations_days_consumed[employee_id][holiday_status_id][False]['closest_allocation_to_expire'] = allocations_sorted[0] if allocations_sorted else False
|
||||
return allocations_days_consumed
|
||||
|
||||
|
||||
def get_employees_days(self, employee_ids, date=None):
|
||||
|
||||
result = {
|
||||
employee_id: {
|
||||
leave_type.id: {
|
||||
'max_leaves': 0,
|
||||
'leaves_taken': 0,
|
||||
'remaining_leaves': 0,
|
||||
'virtual_remaining_leaves': 0,
|
||||
'virtual_leaves_taken': 0,
|
||||
'closest_allocation_to_expire': False,
|
||||
} for leave_type in self
|
||||
} for employee_id in employee_ids
|
||||
}
|
||||
|
||||
if not date:
|
||||
date = fields.Date.to_date(self.env.context.get('default_date_from')) or fields.Date.context_today(self)
|
||||
|
||||
allocations_days_consumed = self._get_employees_days_per_allocation(employee_ids, date)
|
||||
|
||||
leave_keys = ['max_leaves', 'leaves_taken', 'remaining_leaves', 'virtual_remaining_leaves', 'virtual_leaves_taken']
|
||||
|
||||
for employee_id in allocations_days_consumed:
|
||||
for holiday_status_id in allocations_days_consumed[employee_id]:
|
||||
for allocation in allocations_days_consumed[employee_id][holiday_status_id]:
|
||||
if allocation:
|
||||
if allocation.date_to and (allocation.date_to < date or allocation.date_from > date):
|
||||
continue
|
||||
for leave_key in leave_keys:
|
||||
result[employee_id][holiday_status_id if isinstance(holiday_status_id, int) else holiday_status_id.id][leave_key] += allocations_days_consumed[employee_id][holiday_status_id][allocation][leave_key]
|
||||
else:
|
||||
result[employee_id][holiday_status_id if isinstance(holiday_status_id, int) else holiday_status_id.id]['closest_allocation_to_expire'] = allocations_days_consumed[employee_id][holiday_status_id][False]['closest_allocation_to_expire']
|
||||
for leave_key in leave_keys:
|
||||
if allocations_days_consumed[employee_id][holiday_status_id][False].get(leave_key):
|
||||
result[employee_id][holiday_status_id if isinstance(holiday_status_id, int) else holiday_status_id.id][leave_key] = allocations_days_consumed[employee_id][holiday_status_id][False][leave_key]
|
||||
|
||||
return result
|
||||
|
||||
@api.model
|
||||
def get_days_all_request(self):
|
||||
leave_types = sorted(self.search([]).filtered(lambda x: ((x.virtual_remaining_leaves > 0 or x.max_leaves))), key=self._model_sorting_key, reverse=True)
|
||||
return [lt._get_days_request() for lt in leave_types]
|
||||
|
||||
def _get_days_request(self):
|
||||
self.ensure_one()
|
||||
result = self._get_employees_days_per_allocation(self.closest_allocation_to_expire.employee_id.ids)
|
||||
closest_allocation_remaining = 0
|
||||
if self.closest_allocation_to_expire:
|
||||
# Shows the sum of allocation expiring on the same day as the closest to expire
|
||||
employee_allocations = result[self.closest_allocation_to_expire.employee_id.id][self].items()
|
||||
closest_allocation_remaining = sum(
|
||||
res['virtual_remaining_leaves']
|
||||
for alloc, res in employee_allocations
|
||||
if alloc and alloc.date_to == self.closest_allocation_to_expire.date_to
|
||||
)
|
||||
return (self.name, {
|
||||
'remaining_leaves': ('%.2f' % self.remaining_leaves).rstrip('0').rstrip('.'),
|
||||
'usable_remaining_leaves': ('%.2f' % self.virtual_remaining_leaves).rstrip('0').rstrip('.'),
|
||||
'virtual_remaining_leaves': ('%.2f' % (self.max_leaves - self.virtual_leaves_taken)).rstrip('0').rstrip('.'),
|
||||
'max_leaves': ('%.2f' % self.max_leaves).rstrip('0').rstrip('.'),
|
||||
'leaves_taken': ('%.2f' % self.leaves_taken).rstrip('0').rstrip('.'),
|
||||
'virtual_leaves_taken': ('%.2f' % self.virtual_leaves_taken).rstrip('0').rstrip('.'),
|
||||
'leaves_requested': ('%.2f' % (self.virtual_leaves_taken - self.leaves_taken)).rstrip('0').rstrip('.'),
|
||||
'leaves_approved': ('%.2f' % self.leaves_taken).rstrip('0').rstrip('.'),
|
||||
'closest_allocation_remaining': ('%.2f' % closest_allocation_remaining).rstrip('0').rstrip('.'),
|
||||
'closest_allocation_expire': format_date(self.env, self.closest_allocation_to_expire.date_to) if self.closest_allocation_to_expire.date_to else False,
|
||||
'request_unit': self.request_unit,
|
||||
'icon': self.sudo().icon_id.url,
|
||||
}, self.requires_allocation, self.id)
|
||||
|
||||
def _get_contextual_employee_id(self):
|
||||
if 'employee_id' in self._context:
|
||||
employee_id = self._context['employee_id']
|
||||
elif 'default_employee_id' in self._context:
|
||||
employee_id = self._context['default_employee_id']
|
||||
else:
|
||||
employee_id = self.env.user.employee_id.id
|
||||
return employee_id
|
||||
|
||||
@api.depends_context('employee_id', 'default_employee_id')
|
||||
def _compute_leaves(self):
|
||||
data_days = {}
|
||||
employee_id = self._get_contextual_employee_id()
|
||||
|
||||
if employee_id:
|
||||
data_days = (self.get_employees_days(employee_id)[employee_id[0]] if isinstance(employee_id, list) else
|
||||
self.get_employees_days([employee_id])[employee_id])
|
||||
|
||||
for holiday_status in self:
|
||||
result = data_days.get(holiday_status.id, {})
|
||||
holiday_status.max_leaves = result.get('max_leaves', 0)
|
||||
holiday_status.leaves_taken = result.get('leaves_taken', 0)
|
||||
holiday_status.remaining_leaves = result.get('remaining_leaves', 0)
|
||||
holiday_status.virtual_remaining_leaves = result.get('virtual_remaining_leaves', 0)
|
||||
holiday_status.virtual_leaves_taken = result.get('virtual_leaves_taken', 0)
|
||||
holiday_status.closest_allocation_to_expire = result.get('closest_allocation_to_expire', 0)
|
||||
|
||||
def _compute_allocation_count(self):
|
||||
min_datetime = fields.Datetime.to_string(datetime.datetime.now().replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0))
|
||||
max_datetime = fields.Datetime.to_string(datetime.datetime.now().replace(month=12, day=31, hour=23, minute=59, second=59))
|
||||
domain = [
|
||||
('holiday_status_id', 'in', self.ids),
|
||||
('date_from', '>=', min_datetime),
|
||||
('date_from', '<=', max_datetime),
|
||||
('state', 'in', ('confirm', 'validate')),
|
||||
]
|
||||
|
||||
grouped_res = self.env['hr.leave.allocation']._read_group(
|
||||
domain,
|
||||
['holiday_status_id'],
|
||||
['holiday_status_id'],
|
||||
)
|
||||
grouped_dict = dict((data['holiday_status_id'][0], data['holiday_status_id_count']) for data in grouped_res)
|
||||
for allocation in self:
|
||||
allocation.allocation_count = grouped_dict.get(allocation.id, 0)
|
||||
|
||||
def _compute_group_days_leave(self):
|
||||
min_datetime = fields.Datetime.to_string(datetime.datetime.now().replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0))
|
||||
max_datetime = fields.Datetime.to_string(datetime.datetime.now().replace(month=12, day=31, hour=23, minute=59, second=59))
|
||||
domain = [
|
||||
('holiday_status_id', 'in', self.ids),
|
||||
('date_from', '>=', min_datetime),
|
||||
('date_from', '<=', max_datetime),
|
||||
('state', 'in', ('validate', 'validate1', 'confirm')),
|
||||
]
|
||||
grouped_res = self.env['hr.leave']._read_group(
|
||||
domain,
|
||||
['holiday_status_id'],
|
||||
['holiday_status_id'],
|
||||
)
|
||||
grouped_dict = dict((data['holiday_status_id'][0], data['holiday_status_id_count']) for data in grouped_res)
|
||||
for allocation in self:
|
||||
allocation.group_days_leave = grouped_dict.get(allocation.id, 0)
|
||||
|
||||
def _compute_accrual_count(self):
|
||||
accrual_allocations = self.env['hr.leave.accrual.plan']._read_group([('time_off_type_id', 'in', self.ids)], ['time_off_type_id'], ['time_off_type_id'])
|
||||
mapped_data = dict((data['time_off_type_id'][0], data['time_off_type_id_count']) for data in accrual_allocations)
|
||||
for leave_type in self:
|
||||
leave_type.accrual_count = mapped_data.get(leave_type.id, 0)
|
||||
|
||||
@api.depends('employee_requests')
|
||||
def _compute_allocation_validation_type(self):
|
||||
for leave_type in self:
|
||||
if leave_type.employee_requests == 'no':
|
||||
leave_type.allocation_validation_type = 'officer'
|
||||
|
||||
def requested_name_get(self):
|
||||
return self._context.get('holiday_status_name_get', True) and self._context.get('employee_id')
|
||||
|
||||
def name_get(self):
|
||||
if not self.requested_name_get():
|
||||
# leave counts is based on employee_id, would be inaccurate if not based on correct employee
|
||||
return super(HolidaysType, self).name_get()
|
||||
res = []
|
||||
for record in self:
|
||||
name = record.name
|
||||
if record.requires_allocation == "yes" and not self._context.get('from_manager_leave_form'):
|
||||
name = "%(name)s (%(count)s)" % {
|
||||
'name': name,
|
||||
'count': _('%g remaining out of %g') % (
|
||||
float_round(record.virtual_remaining_leaves, precision_digits=2) or 0.0,
|
||||
float_round(record.max_leaves, precision_digits=2) or 0.0,
|
||||
) + (_(' hours') if record.request_unit == 'hour' else _(' days'))
|
||||
}
|
||||
res.append((record.id, name))
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None):
|
||||
""" Override _search to order the results, according to some employee.
|
||||
The order is the following
|
||||
|
||||
- allocation fixed first, then allowing allocation, then free allocation
|
||||
- virtual remaining leaves (higher the better, so using reverse on sorted)
|
||||
|
||||
This override is necessary because those fields are not stored and depends
|
||||
on an employee_id given in context. This sort will be done when there
|
||||
is an employee_id in context and that no other order has been given
|
||||
to the method.
|
||||
"""
|
||||
employee_id = self._get_contextual_employee_id()
|
||||
post_sort = (not count and not order and employee_id)
|
||||
leave_ids = super(HolidaysType, self)._search(args, offset=offset, limit=(None if post_sort else limit), order=order, count=count, access_rights_uid=access_rights_uid)
|
||||
leaves = self.browse(leave_ids)
|
||||
if post_sort:
|
||||
return leaves.sorted(key=self._model_sorting_key, reverse=True).ids[:limit or None]
|
||||
return leave_ids
|
||||
|
||||
def action_see_days_allocated(self):
|
||||
self.ensure_one()
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("hr_holidays.hr_leave_allocation_action_all")
|
||||
date_from = fields.Datetime.to_string(
|
||||
datetime.datetime.now().replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0))
|
||||
action['domain'] = [
|
||||
('holiday_status_id', 'in', self.ids),
|
||||
]
|
||||
action['context'] = {
|
||||
'employee_id': False,
|
||||
'default_holiday_type': 'department',
|
||||
'default_holiday_status_id': self.ids[0],
|
||||
'search_default_approved_state': 1,
|
||||
'search_default_year': 1,
|
||||
}
|
||||
return action
|
||||
|
||||
def action_see_group_leaves(self):
|
||||
self.ensure_one()
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("hr_holidays.hr_leave_action_action_approve_department")
|
||||
action['domain'] = [
|
||||
('holiday_status_id', '=', self.ids[0]),
|
||||
]
|
||||
action['context'] = {
|
||||
'default_holiday_status_id': self.ids[0],
|
||||
'search_default_need_approval_approved': 1,
|
||||
'search_default_this_year': 1,
|
||||
}
|
||||
return action
|
||||
|
||||
def action_see_accrual_plans(self):
|
||||
self.ensure_one()
|
||||
action = self.env["ir.actions.actions"]._for_xml_id("hr_holidays.open_view_accrual_plans")
|
||||
action['domain'] = [
|
||||
('time_off_type_id', '=', self.id),
|
||||
]
|
||||
action['context'] = {
|
||||
'default_time_off_type_id': self.id,
|
||||
}
|
||||
return action
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
|
||||
from odoo import api, models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MailMessageSubtype(models.Model):
|
||||
_inherit = 'mail.message.subtype'
|
||||
|
||||
def _get_department_subtype(self):
|
||||
return self.search([
|
||||
('res_model', '=', 'hr.department'),
|
||||
('parent_id', '=', self.id)])
|
||||
|
||||
def _update_department_subtype(self):
|
||||
for subtype in self:
|
||||
department_subtype = subtype._get_department_subtype()
|
||||
if department_subtype:
|
||||
department_subtype.write({
|
||||
'name': subtype.name,
|
||||
'default': subtype.default,
|
||||
})
|
||||
else:
|
||||
department_subtype = self.create({
|
||||
'name': subtype.name,
|
||||
'res_model': 'hr.department',
|
||||
'default': subtype.default or False,
|
||||
'parent_id': subtype.id,
|
||||
'relation_field': 'department_id',
|
||||
})
|
||||
return department_subtype
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
result = super(MailMessageSubtype, self).create(vals_list)
|
||||
result.filtered(
|
||||
lambda st: st.res_model in ['hr.leave', 'hr.leave.allocation']
|
||||
)._update_department_subtype()
|
||||
return result
|
||||
|
||||
def write(self, vals):
|
||||
result = super(MailMessageSubtype, self).write(vals)
|
||||
self.filtered(
|
||||
lambda subtype: subtype.res_model in ['hr.leave', 'hr.leave.allocation']
|
||||
)._update_department_subtype()
|
||||
return result
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, models
|
||||
from odoo.tools.misc import DEFAULT_SERVER_DATE_FORMAT
|
||||
|
||||
|
||||
class ResPartner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
def _compute_im_status(self):
|
||||
super(ResPartner, self)._compute_im_status()
|
||||
absent_now = self._get_on_leave_ids()
|
||||
for partner in self:
|
||||
if partner.id in absent_now:
|
||||
if partner.im_status == 'online':
|
||||
partner.im_status = 'leave_online'
|
||||
elif partner.im_status == 'away':
|
||||
partner.im_status = 'leave_away'
|
||||
else:
|
||||
partner.im_status = 'leave_offline'
|
||||
|
||||
@api.model
|
||||
def _get_on_leave_ids(self):
|
||||
return self.env['res.users']._get_on_leave_ids(partner=True)
|
||||
|
||||
def mail_partner_format(self, fields=None):
|
||||
"""Override to add the current leave status."""
|
||||
partners_format = super().mail_partner_format(fields=fields)
|
||||
if not fields:
|
||||
fields = {'out_of_office_date_end': True}
|
||||
for partner in self:
|
||||
if 'out_of_office_date_end' in fields:
|
||||
# in the rare case of multi-user partner, return the earliest possible return date
|
||||
dates = partner.mapped('user_ids.leave_date_to')
|
||||
states = partner.mapped('user_ids.current_leave_state')
|
||||
date = sorted(dates)[0] if dates and all(dates) else False
|
||||
state = sorted(states)[0] if states and all(states) else False
|
||||
partners_format.get(partner).update({
|
||||
'out_of_office_date_end': date.strftime(DEFAULT_SERVER_DATE_FORMAT) if state == 'validate' and date else False,
|
||||
})
|
||||
return partners_format
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, Command
|
||||
|
||||
|
||||
class User(models.Model):
|
||||
_inherit = "res.users"
|
||||
|
||||
leave_manager_id = fields.Many2one(related='employee_id.leave_manager_id')
|
||||
show_leaves = fields.Boolean(related='employee_id.show_leaves')
|
||||
allocation_count = fields.Float(related='employee_id.allocation_count')
|
||||
leave_date_to = fields.Date(related='employee_id.leave_date_to')
|
||||
current_leave_state = fields.Selection(related='employee_id.current_leave_state')
|
||||
is_absent = fields.Boolean(related='employee_id.is_absent')
|
||||
allocation_remaining_display = fields.Char(related='employee_id.allocation_remaining_display')
|
||||
allocation_display = fields.Char(related='employee_id.allocation_display')
|
||||
hr_icon_display = fields.Selection(related='employee_id.hr_icon_display')
|
||||
|
||||
@property
|
||||
def SELF_READABLE_FIELDS(self):
|
||||
return super().SELF_READABLE_FIELDS + [
|
||||
'leave_manager_id',
|
||||
'show_leaves',
|
||||
'allocation_count',
|
||||
'leave_date_to',
|
||||
'current_leave_state',
|
||||
'is_absent',
|
||||
'allocation_remaining_display',
|
||||
'allocation_display',
|
||||
'hr_icon_display',
|
||||
]
|
||||
|
||||
def _compute_im_status(self):
|
||||
super(User, self)._compute_im_status()
|
||||
on_leave_user_ids = self._get_on_leave_ids()
|
||||
for user in self:
|
||||
if user.id in on_leave_user_ids:
|
||||
if user.im_status == 'online':
|
||||
user.im_status = 'leave_online'
|
||||
elif user.im_status == 'away':
|
||||
user.im_status = 'leave_away'
|
||||
else:
|
||||
user.im_status = 'leave_offline'
|
||||
|
||||
@api.model
|
||||
def _get_on_leave_ids(self, partner=False):
|
||||
now = fields.Datetime.now()
|
||||
field = 'partner_id' if partner else 'id'
|
||||
self.flush_model(['active'])
|
||||
self.env['hr.leave'].flush_model(['user_id', 'state', 'date_from', 'date_to'])
|
||||
self.env.cr.execute('''SELECT res_users.%s FROM res_users
|
||||
JOIN hr_leave ON hr_leave.user_id = res_users.id
|
||||
AND hr_leave.state = 'validate'
|
||||
AND hr_leave.active = 't'
|
||||
AND res_users.active = 't'
|
||||
AND hr_leave.date_from <= %%s AND hr_leave.date_to >= %%s''' % field, (now, now))
|
||||
return [r[0] for r in self.env.cr.fetchall()]
|
||||
|
||||
def _clean_leave_responsible_users(self):
|
||||
# self = old bunch of leave responsibles
|
||||
# This method compares the current leave managers
|
||||
# and remove the access rights to those who don't
|
||||
# need them anymore
|
||||
approver_group = 'hr_holidays.group_hr_holidays_responsible'
|
||||
if not any(u.has_group(approver_group) for u in self):
|
||||
return
|
||||
|
||||
res = self.env['hr.employee'].read_group(
|
||||
[('leave_manager_id', 'in', self.ids)],
|
||||
['leave_manager_id'],
|
||||
['leave_manager_id'])
|
||||
responsibles_to_remove_ids = set(self.ids) - {x['leave_manager_id'][0] for x in res}
|
||||
if responsibles_to_remove_ids:
|
||||
self.browse(responsibles_to_remove_ids).write({
|
||||
'groups_id': [Command.unlink(self.env.ref(approver_group).id)],
|
||||
})
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
users = super().create(vals_list)
|
||||
users.sudo()._clean_leave_responsible_users()
|
||||
return users
|
||||
174
odoo-bringout-oca-ocb-hr_holidays/hr_holidays/models/resource.py
Normal file
174
odoo-bringout-oca-ocb-hr_holidays/hr_holidays/models/resource.py
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models, api, _
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.osv import expression
|
||||
import pytz
|
||||
from datetime import datetime
|
||||
|
||||
class CalendarLeaves(models.Model):
|
||||
_inherit = "resource.calendar.leaves"
|
||||
|
||||
holiday_id = fields.Many2one("hr.leave", string='Leave Request')
|
||||
|
||||
@api.constrains('date_from', 'date_to', 'calendar_id')
|
||||
def _check_compare_dates(self):
|
||||
all_existing_leaves = self.env['resource.calendar.leaves'].search([
|
||||
('resource_id', '=', False),
|
||||
('company_id', 'in', self.company_id.ids),
|
||||
('date_from', '<=', max(self.mapped('date_to'))),
|
||||
('date_to', '>=', min(self.mapped('date_from'))),
|
||||
])
|
||||
for record in self:
|
||||
if not record.resource_id:
|
||||
existing_leaves = all_existing_leaves.filtered(lambda leave:
|
||||
record.id != leave.id
|
||||
and record['company_id'] == leave['company_id']
|
||||
and record['date_from'] <= leave['date_to']
|
||||
and record['date_to'] >= leave['date_from'])
|
||||
if record.calendar_id:
|
||||
existing_leaves = existing_leaves.filtered(lambda l: not l.calendar_id or l.calendar_id == record.calendar_id)
|
||||
if existing_leaves:
|
||||
raise ValidationError(_('Two public holidays cannot overlap each other for the same working hours.'))
|
||||
|
||||
def _get_domain(self, time_domain_dict):
|
||||
domain = []
|
||||
for date in time_domain_dict:
|
||||
domain = expression.OR([domain, [
|
||||
('employee_company_id', '=', date['company_id']),
|
||||
('date_to', '>', date['date_from']),
|
||||
('date_from', '<', date['date_to'])]
|
||||
])
|
||||
return expression.AND([domain, [('state', '!=', 'refuse'), ('active', '=', True)]])
|
||||
|
||||
def _get_time_domain_dict(self):
|
||||
return [{
|
||||
'company_id' : record.company_id.id,
|
||||
'date_from' : record.date_from,
|
||||
'date_to' : record.date_to
|
||||
} for record in self if not record.resource_id]
|
||||
|
||||
def _reevaluate_leaves(self, time_domain_dict):
|
||||
if not time_domain_dict:
|
||||
return
|
||||
|
||||
domain = self._get_domain(time_domain_dict)
|
||||
leaves = self.env['hr.leave'].search(domain)
|
||||
if not leaves:
|
||||
return
|
||||
|
||||
previous_durations = leaves.mapped('number_of_days')
|
||||
previous_states = leaves.mapped('state')
|
||||
leaves.sudo().write({
|
||||
'state': 'draft',
|
||||
})
|
||||
self.env.add_to_compute(self.env['hr.leave']._fields['number_of_days'], leaves)
|
||||
self.env.add_to_compute(self.env['hr.leave']._fields['duration_display'], leaves)
|
||||
sick_time_status = self.env.ref('hr_holidays.holiday_status_sl')
|
||||
for previous_duration, leave, state in zip(previous_durations, leaves, previous_states):
|
||||
duration_difference = previous_duration - leave.number_of_days
|
||||
if duration_difference > 0 and leave['holiday_allocation_id'] and leave.number_of_days == 0.0:
|
||||
message = _("Due to a change in global time offs, you have been granted %s day(s) back.", duration_difference)
|
||||
leave._notify_change(message)
|
||||
if leave.number_of_days > previous_duration\
|
||||
and leave.holiday_status_id not in sick_time_status:
|
||||
new_leaves = leave.split_leave(time_domain_dict)
|
||||
leaves |= new_leaves
|
||||
previous_states += [state] * len(new_leaves)
|
||||
|
||||
leaves_to_cancel = self.env['hr.leave']
|
||||
for state, leave in zip(previous_states, leaves):
|
||||
leave.write({'state': state})
|
||||
if leave.number_of_days == 0.0:
|
||||
leaves_to_cancel |= leave
|
||||
elif leave.state == 'validate':
|
||||
# recreate the resource leave that were removed by writing state to draft
|
||||
leave.sudo()._create_resource_leave()
|
||||
|
||||
leaves_to_cancel._force_cancel(_("a new public holiday completely overrides this leave."), 'mail.mt_comment')
|
||||
|
||||
def _convert_timezone(self, utc_naive_datetime, tz_from, tz_to):
|
||||
"""
|
||||
Convert a naive date to another timezone that initial timezone
|
||||
used to generate the date.
|
||||
:param utc_naive_datetime: utc date without tzinfo
|
||||
:type utc_naive_datetime: datetime
|
||||
:param tz_from: timezone used to obtained `utc_naive_datetime`
|
||||
:param tz_to: timezone in which we want the date
|
||||
:return: datetime converted into tz_to without tzinfo
|
||||
:rtype: datetime
|
||||
"""
|
||||
naive_datetime_from = utc_naive_datetime.astimezone(tz_from).replace(tzinfo=None)
|
||||
aware_datetime_to = tz_to.localize(naive_datetime_from)
|
||||
utc_naive_datetime_to = aware_datetime_to.astimezone(pytz.utc).replace(tzinfo=None)
|
||||
return utc_naive_datetime_to
|
||||
|
||||
def _ensure_datetime(self, datetime_representation, date_format=None):
|
||||
"""
|
||||
Be sure to get a datetime object if we have the necessary information.
|
||||
:param datetime_reprentation: object which should represent a datetime
|
||||
:rtype: datetime if a correct datetime_represtion, None otherwise
|
||||
"""
|
||||
if isinstance(datetime_representation, datetime):
|
||||
return datetime_representation
|
||||
elif isinstance(datetime_representation, str) and date_format:
|
||||
return datetime.strptime(datetime_representation, date_format)
|
||||
else:
|
||||
return None
|
||||
|
||||
def _prepare_public_holidays_values(self, vals_list):
|
||||
for vals in vals_list:
|
||||
# Manage the case of create a Public Time Off in another timezone
|
||||
# The datetime created has to be in UTC for the calendar's timezone
|
||||
if not vals.get('calendar_id') or vals.get('resource_id') or \
|
||||
not isinstance(vals.get('date_from'), (datetime, str)) or \
|
||||
not isinstance(vals.get('date_to'), (datetime, str)):
|
||||
continue
|
||||
user_tz = pytz.timezone(self.env.user.tz) if self.env.user.tz else pytz.utc
|
||||
calendar_tz = pytz.timezone(self.env['resource.calendar'].browse(vals['calendar_id']).tz)
|
||||
if user_tz != calendar_tz:
|
||||
datetime_from = self._ensure_datetime(vals['date_from'], '%Y-%m-%d %H:%M:%S')
|
||||
datetime_to = self._ensure_datetime(vals['date_to'], '%Y-%m-%d %H:%M:%S')
|
||||
if datetime_from and datetime_to:
|
||||
vals['date_from'] = self._convert_timezone(datetime_from, user_tz, calendar_tz)
|
||||
vals['date_to'] = self._convert_timezone(datetime_to, user_tz, calendar_tz)
|
||||
return vals_list
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
vals_list = self._prepare_public_holidays_values(vals_list)
|
||||
res = super().create(vals_list)
|
||||
time_domain_dict = res._get_time_domain_dict()
|
||||
self._reevaluate_leaves(time_domain_dict)
|
||||
return res
|
||||
|
||||
def write(self, vals):
|
||||
time_domain_dict = self._get_time_domain_dict()
|
||||
res = super().write(vals)
|
||||
time_domain_dict.extend(self._get_time_domain_dict())
|
||||
self._reevaluate_leaves(time_domain_dict)
|
||||
|
||||
return res
|
||||
|
||||
def unlink(self):
|
||||
time_domain_dict = self._get_time_domain_dict()
|
||||
res = super().unlink()
|
||||
self._reevaluate_leaves(time_domain_dict)
|
||||
|
||||
return res
|
||||
|
||||
class ResourceCalendar(models.Model):
|
||||
_inherit = "resource.calendar"
|
||||
|
||||
associated_leaves_count = fields.Integer("Leave Count", compute='_compute_associated_leaves_count')
|
||||
|
||||
def _compute_associated_leaves_count(self):
|
||||
leaves_read_group = self.env['resource.calendar.leaves'].read_group(
|
||||
[('resource_id', '=', False)],
|
||||
['calendar_id'],
|
||||
['calendar_id']
|
||||
)
|
||||
result = dict((data['calendar_id'][0] if data['calendar_id'] else 'global', data['calendar_id_count']) for data in leaves_read_group)
|
||||
global_leave_count = result.get('global', 0)
|
||||
for calendar in self:
|
||||
calendar.associated_leaves_count = result.get(calendar.id, 0) + global_leave_count
|
||||
Loading…
Add table
Add a link
Reference in a new issue