import logging
import pytz

from collections import defaultdict

from datetime import datetime, timedelta, time
from dateutil.relativedelta import relativedelta
from math import ceil
from markupsafe import Markup

from odoo.addons.base.models.ir_model import MODULE_UNINSTALL_FLAG
from odoo.addons.resource.models.utils import HOURS_PER_DAY

from odoo import api, fields, models
from odoo.addons.base.models.res_partner import _tz_get
from odoo.exceptions import AccessError, UserError, ValidationError
from odoo.tools.date_utils import float_to_time
from odoo.fields import Command, Date, Domain
from odoo.tools.float_utils import float_round, float_compare
from odoo.tools.intervals import Intervals
from odoo.tools.misc import clean_context, format_date
from odoo.tools.translate import _

_logger = logging.getLogger(__name__)


def get_employee_from_context(values, context, user_employee_id):
    employee_ids_list = [value[2] for value in values.get('employee_ids', []) if len(value) == 3 and value[0] == Command.SET]
    employee_ids = employee_ids_list[-1] if employee_ids_list else []
    employee_id_value = employee_ids[0] if employee_ids else False
    return employee_id_value or context.get('default_employee_id', context.get('employee_id', user_employee_id))


class HrLeave(models.Model):
    """ Time Off 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.main.attachment', 'mail.activity.mixin']
    _mail_post_access = 'read'

    @api.model
    def default_get(self, fields):
        defaults = super().default_get(fields)
        defaults = self._default_get_request_dates(defaults)
        if self.env.context.get('holiday_status_display_name', True) and 'holiday_status_id' in fields and not defaults.get('holiday_status_id'):
            domain = ['|', ('requires_allocation', '=', False), ('has_valid_allocation', '=', True)]
            defaults['holiday_status_id'] = False
            leave_types = self.env['hr.leave.type'].search(domain, order='sequence')
            selected_leave_type = next(
                (
                    leave_type for leave_type in leave_types
                    if (defaults.get('request_unit_hours') and leave_type['request_unit'] == 'hour') or (not defaults.get('request_unit_hours'))
                ),
                leave_types[0] if leave_types else None,
            )
            if selected_leave_type:
                defaults['holiday_status_id'] = selected_leave_type.id
                defaults['request_unit_hours'] = (selected_leave_type.request_unit == 'hour')

        if 'request_date_from' in fields and 'request_date_from' not in defaults:
            defaults['request_date_from'] = Date.today()
        if 'request_date_to' in fields and 'request_date_to' not in defaults:
            defaults['request_date_to'] = Date.today()

        return defaults

    def _default_get_request_dates(self, values):
        # The UI views initialize date_{from,to} due to how calendar views work.
        # However it is request_date_{from,to} that should be used instead.
        # Instead of overwriting all the javascript methods to use
        # request_date_{from,to} instead of date_{from,to}, we just convert
        # date_{from,to} to request_date_{from,to} here.

        # Request dates are determined during an onchange scenario.
        # To ensure that the values are correct in the client context (UI),
        # the timezone must be applied (because no processing is carried out
        # when these dates are received on the frontend).
        # Note:
        # Without the application of the timezone, days based on UTC datetimes
        # will be returned (and will therefore not be correct for the client).
        client_tz = self.env.tz
        if values.get('date_from'):
            if not values.get('request_date_from'):
                values['request_date_from'] = pytz.utc.localize(values['date_from']).astimezone(client_tz)
            del values['date_from']
        if values.get('date_to'):
            if not values.get('request_date_to'):
                values['request_date_to'] = pytz.utc.localize(values['date_to']).astimezone(client_tz)
            del values['date_to']
        return values

    # description
    name = fields.Char('Description', compute='_compute_description', inverse='_inverse_description', search='_search_description', compute_sudo=False, copy=False)
    private_name = fields.Char('Time Off Description', groups='hr_holidays.group_hr_holidays_responsible')
    state = fields.Selection([
        ('confirm', 'To Approve'),
        ('refuse', 'Refused'),
        ('validate1', 'Second Approval'),
        ('validate', 'Approved'),
        ('cancel', 'Cancelled'),
        ], string='Status', store=True, tracking=True, copy=False, readonly=False, default='confirm')
    user_id = fields.Many2one('res.users', string='User', related='employee_id.user_id', related_sudo=True, compute_sudo=True, store=True, readonly=True, index=True)
    # 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,
        domain="""[
            '|',
                ('requires_allocation', '=', False),
                ('has_valid_allocation', '=', True),
        ]""",
        tracking=True)
    holiday_status_requires_allocation = fields.Boolean(related="holiday_status_id.requires_allocation")
    color = fields.Integer("Color", related='holiday_status_id.color')
    validation_type = fields.Selection(string='Validation Type', related='holiday_status_id.leave_validation_type', readonly=False)
    # HR data

    employee_id = fields.Many2one(
        'hr.employee', string='Employee', index=True, ondelete="restrict", required=True,
        tracking=True, domain=lambda self: self._get_employee_domain(), default=lambda self: self.env.user.employee_id)
    employee_company_id = fields.Many2one(related='employee_id.company_id', string="Employee Company", store=True)
    company_id = fields.Many2one('res.company', compute='_compute_company_id', store=True)
    active_employee = fields.Boolean(related='employee_id.active', string='Employee Active')
    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)
    notes = fields.Text('Reasons', readonly=False)
    # duration
    resource_calendar_id = fields.Many2one('resource.calendar', compute='_compute_resource_calendar_id', store=True, readonly=False, copy=False)
    # allocated leave balance
    max_leaves = fields.Float(compute='_compute_leaves')
    virtual_remaining_leaves = fields.Float(compute='_compute_leaves', string='Available Time Off')
    # These dates are computed based on request_date_{to,from} and should
    # therefore never be set directly.
    date_from = fields.Datetime(
        'Start Date', compute='_compute_date_from_to', store=True, index=True, tracking=True)
    date_to = fields.Datetime(
        'End Date', compute='_compute_date_from_to', store=True, tracking=True)
    number_of_days = fields.Float(
        'Duration (Days)', compute='_compute_duration', store=True, tracking=True,
        help='Number of days of the time off request. Used in the calculation.')
    number_of_hours = fields.Float(
        'Duration (Hours)', compute='_compute_duration', store=True, tracking=True,
        help='Number of hours of the time off request. Used in the calculation.')
    last_several_days = fields.Boolean("All day", compute="_compute_last_several_days")
    duration_display = fields.Char('Requested', compute='_compute_duration_display', store=True)    # details
    # details
    meeting_id = fields.Many2one('calendar.event', string='Meeting', copy=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_approve = fields.Boolean(compute='_compute_can_approve', export_string_translation=False)
    can_validate = fields.Boolean(compute='_compute_can_validate', export_string_translation=False)
    can_refuse = fields.Boolean(compute='_compute_can_refuse', export_string_translation=False)
    can_cancel = fields.Boolean(compute='_compute_can_cancel', export_string_translation=False)
    can_back_to_approve = fields.Boolean(compute='_compute_can_back_to_approve', export_string_translation=False)

    attachment_ids = fields.One2many('ir.attachment', 'res_id', string="Attachments")
    # To display in form view
    supported_attachment_ids = fields.Many2many(
        'ir.attachment', string="Attach File", compute='_compute_supported_attachment_ids',
        inverse='_inverse_supported_attachment_ids')
    supported_attachment_ids_count = fields.Integer(compute='_compute_supported_attachment_ids')
    # UX fields
    leave_type_request_unit = fields.Selection(related='holiday_status_id.request_unit', readonly=True)
    leave_type_support_document = fields.Boolean(related="holiday_status_id.support_document")
    # Interface fields used when not using hour-based computation
    # These are the fields that should be used to manipulate the start- and
    # end-dates of the leave request. date_from and date_to are computed and
    # should therefore not be set directly.
    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.Float(string='Hour from', compute='_compute_request_hour_from_to', readonly=False, store=True)
    request_hour_to = fields.Float(string='Hour to', compute='_compute_request_hour_from_to', readonly=False, store=True)
    # 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_date_to_period = fields.Selection([
        ('am', 'Morning'), ('pm', 'Afternoon')],
        string="Date Period End", default='pm')
    # request type
    request_unit_half = fields.Boolean('Half-Day', compute='_compute_request_unit_half', store=True)
    request_unit_hours = fields.Boolean('Specific Time', compute='_compute_request_unit_hours', store=True)
    # view
    is_hatched = fields.Boolean('Hatched', compute='_compute_is_hatched')
    is_striked = fields.Boolean('Striked', compute='_compute_is_hatched')
    has_mandatory_day = fields.Boolean(compute='_compute_has_mandatory_day')
    leave_type_increases_duration = fields.Char(compute='_compute_leave_type_increases_duration')

    # warning message
    dashboard_warning_message = fields.Char(compute='_compute_dashboard_warning_message')
    _date_check2 = models.Constraint(
        'CHECK ((date_from <= date_to))',
        'The start date must be before or equal to the end date.',
    )
    _date_check3 = models.Constraint(
        'CHECK ((request_date_from <= request_date_to))',
        'The request start date must be before or equal to the request end date.',
    )
    _duration_check = models.Constraint(
        'CHECK ( number_of_days >= 0 )',
        "If you want to change the number of days you should use the 'period' mode",
    )
    _date_to_date_from_index = models.Index("(date_to, date_from)")

    @api.onchange('request_hour_from', 'request_hour_to')
    def _onchange_hours(self):
        # avoid negative or after midnight
        self.request_hour_from = min(max(self.request_hour_from, 0.0), 23.99)
        self.request_hour_to = min(max(self.request_hour_to, 0.0), 24)

    @api.depends('employee_id', 'request_date_from', 'request_date_to', 'request_unit_hours')
    def _compute_request_hour_from_to(self):
        env_company_calendar = self.env.company.resource_calendar_id
        for leave in self:
            calendar = leave.resource_calendar_id or env_company_calendar
            if (not leave.request_unit_hours
                    and leave.employee_id
                    and leave.request_date_from
                    and leave.request_date_to
                    and calendar):
                hour_from, hour_to = leave._get_hour_from_to(leave.request_date_from, leave.request_date_to)
                leave.request_hour_from = hour_from
                leave.request_hour_to = hour_to

    @api.depends('employee_id', 'leave_type_request_unit', 'request_date_from', 'request_date_to',
            'request_hour_from', 'request_hour_to', 'request_date_from_period', 'request_date_to_period')
    def _compute_dashboard_warning_message(self):
        all_leaves = self.search([
            ('date_from', '<', max(self.mapped('date_to'))),
            ('date_to', '>', min(self.mapped('date_from'))),
            ('employee_id', 'in', self.employee_id.ids),
            ('holiday_status_id.allow_request_on_top', '=', False),
            ('state', 'not in', ['cancel', 'refuse']),
        ])
        self.filtered(lambda self: self.state in ['cancel', 'refuse']).dashboard_warning_message = False
        for holiday in self.filtered(lambda self: self.state not in ['cancel', 'refuse']):
            conflicting_holidays = all_leaves.filtered_domain([
                ('employee_id', 'in', holiday.employee_id.ids),
                ('date_from', '<', holiday.date_to),
                ('date_to', '>', holiday.date_from),
                ('id', 'not in', holiday.ids),
            ])
            if not conflicting_holidays:
                holiday.dashboard_warning_message = False
                continue

            conflicting_holidays_list = []
            # Do not display the name of the employee if the conflicting holidays have an employee_id.user_id equivalent to the user id
            holidays_only_have_uid = bool(holiday.employee_id)
            holiday_states = dict(conflicting_holidays.fields_get(allfields=['state'])['state']['selection'])
            for conflicting_holiday in conflicting_holidays:
                conflicting_holiday_data = {
                    'employee_name': conflicting_holiday.employee_id.name,
                    'date_from': format_date(self.env, min(conflicting_holiday.mapped('date_from'))),
                    'date_to': format_date(self.env, min(conflicting_holiday.mapped('date_to'))),
                    'state': holiday_states[conflicting_holiday.state]
                }
                if conflicting_holiday.employee_id.user_id.id != self.env.uid:
                    holidays_only_have_uid = False
                if conflicting_holiday_data not in conflicting_holidays_list:
                    conflicting_holidays_list.append(conflicting_holiday_data)

            msg = ""
            if holidays_only_have_uid:
                msg = self.env._('You\'ve already booked time off which overlaps with this period:')
            else:
                msg = self.env._('An employee already booked time off which overlaps with this period:')

            holiday.dashboard_warning_message = msg + "".join(
                ('\n\t' + self.env._('%(employee_name)s from %(date_from)s to %(date_to)s - %(state)s')) % {
                    'employee_name': conflicting_holiday_data['employee_name'] if not holidays_only_have_uid else "",
                    'date_from': conflicting_holiday_data['date_from'],
                    'date_to': conflicting_holiday_data['date_to'],
                    'state': conflicting_holiday_data['state']
                } for conflicting_holiday_data in conflicting_holidays_list
            )

    @api.depends_context('uid')
    def _compute_description(self):
        self.check_access('read')

        is_officer = self.env.user.has_group('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.env.user.has_group('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.env.user.has_group('hr_holidays.group_hr_holidays_user')
        domain = Domain('private_name', operator, value)

        if not is_officer:
            domain &= Domain('user_id', '=', self.env.user.id)
        query = self.sudo()._search(domain)
        return Domain('id', 'in', query)

    @api.depends('employee_id', 'request_date_from', 'request_date_to')
    def _compute_resource_calendar_id(self):
        leaves_without_emp_or_date = self.filtered(
            lambda leave: not (leave.employee_id and leave.request_date_from and leave.request_date_to)
        )
        valid_leaves = self - leaves_without_emp_or_date
        leaves_without_emp_or_date.resource_calendar_id = self.env.company.resource_calendar_id
        if not valid_leaves:
            return
        employees_by_dates = defaultdict(lambda: self.env['hr.employee'])
        contracts_by_employee = dict(
            self.env['hr.version']._read_group(
                domain=[('employee_id', 'in', self.employee_id.ids)],
                groupby=['employee_id'],
                aggregates=['id:recordset']
            )
        )
        for leave in valid_leaves:
            employees_by_dates[leave.request_date_from] += leave.employee_id
        calendar_by_dates = {date_from: employees._get_calendars(date_from) for date_from, employees in employees_by_dates.items()}
        for leave in valid_leaves:
            calendar = calendar_by_dates.get(leave.request_date_from, {}).get(leave.employee_id.id) \
                        or self.env.company.resource_calendar_id
            # We use the request dates to find the contracts, because date_from
            # and date_to are not set yet at this point. Since these dates are
            # used to get the contracts for which these leaves apply and
            # contract start- and end-dates are just dates (and not datetimes)
            # these dates are comparable.
            contracts = contracts_by_employee.get(leave.employee_id, self.env['hr.version']).filtered(
                lambda c: c.date_start <= leave.request_date_to and
                          (not c.date_end or c.date_end >= leave.request_date_from))
            if contracts:
                # If there are more than one contract they should all have the
                # same calendar, otherwise a constraint is violated.
                calendar = contracts[:1].resource_calendar_id
            leave.resource_calendar_id = calendar

    def _get_overlapping_contracts(self):
        self.ensure_one()
        domain = Domain.AND([
            Domain('employee_id', '=', self.employee_id.id),
            Domain('contract_date_start', '<=', self.date_to),
            Domain.OR([
                Domain('contract_date_end', '>=', self.date_from),
                Domain('contract_date_end', '=', False),
            ])
        ])
        versions = self.env['hr.version'].sudo().search(domain)
        return versions.filtered(lambda v: v._is_overlapping_period(self.date_from.date(), self.date_to.date()))

    @api.constrains('date_from', 'date_to')
    def _check_contracts(self):
        """
            A leave cannot be set across multiple contracts.
            Note: a leave can be across multiple contracts despite this constraint.
            It happens if a leave is correctly created (not across multiple contracts) but
            contracts are later modifed/created in the middle of the leave.
        """
        for holiday in self.filtered('employee_id'):
            versions = holiday._get_overlapping_contracts()
            if len(versions.resource_calendar_id) > 1:
                raise ValidationError(
                    self.env._("""A leave cannot be set across multiple versions with different working schedules.

