[FIX]hr_employee_stats_sheet:convert public holidays in employee tz

This commit is contained in:
2025-11-05 11:41:08 +01:00
parent 20f04d710c
commit 54e3f32eda
2 changed files with 66 additions and 65 deletions

View File

@@ -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

View File

@@ -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")