[IMP]hr_employee_stats_sheet:add dependecie to hr_employee_calendar_planning and identifies the planning using by a employee during a time period
Some checks failed
pre-commit / pre-commit (pull_request) Failing after 1m32s

This commit is contained in:
2025-12-10 14:49:53 +01:00
parent 5b103056d6
commit f91aac7cc8
6 changed files with 218 additions and 101 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "hr_employee_stats_sheet",
"version": "16.0.2.1.1",
"version": "16.0.3.0.0",
"description": "Add global sheet for employee stats",
"summary": "Add global sheet for employee stats",
"author": "Nicolas JEUDY",
@@ -15,6 +15,7 @@
"hr_timesheet",
"hr_timesheet_sheet",
"resource",
"hr_employee_calendar_planning",
"hr_timesheet_sheet_usability_misc",
"hr_timesheet_sheet_usability_akretion",
],

View File

@@ -3,3 +3,4 @@ from . import hr_timesheet_sheet
from . import res_config
from . import res_company
from . import hr_leave_allocation
from . import hr_employee

View File

@@ -0,0 +1,38 @@
import pytz
from odoo import models, _
from datetime import timedelta
from pytz import utc
from odoo.exceptions import UserError
class HrEmployee(models.Model):
_inherit = "hr.employee"
def _get_calendar_in_progress_during_a_time_period(self, date_start, date_end):
"""
get the ressource calendar which was used during the timesheet sheet time period
"""
self.ensure_one()
# find calendar(s) running over the duration of the timesheet
calendars = self.env["hr.employee.calendar"].search(
[
("employee_id", "=", self.id),
("date_start", "<=", date_end),
"|",
("date_end", "=", False), # pas de date de fin OU
("date_end", ">=", date_start), # date de fin après le début
],
)
if len(calendars) > 1:
raise UserError(
_("There is a calendar starting during the timesheet sheet time period for the employee %s "
"Please create a new timesheet sheet starting from the new calendar start date")
% self.display_name
)
# if hr_employee_calendar found, use its calendar_id
elif calendars and calendars[0].calendar_id:
return calendars[0].calendar_id
# if resource calendar not found, use the ressource calendar of the company linked to the employee
elif self.company_id.resource_calendar_id:
return self.company_id.resource_calendar_id
return None

View File

