Initial commit: Hr packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:50 +02:00
commit 62531cd146
2820 changed files with 1432848 additions and 0 deletions

View file

@ -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

View file

@ -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

View file

@ -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)

File diff suppressed because it is too large Load diff

View file

@ -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."
))

View file

@ -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

View file

@ -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)

View file

@ -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.')
]

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View 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