From 9de40b92eec780cdf6c8ab0cd7cae652685aadfe Mon Sep 17 00:00:00 2001 From: Yannick Tivisse Date: Mon, 18 Sep 2023 09:57:56 +0200 Subject: [PATCH] [IMP] hr_holidays: Add hourly frequency on accruals closes odoo/odoo#133441 Signed-off-by: Yannick Tivisse (yti) --- .../models/hr_leave_accrual_plan_level.py | 8 +- .../hr_holidays/models/hr_leave_allocation.py | 43 +++++--- .../tests/test_accrual_allocations.py | 97 ++++++++++++++++++- .../views/hr_leave_accrual_views.xml | 4 + addons/hr_holidays_attendance/__manifest__.py | 1 + .../hr_holidays_attendance/models/__init__.py | 1 + .../models/hr_leave_accrual_plan_level.py | 25 +++++ .../models/hr_leave_allocation.py | 17 ++++ .../hr_holidays_attendance/tests/__init__.py | 1 + .../tests/test_accrual_allocations.py | 70 +++++++++++++ .../views/hr_leave_accrual_level_views.xml | 20 ++++ 11 files changed, 267 insertions(+), 20 deletions(-) create mode 100644 addons/hr_holidays_attendance/models/hr_leave_accrual_plan_level.py create mode 100644 addons/hr_holidays_attendance/tests/test_accrual_allocations.py create mode 100644 addons/hr_holidays_attendance/views/hr_leave_accrual_level_views.xml diff --git a/addons/hr_holidays/models/hr_leave_accrual_plan_level.py b/addons/hr_holidays/models/hr_leave_accrual_plan_level.py index d054bc31dc6..326d5734b50 100644 --- a/addons/hr_holidays/models/hr_leave_accrual_plan_level.py +++ b/addons/hr_holidays/models/hr_leave_accrual_plan_level.py @@ -25,6 +25,7 @@ class AccrualPlanLevel(models.Model): string='sequence', compute='_compute_sequence', store=True, help='Sequence is generated automatically by start time delta.') accrual_plan_id = fields.Many2one('hr.leave.accrual.plan', "Accrual Plan", required=True, ondelete="cascade") + accrued_gain_time = fields.Selection(related='accrual_plan_id.accrued_gain_time') start_count = fields.Integer( "Start after", help="The accrual starts after a defined period from the allocation start date. This field defines the number of days, months or years after which accrual is used.", default="1") @@ -44,6 +45,7 @@ class AccrualPlanLevel(models.Model): ('hour', 'Hours') ], compute="_compute_added_value_type", store=True, required=True, default="day") frequency = fields.Selection([ + ('hourly', 'Hourly'), ('daily', 'Daily'), ('weekly', 'Weekly'), ('bimonthly', 'Twice a month'), @@ -121,7 +123,7 @@ class AccrualPlanLevel(models.Model): _sql_constraints = [ ('check_dates', - "CHECK( (frequency = 'daily') or" + "CHECK( (frequency IN ('daily', 'hourly')) or" "(week_day IS NOT NULL AND frequency = 'weekly') or " "(first_day > 0 AND second_day > first_day AND first_day <= 31 AND second_day <= 31 AND frequency = 'bimonthly') or " "(first_day > 0 AND first_day <= 31 AND frequency = 'monthly')or " @@ -211,7 +213,7 @@ class AccrualPlanLevel(models.Model): Returns the next date with the given last call """ self.ensure_one() - if self.frequency == 'daily': + if self.frequency in ['hourly', 'daily']: return last_call + relativedelta(days=1) elif self.frequency == 'weekly': daynames = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] @@ -260,7 +262,7 @@ class AccrualPlanLevel(models.Model): Contrary to `_get_next_date` this function will return the 01/02 if that date is given """ self.ensure_one() - if self.frequency == 'daily': + if self.frequency in ['hourly', 'daily']: return last_call elif self.frequency == 'weekly': daynames = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] diff --git a/addons/hr_holidays/models/hr_leave_allocation.py b/addons/hr_holidays/models/hr_leave_allocation.py index dea73b09ef5..c1166c1429f 100644 --- a/addons/hr_holidays/models/hr_leave_allocation.py +++ b/addons/hr_holidays/models/hr_leave_allocation.py @@ -401,27 +401,38 @@ class HolidaysAllocation(models.Model): return (previous_level, current_level_idx - 1) return (current_level, current_level_idx) + def _get_accrual_plan_level_work_entry_prorata(self, level, start_period, start_date, end_period, end_date): + self.ensure_one() + datetime_min_time = datetime.min.time() + start_dt = datetime.combine(start_date, datetime_min_time) + end_dt = datetime.combine(end_date, datetime_min_time) + worked = self.employee_id._get_work_days_data_batch(start_dt, end_dt, calendar=self.employee_id.resource_calendar_id)\ + [self.employee_id.id]['hours'] + if start_period != start_date or end_period != end_date: + start_dt = datetime.combine(start_period, datetime_min_time) + end_dt = datetime.combine(end_period, datetime_min_time) + planned_worked = self.employee_id._get_work_days_data_batch(start_dt, end_dt, calendar=self.employee_id.resource_calendar_id)\ + [self.employee_id.id]['hours'] + else: + planned_worked = worked + left = self.employee_id.sudo()._get_leave_days_data_batch(start_dt, end_dt, + domain=[('time_type', '=', 'leave')])[self.employee_id.id]['hours'] + if level.frequency == 'hourly': + if level.accrual_plan_id.is_based_on_worked_time: + work_entry_prorata = planned_worked + else: + work_entry_prorata = planned_worked + left + else: + work_entry_prorata = worked / (left + planned_worked) if (left + planned_worked) else 0 + return work_entry_prorata + def _process_accrual_plan_level(self, level, start_period, start_date, end_period, end_date): """ Returns the added days for that level """ self.ensure_one() - if level.accrual_plan_id.is_based_on_worked_time: - datetime_min_time = datetime.min.time() - start_dt = datetime.combine(start_date, datetime_min_time) - end_dt = datetime.combine(end_date, datetime_min_time) - worked = self.employee_id._get_work_days_data_batch(start_dt, end_dt, calendar=self.employee_id.resource_calendar_id)\ - [self.employee_id.id]['hours'] - if start_period != start_date or end_period != end_date: - start_dt = datetime.combine(start_period, datetime_min_time) - end_dt = datetime.combine(end_period, datetime_min_time) - planned_worked = self.employee_id._get_work_days_data_batch(start_dt, end_dt, calendar=self.employee_id.resource_calendar_id)\ - [self.employee_id.id]['hours'] - else: - planned_worked = worked - left = self.employee_id.sudo()._get_leave_days_data_batch(start_dt, end_dt, - domain=[('time_type', '=', 'leave')])[self.employee_id.id]['hours'] - work_entry_prorata = worked / (left + planned_worked) if (left + planned_worked) else 0 + if level.frequency == 'hourly' or level.accrual_plan_id.is_based_on_worked_time: + work_entry_prorata = self._get_accrual_plan_level_work_entry_prorata(level, start_period, start_date, end_period, end_date) added_value = work_entry_prorata * level.added_value else: added_value = level.added_value diff --git a/addons/hr_holidays/tests/test_accrual_allocations.py b/addons/hr_holidays/tests/test_accrual_allocations.py index c98b1cfbaab..2b23fdf284f 100644 --- a/addons/hr_holidays/tests/test_accrual_allocations.py +++ b/addons/hr_holidays/tests/test_accrual_allocations.py @@ -10,7 +10,7 @@ from odoo.tests import tagged from odoo.addons.hr_holidays.tests.common import TestHrHolidaysCommon -@tagged('post_install', '-at_install') +@tagged('post_install', '-at_install', 'accruals') class TestAccrualAllocations(TestHrHolidaysCommon): @classmethod def setUpClass(cls): @@ -41,6 +41,101 @@ class TestAccrualAllocations(TestHrHolidaysCommon): WHERE id = %s """ % (date, allocation_id)) + def test_frequency_hourly_calendar(self): + with freeze_time("2017-12-5"): + 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': 'day', + 'frequency': 'hourly', + 'cap_accrued_time': True, + 'maximum_leave': 10000 + })], + }) + 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': 0, + 'allocation_type': 'accrual', + }) + allocation.action_confirm() + allocation.action_validate() + self.assertFalse(allocation.nextcall, 'There should be no nextcall set on the allocation.') + self.assertEqual(allocation.number_of_days, 0, 'There should be no days allocated yet.') + allocation._update_accrual() + tomorrow = datetime.date.today() + relativedelta(days=2) + self.assertEqual(allocation.number_of_days, 0, 'There should be no days allocated yet. The accrual starts tomorrow.') + + with freeze_time(tomorrow): + allocation._update_accrual() + nextcall = datetime.date.today() + relativedelta(days=1) + self.assertEqual(allocation.number_of_days, 8, 'There should be 8 day allocated.') + self.assertEqual(allocation.nextcall, nextcall, 'The next call date of the cron should be in 2 days.') + allocation._update_accrual() + self.assertEqual(allocation.number_of_days, 8, 'There should be only 8 day allocated.') + + def test_frequency_hourly_worked_hours(self): + with freeze_time("2017-12-5"): + accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({ + 'name': 'Accrual Plan For Test', + 'is_based_on_worked_time': True, + 'level_ids': [(0, 0, { + 'start_count': 1, + 'start_type': 'day', + 'added_value': 1, + 'added_value_type': 'day', + 'frequency': 'hourly', + 'cap_accrued_time': True, + 'maximum_leave': 10000 + })], + }) + 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': 0, + 'allocation_type': 'accrual', + }) + allocation.action_confirm() + allocation.action_validate() + self.assertFalse(allocation.nextcall, 'There should be no nextcall set on the allocation.') + self.assertEqual(allocation.number_of_days, 0, 'There should be no days allocated yet.') + allocation._update_accrual() + tomorrow = datetime.date.today() + relativedelta(days=2) + self.assertEqual(allocation.number_of_days, 0, 'There should be no days allocated yet. The accrual starts tomorrow.') + + holiday_type = self.env['hr.leave.type'].create({ + 'name': 'Paid Time Off', + 'requires_allocation': 'no', + 'responsible_ids': [(4, self.user_hrmanager_id)], + 'time_type': 'leave', + 'request_unit': 'half_day', + }) + leave = self.env['hr.leave'].create({ + 'name': 'leave', + 'employee_id': self.employee_emp.id, + 'holiday_status_id': holiday_type.id, + 'request_date_from': '2017-12-06 08:00:00', + 'request_date_to': '2017-12-06 17:00:00', + 'request_unit_half': True, + 'request_date_from_period': 'am', + }) + leave.action_validate() + + with freeze_time(tomorrow): + allocation._update_accrual() + nextcall = datetime.date.today() + relativedelta(days=1) + self.assertEqual(allocation.number_of_days, 4, 'There should be 4 day allocated.') + self.assertEqual(allocation.nextcall, nextcall, 'The next call date of the cron should be in 2 days.') + allocation._update_accrual() + self.assertEqual(allocation.number_of_days, 4, 'There should be only 4 day allocated.') + def test_frequency_daily(self): with freeze_time("2017-12-5"): accrual_plan = self.env['hr.leave.accrual.plan'].with_context(tracking_disable=True).create({ diff --git a/addons/hr_holidays/views/hr_leave_accrual_views.xml b/addons/hr_holidays/views/hr_leave_accrual_views.xml index d6f99242f0a..0db9e107379 100644 --- a/addons/hr_holidays/views/hr_leave_accrual_views.xml +++ b/addons/hr_holidays/views/hr_leave_accrual_views.xml @@ -11,11 +11,15 @@