@@ -88,7 +88,7 @@ class HrEmployeeStats(models.Model):
total_planned_hours = 0
if self.employee_id and self.date and not self.is_public_holiday:
dayofweek = int(self.date.strftime("%u")) - 1
calendar_id = self.employee_id.resource_calendar_id
calendar_id = self.employee_id._get_calendar_in_progress_during_a_time_period(self.date,self.date)
week_number = self.date.isocalendar()[1] % 2
if calendar_id.two_weeks_calendar:
hours = calendar_id.attendance_ids.search(

View File

@@ -100,43 +100,6 @@ class HrTimesheetSheet(models.Model):
sheet.recovery_allocation_ids.write({"state": "refuse"})
return res
def _get_contracts_in_progress_during_timesheet_sheet_time_period(self):
"""
get the contracts which was in progress during the timesheet sheet range time
"""
contracts = self.env["hr.contract"].search(
[
("employee_id", "=", self.employee_id.id),
("state", "in", ("open", "close")),
("date_start", "<=", self.date_end),
"|",
("date_end", "=", False), # pas de date de fin OU
("date_end", ">=", self.date_start), # date de fin après le début
],
)
return contracts
def _get_calendar_in_progress_during_timesheet_time_period(self):
"""
get the ressource calendar which was used during the timesheet sheet time period
"""
#checks if only one contract runs over the duration of the timesheet
contracts = self._get_contracts_in_progress_during_timesheet_sheet_time_period()
if len(contracts) > 1:
# check if a new contract start during timesheet sheet time period. If yes, raise an error
raise UserError(
_("There is a contract starting during the timesheet sheet time period for the employee %s"
"Please create a new timesheet sheet starting from the new contract start date")
% self.employee_id.display_name
)
# get the ressource calendar id according to the work contract
elif self.employee_id.resource_calendar_id:
return self.employee_id.resource_calendar_id
#get the ressource calendar linked to the employee
elif self.env.company.resource_calendar_id:
return self.env.company.resource_calendar_id
return None
def _get_working_hours_per_week(self):
"""
Get the weekly working hours for the employee, which is defined by:
@@ -147,7 +110,7 @@ class HrTimesheetSheet(models.Model):
:return: limit recovery hours
"""
# get ressource calendar id used during the timesheet sheet time period
ressource_calendar_id = self._get_calendar_in_progress_during_timesheet_time_period()
ressource_calendar_id = self.employee_id._get_calendar_in_progress_during_a_time_period(self.date_start,self.date_end)
if ressource_calendar_id:
resource_calendar_attendance_ids = self.env[
"resource.calendar.attendance"
@@ -169,7 +132,7 @@ class HrTimesheetSheet(models.Model):
:return: hours per day
"""
# get ressource calendar id used during the timesheet sheet time period
ressource_calendar_id = self._get_calendar_in_progress_during_timesheet_time_period()
ressource_calendar_id = self.employee_id._get_calendar_in_progress_during_a_time_period(self.date_start,self.date_end)
if ressource_calendar_id:
return ressource_calendar_id.hours_per_day
return HOURS_PER_DAY

View File

@@ -37,11 +37,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):
#Crée une feuille de temps pour la semaine du lundi au dimanche
# 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,
@@ -49,7 +51,7 @@ class TestHrEmployeeStatsRecovery(TransactionCase):
})
def _create_stats(self, start_date, nb_days, unit_amount):
# Crée des temps du lundi au vendredi (ou nb_days)
# 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,
@@ -58,16 +60,17 @@ class TestHrEmployeeStatsRecovery(TransactionCase):
'account_id': 1,
'name': 'Work Entry',
})
# Génère les hr_employee_stats pour chaque jour de la période
# 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) # lundi de la semaine dernière
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)
@@ -76,7 +79,13 @@ class TestHrEmployeeStatsRecovery(TransactionCase):
timesheet_sheet.action_generate_recovery_allocation()
def test_no_recovery_hours(self):
start_date = date.today() - timedelta(days=date.today().weekday() + 7) # lundi de la semaine dernière
self.env['hr.employee.calendar'].create({
'employee_id': self.employee.id,
'date_start': Date.to_date("2025-01-01"),
'date_end': None,
'calendar_id': self.base_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, 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
@@ -90,6 +99,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):
self.env['hr.employee.calendar'].create({
'employee_id': self.employee.id,
'date_start': Date.to_date("2025-01-01"),
'date_end': None,
'calendar_id': self.base_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, 5, 8): #créer 5 stats de 8h chacune
@@ -106,9 +121,15 @@ 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):
self.env['hr.employee.calendar'].create({
'employee_id': self.employee.id,
'date_start': Date.to_date("2025-01-01"),
'date_end': None,
'calendar_id': self.base_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, 5, 6): #créer 5 stats de 6h chacune
@@ -125,7 +146,7 @@ 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.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,714285714")
def test_recovery_hours_part_time_employee(self):
part_time_calendar = self.env['resource.calendar'].create({
@@ -141,8 +162,12 @@ class TestHrEmployeeStatsRecovery(TransactionCase):
(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
self.env['hr.employee.calendar'].create({
'employee_id': self.employee.id,
'date_start': Date.to_date("2025-01-01"),
'date_end': None,
'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
@@ -156,8 +181,9 @@ class TestHrEmployeeStatsRecovery(TransactionCase):
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_contract(self):
part_time_calendar = self.env['resource.calendar'].create({
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'}),
@@ -170,35 +196,31 @@ 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 one other starting on thursday
self.env['hr.contract'].create({
'name': 'Contract 1',
#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.today() - timedelta(days=300), # date de début factice
'date_end': date.today() - timedelta(days= date.today().weekday() + 5), # date de fin le mercredi de la semaine dernière
'resource_calendar_id': self.base_calendar.id,
'wage': 2000,
'state': 'close',
'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.contract'].create({
'name': 'Contract 2',
self.env['hr.employee.calendar'].create({
'employee_id': self.employee.id,
'state': 'open',
'date_start': date.today() - timedelta(days= date.today().weekday() + 4), # date de début le jeudi de la semaine dernière
'date_end': False,
'resource_calendar_id': self.base_calendar.id,
'wage': 1500,
'date_start': Date.to_date("2025-08-01"),
'date_end': None,
'calendar_id': employee_part_time_calendar.id,
})
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
#create a timesheet with period including the change of contract
timesheet_sheet = self._create_timesheet_sheet(start_date)
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_contract_sucess(self):
part_time_calendar = self.env['resource.calendar'].create({
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'}),
@@ -211,41 +233,133 @@ 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 one other starting on thursday
self.env['hr.contract'].create({
'name': 'Contract 1',
'employee_id': self.employee.id,
'date_start': date(2025,8,18),
'date_end': date(2025,8,24),
'resource_calendar_id': self.base_calendar.id,
'wage': 2000,
'state': 'close',
})
self.env['hr.contract'].create({
'name': 'Contract 2',
'employee_id': self.employee.id,
'state': 'open',
'date_start': date(2025,8,25),
'date_end': date(2025,8,31),
'resource_calendar_id': part_time_calendar.id,
'wage': 1500,
})
self.employee.resource_calendar_id = part_time_calendar.id
#create a timesheet with period including the change of contract
timesheet_sheet_1 = self._create_timesheet_sheet(date(2025,8,18))
timesheet_sheet_2 = self._create_timesheet_sheet(date(2025,8,25))
#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_recovery_allocation_doesnt_change_with_of_calendar(self):
employee_full_time_calendar = self.base_calendar # full time calendar (from monday to friday)
employee_full_time_calendar.hours_per_day = 7
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': 19, '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': 19, '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': 19, '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': 19, 'day_period': 'afternoon'}),
],
})
employee_part_time_calendar.hours_per_day = 9
#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_1 = 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, 8)
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_1.action_generate_recovery_allocation()
self.assertEqual(timesheet_sheet_1.timesheet_sheet_gap_hours, 5, "timesheet_sheet_gap_hours should be 0",)
self.assertEqual(timesheet_sheet_1.timesheet_sheet_recovery_hours, 6.25, "timesheet_sheet_recovery_hours should be 6,25",)
allocation_1 = self.env["hr.leave.allocation"].search([("timesheet_sheet_id","=",timesheet_sheet_1.id)])
self.assertEqual(len(allocation_1), 1, "There should be one recovery")
self.assertEqual(allocation_1.number_of_days,0.8928571428571429, "The recovery allocation should be for 0.8928571428571429 day")
#create stats during period of part time calendar for the employee
# generation 4 stats of 10h each, the employee is supposed to work 9h per day during 4 days
stats_2 = self._create_stats(Date.to_date("2025-09-08"), 4, 10)
for stat in stats_2:
stat._compute_dayofweek()
stat._compute_hours()
timesheet_sheet_2 = self.env['hr_timesheet.sheet'].create({
'employee_id': self.employee.id,
'date_start': "2025-09-08",
'date_end': "2025-09-14",
})
timesheet_sheet_2.action_generate_recovery_allocation()
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")
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")
self.assertEqual(timesheet_sheet_2.timesheet_sheet_gap_hours, 4, "timesheet_sheet_gap_hours should be 4",)
self.assertEqual(timesheet_sheet_2.timesheet_sheet_recovery_hours, 5, "timesheet_sheet_recovery_hours should be 5",)
allocation_2 = self.env["hr.leave.allocation"].search([("timesheet_sheet_id","=",timesheet_sheet_2.id)])
self.assertEqual(len(allocation_2), 1, "There should be one recovery")
self.assertEqual(allocation_2.number_of_days,0.5555555555555556, "The recovery allocation should be for 0,555555556 (5/9) day")
#check that allocation_1 hasn't changed
self.assertEqual(allocation_1.number_of_days,0.8928571428571429, "The recovery allocation should be for 0,892857143 day")
def test_public_holiday(self):
# create a public holiday
self.env['hr.employee.calendar'].create({
'employee_id': self.employee.id,
'date_start': Date.to_date("2025-01-01"),
'date_end': None,
'calendar_id': self.base_calendar.id,
})
# create a public holiday :
# When you create holidays graphically with a TZ,
# they are saved in the database after conversion to UTC.
# This is why, for a holiday starting on May 1, 2025, at 00:00:00 UTC+2,
# it will be saved in the database as April 30, 2025, at 22:00:00.
self.env["resource.calendar.leaves"].create(
{
"name": "1 mai 2025",