[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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user