From 11713bfb1d22d6d7b898fce942f0de03cc644b30 Mon Sep 17 00:00:00 2001 From: mafo-odoo Date: Tue, 16 Aug 2022 12:26:09 +0000 Subject: [PATCH] [FIX] hr_holidays: accross two years accrual plans period fail: Steps to reproduce: - Create Accrual plan to add 3 days with frequency Yearly on 1 of January and days lost at the end of the year - Submit an Allocation request for this accrual plan starting more than 2 years ago - Run scheduled action "Accrual Time: Updates the number of time off" Current behavior: Allocation request calculation time off computes 0 hours Expected behavior: Allocation request calculation time off computes 3 hours for the period between the first of last year and the first of this year Explanation: With this modification there are two changes: 1) When the attribution day is the first of january, the nextcall set in the _end_of_the_year_accrual is the first day of next year meaning that no days are going to be computed for the last year period. We change that so that if the first of the year is the attribution day then the first of the year is the nextcall and the last call is computed accordingly. 2) If the period to compute the attributed days is acrros two years the begining of the period is not the first of the year anymore but the usual lastcall, this has an impact particularly on based on worked time accrual plans. opw-2948216 closes odoo/odoo#99728 X-original-commit: 88ba0e68c7878ac4b45cd7103bfecf9cfb567d4f Signed-off-by: Fockedey Martin (mafo) Signed-off-by: Kevin Baptiste --- .../hr_holidays/models/hr_leave_allocation.py | 43 ++++++++----------- .../tests/test_accrual_allocations.py | 32 +++++++++++++- 2 files changed, 50 insertions(+), 25 deletions(-) diff --git a/addons/hr_holidays/models/hr_leave_allocation.py b/addons/hr_holidays/models/hr_leave_allocation.py index ecffa3e9cc5..1c56af6cc52 100644 --- a/addons/hr_holidays/models/hr_leave_allocation.py +++ b/addons/hr_holidays/models/hr_leave_allocation.py @@ -330,17 +330,21 @@ class HolidaysAllocation(models.Model): current_level = allocation._get_current_accrual_plan_level_id(first_day_this_year)[0] if not current_level: continue + lastcall = current_level._get_previous_date(first_day_this_year) nextcall = current_level._get_next_date(first_day_this_year) if current_level.action_with_unused_accruals == 'lost': + if lastcall == first_day_this_year: + lastcall = current_level._get_previous_date(first_day_this_year - relativedelta(days=1)) + nextcall = first_day_this_year # Allocations are lost but number_of_days should not be lower than leaves_taken - allocation.write({'number_of_days': allocation.leaves_taken, 'lastcall': first_day_this_year, 'nextcall': nextcall}) + allocation.write({'number_of_days': allocation.leaves_taken, 'lastcall': lastcall, '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': first_day_this_year, 'nextcall': nextcall}) + allocation.write({'number_of_days': number_of_days, 'lastcall': lastcall, 'nextcall': nextcall}) def _get_current_accrual_plan_level_id(self, date, level_ids=False): """ @@ -444,33 +448,24 @@ class HolidaysAllocation(models.Model): current_level_last_date = allocation.date_from + get_timedelta(next_level.start_count, next_level.start_type) if allocation.nextcall != current_level_last_date: nextcall = min(nextcall, current_level_last_date) - + # 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 == 'postponed' and\ + current_level.postpone_max_days > 0: + # 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, allowed_to_keep) 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 if current_level.maximum_leave > 0 and sum(days_added_per_level.values()) > current_level.maximum_leave: days_added_per_level[current_level] -= sum(days_added_per_level.values()) - current_level.maximum_leave - # 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 diff --git a/addons/hr_holidays/tests/test_accrual_allocations.py b/addons/hr_holidays/tests/test_accrual_allocations.py index ac1539b3493..f32aa237b4c 100644 --- a/addons/hr_holidays/tests/test_accrual_allocations.py +++ b/addons/hr_holidays/tests/test_accrual_allocations.py @@ -534,7 +534,7 @@ class TestAccrualAllocations(TestHrHolidaysCommon): 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') + self.assertEqual(allocation.number_of_days, 1, 'The number of days should be reset') def test_unused_accrual_postponed(self): # 1 accrual with 2 levels and level transition after @@ -733,6 +733,36 @@ class TestAccrualAllocations(TestHrHolidaysCommon): allocation.action_validate() with freeze_time('2022-4-4'): allocation._update_accrual() + self.assertEqual(allocation.number_of_days, 4, "Invalid number of days") + + def test_accrual_lost_first_january(self): + 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': 0, + 'start_type': 'day', + 'added_value': 3, + 'added_value_type': 'days', + 'frequency': 'yearly', + 'maximum_leave': 12, + 'action_with_unused_accruals': 'lost', + }) + ], + }) + allocation = self.env['hr.leave.allocation'].with_user(self.user_hrmanager_id).with_context(tracking_disable=True).create({ + 'name': 'Accrual Allocation - Test', + 'accrual_plan_id': accrual_plan.id, + 'employee_id': self.employee_emp.id, + 'holiday_status_id': self.leave_type.id, + 'number_of_days': 0, + 'allocation_type': 'accrual', + 'date_from': datetime.date(2019, 1, 1), + }) + allocation.action_confirm() + allocation.action_validate() + with freeze_time('2022-4-1'): + allocation._update_accrual() self.assertEqual(allocation.number_of_days, 3, "Invalid number of days") def test_accrual_maximum_leaves(self):