19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-25 12:00:11 +01:00
parent e1d89e11e3
commit a1f02d8cc7
225 changed files with 2335 additions and 775 deletions

View file

@ -139,22 +139,23 @@ class HrEmployee(models.Model):
('employee_id', 'in', self.ids),
('date_from', '<=', fields.Datetime.now()),
('date_to', '>=', fields.Datetime.now()),
('holiday_status_id.time_type', '=', 'leave'),
('state', '=', 'validate'),
])
leave_data = {}
for holiday in holidays:
leave_data[holiday.employee_id.id] = {}
leave_data.setdefault(holiday.employee_id.id, {})
leave_data[holiday.employee_id.id]['leave_date_from'] = holiday.date_from.date()
back_on = holiday.employee_id._get_first_working_interval(holiday.date_to)
leave_data[holiday.employee_id.id]['leave_date_to'] = back_on.date() if back_on else None
leave_data[holiday.employee_id.id]['current_leave_state'] = holiday.state
leave_data[holiday.employee_id.id]['is_absent'] = leave_data[holiday.employee_id.id].get('is_absent') or holiday.holiday_status_id.time_type == 'leave'
for employee in self:
employee.leave_date_from = leave_data.get(employee.id, {}).get('leave_date_from')
employee.leave_date_to = leave_data.get(employee.id, {}).get('leave_date_to')
employee.current_leave_state = leave_data.get(employee.id, {}).get('current_leave_state')
employee.is_absent = leave_data.get(employee.id) and leave_data.get(employee.id).get('current_leave_state') == 'validate'
employee_leave_data = leave_data.get(employee.id, {})
employee.leave_date_from = employee_leave_data.get('leave_date_from')
employee.leave_date_to = employee_leave_data.get('leave_date_to')
employee.current_leave_state = employee_leave_data.get('current_leave_state')
employee.is_absent = employee_leave_data and employee_leave_data.get('is_absent') and employee_leave_data.get('current_leave_state') == 'validate'
@api.depends('parent_id')
def _compute_leave_manager(self):
@ -411,6 +412,11 @@ class HrEmployee(models.Model):
return self.env.user.employee_id
def _get_consumed_leaves(self, leave_types, target_date=False, ignore_future=False):
""" This method won't call `_get_future_leaves_on` for the allocations contained by this variable (it will only use the current value of
the `number_of_days` of the allocation, alias `number_of_hours_display`)
`precomputed_allocations`: context variable (recordset) which can be used to pass allocation that are considered to be already computed
"""
employees = self or self._get_contextual_employee()
leaves_domain = [
('holiday_status_id', 'in', leave_types.ids),
@ -486,10 +492,16 @@ class HrEmployee(models.Model):
'to_recheck_leaves': self.env['hr.leave']
})
)
precomputed_allocations = self.env.context.get('precomputed_allocations')
for allocation in allocations:
allocation_data = allocations_leaves_consumed[allocation.employee_id][allocation.holiday_status_id][allocation]
precomputed = False
if precomputed_allocations:
if allocation.id in precomputed_allocations.ids:
allocation = precomputed_allocations.filtered(lambda alloc: alloc._origin.id == allocation.id)[0]
precomputed = True
future_leaves = 0
if allocation.allocation_type == 'accrual':
if allocation.allocation_type == 'accrual' and not precomputed:
future_leaves = allocation._get_future_leaves_on(target_date)
max_leaves = allocation.number_of_hours_display\
if allocation.holiday_status_id.request_unit in ['hour']\
@ -513,7 +525,11 @@ class HrEmployee(models.Model):
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
# Defines the order in which allocation will be used to take the leaves in priority
sorted_leave_allocations = (
allocations_with_date_to.sorted(key='date_to') +
allocations_without_date_to.filtered(lambda alloc: alloc.allocation_type == 'accrual') +
allocations_without_date_to.filtered(lambda alloc: alloc.allocation_type == 'regular'))
if leave_type.request_unit in ['day', 'half_day']:
leave_duration_field = 'number_of_days'

View file

