[IMP] hr_holidays: allow limiting postponed days on accrual allocations

Adds a new option on accrual levels to limit the days that can be
post-poned year by year.

TaskId-2768674

closes odoo/odoo#85281

Related: odoo/upgrade#3279
Signed-off-by: Kevin Baptiste <kba@odoo.com>
This commit is contained in:
William Braeckman
2022-03-30 11:13:21 +02:00
parent 81b56427c6
commit f665aececa
4 changed files with 93 additions and 20 deletions
@@ -121,6 +121,8 @@ class AccrualPlanLevel(models.Model):
('lost', 'Lost')],
string="At the end of the calendar year, unused accruals will be",
default='postponed', required='True')
postpone_max_days = fields.Integer("Maximum amount of accruals to transfer",
help="Set a maximum of days an allocation keeps at the end of the year. 0 for no limit.")
_sql_constraints = [
('check_dates',
@@ -330,11 +330,22 @@ class HolidaysAllocation(models.Model):
def _end_of_year_accrual(self):
# to override in payroll
today = fields.Date.today()
last_day_last_year = today + relativedelta(years=-1, month=12, day=31)
for allocation in self:
current_level = allocation._get_current_accrual_plan_level_id(today)[0]
if current_level and current_level.action_with_unused_accruals == 'lost':
if not current_level:
continue
nextcall = current_level._get_next_date(today)
if current_level.action_with_unused_accruals == 'lost':
# Allocations are lost but number_of_days should not be lower than leaves_taken
allocation.write({'number_of_days': allocation.leaves_taken, 'lastcall': today, 'nextcall': False})
allocation.write({'number_of_days': allocation.leaves_taken, 'lastcall': today, 'nextcall': nextcall})
elif current_level.action_with_unused_accruals == 'postponed' and current_level.postpone_max_days:
# Make sure the period was ran until the last day of last year
if allocation.nextcall:
allocation.nextcall = last_day_last_year
allocation._process_accrual_plans(last_day_last_year, True)
number_of_days = min(allocation.number_of_days - allocation.leaves_taken, current_level.postpone_max_days) + allocation.leaves_taken
allocation.write({'number_of_days': number_of_days, 'lastcall': today, 'nextcall': nextcall})
def _get_current_accrual_plan_level_id(self, date, level_ids=False):
"""
@@ -392,12 +403,13 @@ 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):
def _process_accrual_plans(self, date_to=False, force_period=False):
"""
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 today
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).
"""
today = fields.Date.today()
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.""")
for allocation in self:
level_ids = allocation.accrual_plan_id.level_ids.sorted('sequence')
@@ -406,7 +418,7 @@ class HolidaysAllocation(models.Model):
if not allocation.nextcall:
first_level = level_ids[0]
first_level_start_date = allocation.date_from + get_timedelta(first_level.start_count, first_level.start_type)
if today < first_level_start_date:
if date_to < first_level_start_date:
# Accrual plan is not configured properly or has not started
continue
allocation.lastcall = max(allocation.lastcall, first_level_start_date)
@@ -416,7 +428,7 @@ class HolidaysAllocation(models.Model):
allocation.nextcall = min(second_level_start_date - relativedelta(days=1), allocation.nextcall)
allocation._message_log(body=first_allocation)
days_added_per_level = defaultdict(lambda: 0)
while allocation.nextcall <= today:
while allocation.nextcall <= date_to:
(current_level, current_level_idx) = allocation._get_current_accrual_plan_level_id(allocation.nextcall)
nextcall = current_level._get_next_date(allocation.nextcall)
# Since _get_previous_date returns the given date if it corresponds to a call date
@@ -424,29 +436,46 @@ class HolidaysAllocation(models.Model):
# this is used to prorate the first number of days given to the employee
period_start = current_level._get_previous_date(allocation.lastcall)
period_end = current_level._get_next_date(allocation.lastcall)
# If accruals are lost at the beginning of year, skip accrual until beginning of this year
if current_level.action_with_unused_accruals == 'lost':
this_year_first_day = (today + relativedelta(day=1, month=1)).date()
if period_end < this_year_first_day or period_start < period_end:
allocation.lastcall = allocation.nextcall
allocation.nextcall = nextcall
continue
else:
period_start = max(period_start, this_year_first_day)
# Also prorate this accrual in the event that we are passing from one level to another
if current_level_idx < (len(level_ids) - 1) and allocation.accrual_plan_id.transition_mode == 'immediately':
next_level = level_ids[current_level_idx + 1]
current_level_last_date = allocation.date_from + get_timedelta(next_level.start_count, next_level.start_type) - relativedelta(days=1)
if allocation.nextcall != current_level_last_date:
nextcall = min(nextcall, current_level_last_date)
days_added_per_level[current_level] += allocation._process_accrual_plan_level(
gained_days = allocation._process_accrual_plan_level(
current_level, period_start, allocation.lastcall, period_end, allocation.nextcall)
days_added_per_level[current_level] += gained_days
# We have to check for end of year actions if it is within our period
# since we can create retroactive allocations.
if allocation.lastcall.year < allocation.nextcall.year and\
(current_level.action_with_unused_accruals == 'lost' or\
current_level.postpone_max_days > 0):
after_period_gains = allocation._process_accrual_plan_level(
current_level, period_start, allocation.nextcall + relativedelta(day=1, month=1),
period_end, allocation.nextcall)
if current_level.action_with_unused_accruals == 'postponed':
# Compute number of days kept
allocation_days = allocation.number_of_days - allocation.leaves_taken
allowed_to_keep = max(0, current_level.postpone_max_days - allocation_days)
number_of_days = min(allocation_days, current_level.postpone_max_days)
allocation.number_of_days = number_of_days + allocation.leaves_taken
total_gained_days = sum(days_added_per_level.values())
days_added_per_level.clear()
days_added_per_level[current_level] = min(total_gained_days - after_period_gains, allowed_to_keep)
else:
allocation.number_of_days = allocation.leaves_taken
days_added_per_level.clear()
days_added_per_level[current_level] += after_period_gains
allocation.lastcall = allocation.nextcall
allocation.nextcall = nextcall
if force_period and allocation.nextcall > date_to:
allocation.nextcall = date_to
force_period = False
if days_added_per_level:
number_of_days_to_add = 0
for value in days_added_per_level.values():
number_of_days_to_add += value
number_of_days_to_add = sum(days_added_per_level.values())
# Let's assume the limit of the last level is the correct one
allocation.write({'number_of_days': min(allocation.number_of_days + number_of_days_to_add, current_level.maximum_leave)})
@@ -522,6 +522,9 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
allocation.action_confirm()
allocation.action_validate()
# Reset the cron's lastcall
accrual_cron = self.env['ir.cron'].sudo().env.ref('hr_holidays.hr_leave_allocation_cron_accrual')
accrual_cron.lastcall = datetime.date(2021, 9, 1)
with freeze_time('2022-01-01'):
allocation._update_accrual()
self.assertEqual(allocation.number_of_days, 0, 'There number of days should be reset')
@@ -553,10 +556,48 @@ class TestAccrualAllocations(TestHrHolidaysCommon):
allocation.action_confirm()
allocation.action_validate()
# Reset the cron's lastcall
accrual_cron = self.env['ir.cron'].sudo().env.ref('hr_holidays.hr_leave_allocation_cron_accrual')
accrual_cron.lastcall = datetime.date(2021, 9, 1)
with freeze_time('2022-01-01'):
allocation._update_accrual()
self.assertEqual(allocation.number_of_days, 25, 'The maximum number of days should be reached and kept.')
def test_unused_accrual_postponed_limit(self):
# 1 accrual with 2 levels and level transition after
# This also tests retroactivity
with freeze_time('2021-09-01'):
accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({
'name': 'Accrual Plan For Test',
'level_ids': [(0, 0, {
'start_count': 1,
'start_type': 'day',
'added_value': 1,
'added_value_type': 'days',
'frequency': 'daily',
'maximum_leave': 25,
'action_with_unused_accruals': 'postponed',
'postpone_max_days': 15,
})],
})
allocation = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True).create({
'name': 'Accrual allocation for employee',
'accrual_plan_id': accrual_plan.id,
'employee_id': self.employee_emp.id,
'holiday_status_id': self.leave_type.id,
'number_of_days': 10,
'allocation_type': 'accrual',
})
allocation.action_confirm()
allocation.action_validate()
# Reset the cron's lastcall
accrual_cron = self.env['ir.cron'].sudo().env.ref('hr_holidays.hr_leave_allocation_cron_accrual')
accrual_cron.lastcall = datetime.date(2021, 9, 1)
with freeze_time('2022-01-01'):
allocation._update_accrual()
self.assertEqual(allocation.number_of_days, 15, 'The maximum number of days should be reached and kept.')
def test_accrual_skipped_period(self):
# Test that when an allocation is made in the past and the second level is technically reached
# that the first level is not skipped completely.
@@ -52,6 +52,7 @@
</div>
</div>
<field name="action_with_unused_accruals"/>
<field name="postpone_max_days" attrs="{'invisible': [('action_with_unused_accruals', '!=', 'postponed')]}"/>
</group>
</sheet>
</form>