diff --git a/hr_employee_stats_sheet/models/hr_employee_stats.py b/hr_employee_stats_sheet/models/hr_employee_stats.py index 999fe30..ae170a2 100644 --- a/hr_employee_stats_sheet/models/hr_employee_stats.py +++ b/hr_employee_stats_sheet/models/hr_employee_stats.py @@ -1,9 +1,9 @@ import logging -import datetime +import pytz + from odoo import api, fields, models from datetime import timedelta -from odoo.fields import Date -import pytz +from pytz import utc _logger = logging.getLogger(__name__) @@ -147,7 +147,7 @@ class HrEmployeeStats(models.Model): total_leave_hours = leave_id.number_of_hours_display elif leave_id.request_unit_half: total_leave_hours = self._get_total_planned_hours() / 2 - else: + else : total_leave_hours = self._get_total_planned_hours() return total_leave_hours @@ -167,21 +167,21 @@ class HrEmployeeStats(models.Model): stat.is_public_holiday = stat._is_public_holiday_accordig_to_employe_tz() def _convert_to_employee_tz(self, date): - """Convertit un datetime UTC en datetime dans le fuseau de l'employé.""" + """Convert a UTC datetime to the employee's timezone datetime.""" self.ensure_one() if not date: return None - employee_tz = pytz.timezone(self.sheet_id.employee_id.tz or "UTC") + employee_tz = pytz.timezone(self.employee_id.tz or "UTC") if date.tzinfo is None: dt = pytz.utc.localize(date) - return dt.astimezone(employee_tz) - timedelta(minutes=1) + return dt.astimezone(employee_tz) def _is_public_holiday_accordig_to_employe_tz(self): self.ensure_one() - if not self.date or not self.sheet_id or not self.sheet_id.employee_id: + if not self.date or not self.employee_id: return False #get public holidays for the employee - public_holidays = self.sheet_id.employee_id._get_public_holidays( + public_holidays = self.employee_id._get_public_holidays( self.date, self.date ) if not public_holidays: @@ -198,13 +198,14 @@ class HrEmployeeStats(models.Model): return True else: return False - - def _get_gap_hours( - self, total_hours, total_recovery_hours, total_leave_hours, total_planned_hours - ): + + def _get_gap_hours(self, total_hours, total_recovery_hours, total_leave_hours, total_planned_hours): self.ensure_one() balance = ( - total_hours + total_recovery_hours + total_leave_hours - total_planned_hours + total_hours + + total_recovery_hours + + total_leave_hours + - total_planned_hours ) return balance diff --git a/hr_employee_stats_sheet/tests/test_employee_stats.py b/hr_employee_stats_sheet/tests/test_employee_stats.py index 6b3b0f3..4ca14a6 100644 --- a/hr_employee_stats_sheet/tests/test_employee_stats.py +++ b/hr_employee_stats_sheet/tests/test_employee_stats.py @@ -1,8 +1,9 @@ -from odoo.tests.common import TransactionCase -from odoo.tests import tagged -from datetime import date, timedelta +from datetime import date, datetime, timedelta + from odoo.exceptions import UserError from odoo.fields import Date +from odoo.tests import tagged +from odoo.tests.common import TransactionCase @tagged("post_install", "-at_install") class TestHrEmployeeStatsRecovery(TransactionCase): @@ -37,12 +38,13 @@ class TestHrEmployeeStatsRecovery(TransactionCase): (0, 0, {'name': 'Friday Afternoon', 'dayofweek': '4', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}), ], }) + self.base_calendar.hours_per_day = 7 self.employee.resource_calendar_id = self.base_calendar self.env.company.recovery_type_id = self.recovery_type self.env.company.coef = 25 def _create_timesheet_sheet(self, start_date): - # Create a timesheet sheet for the week (Monday to Sunday) + # Create a timesheet for the week (Monday to Sunday) return self.env['hr_timesheet.sheet'].create({ 'employee_id': self.employee.id, 'date_start': start_date, @@ -69,7 +71,9 @@ class TestHrEmployeeStatsRecovery(TransactionCase): yield stat def test_invalide_recovery_type(self): - start_date = date.today() - timedelta(days=date.today().weekday() + 7) # monday of last week + start_date = date.today() - timedelta( + days=date.today().weekday() + 7 + ) # monday of last week self.recovery_type.request_unit = 'day' self.recovery_type.allows_negative = False timesheet_sheet = self._create_timesheet_sheet(start_date) @@ -81,7 +85,7 @@ class TestHrEmployeeStatsRecovery(TransactionCase): start_date = date.today() - timedelta(days=date.today().weekday() + 7) # monday of last week timesheet_sheet = self._create_timesheet_sheet(start_date) for stat in self._create_stats(start_date, 5, 7): # create 5 stats of 7h each - # Compare calculated recovery hours with the calendar which plans 7h per day + # Compare calculated recovery hours and the calendar which plans 7h per day self.assertEqual(stat.total_hours, 7, "total_hours should be 7",) # the employee worked 7h each day self.assertEqual(stat.total_planned_hours, 7, "total_planned_hours should be 7",) # the calendar plans 7h each day self.assertEqual(stat.total_leave_hours, 0, "total_leave_hours should be 0",) # the employee has no leave on this day @@ -92,10 +96,12 @@ class TestHrEmployeeStatsRecovery(TransactionCase): self.assertEqual(timesheet_sheet.timesheet_sheet_recovery_hours, 0, "timesheet_sheet_recovery_hours should be 0",) def test_positive_recovery_hours(self): - start_date = date.today() - timedelta(days=date.today().weekday() + 7) # monday of last week + start_date = date.today() - timedelta( + days=date.today().weekday() + 7 + ) # monday of last week timesheet_sheet = self._create_timesheet_sheet(start_date) for stat in self._create_stats(start_date, 5, 8): # create 5 stats of 8h each - # Compare calculated recovery hours with the calendar which plans 7h per day + # Compare calculated recovery hours and the calendar which plans 7h per day self.assertEqual(stat.total_hours, 8, "total_hours should be 8",) # the employee worked 8h each day self.assertEqual(stat.total_planned_hours, 7, "total_planned_hours should be 7",) # the calendar plans 7h each day self.assertEqual(stat.total_leave_hours, 0, "total_leave_hours should be 0",) # the employee has no leave on this day @@ -108,13 +114,13 @@ class TestHrEmployeeStatsRecovery(TransactionCase): timesheet_sheet.action_generate_recovery_allocation() recovery_allocation = self.env["hr.leave.allocation"].search([("timesheet_sheet_id","=",timesheet_sheet.id)]) self.assertEqual(len(recovery_allocation), 1, "There should be one recovery") - self.assertEqual(recovery_allocation.number_of_days,0.78125, "The recovery allocation should be for 6.25h/8h = 0.78125 day") + self.assertEqual(recovery_allocation.number_of_days,0.8928571428571429, "The recovery allocation should be for 6.25h/7h = 0.8928571428571429 day") def test_negative_recovery_hours(self): start_date = date.today() - timedelta(days=date.today().weekday() + 7) # monday of last week timesheet_sheet = self._create_timesheet_sheet(start_date) for stat in self._create_stats(start_date, 5, 6): # create 5 stats of 6h each - # Compare calculated recovery hours with the calendar which plans 7h per day + # Compare calculated recovery hours and the calendar which plans 7h per day self.assertEqual(stat.total_hours, 6, "total_hours should be 6",) # the employee worked 6h each day self.assertEqual(stat.total_planned_hours, 7, "total_planned_hours should be 7",) # the calendar plans 7h each day self.assertEqual(stat.total_leave_hours, 0, "total_leave_hours should be 0",) # the employee has no leave on this day @@ -122,12 +128,12 @@ class TestHrEmployeeStatsRecovery(TransactionCase): self.assertEqual(stat.gap_hours, -1, "gap_hours should be -1",) # the employee worked one hour less than planned # The timesheet should count -5 gap hours and -5 recovery hours self.assertEqual(timesheet_sheet.timesheet_sheet_gap_hours, -5, "timesheet_sheet_gap_hours should be -5",) # total gap hours for the week - self.assertEqual(timesheet_sheet.timesheet_sheet_recovery_hours, -5, "timesheet_sheet_recovery_hours should be -5",) # -5h will be the recovery allocation (no coef for deficits) + self.assertEqual(timesheet_sheet.timesheet_sheet_recovery_hours, -5, "timesheet_sheet_recovery_hours should be -5",) # -5h will be the recovery allocation (no coef applied for deficits) # generate the recovery allocation from the timesheet timesheet_sheet.action_generate_recovery_allocation() recovery_allocation = self.env["hr.leave.allocation"].search([("timesheet_sheet_id","=",timesheet_sheet.id)]) self.assertEqual(len(recovery_allocation), 1, "There should be one recovery") - self.assertEqual(recovery_allocation.number_of_days, -0.625, "The recovery allocation should be for -5/8 hours = ") + self.assertEqual(recovery_allocation.number_of_days, -0.7142857142857143, "The recovery allocation should be for -5/7 hours = 0.7142857142857143 days") def test_recovery_hours_part_time_employee(self): part_time_calendar = self.env['resource.calendar'].create({ @@ -144,11 +150,10 @@ class TestHrEmployeeStatsRecovery(TransactionCase): ], }) self.employee.resource_calendar_id = part_time_calendar.id - start_date = date.today() - timedelta(days=date.today().weekday() + 7) # monday of last week timesheet_sheet = self._create_timesheet_sheet(start_date) for stat in self._create_stats(start_date, 4, 8): # create 4 stats of 8h each - # Compare calculated recovery hours with the calendar which plans 7h per day for 4 days + # Compare calculated recovery hours and the calendar which plans 7h per day for 4 days self.assertEqual(stat.total_hours, 8, "total_hours should be 8",) # the employee worked 8h each day self.assertEqual(stat.total_planned_hours, 7, "total_planned_hours should be 7",) # the calendar plans 7h each day self.assertEqual(stat.total_leave_hours, 0, "total_leave_hours should be 0",) # the employee has no leave on this day @@ -172,7 +177,7 @@ class TestHrEmployeeStatsRecovery(TransactionCase): (0, 0, {'name': 'Thursday Afternoon', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}), ], }) - # create one contract ending on wednesday and another starting on thursday + #create one contract ending on wednesday and one other starting on thursday self.env['hr.contract'].create({ 'name': 'Contract 1', 'employee_id': self.employee.id, @@ -193,9 +198,9 @@ class TestHrEmployeeStatsRecovery(TransactionCase): }) self.employee.resource_calendar_id = part_time_calendar.id start_date = date.today() - timedelta(days=date.today().weekday() + 7) # monday of last week - # create a timesheet with a period that includes the contract change + # create a timesheet with period including the change of contract timesheet_sheet = self._create_timesheet_sheet(start_date) - #the create of recovery allocation should raise an error + # the creation of the recovery allocation should raise an error with self.assertRaises(UserError): timesheet_sheet.action_generate_recovery_allocation() @@ -243,41 +248,36 @@ class TestHrEmployeeStatsRecovery(TransactionCase): recovery_allocation = self.env["hr.leave.allocation"].search([("timesheet_sheet_id","=",timesheet_sheet_1.id)]) self.assertEqual(len(recovery_allocation), 1, "There should be one recovery") - def test_public_holiday_detection(self): - # Create a public holiday on the Wednesday of the test week and verify detection - start_date = date.today() - timedelta(days=date.today().weekday() + 7) # monday of last week - timesheet_sheet = self._create_timesheet_sheet(start_date) - holiday_date = start_date + timedelta(days=2) # Wednesday - # create a global public holiday (no resource or calendar restriction) - self.env['resource.calendar.leaves'].create({ - 'name': 'Public Holiday', - 'company_id': self.env.company.id, - 'date_from': Date.to_string(holiday_date), - 'date_to': Date.to_string(holiday_date), - 'resource_id': False, - 'calendar_id': False, - }) - # create a timesheet entry on that day - self.env['account.analytic.line'].create({ - 'employee_id': self.employee.id, - 'date': holiday_date, - 'unit_amount': 7, - 'account_id': 1, - 'name': 'Work Entry', - }) - # create the stat for that date and recompute - stat = self.env['hr.employee.stats'].create({ - 'employee_id': self.employee.id, - 'date': holiday_date, - 'sheet_id': timesheet_sheet.id, - }) - stat._compute_dayofweek() - stat._compute_timesheet_line_ids() - stat._compute_hours() - # The stat must detect the public holiday and planned hours must be zero - self.assertTrue(stat.is_public_holiday, "The day should be detected as a public holiday") - self.assertEqual(stat.total_planned_hours, 0, "total_planned_hours should be 0 on a public holiday") + recovery_allocation = self.env["hr.leave.allocation"].search([("timesheet_sheet_id","=",timesheet_sheet_2.id)]) + self.assertEqual(len(recovery_allocation), 1, "There should be one recovery") + def test_public_holiday(self): + # create a public holiday + self.env["resource.calendar.leaves"].create( + { + "name": "1 mai 2025", + "date_from": datetime(2025,4,30,22,0,0), + "date_to": datetime(2025,5,1,21,0,0), + } + ) + #create 5 stats of 7h each including the public holiday on 1st may + stats = self._create_stats(Date.to_date("2025-04-28"), 5, 7) + for stat in stats: + stat._compute_dayofweek() + stat._compute_hours() + #create 1 timesheet sheet from monday to friday including the public holiday on 1st may + timesheet_sheet = self.env['hr_timesheet.sheet'].create({ + 'employee_id': self.employee.id, + 'date_start': "2025-04-28", + 'date_end': "2025-05-04", + }) + self.assertEqual(timesheet_sheet.timesheet_sheet_gap_hours, 7, "timesheet_sheet_gap_hours should be 7",) + self.assertEqual(timesheet_sheet.timesheet_sheet_recovery_hours, 8.75, "timesheet_sheet_recovery_hours should be 8,75",) + + timesheet_sheet.action_generate_recovery_allocation() + recovery_allocation = self.env["hr.leave.allocation"].search([("timesheet_sheet_id","=",timesheet_sheet.id)]) + self.assertEqual(len(recovery_allocation), 1, "Il doit y avoir une allocation de récupération générée") + self.assertEqual(recovery_allocation.number_of_days,1.25, "The recovery allocation should be 1,25 days")