Please create one time off for each version period.

Time off:
%(time_off)s

Versions:
%(versions)s""",
                      time_off=holiday.display_name,
                      versions='\n'.join(_(
                          "- '%(version)s' from %(start_date)s to %(end_date)s",
                          version=version.name or version.employee_id.name,
                          start_date=format_date(self.env, version.date_start),
                          end_date=format_date(self.env, version.date_end) if version.date_end else self.env._("undefined"),
                      ) for version in versions)))

    @api.depends('request_date_from_period', 'request_date_to_period', 'request_hour_from', 'request_hour_to',
                 'request_date_from', 'request_date_to', 'request_unit_half', 'request_unit_hours', 'employee_id')
    def _compute_date_from_to(self):
        for holiday in self:
            if not holiday.request_date_from:
                holiday.date_from = False
                continue

            if not holiday.request_date_to:
                holiday.date_to = False
                continue

            if holiday.request_unit_hours:
                hour_from = holiday.request_hour_from
                hour_to = holiday.request_hour_to
                if not hour_from or not hour_to:
                    computed_from, computed_to = holiday._get_hour_from_to(holiday.request_date_from, holiday.request_date_to)
                    hour_from = hour_from or computed_from
                    hour_to = hour_to or computed_to

            elif holiday.request_unit_half:
                period_map = {'am': 'morning', 'pm': 'afternoon'}
                from_period = period_map.get(holiday.request_date_from_period)
                to_period = period_map.get(holiday.request_date_to_period)
                if holiday.request_date_from == holiday.request_date_to:
                    day_period = from_period if from_period == to_period else None
                    hour_from, hour_to = holiday._get_hour_from_to(holiday.request_date_from, holiday.request_date_to,
                        day_period)
                else:
                    hour_from, _ = holiday._get_hour_from_to(holiday.request_date_from, holiday.request_date_from, from_period)
                    _, hour_to = holiday._get_hour_from_to(holiday.request_date_to, holiday.request_date_to, to_period)

            else:
                hour_from, hour_to = holiday._get_hour_from_to(holiday.request_date_from, holiday.request_date_to)

            holiday.date_from = self._to_utc(holiday.request_date_from, hour_from, holiday.employee_id or holiday)
            holiday.date_to = self._to_utc(holiday.request_date_to, hour_to, holiday.employee_id or holiday)

    @api.depends('leave_type_request_unit')
    def _compute_request_unit_half(self):
        for holiday in self:
            holiday.request_unit_half = holiday.leave_type_request_unit == 'half_day'

    @api.depends('leave_type_request_unit')
    def _compute_request_unit_hours(self):
        for holiday in self:
            holiday.request_unit_hours = holiday.leave_type_request_unit == 'hour'

    def _get_employee_domain(self):
        domain = [
            ('active', '=', True),
            ('company_id', 'in', self.env.companies.ids),
        ]
        if not self.env.user.has_group('hr_holidays.group_hr_holidays_user'):
            domain += [
                '|',
                ('user_id', '=', self.env.uid),
                ('leave_manager_id', '=', self.env.uid),
            ]
        return domain

    @api.depends('employee_id')
    def _compute_from_employee_id(self):
        for holiday in self:
            if not holiday.holiday_status_id.requires_allocation:
                continue
            if not holiday.employee_id:
                holiday.holiday_status_id = False
            elif holiday.employee_id.user_id != self.env.user and holiday._origin.employee_id != holiday.employee_id:
                if holiday.employee_id and not holiday.holiday_status_id.with_context(employee_id=holiday.employee_id.id).has_valid_allocation:
                    holiday.holiday_status_id = False

    @api.depends('employee_id')
    def _compute_department_id(self):
        for holiday in self:
            holiday.department_id = holiday.employee_id.department_id

    @api.depends('date_from', 'date_to', 'holiday_status_id')
    def _compute_has_mandatory_day(self):
        date_from, date_to = min(self.mapped('date_from')), max(self.mapped('date_to'))
        if date_from and date_to:
            # Sudo to get access to version fields on employee (job_id)
            mandatory_days = self.employee_id.sudo()._get_mandatory_days(
                date_from.date(),
                date_to.date())

            for leave in self:
                department_ids = leave.employee_id.department_id.ids
                domain = [
                    ('start_date', '<=', leave.date_to.date()),
                    ('end_date', '>=', leave.date_from.date()),
                    '|',
                        ('resource_calendar_id', '=', False),
                        ('resource_calendar_id', '=', leave.resource_calendar_id.id),
                ]
                if department_ids:
                    domain += [
                        '|',
                        ('department_ids', '=', False),
                        ('department_ids', 'parent_of', department_ids),
                    ]
                else:
                    domain += [('department_ids', '=', False)]

                if leave.holiday_status_id.company_id:
                    domain += [('company_id', '=', leave.holiday_status_id.company_id.id)]
                leave.has_mandatory_day = leave.date_from and leave.date_to and mandatory_days.filtered_domain(domain)
        else:
            self.has_mandatory_day = False

    @api.depends('leave_type_request_unit', 'number_of_days')
    def _compute_leave_type_increases_duration(self):
        durations = self._get_durations(check_leave_type=False)
        for leave in self:
            days = durations[leave.id][0]
            if leave.leave_type_request_unit == 'day' and leave.holiday_status_requires_allocation and days < leave.number_of_days:
                leave.leave_type_increases_duration = self.env._("According to your working schedule you are expected to work"
                " %(days)s days in this period, but %(nb_days)s days will be used because this leave"
                " %(leave_type_name)s can only be taken by days.",
                days=days, nb_days=leave.number_of_days, leave_type_name=leave.holiday_status_id.name)
            else:
                leave.leave_type_increases_duration = ''

    def _get_durations(self, check_leave_type=True, resource_calendar=None):
        """
        This method is factored out into a separate method from
        _compute_duration so it can be hooked and called without necessarily
        modifying the fields and triggering more computes of fields that
        depend on number_of_hours or number_of_days.
        """
        result = {}
        employee_leaves = self.filtered('employee_id')
        employees_by_dates_calendar = defaultdict(lambda: self.env['hr.employee'])
        for leave in employee_leaves:
            if not leave.date_from or not leave.date_to:
                continue
            employees_by_dates_calendar[(leave.date_from, leave.date_to, leave.holiday_status_id.include_public_holidays_in_duration, resource_calendar or leave.resource_calendar_id)] += leave.employee_id
        # We force the company in the domain as we are more than likely in a compute_sudo
        domain = [('time_type', '=', 'leave'),
                  ('company_id', 'in', self.env.companies.ids + self.env.context.get('allowed_company_ids', [])),
                  # When searching for resource leave intervals, we exclude the one that
                  # is related to the leave we're currently trying to compute for.
                  '|', ('holiday_id', '=', False), ('holiday_id', 'not in', employee_leaves.ids)]
        # Precompute values in batch for performance purposes
        work_time_per_day_mapped = {
            (date_from, date_to, include_public_holidays_in_duration, calendar): employees.with_context(
                    compute_leaves=not include_public_holidays_in_duration)._list_work_time_per_day(date_from, date_to, domain=domain, calendar=calendar)
            for (date_from, date_to, include_public_holidays_in_duration, calendar), employees in employees_by_dates_calendar.items()
        }
        work_days_data_mapped = {
            (date_from, date_to, include_public_holidays_in_duration, calendar): employees._get_work_days_data_batch(date_from, date_to, compute_leaves=not include_public_holidays_in_duration, domain=domain, calendar=calendar)
            for (date_from, date_to, include_public_holidays_in_duration, calendar), employees in employees_by_dates_calendar.items()
        }
        for leave in self:
            calendar = resource_calendar or leave.resource_calendar_id
            if not leave.date_from or not leave.date_to or (not calendar and not leave.employee_id):
                result[leave.id] = (0, 0)
                continue
            hours, days = (0, 0)
            if leave.employee_id:
                # For flexible employees, if it's a single day leave, we force it to the real duration since the virtual intervals might not match reality on that day, especially for custom hours
                # sudo as is_flexible is on version model and employee does not have access to it.
                if leave.employee_id.sudo().is_flexible and leave.request_date_to == leave.request_date_from:
                    public_holidays = self.env['resource.calendar.leaves'].search([
                        ('resource_id', '=', False),
                        ('date_from', '<', leave.date_to),
                        ('date_to', '>', leave.date_from),
                        ('calendar_id', 'in', [False, calendar.id]),
                        ('company_id', '=', leave.company_id.id)
                    ])
                    if public_holidays:
                        public_holidays_intervals = Intervals([(ph.date_from, ph.date_to, ph) for ph in public_holidays])
                        leave_intervals = Intervals([(leave.date_from, leave.date_to, leave)])
                        real_leave_intervals = leave_intervals - public_holidays_intervals
                        hours = 0
                        for start, stop, meta in real_leave_intervals:
                            hours += (stop - start).total_seconds() / 3600
                    else:
                        hours = (leave.date_to - leave.date_from).total_seconds() / 3600
                    if not leave.request_unit_hours and not public_holidays:
                        days = 1 if not leave.request_unit_half or leave.request_date_from_period != leave.request_date_to_period else 0.5
                    else:
                        days = hours / 24
                elif leave.leave_type_request_unit == 'day' and check_leave_type:
                    # list of tuples (day, hours)
                    work_time_per_day_list = work_time_per_day_mapped[leave.date_from, leave.date_to, leave.holiday_status_id.include_public_holidays_in_duration, calendar][leave.employee_id.id]
                    days = len(work_time_per_day_list)
                    hours = sum(map(lambda t: t[1], work_time_per_day_list))
                else:
                    work_days_data = work_days_data_mapped[leave.date_from, leave.date_to, leave.holiday_status_id.include_public_holidays_in_duration, calendar][leave.employee_id.id]
                    hours, days = work_days_data['hours'], work_days_data['days']
            else:
                today_hours = calendar.get_work_hours_count(
                    datetime.combine(leave.date_from.date(), time.min),
                    datetime.combine(leave.date_from.date(), time.max),
                    False)
                hours = calendar.get_work_hours_count(leave.date_from, leave.date_to, compute_leaves=not leave.holiday_status_id.include_public_holidays_in_duration)
                days = hours / (today_hours or HOURS_PER_DAY)
            if leave.leave_type_request_unit == 'day' and check_leave_type:
                days = ceil(days)
            result[leave.id] = (days, hours)
        return result

    @api.depends('date_from', 'date_to', 'resource_calendar_id', 'holiday_status_id.request_unit')
    def _compute_duration(self):
        durations = self._get_durations()
        for leave in self:
            days, hours = durations[leave.id]
            leave.number_of_hours = hours
            leave.number_of_days = days

    @api.depends('employee_company_id')
    def _compute_company_id(self):
        for holiday in self:
            holiday.company_id = holiday.employee_company_id or holiday.department_id.company_id or self.env.company

    @api.depends('number_of_days')
    def _compute_last_several_days(self):
        for holiday in self:
            holiday.last_several_days = holiday.number_of_days > 1

    @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('resource_calendar_id.tz')
    def _compute_tz(self):
        for leave in self:
            leave.tz = leave.resource_calendar_id.tz or self.env.company.resource_calendar_id.tz or self.env.user.tz or 'UTC'

    @api.depends('number_of_hours', 'number_of_days', 'leave_type_request_unit')
    def _compute_duration_display(self):
        for leave in self:
            duration = leave.number_of_days
            unit = _('days')
            display = "%g %s" % (float_round(duration, precision_digits=2), unit)
            if leave.leave_type_request_unit == "hour":
                hours, minutes = divmod(abs(leave.number_of_hours) * 60, 60)
                minutes = round(minutes)
                if minutes == 60:
                    minutes = 0
                    hours += 1
                duration = '%d:%02d' % (hours, minutes)
                unit = _("hours")
                display = f"{duration} {unit}"
            leave.duration_display = display

    @api.depends('state', 'employee_id', 'department_id')
    def _compute_can_approve(self):
        for holiday in self:
            holiday.can_approve = holiday._check_approval_update('validate1', raise_if_not_possible=False)

    @api.depends('state', 'employee_id', 'department_id')
    def _compute_can_back_to_approve(self):
        for holiday in self:
            holiday.can_back_to_approve = holiday.state == 'validate' and holiday._check_approval_update('confirm', raise_if_not_possible=False)

    @api.depends('state', 'employee_id', 'department_id')
    def _compute_can_validate(self):
        for holiday in self:
            holiday.can_validate = holiday._check_approval_update('validate', raise_if_not_possible=False)

    @api.depends('state', 'employee_id', 'department_id')
    def _compute_can_refuse(self):
        for holiday in self:
            holiday.can_refuse = holiday._check_approval_update('refuse', raise_if_not_possible=False)

    @api.depends_context('uid')
    @api.depends('state', 'employee_id')
    def _compute_can_cancel(self):
        for holiday in self:
            holiday.can_cancel = holiday._check_approval_update('cancel', raise_if_not_possible=False)

    @api.depends('state')
    def _compute_is_hatched(self):
        for holiday in self:
            holiday.is_striked = holiday.state == 'refuse'
            holiday.is_hatched = holiday.state not in ['refuse', 'validate']

    @api.depends('leave_type_support_document', 'attachment_ids')
    def _compute_supported_attachment_ids(self):
        for holiday in self:
            holiday.supported_attachment_ids = holiday.attachment_ids
            holiday.supported_attachment_ids_count = len(holiday.attachment_ids.ids)

    @api.depends('employee_id', 'holiday_status_id')
    def _compute_leaves(self):
        date_from = fields.Date.from_string(self.env.context['default_request_date_from']) if 'default_request_date_from' in self.env.context else fields.Date.today()
        employee_days_per_allocation = self.employee_id._get_consumed_leaves(self.holiday_status_id, date_from)[0]
        for leave in self:
            virtual_remaining_leaves = 0
            max_leaves = 0
            for allocation, allocation_dict in employee_days_per_allocation[leave.employee_id][leave.holiday_status_id].items():
                if allocation and (not allocation.date_to or allocation.date_to >= date_from):
                    max_leaves += allocation_dict['max_leaves']
                    virtual_remaining_leaves += allocation_dict['virtual_remaining_leaves']
            leave.virtual_remaining_leaves = virtual_remaining_leaves
            leave.max_leaves = max_leaves

    def _inverse_supported_attachment_ids(self):
        for holiday in self:
            holiday.attachment_ids = holiday.supported_attachment_ids
        self.invalidate_recordset(['attachment_ids'])

    @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:
            if holiday.dashboard_warning_message:
                raise ValidationError(holiday.dashboard_warning_message)

    @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 ['validate1', 'validate']:
                raise ValidationError(_("This modification is not allowed in the current state."))

    def _check_validity(self):
        sorted_leaves = defaultdict(lambda: self.env['hr.leave'])
        for leave in self:
            sorted_leaves[(leave.holiday_status_id, leave.date_from.date())] |= leave
        for (leave_type, date_from), leaves in sorted_leaves.items():
            if not leave_type.requires_allocation:
                continue
            employees = leaves.employee_id
            leave_data = leave_type.get_allocation_data(employees, date_from)
            if leave_type.allows_negative:
                max_excess = leave_type.max_allowed_negative
                is_cancellation = all(leave.state in ('cancel', 'refuse') for leave in leaves)
                for employee in employees:
                    if is_cancellation:
                        continue
                    if not leave_data[employee][0][1]['max_leaves']:
                        raise ValidationError(_("You do not have any allocation for this time off type.\n"
                                                "Please request an allocation before submitting your time off request."))
                    if leave_data[employee] and leave_data[employee][0][1]['virtual_remaining_leaves'] < -max_excess:
                        raise ValidationError(_("There is no valid allocation to cover that request."))
                continue

            previous_leave_data = leave_type.with_context(
                ignored_leave_ids=leaves.ids
            ).get_allocation_data(employees, date_from)
            for employee in employees:
                previous_emp_data = previous_leave_data[employee] and previous_leave_data[employee][0][1]['virtual_excess_data']
                emp_data = leave_data[employee] and leave_data[employee][0][1]['virtual_excess_data']
                if not leave_data[employee][0][1]['max_leaves']:
                    raise ValidationError(_("You do not have any allocation for this time off type.\n"
                                            "Please request an allocation before submitting your time off request."))
                if not previous_emp_data and not emp_data:
                    continue
                if previous_emp_data != emp_data and len(emp_data) >= len(previous_emp_data):
                    raise ValidationError(_("There is no valid allocation to cover that request."))
        is_leave_user = self.env.user.has_group('hr_holidays.group_hr_holidays_user')
        if not is_leave_user and any(leave.has_mandatory_day for leave in self):
            raise ValidationError(_('You are not allowed to request time off on a Mandatory Day'))

    ####################################################
    # ORM Overrides methods
    ####################################################

    @api.depends(
        'tz', 'date_from', 'date_to', 'employee_id',
        'holiday_status_id', 'number_of_hours',
        'leave_type_request_unit', 'number_of_days', 'department_id',
    )
    @api.depends_context('short_name', 'hide_employee_name', 'groupby')
    def _compute_display_name(self):
        for leave in self:
            user_tz = pytz.timezone(leave.tz)
            date_from_utc = leave.date_from and leave.date_from.astimezone(user_tz).date()
            date_to_utc = leave.date_to and leave.date_to.astimezone(user_tz).date()
            time_off_type_display = leave.holiday_status_id.name
            if self.env.context.get('short_name'):
                short_leave_name = leave.name or time_off_type_display or _('Time Off')
                leave.display_name = _("%(name)s: %(duration)s", name=short_leave_name, duration=leave.duration_display)
            else:
                target = leave.employee_id.name or ""
                display_date = format_date(self.env, date_from_utc) or ""
                if leave.number_of_days > 1 and date_from_utc and date_to_utc:
                    display_date += _(' to %(date_to_utc)s',
                        date_to_utc=format_date(self.env, date_to_utc) or ""
                    )
                if not target or self.env.context.get('hide_employee_name') and 'employee_id' in self.env.context.get('group_by', []):
                    leave.display_name = _("%(leave_type)s: %(duration)s (%(start)s)",
                        leave_type=time_off_type_display,
                        duration=leave.duration_display,
                        start=display_date,
                    )
                elif not time_off_type_display:
                    leave.display_name = _("%(person)s: %(duration)s (%(start)s)",
                        person=target,
                        duration=leave.duration_display,
                        start=display_date,
                    )
                else:
                    leave.display_name = _("%(person)s on %(leave_type)s: %(duration)s (%(start)s)",
                        person=target,
                        leave_type=time_off_type_display,
                        duration=leave.duration_display,
                        start=display_date,
                    )

    def onchange(self, values, field_names, fields_spec):
        # Try to force the leave_type display_name when creating new records
        # This is called right after pressing create and returns the display_name for
        # most fields in the view.
        if values and 'employee_id' in fields_spec and 'employee_id' not in self.env.context:
            employee_id = get_employee_from_context(values, self.env.context, self.env.user.employee_id.id)
            self = self.with_context(employee_id=employee_id)
        return super().onchange(values, field_names, fields_spec)

    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)

    def _check_double_validation_rules(self, employees, state):
        if self.env.user.has_group('hr_holidays.group_hr_holidays_manager'):
            return

        is_leave_user = self.env.user.has_group('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.env.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 double validation
                if mapped_validation_type[leave_type_id] == 'both':
                    self._check_double_validation_rules(employee_id, values.get('state', False))

        if any(not vals.get('employee_id') for vals in vals_list):
            raise UserError(_("There is no employee set on the time off. Please make sure you're logged in the correct company."))
        holidays = super(HrLeave, self.with_context(mail_create_nosubscribe=True)).create(vals_list)
        holidays._check_validity()
        self.env['hr.leave.allocation'].invalidate_model(['leaves_taken', 'max_leaves'])  # missing dependency on compute

        for holiday in holidays:
            if not self.env.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(holiday.employee_id.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_approve()
                    holiday_sudo.message_subscribe(partner_ids=holiday._get_responsible_for_approval().partner_id.ids)
                    holiday_sudo.message_post(body=_("The time off has been automatically approved"), subtype_xmlid="mail.mt_comment") # Message from OdooBot (sudo)
                elif not self.env.context.get('import_file'):
                    holiday_sudo.activity_update()
        return holidays

    def write(self, vals):
        values = vals
        is_officer = self.env.user.has_group('hr_holidays.group_hr_holidays_user') or self.env.is_superuser()
        if not is_officer and values.keys() - {'attachment_ids', 'supported_attachment_ids', 'message_main_attachment_id'}:
            if any(hol.date_from.date() < fields.Date.today() and hol.employee_id.leave_manager_id != self.env.user
                   and hol.state not in ('confirm', 'draft') for hol in self):
                raise UserError(_('You must have manager rights to modify/validate a time off that already begun'))
            if any(leave.state == 'cancel' for leave in self):
                raise UserError(_('Only a manager can modify a canceled leave.'))

        # If a leave changes state from validated or if the dates of a validated leave change
        # unlink the corresponding resource calendar leave
        date_fields = {'date_from', 'date_to', 'request_date_from', 'request_date_to'}
        validated_leaves = self.filtered(lambda l: l.state == 'validate')
        if validated_leaves and (('state' in values and values['state'] != 'validate') or date_fields.intersection(values)):
            validated_leaves._remove_resource_leave()

        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().write(values)
        if any(field in values for field in ['request_date_from', 'date_from', 'request_date_from', 'date_to', 'holiday_status_id', 'employee_id', 'state']):
            if not values.get('state') or values.get('state') not in ('refuse', 'cancel'):
                self._check_validity()
            self.env['hr.leave.allocation'].invalidate_model(['leaves_taken', 'max_leaves'])  # missing dependency on compute
        if not self.env.context.get('leave_fast_create'):
            for holiday in self:
                if employee_id:
                    holiday.add_follower(employee_id)

        return result

    @api.ondelete(at_uninstall=False)
    def _unlink_if_correct_states(self):
        error_message = self.env._('Oops! %(state)s Time-Off requests can only be deleted by Administrators.')
        state_description_values = {elem[0]: elem[1] for elem in self._fields['state']._description_selection(self.env)}
        now = fields.Datetime.now().date()

        if not self.env.user.has_group('hr_holidays.group_hr_holidays_user'):
            for hol in self:
                if hol.state not in ['confirm', 'validate1', 'cancel']:
                    raise UserError(error_message % {'state': state_description_values.get(self[:1].state)})
                if hol.date_from.date() < now:
                    raise UserError(_("You can't delete a time off request that is in the past."))
        elif not self.env.user.has_group('hr_holidays.group_hr_holidays_manager'):
            for holiday in self.filtered(lambda holiday: holiday.state not in ['cancel', 'confirm']):
                error_message = self.env._('Oops! %(state)s Time-Off requests can only be deleted by Administrators.')
                raise UserError(error_message % {'state': state_description_values.get(holiday.state)})

    def unlink(self):
        self.sudo()._post_leave_cancel()
        self.env['hr.leave.allocation'].invalidate_model(['leaves_taken', 'max_leaves'])  # missing dependency on compute
        return super(HrLeave, self.with_context(leave_skip_date_check=True)).unlink()

    def copy_data(self, default=None):
        vals_list = super().copy_data(default=default)
        if self.env.context.get('skip_copy_check'):
            return vals_list
        if all(leave.state in ['cancel', 'refuse'] for leave in self):  # No overlap constraint in these cases
            return vals_list
        raise UserError(_('A time off cannot be duplicated.'))

    def _get_redirect_suggested_company(self):
        return self.holiday_status_id.company_id

    ####################################################
    # Business methods
    ####################################################

    def _prepare_resource_leave_vals(self):
        """Hook method for others to inject data
        """
        self.ensure_one()
        return {
            'name': _("%s: Time Off", self.employee_id.name),
            'date_from': self.date_from,
            'holiday_id': self.id,
            'date_to': self.date_to,
            'resource_id': self.employee_id.resource_id.id,
            'calendar_id': self.resource_calendar_id.id,
            'time_type': self.holiday_status_id.time_type,
            'elligible_for_accrual_rate': self.holiday_status_id.elligible_for_accrual_rate,
        }

    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 = [leave._prepare_resource_leave_vals() 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 """
        if self.has_access('write'):
            return self.env['resource.calendar.leaves'].search([('holiday_id', 'in', self.ids)]).sudo().unlink()
        return self.env['resource.calendar.leaves'].search([('holiday_id', 'in', self.ids)]).unlink()

    def _validate_leave_request(self):
        """ Validate time off requests
        by creating a calendar event and a resource time off. """
        holidays = self.filtered("employee_id")
        holidays._create_resource_leave()
        meeting_holidays = holidays.filtered(lambda l: l.holiday_status_id.create_calendar_meeting)
        meetings = self.env['calendar.event']
        if meeting_holidays:
            Meeting = self.env['calendar.event']
            Meeting.check_access('create')
            meeting_values_for_user_id = meeting_holidays._prepare_holidays_meeting_values()
            Meeting = self.env['calendar.event']
            for user_id, meeting_values in meeting_values_for_user_id.items():
                meetings += Meeting.with_user(user_id or self.env.uid).sudo().with_context(clean_context({**self.env.context, **dict(
                                allowed_company_ids=[],
                                no_mail_to_attendees=True,
                                calendar_no_videocall=True,
                                active_model=self._name
                            )})).create(meeting_values)
        Holiday = self.env['hr.leave']
        for meeting in meetings:
            Holiday.browse(meeting.res_id).meeting_id = meeting

        for holiday in holidays:
            user_tz = pytz.timezone(holiday.tz)
            utc_tz = pytz.utc.localize(holiday.date_from).astimezone(user_tz)
            notify_partner_ids = holiday.employee_id.user_id.partner_id.ids
            holiday.message_post(
                body=_(
                    'Your %(leave_type)s planned on %(date)s has been accepted',
                    leave_type=holiday.holiday_status_id.display_name,
                    date=utc_tz.replace(tzinfo=None)
                ),
                partner_ids=notify_partner_ids)


    def _prepare_holidays_meeting_values(self):
        result = defaultdict(list)
        for holiday in self:
            user = holiday.user_id
            meeting_name = _(
                "%(employee)s on Time Off : %(duration)s",
                employee=holiday.employee_id.name or holiday.category_id.name,
                duration=holiday.duration_display)
            allday_value = not holiday.request_unit_half or holiday.request_date_from_period == 'am' and holiday.request_date_to_period == 'pm'
            if holiday.leave_type_request_unit == 'hour':
                allday_value = float_compare(holiday.number_of_days, 1.0, 1) >= 0

            if allday_value:
                # `start` and `stop` are not in UTC for allday events
                leave_tz = pytz.timezone(holiday.tz) if holiday.tz else pytz.UTC
                start_value = pytz.UTC.localize(holiday.date_from).astimezone(leave_tz).replace(tzinfo=None)
                stop_value = pytz.UTC.localize(holiday.date_to).astimezone(leave_tz).replace(tzinfo=None)
            else:
                start_value = holiday.date_from
                stop_value = holiday.date_to

            meeting_values = {
                'name': meeting_name,
                'duration': holiday.number_of_days * (holiday.resource_calendar_id.hours_per_day or HOURS_PER_DAY),
                'description': holiday.notes,
                'user_id': user.id,
                'start': start_value,
                'stop': stop_value,
                'allday': allday_value,
                'privacy': 'confidential',
                'event_tz': user.tz,
                'activity_ids': [(5, 0, 0)],
                'res_id': holiday.id,
            }
            # Add the partner_id (if exist) as an attendee
            partner_id = (user and user.partner_id) or (holiday.employee_id and holiday.employee_id.work_contact_id)
            if partner_id:
                meeting_values['partner_ids'] = [(4, partner_id.id)]
            result[user.id].append(meeting_values)
        return result

    def action_cancel(self):
        self.ensure_one()

        return {
            'name': _('Cancel Time Off'),
            'type': 'ir.actions.act_window',
            'target': 'new',
            'res_model': 'hr.holidays.cancel.leave',
            'view_mode': 'form',
            'views': [[False, 'form']],
            'context': {
                'default_leave_id': self.id,
                'dialog_size': "medium",
            }
        }

    def action_approve(self, check_state=True):
        current_employee = self.env.user.employee_id
        leave_to_approve = self.env['hr.leave']
        leave_to_validate = self.env['hr.leave']
        for leave in self:
            if check_state and leave.can_validate or not check_state and leave.validation_type != "both":
                leave_to_validate += leave
            elif check_state and leave.can_approve or not check_state and leave.validation_type == 'both':
                leave_to_approve += leave
            else:
                raise UserError(self.env._('You cannot approve this leave.'))
        leave_to_approve.write({'state': 'validate1', 'first_approver_id': current_employee.id})
        leave_to_validate._action_validate(check_state)
        if not self.env.context.get('leave_fast_create'):
            self.activity_update()
        return True

    def action_back_to_approval(self):
        self.filtered(lambda l: l.can_back_to_approve)._move_validate_leave_to_confirm()
        return True

    def _move_validate_leave_to_confirm(self):
        self.write({'state': 'confirm'})
        self.activity_update()
        self._post_leave_cancel()

    def _get_leaves_on_public_holiday(self):
        return self.filtered(lambda l: l.employee_id and not l.number_of_days)

    def _split_leaves(self, split_date_from, split_date_to=False):
        """
        This method splits an original leave in two leaves and returns the new one for each leave in self.
        E.g. (start, stop) -> (start, split_date_from - 1day), (split_date_to, stop)
        :param split_date_from: The starting date of the splicing interval (includes)
        :param split_date_to: The ending date of the splicing interval. (not includes)
        :param changes_message: The message will be translated and posted in the first leave's chatter

        If split_date_to is not set; the splicing interval will be equals to [split_date_form, split_date_from -1]
        to avoid one day leave.
        """
        new_leaves_vals = []
        if not split_date_to:
            split_date_to = split_date_from

        # Only leaves that span a period outside of the split interval need
        # to be split.
        multi_day_leaves = self.filtered(lambda l: l.request_date_from < split_date_from or l.request_date_to >= split_date_to)
        for leave in multi_day_leaves:
            new_leave_vals = []
            target_leave_vals = []
            if leave.request_date_from < split_date_from:
                new_leave_vals.append(leave.with_context(skip_copy_check=True).copy_data({
                    'request_date_to': split_date_from + timedelta(days=-1),
                    'state': leave.state
                })[0])

            # Do the same for the new leave after the split
            if leave.request_date_to >= split_date_to:
                new_leave_vals.append(leave.with_context(skip_copy_check=True).copy_data({
                    'request_date_from': split_date_to,
                    'state': leave.state
                })[0])

            # For those two new leaves, only create them if they actually have a non-zero duration.
            for leave_vals in new_leave_vals:
                new_leave = self.env['hr.leave'].new(leave_vals)
                new_leave._compute_date_from_to()
                if new_leave.date_from < new_leave.date_to:
                    target_leave_vals.append(new_leave._convert_to_write(new_leave._cache))

            if target_leave_vals:
                vals = target_leave_vals.pop(0)
                leave.with_context(leave_skip_state_check=True).write({
                    'request_date_from': vals['request_date_from'],
                    'request_date_to': vals['request_date_to'],
                })
                if target_leave_vals:
                    new_leaves_vals.extend(target_leave_vals)

        if not new_leaves_vals:
            return self.env['hr.leave']
        return self.env['hr.leave'].with_context(
            tracking_disable=True,
            mail_activity_automation_skip=True,
            leave_fast_create=True,
            leave_skip_state_check=True
        ).create(new_leaves_vals)

    def _action_validate(self, check_state=True):
        current_employee = self.env.user.employee_id
        leaves = self._get_leaves_on_public_holiday()
        if check_state and any(not holiday.can_validate for holiday in self):
            raise UserError(_('You can\'t validate this leave.'))
        if leaves:
            raise ValidationError(_('The following employees are not supposed to work during that period:\n %s') % ','.join(leaves.mapped('employee_id.name')))

        self.write({'state': 'validate'})

        leaves_second_approver = self.env['hr.leave']
        leaves_first_approver = self.env['hr.leave']

        for leave in self:
            if leave.validation_type == 'both':
                leaves_second_approver += leave
            else:
                leaves_first_approver += leave

        leaves_second_approver.write({'second_approver_id': current_employee.id})
        leaves_first_approver.write({'first_approver_id': current_employee.id})

        self._validate_leave_request()
        if not self.env.context.get('leave_fast_create'):
            self.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 ['confirm', 'validate', 'validate1'] for holiday in self):
            raise UserError(_('Time off request must be confirmed or validated in order to refuse it.'))

        self._notify_manager()
        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})
        # 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.activity_update()
        return True

    def _notify_manager(self):
        leaves = self.filtered(lambda hol: (hol.validation_type == 'both' and hol.state in ['validate1', 'validate']) or (hol.validation_type == 'manager' and hol.state == 'validate'))
        model_description = self.env['ir.model']._get('hr.holidays').name
        for holiday in leaves:
            responsible = holiday.employee_id.leave_manager_id.partner_id.ids
            if responsible:
                holiday.sudo().message_notify(
                    partner_ids=responsible,
                    model_description=model_description,
                    subject=_('Refused Time Off'),
                    body=_(
                        '%(holiday_name)s has been refused.',
                        holiday_name=holiday.display_name,
                    ),
                    email_layout_xmlid="mail.mail_notification_layout",
                    subtitles=[holiday.display_name],
                )

    def _action_user_cancel(self, reason=None):
        self.ensure_one()
        if not self.can_cancel:
            raise ValidationError(_('This time off cannot be cancelled.'))

        self._force_cancel(reason, 'mail.mt_note')

    def _force_cancel(self, reason=None, msg_subtype='mail.mt_comment', notify_responsibles=True):
        leaves = self.browse() if self.env.context.get(MODULE_UNINSTALL_FLAG) else self
        if reason:
            model_description = self.env['ir.model']._get('hr.holidays').display_name
            for leave in leaves:
                body = self.env._(
                    "The time off request has been cancelled for the following reason:%(reason)s",
                    reason=Markup("<p>{reason}</p>").format(reason=reason)
                )
                leave.message_post(
                    body=body,
                    subtype_xmlid=msg_subtype
                )

                if not notify_responsibles:
                    continue

                responsibles = self.env['res.partner']
                # manager
                if (leave.holiday_status_id.leave_validation_type == 'manager' and leave.state == 'validate') or (leave.holiday_status_id.leave_validation_type == 'both' and leave.state == 'validate1'):
                    responsibles = leave.employee_id.leave_manager_id.partner_id
                # officer
                elif leave.holiday_status_id.leave_validation_type == 'hr' and leave.state == 'validate':
                    responsibles = leave.holiday_status_id.responsible_ids.partner_id
                # both
                elif leave.holiday_status_id.leave_validation_type == 'both' and leave.state == 'validate':
                    responsibles = leave.employee_id.leave_manager_id.partner_id
                    responsibles |= leave.holiday_status_id.responsible_ids.partner_id

                if responsibles:
                    body = self.env._(
                        "%(leave_name)s has been cancelled for the following reason: %(reason)s",
                        leave_name=leave.display_name,
                        reason=Markup("<blockquote>{reason}</blockquote>").format(reason=reason),
                    )
                    leave.message_notify(
                        partner_ids=responsibles.ids,
                        model_description=model_description,
                        subject=self.env._('Cancelled Time Off'),
                        body=body,
                        email_layout_xmlid="mail.mail_notification_layout",
                        subtitles=[leave.display_name],
                    )
        leave_sudo = self.sudo()
        leave_sudo.state = "cancel"
        leave_sudo.activity_update()
        leave_sudo._post_leave_cancel()

    def _post_leave_cancel(self):
        self.meeting_id.active = False
        self._remove_resource_leave()

    def action_documents(self):
        domain = [('id', 'in', self.attachment_ids.ids)]
        return {
            'name': _("Supporting Documents"),
            'type': 'ir.actions.act_window',
            'res_model': 'ir.attachment',
            'context': {'create': False},
            'view_mode': 'kanban',
            'domain': domain
        }

    def _get_next_states_by_state(self):
        self.ensure_one()
        state_result = {
            'confirm': set(),
            'validate1': set(),
            'validate': set(),
            'refuse': set(),
            'cancel': set()
        }
        validation_type = self.validation_type

        user_employees = self.env.user.employee_ids
        is_own_leave = self.employee_id in user_employees
        is_in_past = self.date_from and self.date_from.date() < fields.Date.today()

        is_officer = self.env.user.has_group('hr_holidays.group_hr_holidays_user')
        is_time_off_manager = self.employee_id.leave_manager_id == self.env.user

        if is_own_leave and (not is_in_past or is_officer):
            state_result['validate1'].add('cancel')
            state_result['validate'].add('cancel')
            state_result['refuse'].add('cancel')

        if is_officer:
            if validation_type == 'both':
                state_result['confirm'].add('validate1')
                state_result['refuse'].add('validate1')
                state_result['cancel'].add('validate1')
            state_result['confirm'].update({'validate', 'refuse'})
            state_result['validate1'].update({'confirm', 'validate', 'refuse'})
            state_result['validate'].update({'confirm', 'refuse'})
            state_result['refuse'].update({'confirm', 'validate'})
            state_result['cancel'].update({'confirm', 'validate', 'refuse'})
        elif is_time_off_manager:
            if validation_type != 'hr':
                state_result['confirm'].add('refuse')
                state_result['validate'].add('refuse')
            if validation_type == 'both':
                state_result['confirm'].add('validate1')
                state_result['validate1'].add('refuse')
            elif validation_type == 'manager':
                state_result['confirm'].add('validate')
                state_result['refuse'].add('validate')

        return state_result

    def _check_approval_update(self, state, raise_if_not_possible=True):
        """ Check if target state is achievable. """
        if self.env.is_superuser():
            return True

        is_officer = self.env.user.has_group('hr_holidays.group_hr_holidays_user')

        for holiday in self:
            is_time_off_manager = holiday.employee_id.leave_manager_id == self.env.user
            dict_all_possible_state = holiday._get_next_states_by_state()
            validation_type = holiday.validation_type
            error_message = ""
            # Standard Check
            if holiday.state == state:
                error_message = self.env._('You can\'t do the same action twice.')
            elif state == 'validate1' and validation_type != 'both':
                error_message = self.env._('Not possible state. State Approve is only used for leave needed 2 approvals')
            elif holiday.state == 'cancel':
                error_message = self.env._('A cancelled leave cannot be modified.')
            elif state not in dict_all_possible_state.get(holiday.state, {}):
                if state == 'cancel':
                    error_message = self.env._('You can only cancel your own leave. You can cancel a leave only if this leave \
is approved, validated or refused.')
                elif state == 'confirm':
                    error_message = self.env._('You can\'t reset a leave. Cancel/delete this one and create an other')
                elif state == 'validate1':
                    if not is_time_off_manager:
                        error_message = self.env._('Only a Time Off Officer/Manager can approve a leave.')
                    else:
                        error_message = self.env._('You can\'t approve a validated leave.')
                elif state == "validate":
                    if not is_time_off_manager:
                        error_message = self.env._('Only a Time Off Officer/Manager can validate a leave.')
                    elif holiday.state == "refuse":
                        error_message = self.env._('You can\'t approve this refused leave.')
                    else:
                        error_message = self.env._('You can only validate a leave with validation by Time Off Manager.')
                elif state == "refuse":
                    if not is_time_off_manager:
                        error_message = self.env._('Only a Time Off Officer/Manager can refuse a leave.')
                    else:
                        error_message = self.env._('You can\'t refuse a leave with validation by Time Off Officer.')
            elif state != "cancel":
                try:
                    holiday.check_access('write')
                except UserError as e:
                    if raise_if_not_possible:
                        raise UserError(e)
                    return False
                else:
                    continue
            if error_message:
                if raise_if_not_possible:
                    raise UserError(error_message)
                return False
        return True

    @api.model
    def open_pending_requests(self):
        user_employee = self.env.user.employee_id
        employee = self.env['hr.employee']._get_contextual_employee()
        context = {'search_default_approve': True, 'search_default_second_approval': True}
        domain = []
        if employee != user_employee:
            view_name = 'hr_holidays.hr_leave_allocation_view_tree'
            context.update({'search_default_employee_id': employee.id})
        else:
            view_name = 'hr_holidays.hr_leave_allocation_view_tree_my'
            domain = [('employee_id', '=', employee.id)]
        return {
            'name': _('Allocation Requests'),
            'type': 'ir.actions.act_window',
            'res_model': 'hr.leave.allocation',
            'views': [[self.env.ref(view_name).id, 'list']],
            'domain': domain,
            'context': context,
        }
    # ------------------------------------------------------------
    # Activity methods
    # ------------------------------------------------------------

    def _get_responsible_for_approval(self):
        self.ensure_one()

        responsible = self.env['res.users']
        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_ids:
                responsible = self.holiday_status_id.responsible_ids
        return responsible

    def _get_to_clean_activities(self):
        return ['hr_holidays.mail_act_leave_approval', 'hr_holidays.mail_act_leave_second_approval']

    def activity_update(self):
        if self.env.context.get('mail_activity_automation_skip'):
            return

        to_clean, to_do, to_do_confirm_activity = self.env['hr.leave'], self.env['hr.leave'], self.env['hr.leave']
        activity_vals = []
        today = fields.Date.today()
        model_id = self.env['ir.model']._get_id('hr.leave')
        confirm_activity = self.env.ref('hr_holidays.mail_act_leave_approval')
        approval_activity = self.env.ref('hr_holidays.mail_act_leave_second_approval')
        for holiday in self:
            if holiday.state in ['confirm', 'validate1']:
                if holiday.holiday_status_id.leave_validation_type != 'no_validation':
                    if holiday.state == 'confirm':
                        activity_type = confirm_activity
                        note = _(
                            'New %(leave_type)s Request created by %(user)s',
                            leave_type=holiday.holiday_status_id.name,
                            user=holiday.create_uid.name,
                        )
                    else:
                        activity_type = approval_activity
                        note = _(
                            'Second approval request for %(leave_type)s',
                            leave_type=holiday.holiday_status_id.name,
                        )
                        to_do_confirm_activity += holiday
                    user_ids = holiday.sudo()._get_responsible_for_approval().ids
                    for user_id in user_ids:
                        date_deadline = (
                            (holiday.date_from -
                             relativedelta(**{activity_type.delay_unit or 'days': activity_type.delay_count or 0})).date()
                            if holiday.date_from else today)
                        if date_deadline < today:
                            date_deadline = today
                        activity_vals.append({
                            'activity_type_id': activity_type.id,
                            'automated': True,
                            'date_deadline': date_deadline,
                            'note': note,
                            'user_id': user_id,
                            'res_id': holiday.id,
                            'res_model_id': model_id,
                        })
            elif holiday.state == 'validate':
                to_do |= holiday
            elif holiday.state in ['refuse', 'cancel']:
                to_clean |= holiday
        if to_clean:
            to_clean.activity_unlink(self._get_to_clean_activities(), only_automated=False)
        if to_do_confirm_activity:
            to_do_confirm_activity.activity_feedback(['hr_holidays.mail_act_leave_approval'])
        if to_do:
            to_do.activity_feedback(['hr_holidays.mail_act_leave_approval', 'hr_holidays.mail_act_leave_second_approval'])
        self.env['mail.activity'].with_context(short_name=False).create(activity_vals)

    ####################################################
    # Messaging methods
    ####################################################

    def _notify_change(self, message, subtype_xmlid='mail.mt_note'):
        for leave in self:
            leave.message_post(body=message, subtype_xmlid=subtype_xmlid)

            recipient = None
            if leave.user_id:
                recipient = leave.user_id.partner_id.id
            elif leave.employee_id:
                recipient = leave.employee_id.work_contact_id.id

            if recipient:
                self.env['mail.thread'].sudo().message_notify(
                    body=message,
                    partner_ids=[recipient],
                    subject=_('Your Time Off'),
                )

    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()._track_subtype(init_values)

    def message_subscribe(self, partner_ids=None, subtype_ids=None):
        # due to record rule can not allow to add follower and mention on validated leave so subscribe through sudo
        if any(holiday.state in ['validate', 'validate1'] for holiday in self):
            self.check_access('read')
            return super(HrLeave, self.sudo()).message_subscribe(partner_ids=partner_ids, subtype_ids=subtype_ids)
        return super().message_subscribe(partner_ids=partner_ids, subtype_ids=subtype_ids)

    @api.model
    def get_unusual_days(self, date_from, date_to=None):
        employee_id = self.env.context.get('employee_id', False)
        employee = self.env['hr.employee'].browse(employee_id) if employee_id else self.env.user.employee_id
        return employee.sudo(False)._get_unusual_days(date_from, date_to)

    def _to_utc(self, date, hour, resource):
        hour = float_to_time(float(hour))
        holiday_tz = pytz.timezone(resource.tz or self.env.user.tz or 'UTC')
        return holiday_tz.localize(datetime.combine(date, hour)).astimezone(pytz.UTC).replace(tzinfo=None)

    def _get_hour_from_to(self, request_date_from, request_date_to, day_period=None):
        """
        Return the hour_from and hour_to for the given request dates, based on
        the resource calendar.

        If there are no attendances on the exact days of the request, return
        the earliest hour_from and latest hour_to that exist in the schedule.
        """
        calendar = self.resource_calendar_id
        if not calendar:
            return (0, 24)
        calendar.ensure_one()

        hour_from, _ = calendar._get_hours_for_date(request_date_from, day_period)
        _, hour_to = calendar._get_hours_for_date(request_date_to, day_period)

        return (hour_from, hour_to)

    ####################################################
    # Cron methods
    ####################################################

    @api.model
    def _cancel_invalid_leaves(self):
        inspected_date = fields.Date.today() + timedelta(days=31)
        start_datetime = datetime.combine(fields.Date.today(), datetime.min.time())
        end_datetime = datetime.combine(inspected_date, datetime.max.time())
        concerned_leaves = self.search([
            ('date_from', '>=', start_datetime),
            ('date_from', '<=', end_datetime),
            ('state', 'in', ['confirm', 'validate1', 'validate']),
        ], order='date_from desc')
        accrual_allocations = self.env['hr.leave.allocation'].search([
            ('employee_id', 'in', concerned_leaves.employee_id.ids),
            ('holiday_status_id', 'in', concerned_leaves.holiday_status_id.ids),
            ('allocation_type', '=', 'accrual'),
            ('date_from', '<=', end_datetime),
            '|',
            ('date_to', '>=', start_datetime),
            ('date_to', '=', False),
        ])
        # only take leaves linked to accruals
        concerned_leaves = concerned_leaves\
            .filtered(lambda leave: leave.holiday_status_id in accrual_allocations.holiday_status_id)\
            .sorted('date_from', reverse=True)
        reason = _("the accruated amount is insufficient for that duration.")
        for leave in concerned_leaves:
            leave_type = leave.holiday_status_id
            date = leave.date_from.date()
            leave_type_data = leave_type.get_allocation_data(leave.employee_id, date)
            if not leave_type_data[leave.employee_id][0][1]['max_leaves']:
                leave._force_cancel(reason, 'mail.mt_note')
                continue
            exceeding_duration = leave_type_data[leave.employee_id][0][1]['total_virtual_excess']
            excess_limit = leave_type.max_allowed_negative if leave_type.allows_negative else 0
            if exceeding_duration <= excess_limit:
                continue
            leave._force_cancel(reason, 'mail.mt_note')
