Files
hr-tools/hr_employee_stats_sheet/tests/test_employee_stats.py

295 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from odoo.tests.common import TransactionCase
from odoo.tests import tagged
from datetime import date, timedelta, datetime
from odoo.exceptions import UserError
from odoo.fields import Date
@tagged("post_install", "-at_install")
class TestHrEmployeeStatsRecovery(TransactionCase):
def setUp(self):
super().setUp()
self.user = self.env['res.users'].create({
'name': 'Camille',
'login': 'camille',
})
self.recovery_type = self.env['hr.leave.type'].create({
'name': 'Recovery',
'request_unit': 'hour',
'allows_negative': True,
})
self.employee = self.env['hr.employee'].create({
'name': 'Camille',
'user_id': self.user.id,
'tz': 'Europe/Paris',
})
self.base_calendar = self.env['resource.calendar'].create({
'name': 'Default Calendar',
'attendance_ids': [
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Monday Afternoon', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Tuesday Afternoon', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Wednesday Afternoon', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Thursday Afternoon', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Friday Morning', 'dayofweek': '4', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
(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 for the week (Monday to Sunday)
return self.env['hr_timesheet.sheet'].create({
'employee_id': self.employee.id,
'date_start': start_date,
'date_end': start_date + timedelta(days=6),
})
def _create_stats(self, start_date, nb_days, unit_amount):
# Create timesheet lines from Monday to Friday (or nb_days)
for i in range(nb_days):
self.env['account.analytic.line'].create({
'employee_id': self.employee.id,
'date': start_date + timedelta(days=i),
'unit_amount': unit_amount,
'account_id': 1,
'name': 'Work Entry',
})
# Generate hr.employee.stats for each day of the period
stat = self.env['hr.employee.stats'].create({
'employee_id': self.employee.id,
'date': start_date + timedelta(days=i),
})
stat._compute_dayofweek()
stat._compute_hours()
yield stat
def test_invalide_recovery_type(self):
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)
self.env.company.recovery_type_id = self.recovery_type
with self.assertRaises(UserError):
timesheet_sheet.action_generate_recovery_allocation()
def test_no_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, 7): #créer 5 stats de 7h chacune
# Compare les heures de récupération calculées et le calendrier qui prévoit 7h par jour
self.assertEqual(stat.total_hours, 7, "total_hours should be 7",) # l'employé a travaillé 7h chaque jour
self.assertEqual(stat.total_planned_hours, 7, "total_planned_hours should be 7",) # le calendrier prévoit 7h chaque jour
self.assertEqual(stat.total_leave_hours, 0, "total_leave_hours should be 0",) # l'employé n'a pas de congé sur ce jour
self.assertEqual(stat.total_recovery_hours, 0, "total_recovery_hours should be 0",) # l'employé n'a pas posé de récupération sur ce jour
self.assertEqual(stat.gap_hours, 0, "gap_hours should be 0",) # pas de différence entre les heures travaillées et les heures planifiées
# La feuille de temps sur cette période doit compter 0h de déficit et 0h de récupération
self.assertEqual(timesheet_sheet.timesheet_sheet_gap_hours, 0, "timesheet_sheet_gap_hours should be 0",)
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) # lundi de la semaine dernière
timesheet_sheet = self._create_timesheet_sheet(start_date)
for stat in self._create_stats(start_date, 5, 8): #créer 5 stats de 8h chacune
# Compare les heures de récupération calculées et le calendrier qui prévoit 7h par jour
self.assertEqual(stat.total_hours, 8, "total_hours should be 8",) # l'employé a travaillé 8h chaque jour
self.assertEqual(stat.total_planned_hours, 7, "total_planned_hours should be 7",) # le calendrier prévoit 7h chaque jour
self.assertEqual(stat.total_leave_hours, 0, "total_leave_hours should be 0",) # l'employé n'a pas de congé sur ce jour
self.assertEqual(stat.total_recovery_hours, 0, "total_recovery_hours should be 0",) # l'employé n'a pas posé de récupération sur ce jour
self.assertEqual(stat.gap_hours, 1, "gap_hours should be 1",) # l'employée a travaillé une heure de plus que prévu
# La feuille de temps doit compter 5h d'heure sup soit 6,25h de récupération avec la majoration de 25%
self.assertEqual(timesheet_sheet.timesheet_sheet_gap_hours, 5, "timesheet_sheet_gap_hours should be 5",)
self.assertEqual(timesheet_sheet.timesheet_sheet_recovery_hours, 6.25, "timesheet_sheet_recovery_hours should be 6,25",)
# générer l'allocation de récupération depuis la feuille de temps
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.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) # lundi de la semaine dernière
timesheet_sheet = self._create_timesheet_sheet(start_date)
for stat in self._create_stats(start_date, 5, 6): #créer 5 stats de 6h chacune
# Compare les heures de récupération calculées et le calendrier qui prévoit 7h par jour
self.assertEqual(stat.total_hours, 6, "total_hours should be 6",) # l'employé a travaillé 6h chaque jour
self.assertEqual(stat.total_planned_hours, 7, "total_planned_hours should be 7",) # le calendrier prévoit 7h chaque jour
self.assertEqual(stat.total_leave_hours, 0, "total_leave_hours should be 0",) # l'employé n'a pas de congé sur ce jour
self.assertEqual(stat.total_recovery_hours, 0, "total_recovery_hours should be 0",) # l'employé n'a pas posé de récupération sur ce jour
self.assertEqual(stat.gap_hours, -1, "gap_hours should be -1",) # l'employée a travaillé une heure de moins que prévu
# La feuille de temps doit compter -5h de déficit et -5h de récupération
self.assertEqual(timesheet_sheet.timesheet_sheet_gap_hours, -5, "timesheet_sheet_gap_hours should be -5",) # l'employé a travaillé -5h au total sur la semaine
self.assertEqual(timesheet_sheet.timesheet_sheet_recovery_hours, -5, "timesheet_sheet_recovery_hours should be -5",) # -5h sera le montant de l'allocation de récupération (pas de coef appliqué pour les déficites d'heures)
# générer l'allocation de récupération depuis la feuille de temps
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.7142857142857143, "The recovery allocation should be for -5/7 hours = 0,714285714")
def test_recovery_hours_part_time_employee(self):
part_time_calendar = self.env['resource.calendar'].create({
'name': 'Part Time Calendar',
'attendance_ids': [
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Monday Afternoon', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Tuesday Afternoon', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Wednesday Afternoon', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Thursday Afternoon', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
],
})
self.employee.resource_calendar_id = part_time_calendar.id
start_date = date.today() - timedelta(days=date.today().weekday() + 7) # lundi de la semaine dernière
timesheet_sheet = self._create_timesheet_sheet(start_date)
for stat in self._create_stats(start_date, 4, 8): #créer 4 stats de 8h chacune
# Compare les heures de récupération calculées et le calendrier qui prévoit 7h par jour pendant 4 jours
self.assertEqual(stat.total_hours, 8, "total_hours should be 8",) # l'employé a travaillé 6h chaque jour
self.assertEqual(stat.total_planned_hours, 7, "total_planned_hours should be 7",) # le calendrier prévoit 7h chaque jour
self.assertEqual(stat.total_leave_hours, 0, "total_leave_hours should be 0",) # l'employé n'a pas de congé sur ce jour
self.assertEqual(stat.total_recovery_hours, 0, "total_recovery_hours should be 0",) # l'employé n'a pas posé de récupération sur ce jour
self.assertEqual(stat.gap_hours, 1, "gap_hours should be 1",) # l'employée a travaillé une heure de moins que prévu
# La feuille de temps doit compter 4h d'heure sup soit 5h de récupération avec la majoration de 25%
self.assertEqual(timesheet_sheet.timesheet_sheet_gap_hours, 4, "timesheet_sheet_gap_hours should be 4",) # l'employé a travaillé supplémentaire 4h au total sur la semaine
self.assertEqual(timesheet_sheet.timesheet_sheet_recovery_hours, 5, "timesheet_sheet_recovery_hours should be 5",) # 5h sera le montant de l'allocation de récupération (coef de 25% de majoration)
def test_recovery_hours_change_calendar(self):
employee_full_time_calendar = self.base_calendar # full time calendar (from monday to friday)
employee_part_time_calendar = self.env['resource.calendar'].create({
'name': 'Part Time Calendar',
'attendance_ids': [
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Monday Afternoon', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Tuesday Afternoon', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Wednesday Afternoon', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Thursday Afternoon', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
],
})
#create two hr.employee.calendar to change calendar during the timesheet period
self.env['hr.employee.calendar'].create({
'employee_id': self.employee.id,
'date_start': Date.to_date("2023-07-01"),
'date_end': Date.to_date("2025-07-31"),
'calendar_id': employee_full_time_calendar.id,
})
self.env['hr.employee.calendar'].create({
'employee_id': self.employee.id,
'date_start': Date.to_date("2025-08-01"),
'date_end': None,
'calendar_id': employee_part_time_calendar.id,
})
self.employee.resource_calendar_id = employee_part_time_calendar.id
#create recovery hours on a period including the change of calendar
timesheet_sheet = self._create_timesheet_sheet(Date.to_date("2025-07-28")) #a week including the change of calendar on 1st august
#the create of recovery allocation should raise an error
with self.assertRaises(UserError):
timesheet_sheet.action_generate_recovery_allocation()
def test_recovery_hours_change_calendar_sucess(self):
employee_full_time_calendar = self.base_calendar # full time calendar (from monday to friday)
employee_part_time_calendar = self.env['resource.calendar'].create({
'name': 'Part Time Calendar',
'attendance_ids': [
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Monday Afternoon', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Tuesday Afternoon', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Wednesday Afternoon', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
(0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
(0, 0, {'name': 'Thursday Afternoon', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}),
],
})
#create two hr.employee.calendar to change calendar during the timesheet period
self.env['hr.employee.calendar'].create({
'employee_id': self.employee.id,
'date_start': Date.to_date("2023-07-01"),
'date_end': Date.to_date("2025-07-31"),
'calendar_id': employee_full_time_calendar.id,
})
self.env['hr.employee.calendar'].create({
'employee_id': self.employee.id,
'date_start': Date.to_date("2025-08-01"),
'date_end': None,
'calendar_id': employee_part_time_calendar.id,
})
self.employee.resource_calendar_id = employee_part_time_calendar.id
#create stats during period of full time calendar for the employee
timesheet_sheet = self.env['hr_timesheet.sheet'].create({
'employee_id': self.employee.id,
'date_start': "2025-07-07",
'date_end': "2025-07-13",
})
stats = self._create_stats(Date.to_date("2025-07-07"), 5, 7)
for stat in stats:
stat._compute_dayofweek()
stat._compute_hours()
print("stat :", stat.date, stat.total_hours, stat.total_planned_hours, stat.gap_hours)
timesheet_sheet.action_generate_recovery_allocation()
self.assertEqual(timesheet_sheet.timesheet_sheet_gap_hours, 0, "timesheet_sheet_gap_hours should be 0",)
self.assertEqual(timesheet_sheet.timesheet_sheet_recovery_hours, 0, "timesheet_sheet_recovery_hours should be 0",)
#create stats during period of part time calendar for the employee
stats = self._create_stats(Date.to_date("2025-09-08"), 5, 7)
for stat in stats:
stat._compute_dayofweek()
stat._compute_hours()
timesheet_sheet = self.env['hr_timesheet.sheet'].create({
'employee_id': self.employee.id,
'date_start': "2025-09-08",
'date_end': "2025-09-14",
})
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",)
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")