diff --git a/addons/hr/models/hr_employee.py b/addons/hr/models/hr_employee.py
index 65fb1b02041..78e280e4054 100644
--- a/addons/hr/models/hr_employee.py
+++ b/addons/hr/models/hr_employee.py
@@ -507,6 +507,10 @@ class HrEmployeePrivate(models.Model):
# Returns a dict {employee_id: tz}
return {emp.id: emp._get_tz() for emp in self}
+ def _get_calendar_attendances(self, date_from, date_to):
+ self.ensure_one()
+ return self.resource_calendar_id.get_work_duration_data(date_from, date_to)
+
# ---------------------------------------------------------
# Business Methods
# ---------------------------------------------------------
diff --git a/addons/hr_contract/models/hr_employee.py b/addons/hr_contract/models/hr_employee.py
index b78750b7e13..0a2c61f99ea 100644
--- a/addons/hr_contract/models/hr_employee.py
+++ b/addons/hr_contract/models/hr_employee.py
@@ -140,6 +140,13 @@ class Employee(models.Model):
))
return unusual_days
+ def _get_calendar_attendances(self, date_from, date_to):
+ self.ensure_one()
+ valid_contracts = self.sudo()._get_contracts(date_from, date_to, states=['open', 'close'])
+ if not valid_contracts:
+ return super()._get_calendar_attendances(date_from, date_to)
+ return valid_contracts.resource_calendar_id.get_work_duration_data(date_from, date_to)
+
def write(self, vals):
res = super().write(vals)
if vals.get('contract_id'):
diff --git a/addons/hr_holidays/data/ir_cron_data.xml b/addons/hr_holidays/data/ir_cron_data.xml
index 4dca974e8ae..52acb27ecd5 100644
--- a/addons/hr_holidays/data/ir_cron_data.xml
+++ b/addons/hr_holidays/data/ir_cron_data.xml
@@ -11,4 +11,15 @@
-1
+
+
+ Time Off: Cancel invalid leaves
+
+ code
+ model._cancel_invalid_leaves()
+ 1
+ days
+ -1
+
+
diff --git a/addons/hr_holidays/models/hr_employee.py b/addons/hr_holidays/models/hr_employee.py
index c30102e999d..ff236d8749e 100644
--- a/addons/hr_holidays/models/hr_employee.py
+++ b/addons/hr_holidays/models/hr_employee.py
@@ -1,7 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-import datetime
+from datetime import datetime, date, time
+from collections import defaultdict
from dateutil.relativedelta import relativedelta
import pytz
@@ -82,7 +83,7 @@ class HrEmployeeBase(models.AbstractModel):
def _compute_allocation_count(self):
# Don't get allocations that are expired
- current_date = datetime.date.today()
+ current_date = date.today()
data = self.env['hr.leave.allocation']._read_group([
('employee_id', 'in', self.ids),
('holiday_status_id.active', '=', True),
@@ -102,15 +103,15 @@ class HrEmployeeBase(models.AbstractModel):
def _compute_allocation_remaining_display(self):
allocations = self.env['hr.leave.allocation'].search([('employee_id', 'in', self.ids)])
- leaves_taken = allocations.holiday_status_id._get_employees_days_per_allocation(self.ids)
+ leaves_taken = self._get_consumed_leaves(allocations.holiday_status_id)[0]
for employee in self:
employee_remaining_leaves = 0
- for leave_type in leaves_taken[employee.id]:
+ for leave_type in leaves_taken[employee]:
if leave_type.requires_allocation == 'no':
continue
- for allocation in leaves_taken[employee.id][leave_type]:
+ for allocation in leaves_taken[employee][leave_type]:
if allocation:
- virtual_remaining_leaves = leaves_taken[employee.id][leave_type][allocation]['virtual_remaining_leaves']
+ virtual_remaining_leaves = leaves_taken[employee][leave_type][allocation]['virtual_remaining_leaves']
employee_remaining_leaves += virtual_remaining_leaves\
if leave_type.request_unit in ['day', 'half_day']\
else virtual_remaining_leaves / (employee.resource_calendar_id.hours_per_day or HOURS_PER_DAY)
@@ -171,7 +172,7 @@ class HrEmployeeBase(models.AbstractModel):
raise UserError(_('Operation not supported'))
# This search is only used for the 'Absent Today' filter however
# this only returns employees that are absent right now.
- today_date = datetime.datetime.utcnow().date()
+ today_date = datetime.utcnow().date()
today_start = fields.Datetime.to_string(today_date)
today_end = fields.Datetime.to_string(today_date + relativedelta(hours=23, minutes=59, seconds=59))
holidays = self.env['hr.leave'].sudo().search([
@@ -281,11 +282,6 @@ class HrEmployee(models.Model):
},
}
- def _get_contextual_employee(self):
- if self.env.context.get('employee_id'):
- return self.browse(self.env.context['employee_id'])
- return self.env.user.employee_id
-
def _is_leave_user(self):
return self == self.env.user.employee_id and self.user_has_groups('hr_holidays.group_hr_holidays_user')
@@ -317,10 +313,10 @@ class HrEmployee(models.Model):
return list(map(lambda bh: {
'id': -bh.id,
'colorIndex': 0,
- 'end': datetime.datetime.combine(bh.date_to.astimezone(employee_tz), datetime.datetime.max.time()).isoformat(),
+ 'end': datetime.combine(bh.date_to.astimezone(employee_tz), datetime.max.time()).isoformat(),
'endType': "datetime",
'isAllDay': True,
- 'start': datetime.datetime.combine(bh.date_from.astimezone(employee_tz), datetime.datetime.min.time()).isoformat(),
+ 'start': datetime.combine(bh.date_from.astimezone(employee_tz), datetime.min.time()).isoformat(),
'startType': "datetime",
'title': bh.name,
}, public_holidays))
@@ -350,10 +346,10 @@ class HrEmployee(models.Model):
return list(map(lambda sd: {
'id': -sd.id,
'colorIndex': sd.color,
- 'end': datetime.datetime.combine(sd.end_date, datetime.datetime.max.time()).isoformat(),
+ 'end': datetime.combine(sd.end_date, datetime.max.time()).isoformat(),
'endType': "datetime",
'isAllDay': True,
- 'start': datetime.datetime.combine(sd.start_date, datetime.datetime.min.time()).isoformat(),
+ 'start': datetime.combine(sd.start_date, datetime.min.time()).isoformat(),
'startType': "datetime",
'title': sd.name,
}, mandatory_days))
@@ -382,3 +378,184 @@ class HrEmployee(models.Model):
domain += [('department_ids', '=', False)]
return self.env['hr.leave.mandatory.day'].search(domain)
+
+ @api.model
+ def _get_contextual_employee(self):
+ ctx = self.env.context
+ return self.browse(ctx.get('employee_id') or ctx.get('default_employee_id')) or self.env.user.employee_id
+
+ def _get_consumed_leaves(self, leave_types, target_date=False, ignore_future=False):
+ employees = self or self._get_contextual_employee()
+ leaves_domain = [
+ ('holiday_status_id', 'in', leave_types.ids),
+ ('employee_id', 'in', employees.ids),
+ ('state', 'in', ['confirm', 'validate1', 'validate']),
+ ]
+ if not target_date:
+ target_date = fields.Date.today()
+ if ignore_future:
+ leaves_domain.append(('date_from', '<=', target_date))
+ leaves = self.env['hr.leave'].search(leaves_domain)
+ leaves_per_employee_type = defaultdict(lambda: defaultdict(lambda: self.env['hr.leave']))
+ for leave in leaves:
+ leaves_per_employee_type[leave.employee_id][leave.holiday_status_id] |= leave
+
+ allocations = self.env['hr.leave.allocation'].with_context(active_test=False).search([
+ ('employee_id', 'in', employees.ids),
+ ('holiday_status_id', 'in', leave_types.ids),
+ ('state', '=', 'validate'),
+ ]).filtered(lambda al: al.active or not al.employee_id.active)
+ allocations_per_employee_type = defaultdict(lambda: defaultdict(lambda: self.env['hr.leave.allocation']))
+ for allocation in allocations:
+ allocations_per_employee_type[allocation.employee_id][allocation.holiday_status_id] |= allocation
+
+ # allocation_leaves_consumed is a tuple of two dictionnaries.
+ # 1) The first is a dictionary to map the number of days/hours of leaves taken per allocation
+ # The structure is the following:
+ # - KEYS:
+ # allocation_leaves_consumed
+ # |--employee_id
+ # |--holiday_status_id
+ # |--allocation
+ # |--virtual_leaves_taken
+ # |--leaves_taken
+ # |--virtual_remaining_leaves
+ # |--remaining_leaves
+ # |--max_leaves
+ # |--accrual_bonus
+ # - VALUES:
+ # Integer representing the number of (virtual) remaining leaves, (virtual) leaves taken or max leaves for each allocation.
+ # leaves_taken and remaining_leaves only take into account validated leaves, while the "virtual" equivalent are
+ # also based on leaves in "confirm" or "validate1" state.
+ # The unit is in hour or days depending on the leave type request unit
+ # 2) The second is a dictionary mapping the remaining days per employee and per leave type that are either
+ # not taken into account by the allocations, mainly because accruals don't take future leaves into account.
+ # This is used to warn the user if the leaves they takes bring them above their available limit.
+ # - KEYS:
+ # allocation_leaves_consumed
+ # |--employee_id
+ # |--holiday_status_id
+ # |--to_recheck_leaves
+ # |--excess_days
+ # |--exceeding_duration
+ # - VALUES:
+ # "to_recheck_leaves" stores every leave that is not yet taken into account by the "allocation_leaves_consumed" dictionary.
+ # "excess_days" represents the excess amount that somehow isn't taken into account by the first dictionary.
+ # "exceeding_duration" sum up the to_recheck_leaves duration and compares it to the maximum allocated for that time period.
+ allocations_leaves_consumed = defaultdict(lambda: defaultdict(lambda: defaultdict(lambda: defaultdict(lambda: 0))))
+
+ to_recheck_leaves_per_leave_type = defaultdict(lambda:
+ defaultdict(lambda: {
+ 'excess_days': 0,
+ 'exceeding_duration': 0,
+ 'to_recheck_leaves': self.env['hr.leave']
+ })
+ )
+ for allocation in allocations:
+ allocation_data = allocations_leaves_consumed[allocation.employee_id][allocation.holiday_status_id][allocation]
+ future_leaves = 0
+ if allocation.allocation_type == 'accrual':
+ future_leaves = allocation._get_future_leaves_on(target_date)
+ max_leaves = allocation.number_of_hours_display\
+ if allocation.type_request_unit in ['hour']\
+ else allocation.number_of_days_display
+ max_leaves += future_leaves
+ allocation_data.update({
+ 'max_leaves': max_leaves,
+ 'accrual_bonus': future_leaves,
+ 'virtual_remaining_leaves': max_leaves,
+ 'remaining_leaves': max_leaves,
+ 'leaves_taken': 0,
+ 'virtual_leaves_taken': 0,
+ })
+
+ for employee in employees:
+ for leave_type in leave_types:
+ allocations_with_date_to = self.env['hr.leave.allocation']
+ allocations_without_date_to = self.env['hr.leave.allocation']
+ for leave_allocation in allocations_per_employee_type[employee][leave_type]:
+ if leave_allocation.date_to:
+ allocations_with_date_to |= leave_allocation
+ else:
+ allocations_without_date_to |= leave_allocation
+ sorted_leave_allocations = allocations_with_date_to.sorted(key='date_to') + allocations_without_date_to
+
+ if leave_type.request_unit in ['day', 'half_day']:
+ leave_duration_field = 'number_of_days'
+ leave_unit = 'days'
+ else:
+ leave_duration_field = 'number_of_hours_display'
+ leave_unit = 'hours'
+
+ leave_type_data = allocations_leaves_consumed[employee][leave_type]
+ for leave in leaves_per_employee_type[employee][leave_type].sorted('date_from'):
+ leave_duration = leave[leave_duration_field]
+ if leave_type.requires_allocation == 'yes':
+ for allocation in sorted_leave_allocations:
+ # We don't want to include future leaves linked to accruals into the total count of available leaves.
+ # However, we'll need to check if those leaves take more than what will be accrued in total of those days
+ # to give a warning if the total exceeds what will be accrued.
+ if allocation.allocation_type == 'accrual' and leave.date_from.date() > target_date:
+ to_recheck_leaves_per_leave_type[employee][leave_type]['to_recheck_leaves'] |= leave
+ continue
+ interval_start = max(
+ leave.date_from,
+ datetime.combine(allocation.date_from, time.min)
+ ).replace(tzinfo=pytz.UTC)
+ interval_end = min(
+ leave.date_to,
+ datetime.combine(allocation.date_to, time.max)
+ if allocation.date_to else leave.date_to
+ ).replace(tzinfo=pytz.UTC)
+ duration_info = employee._get_calendar_attendances(interval_start, interval_end)
+ max_allowed_duration = min(
+ duration_info['hours' if leave_unit == 'hours' else 'days'],
+ leave_type_data[allocation]['virtual_remaining_leaves']
+ )
+
+ if not max_allowed_duration:
+ continue
+
+ allocated_time = min(max_allowed_duration, leave_duration)
+ leave_type_data[allocation]['virtual_leaves_taken'] += allocated_time
+ leave_type_data[allocation]['virtual_remaining_leaves'] -= allocated_time
+ if leave.state == 'validate':
+ leave_type_data[allocation]['leaves_taken'] += allocated_time
+ leave_type_data[allocation]['remaining_leaves'] -= allocated_time
+
+ leave_duration -= allocated_time
+ if not leave_duration:
+ break
+ if round(leave_duration, 2) > 0:
+ to_recheck_leaves_per_leave_type[employee][leave_type]['excess_days'] += leave_duration
+ else:
+ if leave_unit == 'hour':
+ allocated_time = leave.number_of_hours_display
+ else:
+ allocated_time = leave.number_of_days_display
+ leave_type_data[False]['virtual_leaves_taken'] += allocated_time
+ leave_type_data[False]['virtual_remaining_leaves'] = 0
+ leave_type_data[False]['remaining_leaves'] = 0
+ if leave.state == 'validate':
+ leave_type_data[False]['leaves_taken'] += allocated_time
+
+ for employee in to_recheck_leaves_per_leave_type:
+ for leave_type in to_recheck_leaves_per_leave_type[employee]:
+ content = to_recheck_leaves_per_leave_type[employee][leave_type]
+ consumed_content = allocations_leaves_consumed[employee][leave_type]
+ if content['to_recheck_leaves']:
+ date_to_simulate = max(content['to_recheck_leaves'].mapped('date_from')).date()
+ latest_accrual_bonus = 0
+ date_accrual_bonus = 0
+ virtual_remaining = 0
+ additional_leaves_duration = 0
+ for allocation in consumed_content:
+ latest_accrual_bonus += allocation._get_future_leaves_on(date_to_simulate)
+ date_accrual_bonus += consumed_content[allocation]['accrual_bonus']
+ virtual_remaining += consumed_content[allocation]['virtual_remaining_leaves']
+ for leave in content['to_recheck_leaves']:
+ additional_leaves_duration += leave.number_of_hours if leave_type.request_unit == 'hours' else leave.number_of_days
+ latest_remaining = virtual_remaining - date_accrual_bonus + latest_accrual_bonus
+ content['exceeding_duration'] = round(min(0, latest_remaining - additional_leaves_duration), 2)
+
+ return (allocations_leaves_consumed, to_recheck_leaves_per_leave_type)
diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py
index f5e984882cf..c1a9ad56b6b 100644
--- a/addons/hr_holidays/models/hr_leave.py
+++ b/addons/hr_holidays/models/hr_leave.py
@@ -17,8 +17,8 @@ from odoo import api, Command, fields, models, tools
from odoo.addons.base.models.res_partner import _tz_get
from odoo.addons.resource.models.utils import float_to_time, HOURS_PER_DAY
from odoo.exceptions import AccessError, UserError, ValidationError
-from odoo.tools import float_compare, format_date
-from odoo.tools.float_utils import float_round
+from odoo.tools.float_utils import float_round, float_compare
+from odoo.tools.misc import format_date
from odoo.tools.translate import _
from odoo.osv import expression
@@ -116,12 +116,13 @@ class HolidaysRequest(models.Model):
# 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_user')
- state = fields.Selection([
- ('draft', 'To Submit'),
- ('confirm', 'To Approve'),
- ('refuse', 'Refused'),
- ('validate1', 'Second Approval'),
- ('validate', 'Approved')
+ state = fields.Selection(
+ [
+ ('draft', 'To Submit'),
+ ('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." +
@@ -132,10 +133,16 @@ class HolidaysRequest(models.Model):
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,
- domain="[('company_id', '?=', employee_company_id), '|', ('requires_allocation', '=', 'no'), ('has_valid_allocation', '=', True)]", tracking=True)
- holiday_allocation_id = fields.Many2one(
- 'hr.leave.allocation', compute='_compute_from_holiday_status_id', string="Allocation", store=True, readonly=False)
+ "hr.leave.type", compute='_compute_from_employee_id',
+ store=True, string="Time Off Type",
+ required=True, readonly=False,
+ domain="""[
+ ('company_id', 'in', [employee_company_id, False]),
+ '|',
+ ('requires_allocation', '=', 'no'),
+ ('has_valid_allocation', '=', True),
+ ]""",
+ tracking=True)
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
@@ -691,34 +698,6 @@ class HolidaysRequest(models.Model):
raise ValidationError(_('You can not set two time off that overlap on the same day for the same employees.\nExisting time off:\n%s') %
('\n'.join(conflicting_holidays_strings)))
- @api.constrains('state', 'number_of_days', 'holiday_status_id')
- def _check_holidays(self):
- for holiday in self:
- mapped_days_date = holiday.date_from.date() or None
- mapped_days = self.holiday_status_id.get_employees_days((holiday.employee_id | holiday.sudo().employee_ids).ids, mapped_days_date)
- if holiday.holiday_type != 'employee'\
- or not holiday.employee_id and not holiday.sudo().employee_ids\
- or holiday.holiday_status_id.requires_allocation == 'no':
- continue
- if holiday.employee_id:
- 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.'))
- else:
- unallocated_employees = []
- for employee in holiday.sudo().employee_ids:
- leave_days = mapped_days[employee.id][holiday.holiday_status_id.id]
- if float_compare(leave_days['remaining_leaves'], self.number_of_days, precision_digits=2) == -1\
- or float_compare(leave_days['virtual_remaining_leaves'], self.number_of_days, precision_digits=2) == -1:
- unallocated_employees.append(employee.name)
- if unallocated_employees:
- 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.')
- + _('\nThe employees that lack allocation days are:\n%s',
- (', '.join(unallocated_employees))))
-
@api.constrains('date_from', 'date_to', 'employee_id')
def _check_date_state(self):
if self.env.context.get('leave_skip_state_check'):
@@ -727,6 +706,18 @@ class HolidaysRequest(models.Model):
if holiday.state in ['cancel', 'refuse', 'validate1', 'validate']:
raise ValidationError(_("This modification is not allowed in the current state."))
+ @api.constrains('date_from', 'date_to')
+ def _check_validity(self):
+ for leave in self:
+ leave_type = leave.holiday_status_id
+ if leave_type.requires_allocation == 'no':
+ continue
+ employee = leave.employee_id
+ date_from = leave.date_from.date()
+ leave_data = leave_type.get_allocation_data(employee, date=date_from)
+ if leave_data[employee][0][1]['excess_days']:
+ raise ValidationError(_('The allocation configuration does not allow you to take this leave.'))
+
####################################################
# ORM Overrides methods
####################################################
@@ -767,7 +758,7 @@ class HolidaysRequest(models.Model):
leave.display_name = _("%(leave_type)s: %(duration).2f hours on %(date)s",
leave_type=time_off_type_display,
duration=leave.number_of_hours_display,
- date=fields.Date.to_string(date_from_utc) or "",
+ date=display_date,
)
elif not time_off_type_display:
leave.display_name = _("%(person)s: %(duration).2f hours on %(date)s",
@@ -1110,7 +1101,6 @@ class HolidaysRequest(models.Model):
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):
user_tz = timezone(holiday.tz)
@@ -1614,3 +1604,38 @@ class HolidaysRequest(models.Model):
attendance_to = next((att for att in reversed(attendances) if int(att.dayofweek) <= request_date_to.weekday()), attendances[-1] if attendances else default_value)
return (attendance_from, attendance_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:
+ to_recheck_leaves_per_leave_type = concerned_leaves.employee_id._get_consumed_leaves(leave.holiday_status_id)[1]
+ exceeding_duration = to_recheck_leaves_per_leave_type[leave.employee_id][leave.holiday_status_id]['exceeding_duration']
+ if not exceeding_duration:
+ continue
+ leave._force_cancel(reason, 'mail.mt_note')
diff --git a/addons/hr_holidays/models/hr_leave_allocation.py b/addons/hr_holidays/models/hr_leave_allocation.py
index c1166c1429f..0ab555b8cc2 100644
--- a/addons/hr_holidays/models/hr_leave_allocation.py
+++ b/addons/hr_holidays/models/hr_leave_allocation.py
@@ -37,20 +37,23 @@ class HolidaysAllocation(models.Model):
return [('requires_allocation', '=', 'yes')]
return [('employee_requests', '=', 'yes')]
- name = fields.Char('Description', compute='_compute_description', inverse='_inverse_description', search='_search_description', compute_sudo=False)
+ name = fields.Char(
+ string='Description',
+ compute='_compute_description',
+ inverse='_inverse_description',
+ search='_search_description',
+ compute_sudo=False)
name_validity = fields.Char('Description with validity', compute='_compute_description_validity')
active = fields.Boolean(default=True)
private_name = fields.Char('Allocation Description', groups='hr_holidays.group_hr_holidays_user')
state = fields.Selection([
- ('draft', 'To Submit'),
- ('cancel', 'Cancelled'),
('confirm', 'To Approve'),
('refuse', 'Refused'),
- ('validate', 'Approved')
- ], string='Status', readonly=True, tracking=True, copy=False, default='draft',
- help="The status is set to 'To Submit', when an allocation request is created." +
- "\nThe status is 'To Approve', when an allocation request is confirmed by user." +
- "\nThe status is 'Refused', when an allocation request is refused by manager." +
+ ('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.Date('Start Date', index=True, copy=False, default=fields.Date.context_today,
tracking=True, required=True)
@@ -84,7 +87,6 @@ class HolidaysAllocation(models.Model):
'hr.employee', string='First Approval', readonly=True, copy=False,
help='This area is automatically filled by the user who validates the allocation')
validation_type = fields.Selection(string='Validation Type', related='holiday_status_id.allocation_validation_type', readonly=True)
- can_reset = fields.Boolean('Can reset', compute='_compute_can_reset')
can_approve = fields.Boolean('Can Approve', compute='_compute_can_approve')
type_request_unit = fields.Selection([
('hour', 'Hours'),
@@ -123,10 +125,11 @@ class HolidaysAllocation(models.Model):
('accrual', 'Accrual Allocation')
], string="Allocation Type", default="regular", required=True, readonly=True)
is_officer = fields.Boolean(compute='_compute_is_officer')
- accrual_plan_id = fields.Many2one('hr.leave.accrual.plan', compute="_compute_from_holiday_status_id", store=True, readonly=False, domain="['|', ('time_off_type_id', '=', False), ('time_off_type_id', '=', holiday_status_id)]", tracking=True)
+ accrual_plan_id = fields.Many2one('hr.leave.accrual.plan',
+ compute="_compute_from_holiday_status_id", store=True, readonly=False, tracking=True,
+ domain="['|', ('time_off_type_id', '=', False), ('time_off_type_id', '=', holiday_status_id)]")
max_leaves = fields.Float(compute='_compute_leaves')
leaves_taken = fields.Float(compute='_compute_leaves', string='Time off Taken')
- taken_leave_ids = fields.One2many('hr.leave', 'holiday_allocation_id', domain="[('state', 'in', ['confirm', 'validate1', 'validate'])]")
has_accrual_plan = fields.Boolean(compute='_compute_has_accrual_plan', string='Accrual Plan Available')
_sql_constraints = [
@@ -205,12 +208,13 @@ class HolidaysAllocation(models.Model):
name_validity = _("%s (from %s to No Limit)", allocation.name, allocation.date_from.strftime("%b %d %Y"))
allocation.name_validity = name_validity
- @api.depends('employee_id', 'holiday_status_id', 'taken_leave_ids.number_of_days', 'taken_leave_ids.state')
+ @api.depends('employee_id', 'holiday_status_id')
def _compute_leaves(self):
- employee_days_per_allocation = self.holiday_status_id.with_context(ignore_future=True)._get_employees_days_per_allocation(self.employee_id.ids)
+ date_from = fields.Date.from_string(self._context['default_date_from']) if 'default_date_from' in self._context else fields.Date.today()
+ employee_days_per_allocation = self.employee_id._get_consumed_leaves(self.holiday_status_id, date_from, ignore_future=True)[0]
for allocation in self:
allocation.max_leaves = allocation.number_of_hours_display if allocation.type_request_unit == 'hour' else allocation.number_of_days
- allocation.leaves_taken = employee_days_per_allocation[allocation.employee_id.id][allocation.holiday_status_id][allocation]['leaves_taken']
+ allocation.leaves_taken = employee_days_per_allocation[allocation.employee_id][allocation.holiday_status_id][allocation]['leaves_taken']
@api.depends('number_of_days')
def _compute_number_of_days_display(self):
@@ -235,16 +239,6 @@ class HolidaysAllocation(models.Model):
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:
@@ -308,14 +302,14 @@ class HolidaysAllocation(models.Model):
@api.depends('accrual_plan_id')
def _compute_holiday_status_id(self):
default_holiday_status_id = None
- for holiday in self:
- if not holiday.holiday_status_id:
- if holiday.accrual_plan_id:
- holiday.holiday_status_id = holiday.accrual_plan_id.time_off_type_id
+ for allocation in self:
+ if not allocation.holiday_status_id:
+ if allocation.accrual_plan_id:
+ allocation.holiday_status_id = allocation.accrual_plan_id.time_off_type_id
else:
if not default_holiday_status_id: # fetch when we need it
default_holiday_status_id = self._default_holiday_status_id()
- holiday.holiday_status_id = default_holiday_status_id
+ allocation.holiday_status_id = default_holiday_status_id
@api.depends('holiday_status_id', 'allocation_type', 'number_of_hours_display', 'number_of_days_display', 'date_to')
def _compute_from_holiday_status_id(self):
@@ -446,14 +440,14 @@ class HolidaysAllocation(models.Model):
period_prorata = min(1, call_days / period_days) if period_days else 1
return added_value * period_prorata
- def _process_accrual_plans(self, date_to=False, force_period=False):
+ def _process_accrual_plans(self, date_to=False, force_period=False, log=True):
"""
This method is part of the cron's process.
The goal of this method is to retroactively apply accrual plan levels and progress from nextcall to date_to or today.
If force_period is set, the accrual will run until date_to in a prorated way (used for end of year accrual actions).
"""
date_to = date_to or fields.Date.today()
- first_allocation = _("""This allocation have already ran once, any modification won't be effective to the days allocated to the employee. If you need to change the configuration of the allocation, cancel and create a new one.""")
+ first_allocation = _("""This allocation have already ran once, any modification won't be effective to the days allocated to the employee. If you need to change the configuration of the allocation, delete and create a new one.""")
for allocation in self:
level_ids = allocation.accrual_plan_id.level_ids.sorted('sequence')
if not level_ids:
@@ -478,7 +472,8 @@ class HolidaysAllocation(models.Model):
if len(level_ids) > 1:
second_level_start_date = allocation.date_from + get_timedelta(level_ids[1].start_count, level_ids[1].start_type)
allocation.nextcall = min(second_level_start_date, allocation.nextcall)
- allocation._message_log(body=first_allocation)
+ if log:
+ allocation._message_log(body=first_allocation)
(current_level, current_level_idx) = (False, 0)
current_level_maximum_leave = 0.0
# all subsequent runs, at every loop:
@@ -550,6 +545,25 @@ class HolidaysAllocation(models.Model):
'|', ('nextcall', '=', False), ('nextcall', '<=', today)])
allocations._process_accrual_plans()
+ def _get_future_leaves_on(self, accrual_date):
+ # As computing future accrual allocation days automatically updates the allocation,
+ # We need to create a temporary copy of that allocation to return the difference in number of days
+ # to see how much more days will be allocated from now until that date.
+ self.ensure_one()
+ if not accrual_date or accrual_date <= date.today():
+ return 0
+
+ if not (self.accrual_plan_id
+ and self.state == 'validate'
+ and self.allocation_type == 'accrual'
+ and (not self.date_to or self.date_to > accrual_date)
+ and (not self.nextcall or self.nextcall <= accrual_date)):
+ return 0
+
+ fake_allocation = self.env['hr.leave.allocation'].new(origin=self)
+ fake_allocation.sudo()._process_accrual_plans(accrual_date, log=False)
+ return round((fake_allocation.number_of_days - self.number_of_days), 2)
+
####################################################
# ORM Overrides methods
####################################################
@@ -578,8 +592,13 @@ class HolidaysAllocation(models.Model):
target = allocation.category_id.name
elif allocation.employee_id:
target = allocation.employee_id.name
- else:
+ elif len(allocation.employee_ids) <= 3:
target = ', '.join(allocation.employee_ids.sudo().mapped('name'))
+ else:
+ target = _('%(first)s, %(second)s and %(amount)s others',
+ first=allocation.employee_ids[0].sudo().name,
+ second=allocation.employee_ids[1].sudo().name,
+ amount=len(allocation.employee_ids) - 2)
allocation.display_name = _("Allocation of %s: %.2f %s to %s",
allocation.holiday_status_id.sudo().name,
@@ -604,27 +623,24 @@ class HolidaysAllocation(models.Model):
values.update({'department_id': self.env['hr.employee'].browse(employee_id).department_id.id})
if 'lastcall' not in values:
values['lastcall'] = fields.Date.today()
- holidays = super(HolidaysAllocation, self.with_context(mail_create_nosubscribe=True)).create(vals_list)
- created_from_dashboard = self.env.context.get('created_from_dashboard')
- for holiday in holidays:
+ allocations = super(HolidaysAllocation, self.with_context(mail_create_nosubscribe=True)).create(vals_list)
+ for allocation in allocations:
partners_to_subscribe = set()
- if holiday.employee_id.user_id:
- partners_to_subscribe.add(holiday.employee_id.user_id.partner_id.id)
- if holiday.validation_type == 'officer':
- partners_to_subscribe.add(holiday.employee_id.parent_id.user_id.partner_id.id)
- partners_to_subscribe.add(holiday.employee_id.leave_manager_id.partner_id.id)
- holiday.message_subscribe(partner_ids=tuple(partners_to_subscribe))
+ if allocation.employee_id.user_id:
+ partners_to_subscribe.add(allocation.employee_id.user_id.partner_id.id)
+ if allocation.validation_type == 'officer':
+ partners_to_subscribe.add(allocation.employee_id.parent_id.user_id.partner_id.id)
+ partners_to_subscribe.add(allocation.employee_id.leave_manager_id.partner_id.id)
+ allocation.message_subscribe(partner_ids=tuple(partners_to_subscribe))
if not self._context.get('import_file'):
- holiday.activity_update()
- if holiday.validation_type == 'no' and holiday.state == 'draft':
- holiday.action_confirm()
- if created_from_dashboard:
- holiday.action_confirm()
- return holidays
+ allocation.activity_update()
+ if allocation.validation_type == 'no' and allocation.state == 'confirm':
+ allocation.action_validate()
+ return allocations
def write(self, values):
if not self.env.context.get('toggle_active') and not bool(values.get('active', True)):
- if any(allocation.state not in ['draft', 'cancel', 'refuse'] for allocation in self):
+ if any(allocation.state not in ['refuse'] for allocation in self):
raise UserError(_('You cannot archive an allocation which is in confirm or validate state.'))
employee_id = values.get('employee_id', False)
if values.get('state'):
@@ -636,8 +652,8 @@ class HolidaysAllocation(models.Model):
@api.ondelete(at_uninstall=False)
def _unlink_if_correct_states(self):
state_description_values = {elem[0]: elem[1] for elem in self._fields['state']._description_selection(self.env)}
- for holiday in self.filtered(lambda holiday: holiday.state not in ['draft', 'cancel', 'confirm']):
- raise UserError(_('You cannot delete an allocation request which is in %s state.') % (state_description_values.get(holiday.state),))
+ for allocation in self.filtered(lambda allocation: allocation.state not in ['confirm', 'refuse']):
+ raise UserError(_('You cannot delete an allocation request which is in %s state.') % (state_description_values.get(allocation.state),))
@api.ondelete(at_uninstall=False)
def _unlink_if_no_leaves(self):
@@ -669,80 +685,49 @@ class HolidaysAllocation(models.Model):
'accrual_plan_id': self.accrual_plan_id.id,
} for employee in employees]
- def action_draft(self):
- if any(holiday.state not in ['confirm', 'refuse'] for holiday in self):
- raise UserError(_('Allocation request state must be "Refused" or "To Approve" in order to be reset to Draft.'))
- self.write({
- 'state': 'draft',
- 'approver_id': False,
- })
- linked_requests = self.mapped('linked_request_ids')
- if linked_requests:
- linked_requests.action_draft()
- linked_requests.unlink()
- self.activity_update()
- return True
-
- def action_confirm(self):
- if self.filtered(lambda holiday: holiday.state != 'draft' and holiday.validation_type != 'no'):
- raise UserError(_('Allocation request must be in Draft state ("To Submit") in order to confirm it.'))
- validated_holidays = self.filtered(lambda holiday: holiday.state == 'validate')
- res = (self - validated_holidays).write({'state': 'confirm'})
- self.activity_update()
- no_employee_requests = [holiday.id for holiday in self.sudo() if holiday.holiday_status_id.employee_requests == 'no']
- self.filtered(lambda holiday: (holiday.id in no_employee_requests or holiday.validation_type == 'no') and holiday.state != 'validate').action_validate()
- return res
-
def action_validate(self):
- current_employee = self.env.user.employee_id
- no_employee_requests = [holiday.id for holiday in self.sudo() if holiday.holiday_status_id.employee_requests == 'no']
- if any((holiday.state != 'confirm' and holiday.id not in no_employee_requests and holiday.validation_type != 'no') for holiday in self):
- raise UserError(_('Allocation request must be confirmed in order to approve it.'))
-
self.write({
'state': 'validate',
- 'approver_id': current_employee.id
+ 'approver_id': self.env.user.employee_id.id
})
-
- for holiday in self:
- holiday._action_validate_create_childs()
+ self._action_validate_create_childs()
self.activity_update()
return True
def _action_validate_create_childs(self):
- childs = self.env['hr.leave.allocation']
- # In the case we are in holiday_type `employee` and there is only one employee we can keep the same allocation
- # Otherwise we do need to create an allocation for all employees to have a behaviour that is in line
- # with the other holiday_type
- if self.state == 'validate' and (self.holiday_type in ['category', 'department', 'company'] or
- (self.holiday_type == 'employee' and len(self.employee_ids) > 1)):
- if self.holiday_type == 'employee':
- employees = self.employee_ids
- elif self.holiday_type == 'category':
- employees = self.category_id.employee_ids
- elif self.holiday_type == 'department':
- employees = self.department_id.member_ids
- else:
- employees = self.env['hr.employee'].search([('company_id', '=', self.mode_company_id.id)])
+ allocation_vals = []
+ for allocation in self:
+ # In the case we are in holiday_type `employee` and there is only one employee we can keep the same allocation
+ # Otherwise we do need to create an allocation for all employees to have a behaviour that is in line
+ # with the other holiday_type
+ if allocation.state == 'validate' and (allocation.holiday_type in ['category', 'department', 'company'] or
+ (allocation.holiday_type == 'employee' and len(allocation.employee_ids) > 1)):
+ if allocation.holiday_type == 'employee':
+ employees = allocation.employee_ids
+ elif allocation.holiday_type == 'category':
+ employees = allocation.category_id.employee_ids
+ elif allocation.holiday_type == 'department':
+ employees = allocation.department_id.member_ids
+ else:
+ employees = self.env['hr.employee'].search([('company_id', '=', allocation.mode_company_id.id)])
- allocation_create_vals = self._prepare_holiday_values(employees)
- childs += self.with_context(
+ allocation_vals += allocation._prepare_holiday_values(employees)
+ if allocation_vals:
+ children = self.env['hr.leave.allocation'].with_context(
mail_notify_force_send=False,
mail_activity_automation_skip=True
- ).create(allocation_create_vals)
- if childs:
- childs.action_validate()
- return childs
+ ).create(allocation_vals)
+ children.filtered(lambda c: c.validation_type != 'no').action_validate()
def action_refuse(self):
current_employee = self.env.user.employee_id
- if any(holiday.state not in ['confirm', 'validate', 'validate1'] for holiday in self):
+ if any(allocation.state not in ['confirm', 'validate'] for allocation in self):
raise UserError(_('Allocation request must be confirmed or validated in order to refuse it.'))
- days_per_allocation = self.holiday_status_id._get_employees_days_per_allocation(self.employee_id.ids)
+ days_per_allocation = self.employee_id._get_consumed_leaves(self.holiday_status_id)[0]
for allocation in self:
- days_taken = days_per_allocation[allocation.employee_id.id][allocation.holiday_status_id][allocation]['virtual_leaves_taken']
+ days_taken = days_per_allocation[allocation.employee_id][allocation.holiday_status_id][allocation]['virtual_leaves_taken']
if days_taken > 0:
raise UserError(_('You cannot refuse this allocation request since the employee has already taken leaves for it. Please refuse or delete those leaves first.'))
@@ -763,24 +748,19 @@ class HolidaysAllocation(models.Model):
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
+ for allocation in self:
+ val_type = allocation.holiday_status_id.sudo().allocation_validation_type
if state == 'confirm':
continue
- if state == 'draft':
- if holiday.employee_id != current_employee and not is_manager:
- raise UserError(_('Only a time off Manager can reset other people allocation.'))
- continue
-
- if not is_officer and self.env.user != holiday.employee_id.leave_manager_id and not val_type == 'no':
+ if not is_officer and self.env.user != allocation.employee_id.leave_manager_id and not val_type == 'no':
raise UserError(_('Only a time off Officer/Responsible or Manager can approve or refuse time off requests.'))
- if is_officer or self.env.user == holiday.employee_id.leave_manager_id:
+ if is_officer or self.env.user == allocation.employee_id.leave_manager_id:
# use ir.rule based first access check: department, members, ... (see security.xml)
- holiday.check_access_rule('write')
+ allocation.check_access_rule('write')
- if holiday.employee_id == current_employee and not is_manager and not val_type == 'no':
+ if allocation.employee_id == current_employee and not is_manager and not val_type == 'no':
raise UserError(_('Only a time off Manager can approve its own requests.'))
@api.onchange('allocation_type')
@@ -830,9 +810,7 @@ class HolidaysAllocation(models.Model):
count=allocation.number_of_days,
allocation_type=allocation.holiday_status_id.name
)
- if allocation.state == 'draft':
- to_clean |= allocation
- elif allocation.state == 'confirm':
+ if allocation.state == 'confirm':
if allocation.holiday_status_id.responsible_ids:
user_ids = allocation.sudo()._get_responsible_for_approval().ids
for user_id in user_ids:
@@ -880,7 +858,7 @@ class HolidaysAllocation(models.Model):
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']:
+ if self.state in ['confirm', 'validate']:
ref_action = self._notify_get_action_link('controller', controller='/allocation/refuse', **local_msg_vals)
hr_actions += [{'url': ref_action, 'title': _('Refuse')}]
@@ -899,8 +877,8 @@ class HolidaysAllocation(models.Model):
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(state in ['validate', 'validate1'] for state in self.mapped('state')):
+ if any(state in ['validate'] for state in self.mapped('state')):
self.check_access_rights('read')
self.check_access_rule('read')
return super(HolidaysAllocation, self.sudo()).message_subscribe(partner_ids=partner_ids, subtype_ids=subtype_ids)
- return super(HolidaysAllocation, self).message_subscribe(partner_ids=partner_ids, subtype_ids=subtype_ids)
+ return super().message_subscribe(partner_ids=partner_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
index 77812ad6bfe..5e48c8b2941 100644
--- a/addons/hr_holidays/models/hr_leave_type.py
+++ b/addons/hr_holidays/models/hr_leave_type.py
@@ -3,18 +3,16 @@
# Copyright (c) 2005-2006 Axelor SARL. (http://www.axelor.com)
-import datetime
import logging
+import pytz
from collections import defaultdict
-from datetime import time, timedelta
+from datetime import time, datetime
from odoo import api, fields, models
-from odoo.osv import expression
from odoo.tools import format_date
from odoo.tools.translate import _
from odoo.tools.float_utils import float_round
-from odoo.addons.resource.models.utils import Intervals
_logger = logging.getLogger(__name__)
@@ -28,31 +26,27 @@ class HolidaysType(models.Model):
def _model_sorting_key(self, leave_type):
remaining = leave_type.virtual_remaining_leaves > 0
taken = leave_type.leaves_taken > 0
- return -1*leave_type.sequence, leave_type.employee_requests == 'no' and remaining, leave_type.employee_requests == 'yes' and remaining, taken
+ return -1 * leave_type.sequence, leave_type.employee_requests == 'no' and remaining, leave_type.employee_requests == 'yes' and remaining, taken
name = fields.Char('Time Off Type', required=True, translate=True)
sequence = fields.Integer(default=100,
- help='The type with the smallest sequence is the default value in time off request')
+ 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 = fields.Integer(string='Color', help="The color selected here will be used in every screen with the time off type.")
icon_id = fields.Many2one('ir.attachment', string='Cover Image', domain="[('res_model', '=', 'hr.leave.type'), ('res_field', '=', 'icon_id')]")
active = fields.Boolean('Active', default=True,
help="If the active field is set to false, it will allow you to hide the time off type without removing it.")
+
+ # employee specific computed data
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.')
+ help='This value is given by the sum of all time off requests with a positive value.')
leaves_taken = fields.Float(
compute='_compute_leaves', string='Time off Already Taken',
help='This value is given by the sum of all time off requests with a negative value.')
- remaining_leaves = fields.Float(
- compute='_compute_leaves', string='Remaining Time Off',
- help='Maximum Time Off Allowed - Time Off Already Taken')
virtual_remaining_leaves = fields.Float(
compute='_compute_leaves', search='_search_virtual_remaining_leaves', string='Virtual Remaining Time Off',
help='Maximum Time Off Allowed - Time Off Already Taken - Time Off Waiting Approval')
- virtual_leaves_taken = fields.Float(
- compute='_compute_leaves', string='Virtual Time Off Already Taken',
- help='Sum of validated and non validated time off requests.')
- closest_allocation_to_expire = fields.Many2one('hr.leave.allocation', 'Allocation', compute='_compute_leaves')
+
allocation_count = fields.Integer(
compute='_compute_allocation_count', string='Allocations')
group_days_leave = fields.Float(
@@ -107,59 +101,64 @@ class HolidaysType(models.Model):
or that don't need an allocation
return [('id', domain_operator, [x['id'] for x in res])]
"""
- date_to = self._context.get('default_date_from') or fields.Date.today().strftime('%Y-1-1')
- date_from = self._context.get('default_date_to') or fields.Date.today().strftime('%Y-12-31')
+ date_from = self._context.get('default_date_from') or fields.Date.today().strftime('%Y-1-1')
+ date_to = self._context.get('default_date_to') or fields.Date.today().strftime('%Y-12-31')
employee_id = self._context.get('default_employee_id', self._context.get('employee_id')) or self.env.user.employee_id.id
if not isinstance(value, bool):
raise ValueError('Invalid value: %s' % (value))
if operator not in ['=', '!=']:
raise ValueError('Invalid operator: %s' % (operator))
- new_operator = 'in' if operator == '=' else 'not in'
+ # '!=' True or '=' False
+ if (operator == '=') ^ value:
+ new_operator = 'not in'
+ # '=' True or '!=' False
+ else:
+ new_operator = 'in'
- query = '''
- SELECT
- holiday_status_id
- FROM
- hr_leave_allocation alloc
- WHERE
- alloc.employee_id = %s AND
- alloc.active = True AND alloc.state = 'validate' AND
- (alloc.date_to >= %s OR alloc.date_to IS NULL) AND
- alloc.date_from <= %s
- '''
+ leave_types = self.env['hr.leave.allocation'].search([
+ ('employee_id', '=', employee_id),
+ ('state', '=', 'validate'),
+ ('date_from', '<=', date_to),
+ '|',
+ ('date_to', '>=', date_from),
+ ('date_to', '=', False),
+ ]).holiday_status_id
- self._cr.execute(query, (employee_id or None, date_to, date_from))
+ return [('id', new_operator, leave_types.ids)]
- return [('id', new_operator, [x['holiday_status_id'] for x in self._cr.dictfetchall()])]
-
- @api.depends('requires_allocation')
+ @api.depends('requires_allocation', 'max_leaves', 'virtual_remaining_leaves')
def _compute_valid(self):
- date_to = self._context.get('default_date_to', fields.Datetime.today())
date_from = self._context.get('default_date_from', fields.Datetime.today())
+ date_to = self._context.get('default_date_to', fields.Datetime.today())
employee_id = self._context.get('default_employee_id', self._context.get('employee_id', self.env.user.employee_id.id))
for holiday_type in self:
if holiday_type.requires_allocation == 'yes':
- allocation = self.env['hr.leave.allocation'].search([
+ allocations = self.env['hr.leave.allocation'].search([
('holiday_status_id', '=', holiday_type.id),
+ ('allocation_type', '=', 'accrual'),
('employee_id', '=', employee_id),
+ ('date_from', '<=', date_from),
'|',
('date_to', '>=', date_to),
- '&',
('date_to', '=', False),
- ('date_from', '<=', date_from)])
- holiday_type.has_valid_allocation = bool(allocation)
+ ])
+ allocations = allocations.filtered(lambda alloc:
+ alloc.allocation_type == 'accrual'
+ or (alloc.max_leaves > 0 and alloc.virtual_remaining_leaves > 0)
+ )
+ holiday_type.has_valid_allocation = bool(allocations)
else:
holiday_type.has_valid_allocation = True
def _search_max_leaves(self, operator, value):
value = float(value)
- employee_id = self._get_contextual_employee_id()
+ employee = self.env['hr.employee']._get_contextual_employee()
leaves = defaultdict(int)
- if employee_id:
+ if employee:
allocations = self.env['hr.leave.allocation'].search([
- ('employee_id', '=', employee_id),
+ ('employee_id', '=', employee.id),
('state', '=', 'validate')
])
for allocation in allocations:
@@ -205,291 +204,21 @@ class HolidaysType(models.Model):
return [('id', 'in', valid_leave_types.ids)]
- def _get_employees_days_per_allocation(self, employee_ids, date=None):
- if not date:
- date = fields.Date.to_date(self.env.context.get('default_date_from')) or fields.Date.context_today(self)
-
- leaves_domain = [
- ('employee_id', 'in', employee_ids),
- ('state', 'in', ['confirm', 'validate1', 'validate']),
- ('holiday_status_id', 'in', self.ids)
- ]
- if self.env.context.get("ignore_future"):
- leaves_domain.append(('date_from', '<=', date))
- leaves = self.env['hr.leave'].search(leaves_domain)
-
- allocations = self.env['hr.leave.allocation'].with_context(active_test=False).search([
- ('employee_id', 'in', employee_ids),
- ('state', 'in', ['validate']),
- ('holiday_status_id', 'in', self.ids),
- ])
-
- # The allocation_employees dictionary groups the allocations based on the employee and the holiday type
- # The structure is the following:
- # - KEYS:
- # allocation_employees
- # |--employee_id
- # |--holiday_status_id
- # - VALUES:
- # Intervals with the start and end date of each allocation and associated allocations within this interval
- allocation_employees = defaultdict(lambda: defaultdict(list))
-
- ### Creation of the allocation intervals ###
- for holiday_status_id in allocations.holiday_status_id:
- for employee_id in employee_ids:
- allocation_intervals = Intervals([(
- fields.datetime.combine(allocation.date_from, time.min),
- fields.datetime.combine(allocation.date_to or datetime.date.max, time.max),
- allocation)
- for allocation in allocations.filtered(lambda allocation: allocation.employee_id.id == employee_id and allocation.holiday_status_id == holiday_status_id)])
-
- allocation_employees[employee_id][holiday_status_id] = allocation_intervals
-
- # The leave_employees dictionary groups the leavess based on the employee and the holiday type
- # The structure is the following:
- # - KEYS:
- # leave_employees
- # |--employee_id
- # |--holiday_status_id
- # - VALUES:
- # Intervals with the start and end date of each leave and associated leave within this interval
- leaves_employees = defaultdict(lambda: defaultdict(list))
- leave_intervals = []
-
- ### Creation of the leave intervals ###
- if leaves:
- for holiday_status_id in leaves.holiday_status_id:
- for employee_id in employee_ids:
- leave_intervals = Intervals([(
- fields.datetime.combine(leave.date_from, time.min),
- fields.datetime.combine(leave.date_to, time.max),
- leave)
- for leave in leaves.filtered(lambda leave: leave.employee_id.id == employee_id and leave.holiday_status_id == holiday_status_id)])
-
- leaves_employees[employee_id][holiday_status_id] = leave_intervals
-
- # allocation_days_consumed is a dictionary to map the number of days/hours of leaves taken per allocation
- # The structure is the following:
- # - KEYS:
- # allocation_days_consumed
- # |--employee_id
- # |--holiday_status_id
- # |--allocation
- # |--virtual_leaves_taken
- # |--leaves_taken
- # |--virtual_remaining_leaves
- # |--remaining_leaves
- # |--max_leaves
- # |--closest_allocation_to_expire
- # - VALUES:
- # Integer representing the number of (virtual) remaining leaves, (virtual) leaves taken or max leaves for each allocation.
- # The unit is in hour or days depending on the leave type request unit
- allocations_days_consumed = defaultdict(lambda: defaultdict(lambda: defaultdict(lambda: defaultdict(lambda: 0))))
-
- company_domain = [('company_id', 'in', list(set(self.env.company.ids + self.env.context.get('allowed_company_ids', []))))]
-
- ### Existing leaves assigned to allocations ###
- if leaves_employees:
- for employee_id, leaves_interval_by_status in leaves_employees.items():
- for holiday_status_id in leaves_interval_by_status:
- days_consumed = allocations_days_consumed[employee_id][holiday_status_id]
- if allocation_employees[employee_id][holiday_status_id]:
- allocations = allocation_employees[employee_id][holiday_status_id] & leaves_interval_by_status[holiday_status_id]
- available_allocations = self.env['hr.leave.allocation']
- for allocation_interval in allocations._items:
- available_allocations |= allocation_interval[2]
- # Consume the allocations that are close to expiration first
- sorted_available_allocations = available_allocations.filtered('date_to').sorted(key='date_to')
- sorted_available_allocations += available_allocations.filtered(lambda allocation: not allocation.date_to)
- leave_intervals = leaves_interval_by_status[holiday_status_id]._items
- sorted_allocations_with_remaining_leaves = self.env['hr.leave.allocation']
- for leave_interval in leave_intervals:
- leaves = leave_interval[2]
- for leave in leaves:
- if leave.leave_type_request_unit in ['day', 'half_day']:
- leave_duration = leave.number_of_days
- leave_unit = 'days'
- else:
- leave_duration = leave.number_of_hours_display
- leave_unit = 'hours'
- if holiday_status_id.requires_allocation != 'no':
- for available_allocation in sorted_available_allocations:
- if (available_allocation.date_to and available_allocation.date_to < leave.date_from.date()) \
- or (available_allocation.date_from > leave.date_to.date()):
- continue
- virtual_remaining_leaves = (available_allocation.number_of_days if leave_unit == 'days' else available_allocation.number_of_hours_display) - allocations_days_consumed[employee_id][holiday_status_id][available_allocation]['virtual_leaves_taken']
- max_leaves = min(virtual_remaining_leaves, leave_duration)
- days_consumed[available_allocation]['virtual_leaves_taken'] += max_leaves
- if leave.state == 'validate':
- days_consumed[available_allocation]['leaves_taken'] += max_leaves
- leave_duration -= max_leaves
- # Check valid allocations with still availabe leaves on it
- if days_consumed[available_allocation]['virtual_remaining_leaves'] > 0 and available_allocation.date_to and available_allocation.date_to > date:
- sorted_allocations_with_remaining_leaves |= available_allocation
- if leave_duration > 0:
- # There are not enough allocation for the number of leaves
- days_consumed[False]['virtual_remaining_leaves'] -= leave_duration
- else:
- days_consumed[False]['virtual_leaves_taken'] += leave_duration
- if leave.state == 'validate':
- days_consumed[False]['leaves_taken'] += leave_duration
- # no need to sort the allocations again
- allocations_days_consumed[employee_id][holiday_status_id][False]['closest_allocation_to_expire'] = sorted_allocations_with_remaining_leaves[0] if sorted_allocations_with_remaining_leaves else False
-
- # Future available leaves
- future_allocations_date_from = fields.datetime.combine(date, time.min)
- future_allocations_date_to = fields.datetime.combine(date, time.max) + timedelta(days=5*365)
- for employee_id, allocation_intervals_by_status in allocation_employees.items():
- employee = self.env['hr.employee'].browse(employee_id)
- for holiday_status_id, intervals in allocation_intervals_by_status.items():
- if not intervals:
- continue
- future_allocation_intervals = intervals & Intervals([(
- future_allocations_date_from,
- future_allocations_date_to,
- self.env['hr.leave'])])
- search_date = date
- closest_allocations = self.env['hr.leave.allocation']
- for interval in intervals._items:
- closest_allocations |= interval[2]
- allocations_with_remaining_leaves = self.env['hr.leave.allocation']
- for interval_from, interval_to, interval_allocations in future_allocation_intervals._items:
- if interval_from.date() > search_date:
- continue
- interval_allocations = interval_allocations.filtered('active')
- if not interval_allocations:
- continue
- # If no end date to the allocation, consider the number of days remaining as infinite
- employee_quantity_available = (
- employee._get_work_days_data_batch(interval_from, interval_to, compute_leaves=False, domain=company_domain)[employee_id]
- if interval_to != future_allocations_date_to
- else {'days': float('inf'), 'hours': float('inf')}
- )
- for allocation in interval_allocations:
- if allocation.date_from > search_date:
- continue
- days_consumed = allocations_days_consumed[employee_id][holiday_status_id][allocation]
- if allocation.type_request_unit in ['day', 'half_day']:
- quantity_available = employee_quantity_available['days']
- remaining_days_allocation = (allocation.number_of_days - days_consumed['virtual_leaves_taken'])
- else:
- quantity_available = employee_quantity_available['hours']
- remaining_days_allocation = (allocation.number_of_hours_display - days_consumed['virtual_leaves_taken'])
- if quantity_available <= remaining_days_allocation:
- search_date = interval_to.date() + timedelta(days=1)
- days_consumed['virtual_remaining_leaves'] += min(quantity_available, remaining_days_allocation)
- days_consumed['max_leaves'] = allocation.number_of_days if allocation.type_request_unit in ['day', 'half_day'] else allocation.number_of_hours_display
- days_consumed['remaining_leaves'] = days_consumed['max_leaves'] - days_consumed['leaves_taken']
- if remaining_days_allocation >= quantity_available:
- break
- # Check valid allocations with still availabe leaves on it
- if days_consumed['virtual_remaining_leaves'] > 0 and allocation.date_to and allocation.date_to > date:
- allocations_with_remaining_leaves |= allocation
- allocations_sorted = sorted(allocations_with_remaining_leaves, key=lambda a: a.date_to)
- allocations_days_consumed[employee_id][holiday_status_id][False]['closest_allocation_to_expire'] = allocations_sorted[0] if allocations_sorted else False
- return allocations_days_consumed
-
-
- def get_employees_days(self, employee_ids, date=None):
-
- result = {
- employee_id: {
- leave_type.id: {
- 'max_leaves': 0,
- 'leaves_taken': 0,
- 'remaining_leaves': 0,
- 'virtual_remaining_leaves': 0,
- 'virtual_leaves_taken': 0,
- 'closest_allocation_to_expire': False,
- } for leave_type in self
- } for employee_id in employee_ids
- }
-
- if not date:
- date = fields.Date.to_date(self.env.context.get('default_date_from')) or fields.Date.context_today(self)
-
- allocations_days_consumed = self._get_employees_days_per_allocation(employee_ids, date)
-
- leave_keys = ['max_leaves', 'leaves_taken', 'remaining_leaves', 'virtual_remaining_leaves', 'virtual_leaves_taken']
-
- for employee_id in allocations_days_consumed:
- for holiday_status_id in allocations_days_consumed[employee_id]:
- for allocation in allocations_days_consumed[employee_id][holiday_status_id]:
- if allocation:
- if allocation.date_to and (allocation.date_to < date or allocation.date_from > date):
- continue
- for leave_key in leave_keys:
- result[employee_id][holiday_status_id if isinstance(holiday_status_id, int) else holiday_status_id.id][leave_key] += allocations_days_consumed[employee_id][holiday_status_id][allocation][leave_key]
- else:
- result[employee_id][holiday_status_id if isinstance(holiday_status_id, int) else holiday_status_id.id]['closest_allocation_to_expire'] = allocations_days_consumed[employee_id][holiday_status_id][False]['closest_allocation_to_expire']
- for leave_key in leave_keys:
- if allocations_days_consumed[employee_id][holiday_status_id][False].get(leave_key):
- result[employee_id][holiday_status_id if isinstance(holiday_status_id, int) else holiday_status_id.id][leave_key] = allocations_days_consumed[employee_id][holiday_status_id][False][leave_key]
-
- return result
-
- @api.model
- def get_days_all_request(self):
- leave_types = sorted(self.search([]).filtered(lambda x: ((x.virtual_remaining_leaves > 0 or x.max_leaves))), key=self._model_sorting_key, reverse=True)
- return [lt._get_days_request() for lt in leave_types]
-
- def _get_days_request(self):
- self.ensure_one()
- result = self._get_employees_days_per_allocation(self.closest_allocation_to_expire.employee_id.ids)
- closest_allocation_remaining = 0
- if self.closest_allocation_to_expire:
- # Shows the sum of allocation expiring on the same day as the closest to expire
- employee_allocations = result[self.closest_allocation_to_expire.employee_id.id][self].items()
- closest_allocation_remaining = sum(
- res['virtual_remaining_leaves']
- for alloc, res in employee_allocations
- if alloc and alloc.date_to == self.closest_allocation_to_expire.date_to
- )
- return (self.name, {
- 'remaining_leaves': ('%.2f' % self.remaining_leaves).rstrip('0').rstrip('.'),
- 'virtual_remaining_leaves': ('%.2f' % self.virtual_remaining_leaves).rstrip('0').rstrip('.'),
- 'max_leaves': ('%.2f' % self.max_leaves).rstrip('0').rstrip('.'),
- 'leaves_taken': ('%.2f' % self.leaves_taken).rstrip('0').rstrip('.'),
- 'virtual_leaves_taken': ('%.2f' % self.virtual_leaves_taken).rstrip('0').rstrip('.'),
- 'leaves_requested': ('%.2f' % (self.max_leaves - self.virtual_remaining_leaves - self.leaves_taken)).rstrip('0').rstrip('.'),
- 'leaves_approved': ('%.2f' % self.leaves_taken).rstrip('0').rstrip('.'),
- 'closest_allocation_remaining': ('%.2f' % closest_allocation_remaining).rstrip('0').rstrip('.'),
- 'closest_allocation_expire': format_date(self.env, self.closest_allocation_to_expire.date_to) if self.closest_allocation_to_expire.date_to else False,
- 'request_unit': self.request_unit,
- 'icon': self.sudo().icon_id.url,
- 'id': self.id,
- }, self.requires_allocation, self.id)
-
- def _get_contextual_employee_id(self):
- if 'employee_id' in self._context:
- employee_id = self._context['employee_id']
- elif 'default_employee_id' in self._context:
- employee_id = self._context['default_employee_id']
- else:
- employee_id = self.env.user.employee_id.id
- return employee_id
-
- @api.depends_context('employee_id', 'default_employee_id')
+ @api.depends_context('employee_id', 'default_employee_id', 'default_date_from')
def _compute_leaves(self):
- data_days = {}
- employee_id = self._get_contextual_employee_id()
-
- if employee_id:
- data_days = (self.get_employees_days(employee_id)[employee_id[0]] if isinstance(employee_id, list) else
- self.get_employees_days([employee_id])[employee_id])
-
+ employee = self.env['hr.employee']._get_contextual_employee()
+ date = self._context['default_date_from'] if 'default_date_from' in self._context else None
+ data_days = self.get_allocation_data(employee, date)[employee]
for holiday_status in self:
- result = data_days.get(holiday_status.id, {})
- holiday_status.max_leaves = result.get('max_leaves', 0)
- holiday_status.leaves_taken = result.get('leaves_taken', 0)
- holiday_status.remaining_leaves = result.get('remaining_leaves', 0)
- holiday_status.virtual_remaining_leaves = result.get('virtual_remaining_leaves', 0)
- holiday_status.virtual_leaves_taken = result.get('virtual_leaves_taken', 0)
- holiday_status.closest_allocation_to_expire = result.get('closest_allocation_to_expire', 0)
+ result = [item for item in data_days if item[0] == holiday_status.name]
+ leave_type_tuple = result[0] if result else ('', {})
+ holiday_status.max_leaves = leave_type_tuple[1].get('max_leaves', 0)
+ holiday_status.leaves_taken = leave_type_tuple[1].get('leaves_taken', 0)
+ holiday_status.virtual_remaining_leaves = leave_type_tuple[1].get('virtual_remaining_leaves', 0)
def _compute_allocation_count(self):
- min_datetime = fields.Datetime.to_string(datetime.datetime.now().replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0))
- max_datetime = fields.Datetime.to_string(datetime.datetime.now().replace(month=12, day=31, hour=23, minute=59, second=59))
+ min_datetime = fields.Datetime.to_string(datetime.now().replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0))
+ max_datetime = fields.Datetime.to_string(datetime.now().replace(month=12, day=31, hour=23, minute=59, second=59))
domain = [
('holiday_status_id', 'in', self.ids),
('date_from', '>=', min_datetime),
@@ -507,8 +236,8 @@ class HolidaysType(models.Model):
allocation.allocation_count = grouped_dict.get(allocation.id, 0)
def _compute_group_days_leave(self):
- min_datetime = fields.Datetime.to_string(datetime.datetime.now().replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0))
- max_datetime = fields.Datetime.to_string(datetime.datetime.now().replace(month=12, day=31, hour=23, minute=59, second=59))
+ min_datetime = fields.Datetime.to_string(datetime.now().replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0))
+ max_datetime = fields.Datetime.to_string(datetime.now().replace(month=12, day=31, hour=23, minute=59, second=59))
domain = [
('holiday_status_id', 'in', self.ids),
('date_from', '>=', min_datetime),
@@ -570,8 +299,8 @@ class HolidaysType(models.Model):
is an employee_id in context and that no other order has been given
to the method.
"""
- employee_id = self._get_contextual_employee_id()
- if order == self._order and employee_id:
+ employee = self.env['hr.employee']._get_contextual_employee()
+ if order == self._order and employee:
# retrieve all leaves, sort them, then apply offset and limit
leaves = self.browse(super()._search(domain, access_rights_uid=access_rights_uid))
leaves = leaves.sorted(key=self._model_sorting_key, reverse=True)
@@ -582,8 +311,6 @@ class HolidaysType(models.Model):
def action_see_days_allocated(self):
self.ensure_one()
action = self.env["ir.actions.actions"]._for_xml_id("hr_holidays.hr_leave_allocation_action_all")
- date_from = fields.Datetime.to_string(
- datetime.datetime.now().replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0))
action['domain'] = [
('holiday_status_id', 'in', self.ids),
]
@@ -616,3 +343,125 @@ class HolidaysType(models.Model):
'default_time_off_type_id': self.id,
}
return action
+
+ # ------------------------------------------------------------
+ # Leave - Allocation link methods
+ # ------------------------------------------------------------
+
+ @api.model
+ def get_allocation_data_request(self, date=None):
+ leave_types = self.search([
+ '|',
+ ('company_id', 'in', self.env.context.get('allowed_company_ids')),
+ ('company_id', '=', False),
+ ])
+ employee = self.env['hr.employee']._get_contextual_employee()
+ if employee:
+ return leave_types.get_allocation_data(employee, date)[employee]
+ return []
+
+ def get_allocation_data(self, employees, date=None):
+ allocation_data = defaultdict(list)
+ if date and isinstance(date, str):
+ date = datetime.fromisoformat(date).date()
+ elif date and isinstance(date, datetime):
+ date = date.date()
+ elif not date:
+ date = fields.Date.today()
+
+ allocations_leaves_consumed, extra_data = employees._get_consumed_leaves(self, date)
+ leave_type_requires_allocation = self.filtered(lambda lt: lt.requires_allocation == 'yes')
+
+ for employee in employees:
+ for leave_type in leave_type_requires_allocation:
+ if len(allocations_leaves_consumed[employee][leave_type]) == 0:
+ continue
+ lt_info = (
+ leave_type.name,
+ {
+ 'remaining_leaves': 0,
+ 'virtual_remaining_leaves': 0,
+ 'max_leaves': 0,
+ 'accrual_bonus': 0,
+ 'leaves_taken': 0,
+ 'virtual_leaves_taken': 0,
+ 'leaves_requested': 0,
+ 'leaves_approved': 0,
+ 'closest_allocation_remaining': 0,
+ 'closest_allocation_expire': False,
+ 'holds_changes': False,
+ 'excess_days': extra_data[employee][leave_type]['excess_days'],
+ 'exceeding_duration': extra_data[employee][leave_type]['exceeding_duration'],
+ 'request_unit': leave_type.request_unit,
+ 'icon': leave_type.sudo().icon_id.url,
+ 'has_accrual_allocation': False,
+ },
+ leave_type.requires_allocation,
+ leave_type.id)
+ allocations_now = self.env['hr.leave.allocation']
+ allocations_date = self.env['hr.leave.allocation']
+ allocations_with_remaining_leaves = self.env['hr.leave.allocation']
+ for allocation, data in allocations_leaves_consumed[employee][leave_type].items():
+ # We only need the allocation that are valid at the given date
+ if allocation:
+ if allocation.allocation_type == 'accrual':
+ lt_info[1]['has_accrual_allocation'] = True
+ today = date.today()
+ if allocation.date_from <= today and (not allocation.date_to or allocation.date_to >= today):
+ # we get each allocation available now to indicate visually if
+ # the future evaluation holds changes compared to now
+ allocations_now |= allocation
+ if allocation.date_from <= date and (not allocation.date_to or allocation.date_to >= date):
+ # we get each allocation available now to indicate visually if
+ # the future evaluation holds changes compared to now
+ allocations_date |= allocation
+ if allocation.date_from > date:
+ continue
+ if allocation.date_to and allocation.date_to < date:
+ continue
+ lt_info[1]['remaining_leaves'] += data['remaining_leaves']
+ lt_info[1]['virtual_remaining_leaves'] += data['virtual_remaining_leaves']
+ lt_info[1]['max_leaves'] += data['max_leaves']
+ lt_info[1]['accrual_bonus'] += data['accrual_bonus']
+ lt_info[1]['leaves_taken'] += data['leaves_taken']
+ lt_info[1]['virtual_leaves_taken'] += data['virtual_leaves_taken']
+ lt_info[1]['leaves_requested'] += data['virtual_leaves_taken']
+ lt_info[1]['leaves_approved'] += data['leaves_taken']
+ if data['virtual_remaining_leaves'] > 0:
+ allocations_with_remaining_leaves |= allocation
+ closest_allocation = allocations_with_remaining_leaves[0] if allocations_with_remaining_leaves else self.env['hr.leave.allocation']
+ closest_allocations = allocations_with_remaining_leaves.filtered(lambda a: a.date_to == closest_allocation.date_to)
+ closest_allocation_remaining = 0
+ for closest_allocation in closest_allocations:
+ closest_allocation_remaining += allocations_leaves_consumed[employee][leave_type][closest_allocation]['virtual_remaining_leaves']
+ if closest_allocation.date_to:
+ closest_allocation_expire = format_date(self.env, closest_allocation.date_to)
+ # closest_allocation_duration corresponds to the time remaining before the allocation expires
+ closest_allocation_duration =\
+ employee.resource_calendar_id._attendance_intervals_batch(
+ datetime.combine(closest_allocation.date_to, time.min).replace(tzinfo=pytz.UTC),
+ datetime.combine(date, time.max).replace(tzinfo=pytz.UTC))\
+ if leave_type.request_unit in ['hour']\
+ else (closest_allocation.date_to - date).days + 1
+ else:
+ closest_allocation_expire = False
+ closest_allocation_duration = False
+ # the allocations are assumed to be different from today's allocations if there is any
+ # accrual days granted or if there is any difference between allocations now and on the selected date
+ holds_changes = (lt_info[1]['accrual_bonus'] > 0
+ or bool(allocations_date - allocations_now)
+ or bool(allocations_now - allocations_date))\
+ and date != fields.Date.today()
+ lt_info[1].update({
+ 'closest_allocation_remaining': closest_allocation_remaining,
+ 'closest_allocation_expire': closest_allocation_expire,
+ 'closest_allocation_duration': closest_allocation_duration,
+ 'holds_changes': holds_changes,
+ })
+ allocation_data[employee].append(lt_info)
+ for employee in allocation_data:
+ for leave_type_data in allocation_data[employee]:
+ for key, value in leave_type_data[1].items():
+ if isinstance(value, float):
+ leave_type_data[1][key] = round(value, 2)
+ return allocation_data
diff --git a/addons/hr_holidays/models/resource.py b/addons/hr_holidays/models/resource.py
index a903055b739..7d6a8cf946b 100644
--- a/addons/hr_holidays/models/resource.py
+++ b/addons/hr_holidays/models/resource.py
@@ -68,14 +68,14 @@ class CalendarLeaves(models.Model):
for previous_duration, leave, state in zip(previous_durations, leaves, previous_states):
duration_difference = previous_duration - leave.number_of_days
message = False
- if duration_difference > 0 and leave['holiday_allocation_id']:
+ if duration_difference > 0 and leave.holiday_status_id.requires_allocation == 'yes':
message = _("Due to a change in global time offs, you have been granted %s day(s) back.", duration_difference)
if leave.number_of_days > previous_duration\
and leave.holiday_status_id not in sick_time_status:
message = _("Due to a change in global time offs, %s extra day(s) have been taken from your allocation. Please review this leave if you need it to be changed.", -1 * duration_difference)
try:
leave.write({'state': state})
- leave._check_holidays()
+ leave._check_validity()
except ValidationError:
leave.action_refuse()
message = _("Due to a change in global time offs, this leave no longer has the required amount of available allocation and has been set to refused. Please review this leave.")
diff --git a/addons/hr_holidays/static/src/dashboard/time_off_card.js b/addons/hr_holidays/static/src/dashboard/time_off_card.js
index 7140917f6a2..20d1ec4dc24 100644
--- a/addons/hr_holidays/static/src/dashboard/time_off_card.js
+++ b/addons/hr_holidays/static/src/dashboard/time_off_card.js
@@ -1,19 +1,22 @@
/* @odoo-module */
import { usePopover } from "@web/core/popover/popover_hook";
-import { useNewAllocationRequest } from "@hr_holidays/views/hooks";
-import { Component } from "@odoo/owl";
+import { formatNumber, useNewAllocationRequest } from "@hr_holidays/views/hooks";
+import { Component, onWillRender } from "@odoo/owl";
export class TimeOffCardPopover extends Component {}
TimeOffCardPopover.template = "hr_holidays.TimeOffCardPopover";
TimeOffCardPopover.props = [
"allocated",
+ "accrual_bonus",
"approved",
"planned",
"left",
- "employeeId",
- "holidayStatusId",
+ "warning",
+ "closest",
+ "request_unit",
+ "exceeding_duration",
"close?",
"onClickNewAllocationRequest?",
];
@@ -21,21 +24,37 @@ TimeOffCardPopover.props = [
export class TimeOffCard extends Component {
setup() {
this.popover = usePopover(TimeOffCardPopover, {
- position: "right",
+ position: "bottom",
popoverClass: "bg-view",
});
this.newAllocationRequest = useNewAllocationRequest();
+ this.lang = this.env.services.user.lang;
+ this.formatNumber = formatNumber;
+ this.updateWarning();
+
+ onWillRender(this.updateWarning);
+ }
+
+ updateWarning() {
+ const { data } = this.props;
+ const closeExpire =
+ data.closest_allocation_duration &&
+ data.closest_allocation_duration < data.virtual_remaining_leaves;
+ this.warning = data.exceeding_duration || closeExpire;
}
onClickInfo(ev) {
const { data } = this.props;
this.popover.open(ev.target, {
- allocated: data.max_leaves,
- approved: data.leaves_approved,
- planned: data.leaves_requested,
- left: data.virtual_remaining_leaves,
- employeeId: this.props.employeeId,
- holidayStatusId: this.props.holidayStatusId,
+ allocated: formatNumber(this.lang, data.max_leaves),
+ accrual_bonus: formatNumber(this.lang, data.accrual_bonus),
+ approved: formatNumber(this.lang, data.leaves_approved),
+ planned: formatNumber(this.lang, data.leaves_requested),
+ left: formatNumber(this.lang, data.virtual_remaining_leaves),
+ warning: this.warning,
+ closest: data.closest_allocation_duration,
+ request_unit: data.request_unit,
+ exceeding_duration: data.exceeding_duration,
onClickNewAllocationRequest: this.newAllocationRequestFrom.bind(this),
});
}
diff --git a/addons/hr_holidays/static/src/dashboard/time_off_card.scss b/addons/hr_holidays/static/src/dashboard/time_off_card.scss
index 19acdf49c13..babbb4a23bb 100644
--- a/addons/hr_holidays/static/src/dashboard/time_off_card.scss
+++ b/addons/hr_holidays/static/src/dashboard/time_off_card.scss
@@ -1,13 +1,20 @@
.o_timeoff_card {
- flex-direction: column;
- flex-grow: 1;
display: flex;
+ flex-direction: column;
+ justify-content: start;
+ flex: 1;
text-align: center;
&:not(:last-child) {
border-right: 2px solid $border-color;
}
+ &>* {
+ width: fit-content;
+ margin-left: auto;
+ margin-right: auto;
+ }
+
span {
line-height: 1;
}
@@ -32,17 +39,23 @@
font-size: 10px;
}
+ .o_timeoff_details {
+ width: fit-content;
+ margin: 0 auto;
+ cursor: pointer;
+ }
+
.o_timeoff_info {
display: inline-block;
margin-right: -10px;
-
- span {
- cursor: pointer;
- padding-right: 10px;
- }
+ padding-right: 10px;
}
}
.o_time_off_icon_types img {
filter: var(--timeOffCard-icon-filter);
}
+
+.o_time_off_card_popover_warning {
+ max-width: 300px;
+}
diff --git a/addons/hr_holidays/static/src/dashboard/time_off_card.xml b/addons/hr_holidays/static/src/dashboard/time_off_card.xml
index b07cdf76386..3c43e6b1020 100644
--- a/addons/hr_holidays/static/src/dashboard/time_off_card.xml
+++ b/addons/hr_holidays/static/src/dashboard/time_off_card.xml
@@ -3,6 +3,7 @@
+
@@ -10,9 +11,13 @@
-
+
+
+
-
+ hoursdays
@@ -21,12 +26,10 @@
taken
-
-
-
-
-
-
+
+
(hoursdays
@@ -54,10 +57,30 @@
-
Allocated (new request):
+
+ Allocated (new request):
+
+
+
+ Accrual (Future):
+
Approved:
Planned:
Available:
+
+
+ Only
+ hours
+ days
+ can be used before the allocation expires.
+
+
+ The leaves planned in the future are exceeding the maximum value of the allocation.
+ It will not be possible to take all of them.
+
+