diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/hr_holidays/models | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/hr_holidays/models')
| -rw-r--r-- | addons/hr_holidays/models/__init__.py | 13 | ||||
| -rw-r--r-- | addons/hr_holidays/models/hr_department.py | 56 | ||||
| -rw-r--r-- | addons/hr_holidays/models/hr_employee.py | 216 | ||||
| -rw-r--r-- | addons/hr_holidays/models/hr_leave.py | 1264 | ||||
| -rw-r--r-- | addons/hr_holidays/models/hr_leave_allocation.py | 703 | ||||
| -rw-r--r-- | addons/hr_holidays/models/hr_leave_type.py | 389 | ||||
| -rw-r--r-- | addons/hr_holidays/models/mail_channel.py | 32 | ||||
| -rw-r--r-- | addons/hr_holidays/models/mail_message_subtype.py | 49 | ||||
| -rw-r--r-- | addons/hr_holidays/models/res_partner.py | 22 | ||||
| -rw-r--r-- | addons/hr_holidays/models/res_users.py | 77 | ||||
| -rw-r--r-- | addons/hr_holidays/models/resource.py | 10 |
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') |