@ -910,9 +910,11 @@ Versions:
if any(leave.state == 'cancel' for leave in self):
raise UserError(_('Only a manager can modify a canceled leave.'))
# Unlink existing resource.calendar.leaves for validated time off
if 'state' in values and values['state'] != 'validate':
validated_leaves = self.filtered(lambda l: l.state == 'validate')
# 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)

View file

@ -203,8 +203,8 @@ class HrLeaveAllocation(models.Model):
@api.depends('employee_id', 'holiday_status_id')
def _compute_leaves(self):
date_from = fields.Date.from_string(self.env.context['default_date_from']) if 'default_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]
date_from = 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:
origin = allocation._origin
virtual_leave = employee_days_per_allocation[origin.employee_id][origin.holiday_status_id][origin]
@ -436,6 +436,16 @@ class HrLeaveAllocation(models.Model):
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).
"""
def _get_leaves_taken(allocation):
precomputed_allocations = allocation
if context_precomputed := self.env.context.get('precomputed_allocations'):
precomputed_allocations |= context_precomputed
# By setting `precomputed_allocations`, avoid infinite loop (otherwise _get_consumed_leaves -> _get_future_leaves_on -> _process_accrual_plans -> ...)
employee_days_per_allocation = allocation.employee_id.with_context(precomputed_allocations=precomputed_allocations)._get_consumed_leaves(
allocation.holiday_status_id, allocation.nextcall, ignore_future=True)[0]
origin = allocation._origin
leaves_taken = employee_days_per_allocation[origin.employee_id][origin.holiday_status_id][origin]['leaves_taken']
return leaves_taken
date_to = date_to or fields.Date.today()
already_accrued = {allocation.id: allocation.already_accrued or (allocation.number_of_days != 0 and allocation.accrual_plan_id.accrued_gain_time == 'start') for allocation in self}
@ -452,10 +462,6 @@ class HrLeaveAllocation(models.Model):
# even if the value doesn't change. This is the best performance atm.
first_level = level_ids[0]
first_level_start_date = allocation.date_from + get_timedelta(first_level.start_count, first_level.start_type)
if allocation.holiday_status_id.request_unit in ["day", "half_day"]:
leaves_taken = allocation.leaves_taken
else:
leaves_taken = allocation.leaves_taken / allocation.employee_id._get_hours_per_day(allocation.date_from)
allocation.already_accrued = already_accrued[allocation.id]
# first time the plan is run, initialize nextcall and take carryover / level transition into account
if not allocation.nextcall:
@ -480,6 +486,10 @@ class HrLeaveAllocation(models.Model):
# get current level and normal period boundaries, then set nextcall, adjusted for level transition and carryover
# add days, trimmed if there is a maximum_leave
while allocation.nextcall <= date_to:
if allocation.holiday_status_id.request_unit in ["day", "half_day"]:
leaves_taken = _get_leaves_taken(allocation)
else:
leaves_taken = _get_leaves_taken(allocation) / allocation.employee_id._get_hours_per_day(allocation.nextcall or allocation.date_from)
(current_level, current_level_idx) = allocation._get_current_accrual_plan_level_id(allocation.nextcall)
if not current_level:
break
@ -487,7 +497,7 @@ class HrLeaveAllocation(models.Model):
if current_level.added_value_type == "day":
current_level_maximum_leave = current_level.maximum_leave
else:
current_level_maximum_leave = current_level.maximum_leave / allocation.employee_id._get_hours_per_day(allocation.date_from)
current_level_maximum_leave = current_level.maximum_leave / allocation.employee_id._get_hours_per_day(allocation.nextcall or allocation.date_from)
nextcall = current_level._get_next_date(allocation.nextcall)
# Since _get_previous_date returns the given date if it corresponds to a call date
# this will always return lastcall except possibly on the first call
@ -530,7 +540,7 @@ class HrLeaveAllocation(models.Model):
# allocation.expiring_carryover_days - allocation.leaves_taken or 0 if all the expiring days were used
# to take time off.
# This ensures that only the days that weren't used to take time off will expire.
expiring_days = max(0, allocation.expiring_carryover_days - allocation.leaves_taken)
expiring_days = max(0, allocation.expiring_carryover_days - leaves_taken)
allocation.number_of_days = max(0, allocation.number_of_days - expiring_days)
allocation.expiring_carryover_days = 0
@ -571,6 +581,7 @@ class HrLeaveAllocation(models.Model):
if allocation.accrual_plan_id.accrued_gain_time == 'start' and allocation.last_executed_carryover_date:
last_carryover_date = allocation.last_executed_carryover_date
carryover_level, carryover_level_idx = allocation._get_current_accrual_plan_level_id(last_carryover_date)
carryover_period_start = carryover_level._get_previous_date(last_carryover_date)
carryover_period_end = carryover_level._get_next_date(last_carryover_date)
# Adjust carryover_period_end based on level_transition.
if carryover_level_idx < (len(level_ids) - 1) and allocation.accrual_plan_id.transition_mode == 'immediately':
@ -586,7 +597,8 @@ class HrLeaveAllocation(models.Model):
# That is why (allocation.nextcall == period_end) is used instead of (is_accrual_date)
accrued = not allocation.already_accrued and allocation.nextcall == period_end
# If the days were accrued on the carryover period, then apply the carryover policy
if accrued and last_carryover_date <= allocation.nextcall <= carryover_period_end:
# If allocation.actual_lastcall == carryover_period_start, it means this loop has already been run once (skip to avoid applying the carryover twice)
if accrued and last_carryover_date <= allocation.nextcall <= carryover_period_end and allocation.actual_lastcall != carryover_period_start:
if carryover_level.action_with_unused_accruals == 'lost' or carryover_level.carryover_options == 'limited':
allocation.last_executed_carryover_date = carryover_date
allocated_days_left = allocation.number_of_days - leaves_taken
@ -622,7 +634,8 @@ class HrLeaveAllocation(models.Model):
current_level_maximum_leave = current_level.maximum_leave / allocation.employee_id._get_hours_per_day(allocation.date_from)
if allocation.actual_lastcall in {period_start, allocation.date_from} | set(level_start.keys())\
or (allocation.actual_lastcall - get_timedelta(current_level.accrual_validity_count, current_level.accrual_validity_type)
in {period_start, allocation.date_from} | set(level_start.keys())):
in {period_start, allocation.date_from} | set(level_start.keys())):
leaves_taken = _get_leaves_taken(allocation)
allocation._add_days_to_allocation(current_level, current_level_maximum_leave, leaves_taken, period_start, allocation.nextcall)
allocation.already_accrued = True
@ -655,8 +668,8 @@ class HrLeaveAllocation(models.Model):
and (not self.nextcall or self.nextcall <= accrual_date)):
return 0
fake_allocation = self.env['hr.leave.allocation'].with_context(default_date_from=accrual_date).new(origin=self)
fake_allocation.sudo().with_context(default_date_from=accrual_date)._process_accrual_plans(accrual_date, log=False)
fake_allocation = self.env['hr.leave.allocation'].new(origin=self)
fake_allocation.sudo()._process_accrual_plans(accrual_date, log=False)
if self.holiday_status_id.request_unit in ['hour']:
res = float_round(fake_allocation.number_of_hours_display - self.number_of_hours_display, precision_digits=2)
else:

View file

@ -675,8 +675,8 @@ class HrLeaveType(models.Model):
def _get_carried_over_days_expiration_data(self, allocations, target_date):
fake_allocations = self.env['hr.leave.allocation']
for allocation in allocations:
fake_allocations |= self.env['hr.leave.allocation'].with_context(default_date_from=target_date).new(origin=allocation)
fake_allocations.sudo().with_context(default_date_from=target_date)._process_accrual_plans(target_date, log=False)
fake_allocations |= self.env['hr.leave.allocation'].new(origin=allocation)
fake_allocations.sudo()._process_accrual_plans(target_date, log=False)
carried_over_days_expiration_data = {
fake_allocation._origin:
{