summaryrefslogtreecommitdiff
path: root/addons/hr_holidays/models
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/hr_holidays/models
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/hr_holidays/models')
-rw-r--r--addons/hr_holidays/models/__init__.py13
-rw-r--r--addons/hr_holidays/models/hr_department.py56
-rw-r--r--addons/hr_holidays/models/hr_employee.py216
-rw-r--r--addons/hr_holidays/models/hr_leave.py1264
-rw-r--r--addons/hr_holidays/models/hr_leave_allocation.py703
-rw-r--r--addons/hr_holidays/models/hr_leave_type.py389
-rw-r--r--addons/hr_holidays/models/mail_channel.py32
-rw-r--r--addons/hr_holidays/models/mail_message_subtype.py49
-rw-r--r--addons/hr_holidays/models/res_partner.py22
-rw-r--r--addons/hr_holidays/models/res_users.py77
-rw-r--r--addons/hr_holidays/models/resource.py10
11 files changed, 2831 insertions, 0 deletions
diff --git a/addons/hr_holidays/models/__init__.py b/addons/hr_holidays/models/__init__.py
new file mode 100644
index 00000000..0697c64a
--- /dev/null
+++ b/addons/hr_holidays/models/__init__.py
@@ -0,0 +1,13 @@
+# -*- 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 mail_channel
+from . import mail_message_subtype
+from . import res_partner
+from . import res_users
diff --git a/addons/hr_holidays/models/hr_department.py b/addons/hr_holidays/models/hr_department.py
new file mode 100644
index 00000000..d076113e
--- /dev/null
+++ b/addons/hr_holidays/models/hr_department.py
@@ -0,0 +1,56 @@
+# -*- 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
+
+
+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')
+ total_employee = fields.Integer(
+ compute='_compute_total_employee', string='Total Employee')
+
+ 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 _compute_total_employee(self):
+ emp_data = self.env['hr.employee'].read_group([('department_id', 'in', self.ids)], ['department_id'], ['department_id'])
+ result = dict((data['department_id'][0], data['department_id_count']) for data in emp_data)
+ for department in self:
+ department.total_employee = result.get(department.id, 0)
diff --git a/addons/hr_holidays/models/hr_employee.py b/addons/hr_holidays/models/hr_employee.py
new file mode 100644
index 00000000..ceb0d2c5
--- /dev/null
+++ b/addons/hr_holidays/models/hr_employee.py
@@ -0,0 +1,216 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import datetime
+
+from odoo import api, fields, models
+from odoo.tools.float_utils import float_round
+
+
+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,
+ 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')
+ ])
+ current_leave_id = fields.Many2one('hr.leave.type', compute='_compute_leave_status', string="Current Time Off Type")
+ 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')
+ allocation_used_count = fields.Float('Total number of days off used', compute='_compute_total_allocation_used')
+ 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_used_display = fields.Char(compute='_compute_total_allocation_used')
+ hr_icon_display = fields.Selection(selection_add=[('presence_holiday_absent', 'On leave'),
+ ('presence_holiday_present', 'Present but on leave')])
+
+ def _get_date_start_work(self):
+ return self.create_date
+
+ 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.allocation_type='fixed' OR s.allocation_type='fixed_allocation') 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):
+ data = self.env['hr.leave.allocation'].read_group([
+ ('employee_id', 'in', self.ids),
+ ('holiday_status_id.active', '=', True),
+ ('state', '=', 'validate'),
+ ], ['number_of_days:sum', 'employee_id'], ['employee_id'])
+ rg_results = dict((d['employee_id'][0], d['number_of_days']) for d in data)
+ for employee in self:
+ employee.allocation_count = float_round(rg_results.get(employee.id, 0.0), precision_digits=2)
+ employee.allocation_display = "%g" % employee.allocation_count
+
+ def _compute_total_allocation_used(self):
+ for employee in self:
+ employee.allocation_used_count = float_round(employee.allocation_count - employee.remaining_leaves, precision_digits=2)
+ employee.allocation_used_display = "%g" % employee.allocation_used_count
+
+ 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_icon_display not in ['presence_present', 'presence_absent_active']
+ and employee.is_absent)
+ employees_absent.update({'hr_icon_display': 'presence_holiday_absent'})
+ employees_present = self.filtered(lambda employee:
+ employee.hr_icon_display in ['presence_present', 'presence_absent_active']
+ 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', 'not in', ('cancel', 'refuse'))
+ ])
+ 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
+ leave_data[holiday.employee_id.id]['current_leave_id'] = holiday.holiday_status_id.id
+
+ 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.current_leave_id = leave_data.get(employee.id, {}).get('current_leave_id')
+ employee.is_absent = leave_data.get(employee.id) and leave_data.get(employee.id, {}).get('current_leave_state') not in ['cancel', 'refuse', 'draft']
+
+ @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):
+ holidays = self.env['hr.leave'].sudo().search([
+ ('employee_id', '!=', False),
+ ('state', 'not in', ['cancel', 'refuse']),
+ ('date_from', '<=', datetime.datetime.utcnow()),
+ ('date_to', '>=', datetime.datetime.utcnow())
+ ])
+ return [('id', 'in', holidays.mapped('employee_id').ids)]
+
+ @api.model
+ def create(self, values):
+ if 'parent_id' in values:
+ manager = self.env['hr.employee'].browse(values['parent_id']).user_id
+ values['leave_manager_id'] = values.get('leave_manager_id', manager.id)
+ if values.get('leave_manager_id', False):
+ approver_group = self.env.ref('hr_holidays.group_hr_holidays_responsible', raise_if_not_found=False)
+ if approver_group:
+ approver_group.sudo().write({'users': [(4, values['leave_manager_id'])]})
+ return super(HrEmployeeBase, self).create(values)
+
+ 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']:
+ old_managers -= self.env['res.users'].browse(values['leave_manager_id'])
+ approver_group = self.env.ref('hr_holidays.group_hr_holidays_responsible', raise_if_not_found=False)
+ if approver_group:
+ approver_group.sudo().write({'users': [(4, values['leave_manager_id'])]})
+
+ res = super(HrEmployeeBase, self).write(values)
+ # remove users from the Responsible group if they are no longer leave managers
+ old_managers._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 HrEmployeePrivate(models.Model):
+ _inherit = 'hr.employee'
+
+class HrEmployeePublic(models.Model):
+ _inherit = 'hr.employee.public'
+
+ def _compute_leave_status(self):
+ super()._compute_leave_status()
+ self.current_leave_id = False
diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py
new file mode 100644
index 00000000..e208ae37
--- /dev/null
+++ b/addons/hr_holidays/models/hr_leave.py
@@ -0,0 +1,1264 @@
+# -*- 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 logging
+import math
+
+from collections import namedtuple
+
+from datetime import datetime, date, timedelta, time
+from dateutil.rrule import rrule, DAILY
+from pytz import timezone, UTC
+
+from odoo import api, fields, models, SUPERUSER_ID, tools
+from odoo.addons.base.models.res_partner import _tz_get
+from odoo.addons.resource.models.resource import float_to_time, HOURS_PER_DAY
+from odoo.exceptions import AccessError, UserError, ValidationError
+from odoo.tools import float_compare
+from odoo.tools.float_utils import float_round
+from odoo.tools.translate import _
+from odoo.osv import expression
+
+_logger = logging.getLogger(__name__)
+
+# Used to agglomerate the attendances in order to find the hour_from and hour_to
+# See _compute_date_from_to
+DummyAttendance = namedtuple('DummyAttendance', 'hour_from, hour_to, dayofweek, day_period, week_type')
+
+class HolidaysRequest(models.Model):
+ """ Leave Requests Access specifications
+
+ - a regular employee / user
+ - can see all leaves;
+ - cannot see name field of leaves belonging to other user as it may contain
+ private information that we don't want to share to other people than
+ HR people;
+ - can modify only its own not validated leaves (except writing on state to
+ bypass approval);
+ - can discuss on its leave requests;
+ - can reset only its own leaves;
+ - cannot validate any leaves;
+ - an Officer
+ - can see all leaves;
+ - can validate "HR" single validation leaves from people if
+ - he is the employee manager;
+ - he is the department manager;
+ - he is member of the same department;
+ - target employee has no manager and no department manager;
+ - can validate "Manager" single validation leaves from people if
+ - he is the employee manager;
+ - he is the department manager;
+ - target employee has no manager and no department manager;
+ - can first validate "Both" double validation leaves from people like "HR"
+ single validation, moving the leaves to validate1 state;
+ - cannot validate its own leaves;
+ - can reset only its own leaves;
+ - can refuse all leaves;
+ - a Manager
+ - can do everything he wants
+
+ On top of that multicompany rules apply based on company defined on the
+ leave request leave type.
+ """
+ _name = "hr.leave"
+ _description = "Time Off"
+ _order = "date_from desc"
+ _inherit = ['mail.thread', 'mail.activity.mixin']
+ _mail_post_access = 'read'
+
+ @api.model
+ def default_get(self, fields_list):
+ defaults = super(HolidaysRequest, self).default_get(fields_list)
+ defaults = self._default_get_request_parameters(defaults)
+
+ if 'holiday_status_id' in fields_list and not defaults.get('holiday_status_id'):
+ lt = self.env['hr.leave.type'].search([('valid', '=', True)], limit=1)
+
+ if lt:
+ defaults['holiday_status_id'] = lt.id
+
+ if 'state' in fields_list and not defaults.get('state'):
+ lt = self.env['hr.leave.type'].browse(defaults.get('holiday_status_id'))
+ defaults['state'] = 'confirm' if lt and lt.leave_validation_type != 'no_validation' else 'draft'
+
+ now = fields.Datetime.now()
+ if 'date_from' not in defaults:
+ defaults.update({'date_from': now})
+ if 'date_to' not in defaults:
+ defaults.update({'date_to': now})
+ return defaults
+
+ def _default_get_request_parameters(self, values):
+ new_values = dict(values)
+ global_from, global_to = False, False
+ # TDE FIXME: consider a mapping on several days that is not the standard
+ # calendar widget 7-19 in user's TZ is some custom input
+ if values.get('date_from'):
+ user_tz = self.env.user.tz or 'UTC'
+ localized_dt = timezone('UTC').localize(values['date_from']).astimezone(timezone(user_tz))
+ global_from = localized_dt.time().hour == 7 and localized_dt.time().minute == 0
+ new_values['request_date_from'] = localized_dt.date()
+ if values.get('date_to'):
+ user_tz = self.env.user.tz or 'UTC'
+ localized_dt = timezone('UTC').localize(values['date_to']).astimezone(timezone(user_tz))
+ global_to = localized_dt.time().hour == 19 and localized_dt.time().minute == 0
+ new_values['request_date_to'] = localized_dt.date()
+ if global_from and global_to:
+ new_values['request_unit_custom'] = True
+ return new_values
+
+ # description
+ name = fields.Char('Description', compute='_compute_description', inverse='_inverse_description', search='_search_description', compute_sudo=False)
+ private_name = fields.Char('Time Off Description', groups='hr_holidays.group_hr_holidays_user')
+ state = fields.Selection([
+ ('draft', 'To Submit'),
+ ('cancel', 'Cancelled'), # YTI This state seems to be unused. To remove
+ ('confirm', 'To Approve'),
+ ('refuse', 'Refused'),
+ ('validate1', 'Second Approval'),
+ ('validate', 'Approved')
+ ], string='Status', compute='_compute_state', store=True, tracking=True, copy=False, readonly=False,
+ help="The status is set to 'To Submit', when a time off request is created." +
+ "\nThe status is 'To Approve', when time off request is confirmed by user." +
+ "\nThe status is 'Refused', when time off request is refused by manager." +
+ "\nThe status is 'Approved', when time off request is approved by manager.")
+ payslip_status = fields.Boolean('Reported in last payslips', help='Green this button when the time off has been taken into account in the payslip.', copy=False)
+ report_note = fields.Text('HR Comments', copy=False, groups="hr_holidays.group_hr_holidays_manager")
+ user_id = fields.Many2one('res.users', string='User', related='employee_id.user_id', related_sudo=True, compute_sudo=True, store=True, default=lambda self: self.env.uid, readonly=True)
+ manager_id = fields.Many2one('hr.employee', compute='_compute_from_employee_id', store=True, readonly=False)
+ # leave type configuration
+ holiday_status_id = fields.Many2one(
+ "hr.leave.type", compute='_compute_from_employee_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=[('valid', '=', True)])
+ validation_type = fields.Selection(string='Validation Type', related='holiday_status_id.leave_validation_type', readonly=False)
+ # HR data
+
+ employee_id = fields.Many2one(
+ 'hr.employee', compute='_compute_from_holiday_type', store=True, string='Employee', index=True, readonly=False, ondelete="restrict",
+ states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate1': [('readonly', True)], 'validate': [('readonly', True)]},
+ tracking=True)
+ tz_mismatch = fields.Boolean(compute='_compute_tz_mismatch')
+ tz = fields.Selection(_tz_get, compute='_compute_tz')
+ department_id = fields.Many2one(
+ 'hr.department', compute='_compute_department_id', store=True, string='Department', readonly=False,
+ states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate1': [('readonly', True)], 'validate': [('readonly', True)]})
+ notes = fields.Text('Reasons', readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]})
+ # duration
+ date_from = fields.Datetime(
+ 'Start Date', compute='_compute_date_from_to', store=True, readonly=False, index=True, copy=False, required=True, tracking=True,
+ states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate1': [('readonly', True)], 'validate': [('readonly', True)]})
+ date_to = fields.Datetime(
+ 'End Date', compute='_compute_date_from_to', store=True, readonly=False, copy=False, required=True, tracking=True,
+ states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate1': [('readonly', True)], 'validate': [('readonly', True)]})
+ number_of_days = fields.Float(
+ 'Duration (Days)', compute='_compute_number_of_days', store=True, readonly=False, copy=False, tracking=True,
+ help='Number of days of the time off request. Used in the calculation. To manually correct the duration, use this field.')
+ number_of_days_display = fields.Float(
+ 'Duration in days', compute='_compute_number_of_days_display', readonly=True,
+ help='Number of days of the time off request according to your working schedule. Used for interface.')
+ number_of_hours_display = fields.Float(
+ 'Duration in hours', compute='_compute_number_of_hours_display', readonly=True,
+ help='Number of hours of the time off request according to your working schedule. Used for interface.')
+ number_of_hours_text = fields.Char(compute='_compute_number_of_hours_text')
+ duration_display = fields.Char('Requested (Days/Hours)', compute='_compute_duration_display', store=True,
+ help="Field allowing to see the leave request duration in days or hours depending on the leave_type_request_unit") # details
+ # details
+ meeting_id = fields.Many2one('calendar.event', string='Meeting', copy=False)
+ parent_id = fields.Many2one('hr.leave', string='Parent', copy=False)
+ linked_request_ids = fields.One2many('hr.leave', 'parent_id', string='Linked Requests')
+ 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='By Employee: Allocation/Request for individual Employee, By Employee Tag: Allocation/Request for group of employees in category')
+ category_id = fields.Many2one(
+ 'hr.employee.category', compute='_compute_from_holiday_type', store=True, string='Employee Tag',
+ states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, help='Category of Employee')
+ mode_company_id = fields.Many2one(
+ 'res.company', compute='_compute_from_holiday_type', store=True, string='Company Mode',
+ states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]})
+ first_approver_id = fields.Many2one(
+ 'hr.employee', string='First Approval', readonly=True, copy=False,
+ help='This area is automatically filled by the user who validate the time off')
+ second_approver_id = fields.Many2one(
+ 'hr.employee', string='Second Approval', readonly=True, copy=False,
+ help='This area is automatically filled by the user who validate the time off with second level (If time off type need second validation)')
+ can_reset = fields.Boolean('Can reset', compute='_compute_can_reset')
+ can_approve = fields.Boolean('Can Approve', compute='_compute_can_approve')
+
+ # UX fields
+ leave_type_request_unit = fields.Selection(related='holiday_status_id.request_unit', readonly=True)
+ # Interface fields used when not using hour-based computation
+ request_date_from = fields.Date('Request Start Date')
+ request_date_to = fields.Date('Request End Date')
+ # Interface fields used when using hour-based computation
+ request_hour_from = fields.Selection([
+ ('0', '12:00 AM'), ('0.5', '12:30 AM'),
+ ('1', '1:00 AM'), ('1.5', '1:30 AM'),
+ ('2', '2:00 AM'), ('2.5', '2:30 AM'),
+ ('3', '3:00 AM'), ('3.5', '3:30 AM'),
+ ('4', '4:00 AM'), ('4.5', '4:30 AM'),
+ ('5', '5:00 AM'), ('5.5', '5:30 AM'),
+ ('6', '6:00 AM'), ('6.5', '6:30 AM'),
+ ('7', '7:00 AM'), ('7.5', '7:30 AM'),
+ ('8', '8:00 AM'), ('8.5', '8:30 AM'),
+ ('9', '9:00 AM'), ('9.5', '9:30 AM'),
+ ('10', '10:00 AM'), ('10.5', '10:30 AM'),
+ ('11', '11:00 AM'), ('11.5', '11:30 AM'),
+ ('12', '12:00 PM'), ('12.5', '12:30 PM'),
+ ('13', '1:00 PM'), ('13.5', '1:30 PM'),
+ ('14', '2:00 PM'), ('14.5', '2:30 PM'),
+ ('15', '3:00 PM'), ('15.5', '3:30 PM'),
+ ('16', '4:00 PM'), ('16.5', '4:30 PM'),
+ ('17', '5:00 PM'), ('17.5', '5:30 PM'),
+ ('18', '6:00 PM'), ('18.5', '6:30 PM'),
+ ('19', '7:00 PM'), ('19.5', '7:30 PM'),
+ ('20', '8:00 PM'), ('20.5', '8:30 PM'),
+ ('21', '9:00 PM'), ('21.5', '9:30 PM'),
+ ('22', '10:00 PM'), ('22.5', '10:30 PM'),
+ ('23', '11:00 PM'), ('23.5', '11:30 PM')], string='Hour from')
+ request_hour_to = fields.Selection([
+ ('0', '12:00 AM'), ('0.5', '12:30 AM'),
+ ('1', '1:00 AM'), ('1.5', '1:30 AM'),
+ ('2', '2:00 AM'), ('2.5', '2:30 AM'),
+ ('3', '3:00 AM'), ('3.5', '3:30 AM'),
+ ('4', '4:00 AM'), ('4.5', '4:30 AM'),
+ ('5', '5:00 AM'), ('5.5', '5:30 AM'),
+ ('6', '6:00 AM'), ('6.5', '6:30 AM'),
+ ('7', '7:00 AM'), ('7.5', '7:30 AM'),
+ ('8', '8:00 AM'), ('8.5', '8:30 AM'),
+ ('9', '9:00 AM'), ('9.5', '9:30 AM'),
+ ('10', '10:00 AM'), ('10.5', '10:30 AM'),
+ ('11', '11:00 AM'), ('11.5', '11:30 AM'),
+ ('12', '12:00 PM'), ('12.5', '12:30 PM'),
+ ('13', '1:00 PM'), ('13.5', '1:30 PM'),
+ ('14', '2:00 PM'), ('14.5', '2:30 PM'),
+ ('15', '3:00 PM'), ('15.5', '3:30 PM'),
+ ('16', '4:00 PM'), ('16.5', '4:30 PM'),
+ ('17', '5:00 PM'), ('17.5', '5:30 PM'),
+ ('18', '6:00 PM'), ('18.5', '6:30 PM'),
+ ('19', '7:00 PM'), ('19.5', '7:30 PM'),
+ ('20', '8:00 PM'), ('20.5', '8:30 PM'),
+ ('21', '9:00 PM'), ('21.5', '9:30 PM'),
+ ('22', '10:00 PM'), ('22.5', '10:30 PM'),
+ ('23', '11:00 PM'), ('23.5', '11:30 PM')], string='Hour to')
+ # used only when the leave is taken in half days
+ request_date_from_period = fields.Selection([
+ ('am', 'Morning'), ('pm', 'Afternoon')],
+ string="Date Period Start", default='am')
+ # request type
+ request_unit_half = fields.Boolean('Half Day', compute='_compute_request_unit_half', store=True, readonly=False)
+ request_unit_hours = fields.Boolean('Custom Hours', compute='_compute_request_unit_hours', store=True, readonly=False)
+ request_unit_custom = fields.Boolean('Days-long custom hours', compute='_compute_request_unit_custom', store=True, readonly=False)
+
+ _sql_constraints = [
+ ('type_value',
+ "CHECK((holiday_type='employee' AND employee_id IS NOT NULL) or "
+ "(holiday_type='company' AND mode_company_id IS NOT NULL) or "
+ "(holiday_type='category' AND category_id IS NOT NULL) or "
+ "(holiday_type='department' AND department_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."),
+ ('date_check2', "CHECK ((date_from <= date_to))", "The start date must be anterior to the end date."),
+ ('duration_check', "CHECK ( number_of_days >= 0 )", "If you want to change the number of days you should use the 'period' mode"),
+ ]
+
+ def _auto_init(self):
+ res = super(HolidaysRequest, self)._auto_init()
+ tools.create_index(self._cr, 'hr_leave_date_to_date_from_index',
+ self._table, ['date_to', 'date_from'])
+ return res
+
+ @api.depends_context('uid')
+ def _compute_description(self):
+ self.check_access_rights('read')
+ self.check_access_rule('read')
+
+ is_officer = self.user_has_groups('hr_holidays.group_hr_holidays_user')
+
+ for leave in self:
+ if is_officer or leave.user_id == self.env.user or leave.employee_id.leave_manager_id == self.env.user:
+ leave.name = leave.sudo().private_name
+ else:
+ leave.name = '*****'
+
+ def _inverse_description(self):
+ is_officer = self.user_has_groups('hr_holidays.group_hr_holidays_user')
+
+ for leave in self:
+ if is_officer or leave.user_id == self.env.user or leave.employee_id.leave_manager_id == self.env.user:
+ leave.sudo().private_name = leave.name
+
+ def _search_description(self, operator, value):
+ is_officer = self.user_has_groups('hr_holidays.group_hr_holidays_user')
+ domain = [('private_name', operator, value)]
+
+ if not is_officer:
+ domain = expression.AND([domain, [('user_id', '=', self.env.user.id)]])
+
+ leaves = self.search(domain)
+ return [('id', 'in', leaves.ids)]
+
+ @api.depends('holiday_status_id')
+ def _compute_state(self):
+ for holiday in self:
+ if self.env.context.get('unlink') and holiday.state == 'draft':
+ # Otherwise the record in draft with validation_type in (hr, manager, both) will be set to confirm
+ # and a simple internal user will not be able to delete his own draft record
+ holiday.state = 'draft'
+ else:
+ holiday.state = 'confirm' if holiday.validation_type != 'no_validation' else 'draft'
+
+ @api.depends('request_date_from_period', 'request_hour_from', 'request_hour_to', 'request_date_from', 'request_date_to',
+ 'request_unit_half', 'request_unit_hours', 'request_unit_custom', 'employee_id')
+ def _compute_date_from_to(self):
+ for holiday in self:
+ if holiday.request_date_from and holiday.request_date_to and holiday.request_date_from > holiday.request_date_to:
+ holiday.request_date_to = holiday.request_date_from
+ if not holiday.request_date_from:
+ holiday.date_from = False
+ elif not holiday.request_unit_half and not holiday.request_unit_hours and not holiday.request_date_to:
+ holiday.date_to = False
+ else:
+ if holiday.request_unit_half or holiday.request_unit_hours:
+ holiday.request_date_to = holiday.request_date_from
+ resource_calendar_id = holiday.employee_id.resource_calendar_id or self.env.company.resource_calendar_id
+ domain = [('calendar_id', '=', resource_calendar_id.id), ('display_type', '=', False)]
+ attendances = self.env['resource.calendar.attendance'].read_group(domain, ['ids:array_agg(id)', 'hour_from:min(hour_from)', 'hour_to:max(hour_to)', 'week_type', 'dayofweek', 'day_period'], ['week_type', 'dayofweek', 'day_period'], lazy=False)
+
+ # Must be sorted by dayofweek ASC and day_period DESC
+ attendances = sorted([DummyAttendance(group['hour_from'], group['hour_to'], group['dayofweek'], group['day_period'], group['week_type']) for group in attendances], key=lambda att: (att.dayofweek, att.day_period != 'morning'))
+
+ default_value = DummyAttendance(0, 0, 0, 'morning', False)
+
+ if resource_calendar_id.two_weeks_calendar:
+ # find week type of start_date
+ start_week_type = int(math.floor((holiday.request_date_from.toordinal() - 1) / 7) % 2)
+ attendance_actual_week = [att for att in attendances if att.week_type is False or int(att.week_type) == start_week_type]
+ attendance_actual_next_week = [att for att in attendances if att.week_type is False or int(att.week_type) != start_week_type]
+ # First, add days of actual week coming after date_from
+ attendance_filtred = [att for att in attendance_actual_week if int(att.dayofweek) >= holiday.request_date_from.weekday()]
+ # Second, add days of the other type of week
+ attendance_filtred += list(attendance_actual_next_week)
+ # Third, add days of actual week (to consider days that we have remove first because they coming before date_from)
+ attendance_filtred += list(attendance_actual_week)
+
+ end_week_type = int(math.floor((holiday.request_date_to.toordinal() - 1) / 7) % 2)
+ attendance_actual_week = [att for att in attendances if att.week_type is False or int(att.week_type) == end_week_type]
+ attendance_actual_next_week = [att for att in attendances if att.week_type is False or int(att.week_type) != end_week_type]
+ attendance_filtred_reversed = list(reversed([att for att in attendance_actual_week if int(att.dayofweek) <= holiday.request_date_to.weekday()]))
+ attendance_filtred_reversed += list(reversed(attendance_actual_next_week))
+ attendance_filtred_reversed += list(reversed(attendance_actual_week))
+
+ # find first attendance coming after first_day
+ attendance_from = attendance_filtred[0]
+ # find last attendance coming before last_day
+ attendance_to = attendance_filtred_reversed[0]
+ else:
+ # find first attendance coming after first_day
+ attendance_from = next((att for att in attendances if int(att.dayofweek) >= holiday.request_date_from.weekday()), attendances[0] if attendances else default_value)
+ # find last attendance coming before last_day
+ attendance_to = next((att for att in reversed(attendances) if int(att.dayofweek) <= holiday.request_date_to.weekday()), attendances[-1] if attendances else default_value)
+
+ compensated_request_date_from = holiday.request_date_from
+ compensated_request_date_to = holiday.request_date_to
+
+ if holiday.request_unit_half:
+ if holiday.request_date_from_period == 'am':
+ hour_from = float_to_time(attendance_from.hour_from)
+ hour_to = float_to_time(attendance_from.hour_to)
+ else:
+ hour_from = float_to_time(attendance_to.hour_from)
+ hour_to = float_to_time(attendance_to.hour_to)
+ elif holiday.request_unit_hours:
+ hour_from = float_to_time(float(holiday.request_hour_from))
+ hour_to = float_to_time(float(holiday.request_hour_to))
+ elif holiday.request_unit_custom:
+ hour_from = holiday.date_from.time()
+ hour_to = holiday.date_to.time()
+ compensated_request_date_from = holiday._adjust_date_based_on_tz(holiday.request_date_from, hour_from)
+ compensated_request_date_to = holiday._adjust_date_based_on_tz(holiday.request_date_to, hour_to)
+ else:
+ hour_from = float_to_time(attendance_from.hour_from)
+ hour_to = float_to_time(attendance_to.hour_to)
+
+ holiday.date_from = timezone(holiday.tz).localize(datetime.combine(compensated_request_date_from, hour_from)).astimezone(UTC).replace(tzinfo=None)
+ holiday.date_to = timezone(holiday.tz).localize(datetime.combine(compensated_request_date_to, hour_to)).astimezone(UTC).replace(tzinfo=None)
+
+ @api.depends('holiday_status_id', 'request_unit_hours', 'request_unit_custom')
+ def _compute_request_unit_half(self):
+ for holiday in self:
+ if holiday.holiday_status_id or holiday.request_unit_hours or holiday.request_unit_custom:
+ holiday.request_unit_half = False
+
+ @api.depends('holiday_status_id', 'request_unit_half', 'request_unit_custom')
+ def _compute_request_unit_hours(self):
+ for holiday in self:
+ if holiday.holiday_status_id or holiday.request_unit_half or holiday.request_unit_custom:
+ holiday.request_unit_hours = False
+
+ @api.depends('holiday_status_id', 'request_unit_half', 'request_unit_hours')
+ def _compute_request_unit_custom(self):
+ for holiday in self:
+ if holiday.holiday_status_id or holiday.request_unit_half or holiday.request_unit_hours:
+ holiday.request_unit_custom = False
+
+ @api.depends('holiday_type')
+ def _compute_from_holiday_type(self):
+ for holiday in self:
+ if holiday.holiday_type == 'employee':
+ if not holiday.employee_id:
+ holiday.employee_id = self.env.user.employee_id
+ holiday.mode_company_id = False
+ holiday.category_id = False
+ elif holiday.holiday_type == 'company':
+ holiday.employee_id = False
+ if not holiday.mode_company_id:
+ holiday.mode_company_id = self.env.company.id
+ holiday.category_id = False
+ elif holiday.holiday_type == 'department':
+ holiday.employee_id = False
+ holiday.mode_company_id = False
+ holiday.category_id = False
+ elif holiday.holiday_type == 'category':
+ holiday.employee_id = False
+ holiday.mode_company_id = False
+ else:
+ holiday.employee_id = self.env.context.get('default_employee_id') or self.env.user.employee_id
+
+ @api.depends('employee_id')
+ def _compute_from_employee_id(self):
+ for holiday in self:
+ holiday.manager_id = holiday.employee_id.parent_id.id
+ if holiday.employee_id.user_id != self.env.user and self._origin.employee_id != holiday.employee_id:
+ holiday.holiday_status_id = False
+
+ @api.depends('employee_id', 'holiday_type')
+ def _compute_department_id(self):
+ for holiday in self:
+ if holiday.employee_id:
+ holiday.department_id = holiday.employee_id.department_id
+ elif holiday.holiday_type == 'department':
+ if not holiday.department_id:
+ holiday.department_id = self.env.user.employee_id.department_id
+ else:
+ holiday.department_id = False
+
+ @api.depends('date_from', 'date_to', 'employee_id')
+ def _compute_number_of_days(self):
+ for holiday in self:
+ if holiday.date_from and holiday.date_to:
+ holiday.number_of_days = holiday._get_number_of_days(holiday.date_from, holiday.date_to, holiday.employee_id.id)['days']
+ else:
+ holiday.number_of_days = 0
+
+ @api.depends('tz')
+ @api.depends_context('uid')
+ def _compute_tz_mismatch(self):
+ for leave in self:
+ leave.tz_mismatch = leave.tz != self.env.user.tz
+
+ @api.depends('request_unit_custom', 'employee_id', 'holiday_type', 'department_id.company_id.resource_calendar_id.tz', 'mode_company_id.resource_calendar_id.tz')
+ def _compute_tz(self):
+ for leave in self:
+ tz = False
+ if leave.request_unit_custom:
+ tz = 'UTC' # custom -> already in UTC
+ elif leave.holiday_type == 'employee':
+ tz = leave.employee_id.tz
+ elif leave.holiday_type == 'department':
+ tz = leave.department_id.company_id.resource_calendar_id.tz
+ elif leave.holiday_type == 'company':
+ tz = leave.mode_company_id.resource_calendar_id.tz
+ leave.tz = tz or self.env.company.resource_calendar_id.tz or self.env.user.tz or 'UTC'
+
+ @api.depends('number_of_days')
+ def _compute_number_of_days_display(self):
+ for holiday in self:
+ holiday.number_of_days_display = holiday.number_of_days
+
+ def _get_calendar(self):
+ self.ensure_one()
+ return self.employee_id.resource_calendar_id or self.env.company.resource_calendar_id
+
+ @api.depends('number_of_days')
+ def _compute_number_of_hours_display(self):
+ for holiday in self:
+ calendar = holiday._get_calendar()
+ if holiday.date_from and holiday.date_to:
+ # Take attendances into account, in case the leave validated
+ # Otherwise, this will result into number_of_hours = 0
+ # and number_of_hours_display = 0 or (#day * calendar.hours_per_day),
+ # which could be wrong if the employee doesn't work the same number
+ # hours each day
+ if holiday.state == 'validate':
+ start_dt = holiday.date_from
+ end_dt = holiday.date_to
+ if not start_dt.tzinfo:
+ start_dt = start_dt.replace(tzinfo=UTC)
+ if not end_dt.tzinfo:
+ end_dt = end_dt.replace(tzinfo=UTC)
+ resource = holiday.employee_id.resource_id
+ intervals = calendar._attendance_intervals_batch(start_dt, end_dt, resource)[resource.id] \
+ - calendar._leave_intervals_batch(start_dt, end_dt, None)[False] # Substract Global Leaves
+ number_of_hours = sum((stop - start).total_seconds() / 3600 for start, stop, dummy in intervals)
+ else:
+ number_of_hours = holiday._get_number_of_days(holiday.date_from, holiday.date_to, holiday.employee_id.id)['hours']
+ holiday.number_of_hours_display = number_of_hours or (holiday.number_of_days * (calendar.hours_per_day or HOURS_PER_DAY))
+ else:
+ holiday.number_of_hours_display = 0
+
+ @api.depends('number_of_hours_display', 'number_of_days_display')
+ def _compute_duration_display(self):
+ for leave in self:
+ leave.duration_display = '%g %s' % (
+ (float_round(leave.number_of_hours_display, precision_digits=2)
+ if leave.leave_type_request_unit == 'hour'
+ else float_round(leave.number_of_days_display, precision_digits=2)),
+ _('hours') if leave.leave_type_request_unit == 'hour' else _('days'))
+
+ @api.depends('number_of_hours_display')
+ def _compute_number_of_hours_text(self):
+ # YTI Note: All this because a readonly field takes all the width on edit mode...
+ for leave in self:
+ leave.number_of_hours_text = '%s%g %s%s' % (
+ '' if leave.request_unit_half or leave.request_unit_hours else '(',
+ float_round(leave.number_of_hours_display, precision_digits=2),
+ _('Hours'),
+ '' if leave.request_unit_half or leave.request_unit_hours else ')')
+
+ @api.depends('state', 'employee_id', 'department_id')
+ def _compute_can_reset(self):
+ for holiday in self:
+ try:
+ holiday._check_approval_update('draft')
+ except (AccessError, UserError):
+ holiday.can_reset = False
+ else:
+ holiday.can_reset = True
+
+ @api.depends('state', 'employee_id', 'department_id')
+ def _compute_can_approve(self):
+ for holiday in self:
+ try:
+ if holiday.state == 'confirm' and holiday.validation_type == 'both':
+ holiday._check_approval_update('validate1')
+ else:
+ holiday._check_approval_update('validate')
+ except (AccessError, UserError):
+ holiday.can_approve = False
+ else:
+ holiday.can_approve = True
+
+ @api.constrains('date_from', 'date_to', 'employee_id')
+ def _check_date(self):
+ if self.env.context.get('leave_skip_date_check', False):
+ return
+ for holiday in self.filtered('employee_id'):
+ domain = [
+ ('date_from', '<', holiday.date_to),
+ ('date_to', '>', holiday.date_from),
+ ('employee_id', '=', holiday.employee_id.id),
+ ('id', '!=', holiday.id),
+ ('state', 'not in', ['cancel', 'refuse']),
+ ]
+ nholidays = self.search_count(domain)
+ if nholidays:
+ raise ValidationError(_('You can not set 2 time off that overlaps on the same day for the same employee.'))
+
+ @api.constrains('state', 'number_of_days', 'holiday_status_id')
+ def _check_holidays(self):
+ mapped_days = self.mapped('holiday_status_id').get_employees_days(self.mapped('employee_id').ids)
+ for holiday in self:
+ if holiday.holiday_type != 'employee' or not holiday.employee_id or holiday.holiday_status_id.allocation_type == 'no':
+ continue
+ leave_days = mapped_days[holiday.employee_id.id][holiday.holiday_status_id.id]
+ if float_compare(leave_days['remaining_leaves'], 0, precision_digits=2) == -1 or float_compare(leave_days['virtual_remaining_leaves'], 0, precision_digits=2) == -1:
+ raise ValidationError(_('The number of remaining time off is not sufficient for this time off type.\n'
+ 'Please also check the time off waiting for validation.'))
+
+ @api.constrains('date_from', 'date_to', 'employee_id')
+ def _check_date_state(self):
+ if self.env.context.get('leave_skip_state_check'):
+ return
+ for holiday in self:
+ if holiday.state in ['cancel', 'refuse', 'validate1', 'validate']:
+ raise ValidationError(_("This modification is not allowed in the current state."))
+
+ def _get_number_of_days(self, date_from, date_to, employee_id):
+ """ Returns a float equals to the timedelta between two dates given as string."""
+ if employee_id:
+ employee = self.env['hr.employee'].browse(employee_id)
+ result = employee._get_work_days_data_batch(date_from, date_to)[employee.id]
+ if self.request_unit_half and result['hours'] > 0:
+ result['days'] = 0.5
+ return result
+
+ today_hours = self.env.company.resource_calendar_id.get_work_hours_count(
+ datetime.combine(date_from.date(), time.min),
+ datetime.combine(date_from.date(), time.max),
+ False)
+
+ hours = self.env.company.resource_calendar_id.get_work_hours_count(date_from, date_to)
+ days = hours / (today_hours or HOURS_PER_DAY) if not self.request_unit_half else 0.5
+ return {'days': days, 'hours': hours}
+
+ def _adjust_date_based_on_tz(self, leave_date, hour):
+ """ request_date_{from,to} are local to the user's tz but hour_{from,to} are in UTC.
+
+ In some cases they are combined (assuming they are in the same tz) as a datetime. When
+ that happens it's possible we need to adjust one of the dates. This function adjust the
+ date, so that it can be passed to datetime().
+
+ E.g. a leave in US/Pacific for one day:
+ - request_date_from: 1st of Jan
+ - request_date_to: 1st of Jan
+ - hour_from: 15:00 (7:00 local)
+ - hour_to: 03:00 (19:00 local) <-- this happens on the 2nd of Jan in UTC
+ """
+ user_tz = timezone(self.env.user.tz if self.env.user.tz else 'UTC')
+ request_date_to_utc = UTC.localize(datetime.combine(leave_date, hour)).astimezone(user_tz).replace(tzinfo=None)
+ if request_date_to_utc.date() < leave_date:
+ return leave_date + timedelta(days=1)
+ elif request_date_to_utc.date() > leave_date:
+ return leave_date - timedelta(days=1)
+ else:
+ return leave_date
+
+ ####################################################
+ # ORM Overrides methods
+ ####################################################
+
+ def name_get(self):
+ res = []
+ for leave in self:
+ if self.env.context.get('short_name'):
+ if leave.leave_type_request_unit == 'hour':
+ res.append((leave.id, _("%s : %.2f hours") % (leave.name or leave.holiday_status_id.name, leave.number_of_hours_display)))
+ else:
+ res.append((leave.id, _("%s : %.2f days") % (leave.name or leave.holiday_status_id.name, leave.number_of_days)))
+ else:
+ if leave.holiday_type == 'company':
+ target = leave.mode_company_id.name
+ elif leave.holiday_type == 'department':
+ target = leave.department_id.name
+ elif leave.holiday_type == 'category':
+ target = leave.category_id.name
+ else:
+ target = leave.employee_id.name
+ if leave.leave_type_request_unit == 'hour':
+ if self.env.context.get('hide_employee_name') and 'employee_id' in self.env.context.get('group_by', []):
+ res.append((
+ leave.id,
+ _("%(person)s on %(leave_type)s: %(duration).2f hours on %(date)s",
+ person=target,
+ leave_type=leave.holiday_status_id.name,
+ duration=leave.number_of_hours_display,
+ date=fields.Date.to_string(leave.date_from),
+ )
+ ))
+ else:
+ res.append((
+ leave.id,
+ _("%(person)s on %(leave_type)s: %(duration).2f hours on %(date)s",
+ person=target,
+ leave_type=leave.holiday_status_id.name,
+ duration=leave.number_of_hours_display,
+ date=fields.Date.to_string(leave.date_from),
+ )
+ ))
+ else:
+ display_date = fields.Date.to_string(leave.date_from)
+ if leave.number_of_days > 1:
+ display_date += ' ⇨ %s' % fields.Date.to_string(leave.date_to)
+ if self.env.context.get('hide_employee_name') and 'employee_id' in self.env.context.get('group_by', []):
+ res.append((
+ leave.id,
+ _("%(leave_type)s: %(duration).2f days (%(start)s)",
+ leave_type=leave.holiday_status_id.name,
+ duration=leave.number_of_days,
+ start=display_date,
+ )
+ ))
+ else:
+ res.append((
+ leave.id,
+ _("%(person)s on %(leave_type)s: %(duration).2f days (%(start)s)",
+ person=target,
+ leave_type=leave.holiday_status_id.name,
+ duration=leave.number_of_days,
+ start=display_date,
+ )
+ ))
+ 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.constrains('holiday_status_id', 'date_to', 'date_from')
+ def _check_leave_type_validity(self):
+ for leave in self:
+ vstart = leave.holiday_status_id.validity_start
+ vstop = leave.holiday_status_id.validity_stop
+ dfrom = leave.date_from
+ dto = leave.date_to
+ if leave.holiday_status_id.validity_start and leave.holiday_status_id.validity_stop:
+ if dfrom and dto and (dfrom.date() < vstart or dto.date() > vstop):
+ raise ValidationError(_(
+ '%(leave_type)s are only valid between %(start)s and %(end)s',
+ leave_type=leave.holiday_status_id.display_name,
+ start=leave.holiday_status_id.validity_start,
+ end=leave.holiday_status_id.validity_stop
+ ))
+ elif leave.holiday_status_id.validity_start:
+ if dfrom and (dfrom.date() < vstart):
+ raise ValidationError(_(
+ '%(leave_type)s are only valid starting from %(date)s',
+ leave_type=leave.holiday_status_id.display_name,
+ date=leave.holiday_status_id.validity_start
+ ))
+ elif leave.holiday_status_id.validity_stop:
+ if dto and (dto.date() > vstop):
+ raise ValidationError(_(
+ '%(leave_type)s are only valid until %(date)s',
+ leave_type=leave.holiday_status_id.display_name,
+ date=leave.holiday_status_id.validity_stop
+ ))
+
+ def _check_double_validation_rules(self, employees, state):
+ if self.user_has_groups('hr_holidays.group_hr_holidays_manager'):
+ return
+
+ is_leave_user = self.user_has_groups('hr_holidays.group_hr_holidays_user')
+ if state == 'validate1':
+ employees = employees.filtered(lambda employee: employee.leave_manager_id != self.env.user)
+ if employees and not is_leave_user:
+ raise AccessError(_('You cannot first approve a time off for %s, because you are not his time off manager', employees[0].name))
+ elif state == 'validate' and not is_leave_user:
+ # Is probably handled via ir.rule
+ raise AccessError(_('You don\'t have the rights to apply second approval on a time off request'))
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ """ Override to avoid automatic logging of creation """
+ if not self._context.get('leave_fast_create'):
+ leave_types = self.env['hr.leave.type'].browse([values.get('holiday_status_id') for values in vals_list if values.get('holiday_status_id')])
+ mapped_validation_type = {leave_type.id: leave_type.leave_validation_type for leave_type in leave_types}
+
+ for values in vals_list:
+ employee_id = values.get('employee_id', False)
+ leave_type_id = values.get('holiday_status_id')
+ # Handle automatic department_id
+ if not values.get('department_id'):
+ values.update({'department_id': self.env['hr.employee'].browse(employee_id).department_id.id})
+
+ # Handle no_validation
+ if mapped_validation_type[leave_type_id] == 'no_validation':
+ values.update({'state': 'confirm'})
+
+ if 'state' not in values:
+ # To mimic the behavior of compute_state that was always triggered, as the field was readonly
+ values['state'] = 'confirm' if mapped_validation_type[leave_type_id] != 'no_validation' else 'draft'
+
+ # Handle double validation
+ if mapped_validation_type[leave_type_id] == 'both':
+ self._check_double_validation_rules(employee_id, values.get('state', False))
+
+ holidays = super(HolidaysRequest, self.with_context(mail_create_nosubscribe=True)).create(vals_list)
+
+ for holiday in holidays:
+ if not self._context.get('leave_fast_create'):
+ # Everything that is done here must be done using sudo because we might
+ # have different create and write rights
+ # eg : holidays_user can create a leave request with validation_type = 'manager' for someone else
+ # but they can only write on it if they are leave_manager_id
+ holiday_sudo = holiday.sudo()
+ holiday_sudo.add_follower(employee_id)
+ if holiday.validation_type == 'manager':
+ holiday_sudo.message_subscribe(partner_ids=holiday.employee_id.leave_manager_id.partner_id.ids)
+ if holiday.validation_type == 'no_validation':
+ # Automatic validation should be done in sudo, because user might not have the rights to do it by himself
+ holiday_sudo.action_validate()
+ holiday_sudo.message_subscribe(partner_ids=[holiday._get_responsible_for_approval().partner_id.id])
+ holiday_sudo.message_post(body=_("The time off has been automatically approved"), subtype_xmlid="mail.mt_comment") # Message from OdooBot (sudo)
+ elif not self._context.get('import_file'):
+ holiday_sudo.activity_update()
+ return holidays
+
+ def write(self, values):
+ is_officer = self.env.user.has_group('hr_holidays.group_hr_holidays_user') or self.env.is_superuser()
+
+ if not is_officer:
+ if any(hol.date_from.date() < fields.Date.today() and hol.employee_id.leave_manager_id != self.env.user for hol in self):
+ raise UserError(_('You must have manager rights to modify/validate a time off that already begun'))
+
+ employee_id = values.get('employee_id', False)
+ if not self.env.context.get('leave_fast_create'):
+ if values.get('state'):
+ self._check_approval_update(values['state'])
+ if any(holiday.validation_type == 'both' for holiday in self):
+ if values.get('employee_id'):
+ employees = self.env['hr.employee'].browse(values.get('employee_id'))
+ else:
+ employees = self.mapped('employee_id')
+ self._check_double_validation_rules(employees, values['state'])
+ if 'date_from' in values:
+ values['request_date_from'] = values['date_from']
+ if 'date_to' in values:
+ values['request_date_to'] = values['date_to']
+ result = super(HolidaysRequest, self).write(values)
+ if not self.env.context.get('leave_fast_create'):
+ for holiday in self:
+ if employee_id:
+ holiday.add_follower(employee_id)
+ return result
+
+ def unlink(self):
+ error_message = _('You cannot delete a time off which is in %s state')
+ state_description_values = {elem[0]: elem[1] for elem in self._fields['state']._description_selection(self.env)}
+
+ if not self.user_has_groups('hr_holidays.group_hr_holidays_user'):
+ if any(hol.state != 'draft' for hol in self):
+ raise UserError(error_message % state_description_values.get(self[:1].state))
+ else:
+ for holiday in self.filtered(lambda holiday: holiday.state not in ['draft', 'cancel', 'confirm']):
+ raise UserError(error_message % (state_description_values.get(holiday.state),))
+ return super(HolidaysRequest, self.with_context(leave_skip_date_check=True, unlink=True)).unlink()
+
+ def copy_data(self, default=None):
+ if default and 'date_from' in default and 'date_to' in default:
+ default['request_date_from'] = default.get('date_from')
+ default['request_date_to'] = default.get('date_to')
+ return super().copy_data(default)
+ raise UserError(_('A time off cannot be duplicated.'))
+
+ def _get_mail_redirect_suggested_company(self):
+ return self.holiday_status_id.company_id
+
+ @api.model
+ def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
+ if not self.user_has_groups('hr_holidays.group_hr_holidays_user') and 'private_name' in groupby:
+ raise UserError(_('Such grouping is not allowed.'))
+ return super(HolidaysRequest, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy)
+
+ ####################################################
+ # Business methods
+ ####################################################
+
+ def _create_resource_leave(self):
+ """ This method will create entry in resource calendar time off object at the time of holidays validated
+ :returns: created `resource.calendar.leaves`
+ """
+ vals_list = [{
+ 'name': leave.name,
+ 'date_from': leave.date_from,
+ 'holiday_id': leave.id,
+ 'date_to': leave.date_to,
+ 'resource_id': leave.employee_id.resource_id.id,
+ 'calendar_id': leave.employee_id.resource_calendar_id.id,
+ 'time_type': leave.holiday_status_id.time_type,
+ } for leave in self]
+ return self.env['resource.calendar.leaves'].sudo().create(vals_list)
+
+ def _remove_resource_leave(self):
+ """ This method will create entry in resource calendar time off object at the time of holidays cancel/removed """
+ return self.env['resource.calendar.leaves'].search([('holiday_id', 'in', self.ids)]).unlink()
+
+ def _validate_leave_request(self):
+ """ Validate time off requests (holiday_type='employee')
+ by creating a calendar event and a resource time off. """
+ holidays = self.filtered(lambda request: request.holiday_type == 'employee')
+ holidays._create_resource_leave()
+ meeting_holidays = holidays.filtered(lambda l: l.holiday_status_id.create_calendar_meeting)
+ if meeting_holidays:
+ meeting_values = meeting_holidays._prepare_holidays_meeting_values()
+ meetings = self.env['calendar.event'].with_context(
+ no_mail_to_attendees=True,
+ active_model=self._name
+ ).create(meeting_values)
+ for holiday, meeting in zip(meeting_holidays, meetings):
+ holiday.meeting_id = meeting
+
+ def _prepare_holidays_meeting_values(self):
+ result = []
+ company_calendar = self.env.company.resource_calendar_id
+ for holiday in self:
+ calendar = holiday.employee_id.resource_calendar_id or company_calendar
+ if holiday.leave_type_request_unit == 'hour':
+ meeting_name = _("%s on Time Off : %.2f hour(s)") % (holiday.employee_id.name or holiday.category_id.name, holiday.number_of_hours_display)
+ else:
+ meeting_name = _("%s on Time Off : %.2f day(s)") % (holiday.employee_id.name or holiday.category_id.name, holiday.number_of_days)
+ meeting_values = {
+ 'name': meeting_name,
+ 'duration': holiday.number_of_days * (calendar.hours_per_day or HOURS_PER_DAY),
+ 'description': holiday.notes,
+ 'user_id': holiday.user_id.id,
+ 'start': holiday.date_from,
+ 'stop': holiday.date_to,
+ 'allday': False,
+ 'privacy': 'confidential',
+ 'event_tz': holiday.user_id.tz,
+ 'activity_ids': [(5, 0, 0)],
+ }
+ # Add the partner_id (if exist) as an attendee
+ if holiday.user_id and holiday.user_id.partner_id:
+ meeting_values['partner_ids'] = [
+ (4, holiday.user_id.partner_id.id)]
+ result.append(meeting_values)
+ return result
+
+ # YTI TODO: Remove me in master
+ def _prepare_holiday_values(self, employee):
+ return self._prepare_employees_holiday_values(employee)[0]
+
+ def _prepare_employees_holiday_values(self, employees):
+ self.ensure_one()
+ work_days_data = employees._get_work_days_data_batch(self.date_from, self.date_to)
+ return [{
+ 'name': self.name,
+ 'holiday_type': 'employee',
+ 'holiday_status_id': self.holiday_status_id.id,
+ 'date_from': self.date_from,
+ 'date_to': self.date_to,
+ 'request_date_from': self.request_date_from,
+ 'request_date_to': self.request_date_to,
+ 'notes': self.notes,
+ 'number_of_days': work_days_data[employee.id]['days'],
+ 'parent_id': self.id,
+ 'employee_id': employee.id,
+ 'state': 'validate',
+ } for employee in employees if work_days_data[employee.id]['days']]
+
+ def action_draft(self):
+ if any(holiday.state not in ['confirm', 'refuse'] for holiday in self):
+ raise UserError(_('Time off request state must be "Refused" or "To Approve" in order to be reset to draft.'))
+ self.write({
+ 'state': 'draft',
+ 'first_approver_id': False,
+ 'second_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'):
+ raise UserError(_('Time off request must be in Draft state ("To Submit") in order to confirm it.'))
+ self.write({'state': 'confirm'})
+ holidays = self.filtered(lambda leave: leave.validation_type == 'no_validation')
+ if holidays:
+ # Automatic validation should be done in sudo, because user might not have the rights to do it by himself
+ holidays.sudo().action_validate()
+ self.activity_update()
+ return True
+
+ def action_approve(self):
+ # if validation_type == 'both': this method is the first approval approval
+ # if validation_type != 'both': this method calls action_validate() below
+ if any(holiday.state != 'confirm' for holiday in self):
+ raise UserError(_('Time off request must be confirmed ("To Approve") in order to approve it.'))
+
+ current_employee = self.env.user.employee_id
+ self.filtered(lambda hol: hol.validation_type == 'both').write({'state': 'validate1', 'first_approver_id': current_employee.id})
+
+
+ # Post a second message, more verbose than the tracking message
+ for holiday in self.filtered(lambda holiday: holiday.employee_id.user_id):
+ holiday.message_post(
+ body=_(
+ 'Your %(leave_type)s planned on %(date)s has been accepted',
+ leave_type=holiday.holiday_status_id.display_name,
+ date=holiday.date_from
+ ),
+ partner_ids=holiday.employee_id.user_id.partner_id.ids)
+
+ self.filtered(lambda hol: not hol.validation_type == 'both').action_validate()
+ if not self.env.context.get('leave_fast_create'):
+ self.activity_update()
+ return True
+
+ def action_validate(self):
+ current_employee = self.env.user.employee_id
+ leaves = self.filtered(lambda l: l.employee_id and not l.number_of_days)
+ if leaves:
+ raise ValidationError(_('The following employees are not supposed to work during that period:\n %s') % ','.join(leaves.mapped('employee_id.name')))
+
+ if any(holiday.state not in ['confirm', 'validate1'] and holiday.validation_type != 'no_validation' for holiday in self):
+ raise UserError(_('Time off request must be confirmed in order to approve it.'))
+
+ self.write({'state': 'validate'})
+ self.filtered(lambda holiday: holiday.validation_type == 'both').write({'second_approver_id': current_employee.id})
+ self.filtered(lambda holiday: holiday.validation_type != 'both').write({'first_approver_id': current_employee.id})
+
+ for holiday in self.filtered(lambda holiday: holiday.holiday_type != 'employee'):
+ if holiday.holiday_type == 'category':
+ employees = holiday.category_id.employee_ids
+ elif holiday.holiday_type == 'company':
+ employees = self.env['hr.employee'].search([('company_id', '=', holiday.mode_company_id.id)])
+ else:
+ employees = holiday.department_id.member_ids
+
+ conflicting_leaves = self.env['hr.leave'].with_context(
+ tracking_disable=True,
+ mail_activity_automation_skip=True,
+ leave_fast_create=True
+ ).search([
+ ('date_from', '<=', holiday.date_to),
+ ('date_to', '>', holiday.date_from),
+ ('state', 'not in', ['cancel', 'refuse']),
+ ('holiday_type', '=', 'employee'),
+ ('employee_id', 'in', employees.ids)])
+
+ if conflicting_leaves:
+ # YTI: More complex use cases could be managed in master
+ if holiday.leave_type_request_unit != 'day' or any(l.leave_type_request_unit == 'hour' for l in conflicting_leaves):
+ raise ValidationError(_('You can not have 2 time off that overlaps on the same day.'))
+
+ # keep track of conflicting leaves states before refusal
+ target_states = {l.id: l.state for l in conflicting_leaves}
+ conflicting_leaves.action_refuse()
+ split_leaves_vals = []
+ for conflicting_leave in conflicting_leaves:
+ if conflicting_leave.leave_type_request_unit == 'half_day' and conflicting_leave.request_unit_half:
+ continue
+
+ # Leaves in days
+ if conflicting_leave.date_from < holiday.date_from:
+ before_leave_vals = conflicting_leave.copy_data({
+ 'date_from': conflicting_leave.date_from.date(),
+ 'date_to': holiday.date_from.date() + timedelta(days=-1),
+ 'state': target_states[conflicting_leave.id],
+ })[0]
+ before_leave = self.env['hr.leave'].new(before_leave_vals)
+ before_leave._compute_date_from_to()
+
+ # Could happen for part-time contract, that time off is not necessary
+ # anymore.
+ # Imagine you work on monday-wednesday-friday only.
+ # You take a time off on friday.
+ # We create a company time off on friday.
+ # By looking at the last attendance before the company time off
+ # start date to compute the date_to, you would have a date_from > date_to.
+ # Just don't create the leave at that time. That's the reason why we use
+ # new instead of create. As the leave is not actually created yet, the sql
+ # constraint didn't check date_from < date_to yet.
+ if before_leave.date_from < before_leave.date_to:
+ split_leaves_vals.append(before_leave._convert_to_write(before_leave._cache))
+ if conflicting_leave.date_to > holiday.date_to:
+ after_leave_vals = conflicting_leave.copy_data({
+ 'date_from': holiday.date_to.date() + timedelta(days=1),
+ 'date_to': conflicting_leave.date_to.date(),
+ 'state': target_states[conflicting_leave.id],
+ })[0]
+ after_leave = self.env['hr.leave'].new(after_leave_vals)
+ after_leave._compute_date_from_to()
+ # Could happen for part-time contract, that time off is not necessary
+ # anymore.
+ if after_leave.date_from < after_leave.date_to:
+ split_leaves_vals.append(after_leave._convert_to_write(after_leave._cache))
+
+ split_leaves = self.env['hr.leave'].with_context(
+ tracking_disable=True,
+ mail_activity_automation_skip=True,
+ leave_fast_create=True,
+ leave_skip_state_check=True
+ ).create(split_leaves_vals)
+
+ split_leaves.filtered(lambda l: l.state in 'validate')._validate_leave_request()
+
+ values = holiday._prepare_employees_holiday_values(employees)
+ leaves = self.env['hr.leave'].with_context(
+ tracking_disable=True,
+ mail_activity_automation_skip=True,
+ leave_fast_create=True,
+ leave_skip_state_check=True,
+ ).create(values)
+
+ leaves._validate_leave_request()
+
+ employee_requests = self.filtered(lambda hol: hol.holiday_type == 'employee')
+ employee_requests._validate_leave_request()
+ if not self.env.context.get('leave_fast_create'):
+ employee_requests.filtered(lambda holiday: holiday.validation_type != 'no_validation').activity_update()
+ return True
+
+ def action_refuse(self):
+ current_employee = self.env.user.employee_id
+ if any(holiday.state not in ['draft', 'confirm', 'validate', 'validate1'] for holiday in self):
+ raise UserError(_('Time off request must be confirmed or validated in order to refuse it.'))
+
+ validated_holidays = self.filtered(lambda hol: hol.state == 'validate1')
+ validated_holidays.write({'state': 'refuse', 'first_approver_id': current_employee.id})
+ (self - validated_holidays).write({'state': 'refuse', 'second_approver_id': current_employee.id})
+ # Delete the meeting
+ self.mapped('meeting_id').write({'active': False})
+ # If a category that created several holidays, cancel all related
+ linked_requests = self.mapped('linked_request_ids')
+ if linked_requests:
+ linked_requests.action_refuse()
+
+ # Post a second message, more verbose than the tracking message
+ for holiday in self:
+ if holiday.employee_id.user_id:
+ holiday.message_post(
+ body=_('Your %(leave_type)s planned on %(date)s has been refused', leave_type=holiday.holiday_status_id.display_name, date=holiday.date_from),
+ partner_ids=holiday.employee_id.user_id.partner_id.ids)
+
+ self._remove_resource_leave()
+ 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
+ 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.validation_type
+
+ if not is_manager and state != 'confirm':
+ if state == 'draft':
+ if holiday.state == 'refuse':
+ raise UserError(_('Only a Time Off Manager can reset a refused leave.'))
+ if holiday.date_from and holiday.date_from.date() <= fields.Date.today():
+ raise UserError(_('Only a Time Off Manager can reset a started leave.'))
+ if holiday.employee_id != current_employee:
+ raise UserError(_('Only a Time Off Manager can reset other people leaves.'))
+ else:
+ if val_type == 'no_validation' and current_employee == holiday.employee_id:
+ continue
+ # use ir.rule based first access check: department, members, ... (see security.xml)
+ holiday.check_access_rule('write')
+
+ # This handles states validate1 validate and refuse
+ if holiday.employee_id == current_employee:
+ raise UserError(_('Only a Time Off Manager can approve/refuse its own requests.'))
+
+ if (state == 'validate1' and val_type == 'both') or (state == 'validate' and val_type == 'manager') and holiday.holiday_type == 'employee':
+ if not is_officer and self.env.user != holiday.employee_id.leave_manager_id:
+ raise UserError(_('You must be either %s\'s manager or Time off Manager to approve this leave') % (holiday.employee_id.name))
+
+ # ------------------------------------------------------------
+ # Activity methods
+ # ------------------------------------------------------------
+
+ def _get_responsible_for_approval(self):
+ self.ensure_one()
+
+ responsible = self.env.user
+
+ if self.holiday_type != 'employee':
+ return responsible
+
+ if self.validation_type == 'manager' or (self.validation_type == 'both' and self.state == 'confirm'):
+ if self.employee_id.leave_manager_id:
+ responsible = self.employee_id.leave_manager_id
+ elif self.employee_id.parent_id.user_id:
+ responsible = self.employee_id.parent_id.user_id
+ elif self.validation_type == 'hr' or (self.validation_type == 'both' and self.state == 'validate1'):
+ 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'], self.env['hr.leave']
+ for holiday in self:
+ start = UTC.localize(holiday.date_from).astimezone(timezone(holiday.employee_id.tz or 'UTC'))
+ end = UTC.localize(holiday.date_to).astimezone(timezone(holiday.employee_id.tz or 'UTC'))
+ note = _(
+ 'New %(leave_type)s Request created by %(user)s from %(start)s to %(end)s',
+ leave_type=holiday.holiday_status_id.name,
+ user=holiday.create_uid.name,
+ start=start,
+ end=end
+ )
+ if holiday.state == 'draft':
+ to_clean |= holiday
+ elif holiday.state == 'confirm':
+ holiday.activity_schedule(
+ 'hr_holidays.mail_act_leave_approval',
+ note=note,
+ user_id=holiday.sudo()._get_responsible_for_approval().id or self.env.user.id)
+ elif holiday.state == 'validate1':
+ holiday.activity_feedback(['hr_holidays.mail_act_leave_approval'])
+ holiday.activity_schedule(
+ 'hr_holidays.mail_act_leave_second_approval',
+ note=note,
+ user_id=holiday.sudo()._get_responsible_for_approval().id or self.env.user.id)
+ elif holiday.state == 'validate':
+ to_do |= holiday
+ elif holiday.state == 'refuse':
+ to_clean |= holiday
+ if to_clean:
+ to_clean.activity_unlink(['hr_holidays.mail_act_leave_approval', 'hr_holidays.mail_act_leave_second_approval'])
+ if to_do:
+ to_do.activity_feedback(['hr_holidays.mail_act_leave_approval', 'hr_holidays.mail_act_leave_second_approval'])
+
+ ####################################################
+ # Messaging methods
+ ####################################################
+
+ def _track_subtype(self, init_values):
+ if 'state' in init_values and self.state == 'validate':
+ leave_notif_subtype = self.holiday_status_id.leave_notif_subtype_id
+ return leave_notif_subtype or self.env.ref('hr_holidays.mt_leave')
+ return super(HolidaysRequest, self)._track_subtype(init_values)
+
+ def _notify_get_groups(self, msg_vals=None):
+ """ Handle HR users and officers recipients that can validate or refuse holidays
+ directly from email. """
+ groups = super(HolidaysRequest, self)._notify_get_groups(msg_vals=msg_vals)
+ 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='/leave/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='/leave/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, channel_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(HolidaysRequest, self.sudo()).message_subscribe(partner_ids=partner_ids, channel_ids=channel_ids, subtype_ids=subtype_ids)
+ return super(HolidaysRequest, self).message_subscribe(partner_ids=partner_ids, channel_ids=channel_ids, subtype_ids=subtype_ids)
+
+ @api.model
+ def get_unusual_days(self, date_from, date_to=None):
+ # Checking the calendar directly allows to not grey out the leaves taken
+ # by the employee
+ calendar = self.env.user.employee_id.resource_calendar_id
+ if not calendar:
+ return {}
+ dfrom = datetime.combine(fields.Date.from_string(date_from), time.min).replace(tzinfo=UTC)
+ dto = datetime.combine(fields.Date.from_string(date_to), time.max).replace(tzinfo=UTC)
+
+ works = {d[0].date() for d in calendar._work_intervals_batch(dfrom, dto)[False]}
+ return {fields.Date.to_string(day.date()): (day.date() not in works) for day in rrule(DAILY, dfrom, until=dto)}
diff --git a/addons/hr_holidays/models/hr_leave_allocation.py b/addons/hr_holidays/models/hr_leave_allocation.py
new file mode 100644
index 00000000..5a7b5001
--- /dev/null
+++ b/addons/hr_holidays/models/hr_leave_allocation.py
@@ -0,0 +1,703 @@
+# -*- 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 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.exceptions import AccessError, UserError, ValidationError
+from odoo.tools.translate import _
+from odoo.tools.float_utils import float_round
+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 = [('valid', '=', True)]
+ else:
+ domain = [('valid', '=', True), ('allocation_type', '=', 'fixed_allocation')]
+ return self.env['hr.leave.type'].search(domain, limit=1)
+
+ def _holiday_status_id_domain(self):
+ if self.user_has_groups('hr_holidays.group_hr_holidays_manager'):
+ return [('valid', '=', True), ('allocation_type', '!=', 'no')]
+ return [('valid', '=', True), ('allocation_type', '=', 'fixed_allocation')]
+
+ name = fields.Char('Description', compute='_compute_description', inverse='_inverse_description', search='_search_description', compute_sudo=False)
+ 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'),
+ ('validate1', 'Second Approval'),
+ ('validate', 'Approved')
+ ], string='Status', readonly=True, tracking=True, copy=False, default='confirm',
+ 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.Datetime(
+ 'Start Date', readonly=True, index=True, copy=False, default=fields.Date.context_today,
+ states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, tracking=True)
+ date_to = fields.Datetime(
+ 'End Date', compute='_compute_from_holiday_status_id', store=True, readonly=False, 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_from_employee_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=_holiday_status_id_domain)
+ employee_id = fields.Many2one(
+ 'hr.employee', compute='_compute_from_holiday_type', store=True, string='Employee', index=True, readonly=False, ondelete="restrict", tracking=True,
+ states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate1': [('readonly', True)], 'validate': [('readonly', True)]})
+ manager_id = fields.Many2one('hr.employee', compute='_compute_from_employee_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: Number of days allocated in addition to the ones you will get via 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')
+ first_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')
+ second_approver_id = fields.Many2one(
+ 'hr.employee', string='Second Approval', readonly=True, copy=False,
+ help='This area is automatically filled by the user who validates the allocation with second level (If allocation type need second validation)')
+ 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")
+ 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)], 'validate1': [('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)], 'validate1': [('readonly', True)], 'validate': [('readonly', True)]})
+ # accrual configuration
+ 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)]})
+ accrual_limit = fields.Integer('Balance limit', default=0, help="Maximum of allocation for accrual; 0 means no maximum.")
+ number_per_interval = fields.Float("Number of unit per interval", compute='_compute_from_holiday_status_id', store=True, readonly=False,
+ states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate1': [('readonly', True)], 'validate': [('readonly', True)]})
+ interval_number = fields.Integer("Number of unit between two intervals", compute='_compute_from_holiday_status_id', store=True, readonly=False,
+ states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate1': [('readonly', True)], 'validate': [('readonly', True)]})
+ unit_per_interval = fields.Selection([
+ ('hours', 'Hours'),
+ ('days', 'Days')
+ ], compute='_compute_from_holiday_status_id', store=True, string="Unit of time added at each interval", readonly=False,
+ states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate1': [('readonly', True)], 'validate': [('readonly', True)]})
+ interval_unit = fields.Selection([
+ ('days', 'Days'),
+ ('weeks', 'Weeks'),
+ ('months', 'Months'),
+ ('years', 'Years')
+ ], compute='_compute_from_holiday_status_id', store=True, string="Unit of time between two intervals", readonly=False,
+ states={'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate1': [('readonly', True)], 'validate': [('readonly', True)]})
+ nextcall = fields.Date("Date of the next accrual allocation", default=False, readonly=True)
+ max_leaves = fields.Float(compute='_compute_leaves')
+ leaves_taken = fields.Float(compute='_compute_leaves')
+
+ _sql_constraints = [
+ ('type_value',
+ "CHECK( (holiday_type='employee' AND employee_id IS NOT NULL) 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 )", "The number of days must be greater than 0."),
+ ('number_per_interval_check', "CHECK(number_per_interval > 0)", "The number per interval should be greater than 0"),
+ ('interval_number_check', "CHECK(interval_number > 0)", "The interval number should be greater than 0"),
+ ]
+
+ @api.model
+ def _update_accrual(self):
+ """
+ Method called by the cron task in order to increment the number_of_days when
+ necessary.
+ """
+ today = fields.Date.from_string(fields.Date.today())
+
+ holidays = self.search([('allocation_type', '=', 'accrual'), ('employee_id.active', '=', True), ('state', '=', 'validate'), ('holiday_type', '=', 'employee'),
+ '|', ('date_to', '=', False), ('date_to', '>', fields.Datetime.now()),
+ '|', ('nextcall', '=', False), ('nextcall', '<=', today)])
+
+ for holiday in holidays:
+ values = {}
+
+ delta = relativedelta(days=0)
+
+ if holiday.interval_unit == 'days':
+ delta = relativedelta(days=holiday.interval_number)
+ if holiday.interval_unit == 'weeks':
+ delta = relativedelta(weeks=holiday.interval_number)
+ if holiday.interval_unit == 'months':
+ delta = relativedelta(months=holiday.interval_number)
+ if holiday.interval_unit == 'years':
+ delta = relativedelta(years=holiday.interval_number)
+
+ if holiday.nextcall:
+ values['nextcall'] = holiday.nextcall + delta
+ else:
+ values['nextcall'] = holiday.date_from
+ while values['nextcall'] <= datetime.combine(today, time(0, 0, 0)):
+ values['nextcall'] += delta
+
+ period_start = datetime.combine(today, time(0, 0, 0)) - delta
+ period_end = datetime.combine(today, time(0, 0, 0))
+
+ # We have to check when the employee has been created
+ # in order to not allocate him/her too much leaves
+ start_date = holiday.employee_id._get_date_start_work()
+ # If employee is created after the period, we cancel the computation
+ if period_end <= start_date or period_end < holiday.date_from:
+ holiday.write(values)
+ continue
+
+ # If employee created during the period, taking the date at which he has been created
+ if period_start <= start_date:
+ period_start = start_date
+
+ employee = holiday.employee_id
+ worked = employee._get_work_days_data_batch(
+ period_start, period_end,
+ domain=[('holiday_id.holiday_status_id.unpaid', '=', True), ('time_type', '=', 'leave')]
+ )[employee.id]['days']
+ left = employee._get_leave_days_data_batch(
+ period_start, period_end,
+ domain=[('holiday_id.holiday_status_id.unpaid', '=', True), ('time_type', '=', 'leave')]
+ )[employee.id]['days']
+ prorata = worked / (left + worked) if worked else 0
+
+ days_to_give = holiday.number_per_interval
+ if holiday.unit_per_interval == 'hours':
+ # As we encode everything in days in the database we need to convert
+ # the number of hours into days for this we use the
+ # mean number of hours set on the employee's calendar
+ days_to_give = days_to_give / (employee.resource_calendar_id.hours_per_day or HOURS_PER_DAY)
+
+ values['number_of_days'] = holiday.number_of_days + days_to_give * prorata
+ if holiday.accrual_limit > 0:
+ values['number_of_days'] = min(values['number_of_days'], holiday.accrual_limit)
+
+ holiday.write(values)
+
+ @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.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.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('employee_id', 'holiday_status_id')
+ def _compute_leaves(self):
+ for allocation in self:
+ leave_type = allocation.holiday_status_id.with_context(employee_id=allocation.employee_id.id)
+ allocation.max_leaves = leave_type.max_leaves
+ allocation.leaves_taken = leave_type.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', 'employee_id')
+ def _compute_number_of_hours_display(self):
+ for allocation in self:
+ if allocation.parent_id and allocation.parent_id.type_request_unit == "hour":
+ allocation.number_of_hours_display = allocation.number_of_days * HOURS_PER_DAY
+ elif allocation.number_of_days:
+ allocation.number_of_hours_display = allocation.number_of_days * (allocation.employee_id.sudo().resource_id.calendar_id.hours_per_day or HOURS_PER_DAY)
+ else:
+ allocation.number_of_hours_display = 0.0
+
+ @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 == 'both':
+ allocation._check_approval_update('validate1')
+ else:
+ allocation._check_approval_update('validate')
+ except (AccessError, UserError):
+ allocation.can_approve = False
+ else:
+ allocation.can_approve = True
+
+ @api.depends('holiday_type')
+ def _compute_from_holiday_type(self):
+ for allocation in self:
+ if allocation.holiday_type == 'employee':
+ if not allocation.employee_id:
+ allocation.employee_id = self.env.user.employee_id
+ allocation.mode_company_id = False
+ allocation.category_id = False
+ if allocation.holiday_type == 'company':
+ allocation.employee_id = 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_id = False
+ allocation.mode_company_id = False
+ allocation.category_id = False
+ elif allocation.holiday_type == 'category':
+ allocation.employee_id = False
+ allocation.mode_company_id = False
+ elif not allocation.employee_id and not allocation._origin.employee_id:
+ allocation.employee_id = self.env.context.get('default_employee_id') or self.env.user.employee_id
+
+ @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_from_employee_id(self):
+ default_holiday_status_id = self._default_holiday_status_id()
+ for holiday in self:
+ holiday.manager_id = holiday.employee_id and holiday.employee_id.parent_id
+ if holiday.employee_id.user_id != self.env.user and holiday._origin.employee_id != holiday.employee_id:
+ holiday.holiday_status_id = False
+ elif not holiday.holiday_status_id and not holiday._origin.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')
+ def _compute_from_holiday_status_id(self):
+ 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 HOURS_PER_DAY)
+
+ # set default values
+ if not allocation.interval_number and not allocation._origin.interval_number:
+ allocation.interval_number = 1
+ if not allocation.number_per_interval and not allocation._origin.number_per_interval:
+ allocation.number_per_interval = 1
+ if not allocation.unit_per_interval and not allocation._origin.unit_per_interval:
+ allocation.unit_per_interval = 'hours'
+ if not allocation.interval_unit and not allocation._origin.interval_unit:
+ allocation.interval_unit = 'weeks'
+
+ if allocation.holiday_status_id.validity_stop and allocation.date_to:
+ new_date_to = datetime.combine(allocation.holiday_status_id.validity_stop, time.max)
+ if new_date_to < allocation.date_to:
+ allocation.date_to = new_date_to
+
+ if allocation.allocation_type == 'accrual':
+ if allocation.holiday_status_id.request_unit == 'hour':
+ allocation.unit_per_interval = 'hours'
+ else:
+ allocation.unit_per_interval = 'days'
+ else:
+ allocation.interval_number = 1
+ allocation.interval_unit = 'weeks'
+ allocation.number_per_interval = 1
+ allocation.unit_per_interval = 'hours'
+
+ ####################################################
+ # ORM Overrides methods
+ ####################################################
+
+ 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
+ else:
+ target = allocation.employee_id.sudo().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.constrains('holiday_status_id')
+ def _check_leave_type_validity(self):
+ for allocation in self:
+ if allocation.holiday_status_id.validity_stop:
+ vstop = allocation.holiday_status_id.validity_stop
+ today = fields.Date.today()
+
+ if vstop < today:
+ raise ValidationError(_(
+ 'You can allocate %(allocation_type)s only before %(date)s.',
+ allocation_type=allocation.holiday_status_id.display_name,
+ date=allocation.holiday_status_id.validity_stop
+ ))
+
+ @api.model
+ def create(self, values):
+ """ Override to avoid automatic logging of creation """
+ 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})
+ holiday = super(HolidaysAllocation, self.with_context(mail_create_nosubscribe=True)).create(values)
+ holiday.add_follower(employee_id)
+ if holiday.validation_type == 'hr':
+ holiday.message_subscribe(partner_ids=(holiday.employee_id.parent_id.user_id.partner_id | holiday.employee_id.leave_manager_id.partner_id).ids)
+ if not self._context.get('import_file'):
+ holiday.activity_update()
+ return holiday
+
+ def write(self, values):
+ 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
+
+ def unlink(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),))
+ return super(HolidaysAllocation, self).unlink()
+
+ def _get_mail_redirect_suggested_company(self):
+ return self.holiday_status_id.company_id
+
+ ####################################################
+ # Business methods
+ ####################################################
+
+ def _prepare_holiday_values(self, employee):
+ self.ensure_one()
+ values = {
+ '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,
+ 'allocation_type': self.allocation_type,
+ 'date_from': self.date_from,
+ 'date_to': self.date_to,
+ 'interval_unit': self.interval_unit,
+ 'interval_number': self.interval_number,
+ 'number_per_interval': self.number_per_interval,
+ 'unit_per_interval': self.unit_per_interval,
+ }
+ return values
+
+ 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',
+ 'first_approver_id': False,
+ 'second_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'):
+ raise UserError(_('Allocation request must be in Draft state ("To Submit") in order to confirm it.'))
+ res = self.write({'state': 'confirm'})
+ self.activity_update()
+ return res
+
+ def action_approve(self):
+ # if validation_type == 'both': this method is the first approval approval
+ # if validation_type != 'both': this method calls action_validate() below
+ if any(holiday.state != 'confirm' for holiday in self):
+ raise UserError(_('Allocation request must be confirmed ("To Approve") in order to approve it.'))
+
+ current_employee = self.env.user.employee_id
+
+ self.filtered(lambda hol: hol.validation_type == 'both').write({'state': 'validate1', 'first_approver_id': current_employee.id})
+ self.filtered(lambda hol: not hol.validation_type == 'both').action_validate()
+ self.activity_update()
+
+ def action_validate(self):
+ current_employee = self.env.user.employee_id
+ for holiday in self:
+ if holiday.state not in ['confirm', 'validate1']:
+ raise UserError(_('Allocation request must be confirmed in order to approve it.'))
+
+ holiday.write({'state': 'validate'})
+ if holiday.validation_type == 'both':
+ holiday.write({'second_approver_id': current_employee.id})
+ else:
+ holiday.write({'first_approver_id': current_employee.id})
+
+ holiday._action_validate_create_childs()
+ self.activity_update()
+ return True
+
+ def _action_validate_create_childs(self):
+ childs = self.env['hr.leave.allocation']
+ if self.state == 'validate' and self.holiday_type in ['category', 'department', 'company']:
+ if 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)])
+
+ for employee in employees:
+ childs += self.with_context(
+ mail_notify_force_send=False,
+ mail_activity_automation_skip=True
+ ).create(self._prepare_holiday_values(employee))
+ # TODO is it necessary to interleave the calls?
+ childs.action_approve()
+ if childs and self.validation_type == 'both':
+ 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.'))
+
+ validated_holidays = self.filtered(lambda hol: hol.state == 'validate1')
+ validated_holidays.write({'state': 'refuse', 'first_approver_id': current_employee.id})
+ (self - validated_holidays).write({'state': 'refuse', 'second_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:
+ 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:
+ raise UserError(_('Only a time off Manager can approve its own requests.'))
+
+ if (state == 'validate1' and val_type == 'both') or (state == 'validate' and val_type == 'manager'):
+ if self.env.user == holiday.employee_id.leave_manager_id and self.env.user != holiday.employee_id.user_id:
+ continue
+ manager = holiday.employee_id.parent_id or holiday.employee_id.department_id.manager_id
+ if (manager != current_employee) and not is_manager:
+ raise UserError(_('You must be either %s\'s manager or time off manager to approve this time off') % (holiday.employee_id.name))
+
+ if state == 'validate' and val_type == 'both':
+ if not is_officer:
+ raise UserError(_('Only a Time off Approver can apply the second approval on allocation requests.'))
+
+ # ------------------------------------------------------------
+ # Activity methods
+ # ------------------------------------------------------------
+
+ def _get_responsible_for_approval(self):
+ self.ensure_one()
+ responsible = self.env.user
+
+ if self.validation_type == 'manager' or (self.validation_type == 'both' and self.state == 'confirm'):
+ if self.employee_id.leave_manager_id:
+ responsible = self.employee_id.leave_manager_id
+ elif self.validation_type == 'hr' or (self.validation_type == 'both' and self.state == 'validate1'):
+ 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:
+ 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_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_groups(msg_vals=msg_vals)
+ 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, channel_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, channel_ids=channel_ids, subtype_ids=subtype_ids)
+ return super(HolidaysAllocation, self).message_subscribe(partner_ids=partner_ids, channel_ids=channel_ids, subtype_ids=subtype_ids)
diff --git a/addons/hr_holidays/models/hr_leave_type.py b/addons/hr_holidays/models/hr_leave_type.py
new file mode 100644
index 00000000..e0687b1c
--- /dev/null
+++ b/addons/hr_holidays/models/hr_leave_type.py
@@ -0,0 +1,389 @@
+# -*- 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 odoo import api, fields, models
+from odoo.exceptions import ValidationError
+from odoo.osv import expression
+from odoo.tools.translate import _
+from odoo.tools.float_utils import float_round
+
+_logger = logging.getLogger(__name__)
+
+
+class HolidaysType(models.Model):
+ _name = "hr.leave.type"
+ _description = "Time Off Type"
+
+ @api.model
+ def _model_sorting_key(self, leave_type):
+ remaining = leave_type.virtual_remaining_leaves > 0
+ taken = leave_type.leaves_taken > 0
+ return leave_type.allocation_type == 'fixed' and remaining, leave_type.allocation_type == 'fixed_allocation' and remaining, taken
+
+ name = fields.Char('Time Off Type', required=True, translate=True)
+ code = fields.Char('Code')
+ 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.')
+ 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.')
+ group_days_allocation = fields.Float(
+ compute='_compute_group_days_allocation', string='Days Allocated')
+ 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',
+ domain=lambda self: [('groups_id', 'in', self.env.ref('hr_holidays.group_hr_holidays_user').id)],
+ help="This user will be responsible for approving this type of time off. "
+ "This is only used when validation is 'By Time Off Officer' or 'By Employee's Manager and Time Off Officer'",)
+ leave_validation_type = fields.Selection([
+ ('no_validation', 'No Validation'),
+ ('hr', 'By Time Off Officer'),
+ ('manager', "By Employee's Manager"),
+ ('both', "By Employee's Manager and Time Off Officer")], default='hr', string='Leave Validation')
+ allocation_validation_type = fields.Selection([
+ ('hr', 'By Time Off Officer'),
+ ('manager', "By Employee's Manager"),
+ ('both', "By Employee's Manager and Time Off Officer")], default='manager', string='Allocation Validation')
+ allocation_type = fields.Selection([
+ ('no', 'No Limit'),
+ ('fixed_allocation', 'Allow Employees Requests'),
+ ('fixed', 'Set by Time Off Officer')],
+ default='no', string='Mode',
+ help='\tNo Limit: no allocation by default, users can freely request time off; '
+ '\tAllow Employees Requests: allocated by HR and users can request time off and allocations; '
+ '\tSet by Time Off Officer: allocated by HR and cannot be bypassed; users can request time off;')
+ validity_start = fields.Date("From",
+ help='Adding validity to types of time off so that it cannot be selected outside this time period')
+ validity_stop = fields.Date("To")
+ valid = 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([('leave', 'Time Off'), ('other', 'Other')], default='leave', string="Kind of Leave",
+ help="Whether this should be computed as a holiday or as work time (eg: formation)")
+ 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))
+
+ @api.constrains('validity_start', 'validity_stop')
+ def _check_validity_dates(self):
+ for leave_type in self:
+ if leave_type.validity_start and leave_type.validity_stop and \
+ leave_type.validity_start > leave_type.validity_stop:
+ raise ValidationError(_("End of validity period should be greater than start of validity period"))
+
+ @api.depends('validity_start', 'validity_stop')
+ def _compute_valid(self):
+ dt = self._context.get('default_date_from') or fields.Date.context_today(self)
+
+ for holiday_type in self:
+ if holiday_type.validity_start and holiday_type.validity_stop:
+ holiday_type.valid = ((dt < holiday_type.validity_stop) and (dt > holiday_type.validity_start))
+ elif holiday_type.validity_start and (dt > holiday_type.validity_start):
+ holiday_type.valid = False
+ else:
+ holiday_type.valid = True
+
+ def _search_valid(self, operator, value):
+ dt = self._context.get('default_date_from', False)
+
+ if not dt:
+ return []
+ signs = ['>=', '<='] if operator == '=' else ['<=', '>=']
+
+ return ['|', ('validity_stop', operator, False), '&',
+ ('validity_stop', signs[0] if value else signs[1], dt),
+ ('validity_start', signs[1] if value else signs[0], dt)]
+
+ 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.allocation_type != 'no':
+ 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)]
+
+ # YTI TODO: Remove me in master
+ def get_days(self, employee_id):
+ return self.get_employees_days([employee_id])[employee_id]
+
+ def get_employees_days(self, employee_ids):
+ result = {
+ employee_id: {
+ leave_type.id: {
+ 'max_leaves': 0,
+ 'leaves_taken': 0,
+ 'remaining_leaves': 0,
+ 'virtual_remaining_leaves': 0,
+ 'virtual_leaves_taken': 0,
+ } for leave_type in self
+ } for employee_id in employee_ids
+ }
+
+ requests = self.env['hr.leave'].search([
+ ('employee_id', 'in', employee_ids),
+ ('state', 'in', ['confirm', 'validate1', 'validate']),
+ ('holiday_status_id', 'in', self.ids)
+ ])
+
+ allocations = self.env['hr.leave.allocation'].search([
+ ('employee_id', 'in', employee_ids),
+ ('state', 'in', ['confirm', 'validate1', 'validate']),
+ ('holiday_status_id', 'in', self.ids)
+ ])
+
+ for request in requests:
+ status_dict = result[request.employee_id.id][request.holiday_status_id.id]
+ status_dict['virtual_remaining_leaves'] -= (request.number_of_hours_display
+ if request.leave_type_request_unit == 'hour'
+ else request.number_of_days)
+ status_dict['virtual_leaves_taken'] += (request.number_of_hours_display
+ if request.leave_type_request_unit == 'hour'
+ else request.number_of_days)
+ if request.state == 'validate':
+ status_dict['leaves_taken'] += (request.number_of_hours_display
+ if request.leave_type_request_unit == 'hour'
+ else request.number_of_days)
+ status_dict['remaining_leaves'] -= (request.number_of_hours_display
+ if request.leave_type_request_unit == 'hour'
+ else request.number_of_days)
+
+ for allocation in allocations.sudo():
+ status_dict = result[allocation.employee_id.id][allocation.holiday_status_id.id]
+ if allocation.state == 'validate':
+ # note: add only validated allocation even for the virtual
+ # count; otherwise pending then refused allocation allow
+ # the employee to create more leaves than possible
+ status_dict['virtual_remaining_leaves'] += (allocation.number_of_hours_display
+ if allocation.type_request_unit == 'hour'
+ else allocation.number_of_days)
+ status_dict['max_leaves'] += (allocation.number_of_hours_display
+ if allocation.type_request_unit == 'hour'
+ else allocation.number_of_days)
+ status_dict['remaining_leaves'] += (allocation.number_of_hours_display
+ if allocation.type_request_unit == 'hour'
+ else allocation.number_of_days)
+ return result
+
+ @api.model
+ def get_days_all_request(self):
+ leave_types = sorted(self.search([]).filtered(lambda x: x.virtual_remaining_leaves or x.max_leaves), key=self._model_sorting_key, reverse=True)
+ return [(lt.name, {
+ 'remaining_leaves': ('%.2f' % lt.remaining_leaves).rstrip('0').rstrip('.'),
+ 'virtual_remaining_leaves': ('%.2f' % lt.virtual_remaining_leaves).rstrip('0').rstrip('.'),
+ 'max_leaves': ('%.2f' % lt.max_leaves).rstrip('0').rstrip('.'),
+ 'leaves_taken': ('%.2f' % lt.leaves_taken).rstrip('0').rstrip('.'),
+ 'virtual_leaves_taken': ('%.2f' % lt.virtual_leaves_taken).rstrip('0').rstrip('.'),
+ 'request_unit': lt.request_unit,
+ }, lt.allocation_type, lt.validity_stop)
+ for lt in leave_types]
+
+ 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
+
+ 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]
+
+ 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)
+
+ def _compute_group_days_allocation(self):
+ domain = [
+ ('holiday_status_id', 'in', self.ids),
+ ('holiday_type', '!=', 'employee'),
+ ('state', '=', 'validate'),
+ ]
+ domain2 = [
+ '|',
+ ('date_from', '>=', fields.Datetime.to_string(datetime.datetime.now().replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0))),
+ ('date_from', '=', False),
+ ]
+ grouped_res = self.env['hr.leave.allocation'].read_group(
+ expression.AND([domain, domain2]),
+ ['holiday_status_id', 'number_of_days'],
+ ['holiday_status_id'],
+ )
+ grouped_dict = dict((data['holiday_status_id'][0], data['number_of_days']) for data in grouped_res)
+ for allocation in self:
+ allocation.group_days_allocation = grouped_dict.get(allocation.id, 0)
+
+ def _compute_group_days_leave(self):
+ grouped_res = self.env['hr.leave'].read_group(
+ [('holiday_status_id', 'in', self.ids), ('holiday_type', '=', 'employee'), ('state', '=', 'validate'),
+ ('date_from', '>=', fields.Datetime.to_string(datetime.datetime.now().replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0)))],
+ ['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 name_get(self):
+ if not self._context.get('employee_id'):
+ # 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.allocation_type != 'no':
+ 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")
+ domain = [
+ ('holiday_status_id', 'in', self.ids),
+ ('holiday_type', '!=', 'employee'),
+ ]
+ domain2 = [
+ '|',
+ ('date_from', '>=', fields.Datetime.to_string(datetime.datetime.now().replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0))),
+ ('date_from', '=', False),
+ ]
+ action['domain'] = expression.AND([domain, domain2])
+ action['context'] = {
+ 'default_holiday_type': 'department',
+ 'default_holiday_status_id': self.ids[0],
+ }
+ 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]),
+ ('date_from', '>=', fields.Datetime.to_string(datetime.datetime.now().replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0)))
+ ]
+ action['context'] = {
+ 'default_holiday_status_id': self.ids[0],
+ }
+ return action
diff --git a/addons/hr_holidays/models/mail_channel.py b/addons/hr_holidays/models/mail_channel.py
new file mode 100644
index 00000000..166caa37
--- /dev/null
+++ b/addons/hr_holidays/models/mail_channel.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models
+
+
+class Channel(models.Model):
+ _inherit = 'mail.channel'
+
+ def partner_info(self, all_partners, direct_partners):
+ partner_infos = super(Channel, self).partner_info(all_partners, direct_partners)
+ # only search for leave out_of_office_date_end if im_status is on leave
+ partners_on_leave = [partner_id for partner_id in direct_partners.ids if 'leave' in partner_infos[partner_id]['im_status']]
+ if partners_on_leave:
+ now = fields.Datetime.now()
+ self.env.cr.execute('''SELECT res_users.partner_id as partner_id, hr_leave.date_to as date_to
+ FROM res_users
+ JOIN hr_leave ON hr_leave.user_id = res_users.id
+ AND hr_leave.state not in ('cancel', 'refuse')
+ AND res_users.active = 't'
+ AND hr_leave.date_from <= %s
+ AND hr_leave.date_to >= %s
+ AND res_users.partner_id in %s''', (now, now, tuple(partners_on_leave)))
+ out_of_office_infos = dict(((res['partner_id'], res) for res in self.env.cr.dictfetchall()))
+ for partner_id, out_of_office_info in out_of_office_infos.items():
+ partner_infos[partner_id]['out_of_office_date_end'] = out_of_office_info['date_to']
+
+ # fill empty values for the consistency of the result
+ for partner_info in partner_infos.values():
+ partner_info.setdefault('out_of_office_date_end', False)
+
+ return partner_infos
diff --git a/addons/hr_holidays/models/mail_message_subtype.py b/addons/hr_holidays/models/mail_message_subtype.py
new file mode 100644
index 00000000..f650ec1a
--- /dev/null
+++ b/addons/hr_holidays/models/mail_message_subtype.py
@@ -0,0 +1,49 @@
+# -*- 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
+ def create(self, vals):
+ result = super(MailMessageSubtype, self).create(vals)
+ if result.res_model in ['hr.leave', 'hr.leave.allocation']:
+ result._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
diff --git a/addons/hr_holidays/models/res_partner.py b/addons/hr_holidays/models/res_partner.py
new file mode 100644
index 00000000..dde82f5c
--- /dev/null
+++ b/addons/hr_holidays/models/res_partner.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, models
+
+
+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'
+ 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)
diff --git a/addons/hr_holidays/models/res_users.py b/addons/hr_holidays/models/res_users.py
new file mode 100644
index 00000000..24f70cde
--- /dev/null
+++ b/addons/hr_holidays/models/res_users.py
@@ -0,0 +1,77 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models
+
+
+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_used_count = fields.Float(related='employee_id.allocation_used_count')
+ allocation_count = fields.Float(related='employee_id.allocation_count')
+ leave_date_to = fields.Date(related='employee_id.leave_date_to')
+ is_absent = fields.Boolean(related='employee_id.is_absent')
+ allocation_used_display = fields.Char(related='employee_id.allocation_used_display')
+ allocation_display = fields.Char(related='employee_id.allocation_display')
+ hr_icon_display = fields.Selection(related='employee_id.hr_icon_display')
+
+ def __init__(self, pool, cr):
+ """ Override of __init__ to add access rights.
+ Access rights are disabled by default, but allowed
+ on some specific fields defined in self.SELF_{READ/WRITE}ABLE_FIELDS.
+ """
+
+ readable_fields = [
+ 'leave_manager_id',
+ 'show_leaves',
+ 'allocation_used_count',
+ 'allocation_count',
+ 'leave_date_to',
+ 'is_absent',
+ 'allocation_used_display',
+ 'allocation_display',
+ 'hr_icon_display',
+ ]
+ init_res = super(User, self).__init__(pool, cr)
+ # duplicate list to avoid modifying the original reference
+ type(self).SELF_READABLE_FIELDS = type(self).SELF_READABLE_FIELDS + readable_fields
+ return init_res
+
+ 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'
+ 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.env.cr.execute('''SELECT res_users.%s FROM res_users
+ JOIN hr_leave ON hr_leave.user_id = res_users.id
+ AND state not in ('cancel', 'refuse')
+ AND res_users.active = 't'
+ AND date_from <= %%s AND 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 = self.env.ref('hr_holidays.group_hr_holidays_responsible', raise_if_not_found=False)
+ if not self or not approver_group:
+ 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}
+ approver_group.sudo().write({
+ 'users': [(3, manager_id) for manager_id in responsibles_to_remove_ids]})
diff --git a/addons/hr_holidays/models/resource.py b/addons/hr_holidays/models/resource.py
new file mode 100644
index 00000000..1fa15350
--- /dev/null
+++ b/addons/hr_holidays/models/resource.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models
+
+
+class CalendarLeaves(models.Model):
+ _inherit = "resource.calendar.leaves"
+
+ holiday_id = fields.Many2one("hr.leave", string='Leave Request')