16.0-planning_and_public_holidays #15

Merged
laetitiadacosta merged 4 commits from 16.0-planning_and_public_holidays into 16.0 2025-12-15 11:20:12 +00:00
13 changed files with 293 additions and 106 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "hr_employee_stats_sheet", "name": "hr_employee_stats_sheet",
"version": "16.0.2.1.1", "version": "16.0.3.0.0",
"description": "Add global sheet for employee stats", "description": "Add global sheet for employee stats",
"summary": "Add global sheet for employee stats", "summary": "Add global sheet for employee stats",
"author": "Nicolas JEUDY", "author": "Nicolas JEUDY",
@@ -15,6 +15,9 @@
"hr_timesheet", "hr_timesheet",
"hr_timesheet_sheet", "hr_timesheet_sheet",
"resource", "resource",
"hr_employee_calendar_planning",
"hr_timesheet_sheet_usability_misc",
"hr_timesheet_sheet_usability_akretion",
], ],
"data": [ "data": [
"security/ir.model.access.csv", "security/ir.model.access.csv",

View File

@@ -3,3 +3,4 @@ from . import hr_timesheet_sheet
from . import res_config from . import res_config
from . import res_company from . import res_company
from . import hr_leave_allocation 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:
mondot marked this conversation as resolved Outdated

est-ce que ce if est encore pertinent ?

est-ce que ce if est encore pertinent ?

Effectivement avec la dépendance à hr_employee_planning_calendar, le self.resource_calendar_id n'est plus utile

Effectivement avec la dépendance à hr_employee_planning_calendar, le self.resource_calendar_id n'est plus utile
return self.company_id.resource_calendar_id
return None

View File

@@ -1,9 +1,10 @@
import logging import logging
import pytz
from odoo import api, fields, models from odoo import api, fields, models, _
from datetime import timedelta from datetime import timedelta
from pytz import utc
_logger = logging.getLogger(__name__) from odoo.exceptions import UserError
class HrEmployeeStats(models.Model): class HrEmployeeStats(models.Model):
@@ -86,7 +87,7 @@ class HrEmployeeStats(models.Model):
total_planned_hours = 0 total_planned_hours = 0
if self.employee_id and self.date and not self.is_public_holiday: if self.employee_id and self.date and not self.is_public_holiday:
dayofweek = int(self.date.strftime("%u")) - 1 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 week_number = self.date.isocalendar()[1] % 2
if calendar_id.two_weeks_calendar: if calendar_id.two_weeks_calendar:
hours = calendar_id.attendance_ids.search( hours = calendar_id.attendance_ids.search(
@@ -169,8 +170,44 @@ class HrEmployeeStats(models.Model):
stat.is_public_holiday = False stat.is_public_holiday = False
continue continue
stat.dayofweek = int(stat.date.strftime("%u")) - 1 stat.dayofweek = int(stat.date.strftime("%u")) - 1
stat.is_public_holiday = bool(stat.sheet_id.employee_id._get_public_holidays(stat.date, stat.date - timedelta(days=1))) stat.is_public_holiday = stat._is_public_holiday_accordig_to_employe_tz()
def _convert_to_employee_tz(self, date):
"""Convert a UTC datetime to the employee's timezone datetime."""
self.ensure_one()
if not date:
return None
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)
def _is_public_holiday_accordig_to_employe_tz(self):
self.ensure_one()
if not self.date or not self.employee_id:
return False
#get public holidays for the employee
public_holidays = self.employee_id._get_public_holidays(
self.date, self.date
)
if not public_holidays:
return False
if len(public_holidays) > 1:
raise UserError(
mondot marked this conversation as resolved Outdated

if len(public_holidays) > 1:
raise UserError("un msg bien clair pour que le client nous contacte et qu'on corrige le soucis")

if len(public_holidays) > 1: raise UserError("un msg bien clair pour que le client nous contacte et qu'on corrige le soucis")

après vérification _get_public_holidays, on n'a effectivement pas censé avoir plusieurs jours fériés pour un meme calendrier, mais par sécurité j'ai ajouté

if len(public_holidays) > 1: raise UserError( _("Several holidays have been found ont he date '%s'. Please correct the anomaly before continuing.") % self.date )

après vérification _get_public_holidays, on n'a effectivement pas censé avoir plusieurs jours fériés pour un meme calendrier, mais par sécurité j'ai ajouté ` if len(public_holidays) > 1: raise UserError( _("Several holidays have been found ont he date '%s'. Please correct the anomaly before continuing.") % self.date ) `
_("Several holidays have been found ont he date '%s'. Please correct the anomaly before continuing.") % self.date
)
ph = public_holidays[0]
# Convert public holiday to the employee timezone
ph_datetime_from_tz = self._convert_to_employee_tz(ph.date_from)
ph_datetime_to_tz = self._convert_to_employee_tz(ph.date_to)
# Convert datetime to date
ph_date_from = ph_datetime_from_tz.date()
ph_date_to = ph_datetime_to_tz.date()
# Check if the stat date falls within the public holiday range after conversion in employee tz
if ph_date_from <= self.date <= ph_date_to:
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() self.ensure_one()
balance = ( balance = (

View File

@@ -100,43 +100,6 @@ class HrTimesheetSheet(models.Model):
sheet.recovery_allocation_ids.write({"state": "refuse"}) sheet.recovery_allocation_ids.write({"state": "refuse"})
return res 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): def _get_working_hours_per_week(self):
mondot marked this conversation as resolved Outdated

parties commentées à supprimer

parties commentées à supprimer

fait

fait
""" """
Get the weekly working hours for the employee, which is defined by: Get the weekly working hours for the employee, which is defined by:
@@ -147,7 +110,7 @@ class HrTimesheetSheet(models.Model):
:return: limit recovery hours :return: limit recovery hours
""" """
# get ressource calendar id used during the timesheet sheet time period # 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: if ressource_calendar_id:
resource_calendar_attendance_ids = self.env[ resource_calendar_attendance_ids = self.env[
"resource.calendar.attendance" "resource.calendar.attendance"
@@ -169,7 +132,7 @@ class HrTimesheetSheet(models.Model):
:return: hours per day :return: hours per day
""" """
# get ressource calendar id used during the timesheet sheet time period # 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: if ressource_calendar_id:
return ressource_calendar_id.hours_per_day return ressource_calendar_id.hours_per_day
return HOURS_PER_DAY return HOURS_PER_DAY

View File

@@ -1,7 +1,8 @@
from odoo.tests.common import TransactionCase from odoo.tests.common import TransactionCase
from odoo.tests import tagged from odoo.tests import tagged
from datetime import date, timedelta from datetime import date, timedelta, datetime
from odoo.exceptions import UserError from odoo.exceptions import UserError
from odoo.fields import Date
@tagged("post_install", "-at_install") @tagged("post_install", "-at_install")
class TestHrEmployeeStatsRecovery(TransactionCase): class TestHrEmployeeStatsRecovery(TransactionCase):
@@ -19,6 +20,7 @@ class TestHrEmployeeStatsRecovery(TransactionCase):
self.employee = self.env['hr.employee'].create({ self.employee = self.env['hr.employee'].create({
'name': 'Camille', 'name': 'Camille',
'user_id': self.user.id, 'user_id': self.user.id,
'tz': 'Europe/Paris',
}) })
self.base_calendar = self.env['resource.calendar'].create({ self.base_calendar = self.env['resource.calendar'].create({
'name': 'Default Calendar', 'name': 'Default Calendar',
@@ -35,11 +37,13 @@ class TestHrEmployeeStatsRecovery(TransactionCase):
(0, 0, {'name': 'Friday Afternoon', 'dayofweek': '4', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}), (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.employee.resource_calendar_id = self.base_calendar
self.env.company.recovery_type_id = self.recovery_type
self.env.company.coef = 25 self.env.company.coef = 25
def _create_timesheet_sheet(self, start_date): 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({ return self.env['hr_timesheet.sheet'].create({
'employee_id': self.employee.id, 'employee_id': self.employee.id,
'date_start': start_date, 'date_start': start_date,
@@ -47,7 +51,7 @@ class TestHrEmployeeStatsRecovery(TransactionCase):
}) })
def _create_stats(self, start_date, nb_days, unit_amount): 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): for i in range(nb_days):
self.env['account.analytic.line'].create({ self.env['account.analytic.line'].create({
'employee_id': self.employee.id, 'employee_id': self.employee.id,
@@ -56,16 +60,17 @@ class TestHrEmployeeStatsRecovery(TransactionCase):
'account_id': 1, 'account_id': 1,
'name': 'Work Entry', '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({ stat = self.env['hr.employee.stats'].create({
'employee_id': self.employee.id, 'employee_id': self.employee.id,
'date': start_date + timedelta(days=i), 'date': start_date + timedelta(days=i),
}) })
stat._compute_dayofweek()
stat._compute_hours() stat._compute_hours()
yield stat yield stat
def test_invalide_recovery_type(self): 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.request_unit = 'day'
self.recovery_type.allows_negative = False self.recovery_type.allows_negative = False
timesheet_sheet = self._create_timesheet_sheet(start_date) timesheet_sheet = self._create_timesheet_sheet(start_date)
@@ -74,7 +79,13 @@ class TestHrEmployeeStatsRecovery(TransactionCase):
timesheet_sheet.action_generate_recovery_allocation() timesheet_sheet.action_generate_recovery_allocation()
def test_no_recovery_hours(self): 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) 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 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 # Compare les heures de récupération calculées et le calendrier qui prévoit 7h par jour
@@ -88,6 +99,12 @@ class TestHrEmployeeStatsRecovery(TransactionCase):
self.assertEqual(timesheet_sheet.timesheet_sheet_recovery_hours, 0, "timesheet_sheet_recovery_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): 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 start_date = date.today() - timedelta(days=date.today().weekday() + 7) # lundi de la semaine dernière
timesheet_sheet = self._create_timesheet_sheet(start_date) 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 for stat in self._create_stats(start_date, 5, 8): #créer 5 stats de 8h chacune
@@ -104,9 +121,15 @@ class TestHrEmployeeStatsRecovery(TransactionCase):
timesheet_sheet.action_generate_recovery_allocation() timesheet_sheet.action_generate_recovery_allocation()
recovery_allocation = self.env["hr.leave.allocation"].search([("timesheet_sheet_id","=",timesheet_sheet.id)]) 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(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): 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 start_date = date.today() - timedelta(days=date.today().weekday() + 7) # lundi de la semaine dernière
timesheet_sheet = self._create_timesheet_sheet(start_date) 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 for stat in self._create_stats(start_date, 5, 6): #créer 5 stats de 6h chacune
@@ -123,7 +146,7 @@ class TestHrEmployeeStatsRecovery(TransactionCase):
timesheet_sheet.action_generate_recovery_allocation() timesheet_sheet.action_generate_recovery_allocation()
recovery_allocation = self.env["hr.leave.allocation"].search([("timesheet_sheet_id","=",timesheet_sheet.id)]) 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(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): def test_recovery_hours_part_time_employee(self):
part_time_calendar = self.env['resource.calendar'].create({ part_time_calendar = self.env['resource.calendar'].create({
@@ -139,8 +162,12 @@ class TestHrEmployeeStatsRecovery(TransactionCase):
(0, 0, {'name': 'Thursday Afternoon', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}), (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 start_date = date.today() - timedelta(days=date.today().weekday() + 7) # lundi de la semaine dernière
timesheet_sheet = self._create_timesheet_sheet(start_date) 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 for stat in self._create_stats(start_date, 4, 8): #créer 4 stats de 8h chacune
@@ -154,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_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) 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): def test_recovery_hours_change_calendar(self):
part_time_calendar = self.env['resource.calendar'].create({ 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', 'name': 'Part Time Calendar',
'attendance_ids': [ 'attendance_ids': [
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}), (0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
@@ -168,35 +196,31 @@ class TestHrEmployeeStatsRecovery(TransactionCase):
(0, 0, {'name': 'Thursday Afternoon', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}), (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({ #create two hr.employee.calendar to change calendar during the timesheet period
'name': 'Contract 1', self.env['hr.employee.calendar'].create({
'employee_id': self.employee.id, 'employee_id': self.employee.id,
'date_start': date.today() - timedelta(days=300), # date de début factice 'date_start': Date.to_date("2023-07-01"),
'date_end': date.today() - timedelta(days= date.today().weekday() + 5), # date de fin le mercredi de la semaine dernière 'date_end': Date.to_date("2025-07-31"),
'resource_calendar_id': self.base_calendar.id, 'calendar_id': employee_full_time_calendar.id,
'wage': 2000,
'state': 'close',
}) })
self.env['hr.contract'].create({ self.env['hr.employee.calendar'].create({
'name': 'Contract 2',
'employee_id': self.employee.id, 'employee_id': self.employee.id,
'state': 'open', 'date_start': Date.to_date("2025-08-01"),
'date_start': date.today() - timedelta(days= date.today().weekday() + 4), # date de début le jeudi de la semaine dernière 'date_end': None,
'date_end': False, 'calendar_id': employee_part_time_calendar.id,
'resource_calendar_id': self.base_calendar.id,
'wage': 1500,
}) })
self.employee.resource_calendar_id = part_time_calendar.id self.employee.resource_calendar_id = employee_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 #create recovery hours on a period including the change of calendar
timesheet_sheet = self._create_timesheet_sheet(start_date) 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 #the create of recovery allocation should raise an error
with self.assertRaises(UserError): with self.assertRaises(UserError):
timesheet_sheet.action_generate_recovery_allocation() timesheet_sheet.action_generate_recovery_allocation()
def test_recovery_hours_change_contract_sucess(self): def test_recovery_hours_change_calendar_sucess(self):
part_time_calendar = self.env['resource.calendar'].create({ 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', 'name': 'Part Time Calendar',
'attendance_ids': [ 'attendance_ids': [
(0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}), (0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 9, 'hour_to': 12, 'day_period': 'morning'}),
@@ -209,39 +233,160 @@ class TestHrEmployeeStatsRecovery(TransactionCase):
(0, 0, {'name': 'Thursday Afternoon', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}), (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()
mondot marked this conversation as resolved Outdated

Dans l'idéal il faudrait reproduire la situation où on ajoute le 1er mai de minuit à minuit et avec le jeu des timezones ça crée un resource.calendar.leaves de 22h à 22h.

Si trop compliqué, on ajoute seulement un commentaire

Dans l'idéal il faudrait reproduire la situation où on ajoute le 1er mai de minuit à minuit et avec le jeu des timezones ça crée un resource.calendar.leaves de 22h à 22h. Si trop compliqué, on ajoute seulement un commentaire

j'ai ajouté des commentaires

j'ai ajouté des commentaires
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() 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() timesheet_sheet_2.action_generate_recovery_allocation()
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")
recovery_allocation = self.env["hr.leave.allocation"].search([("timesheet_sheet_id","=",timesheet_sheet_1.id)]) def test_public_holiday(self):
self.assertEqual(len(recovery_allocation), 1, "There should be one recovery") self.env['hr.employee.calendar'].create({
'employee_id': self.employee.id,
recovery_allocation = self.env["hr.leave.allocation"].search([("timesheet_sheet_id","=",timesheet_sheet_2.id)]) 'date_start': Date.to_date("2025-01-01"),
self.assertEqual(len(recovery_allocation), 1, "There should be one recovery") '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",
"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",
})
# the employee has worked 7h on first may (public holiday) instead of 0h
# so the gap hours should be 7h and recovery hours 8,75h with coef 25%
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")

View File

@@ -1,6 +1,6 @@
{ {
"name": "hr_timesheet_sheet_usability", "name": "hr_timesheet_sheet_usability_misc",
"version": "16.0.1.0.0", "version": "16.0.1.1.0",
"description": "Various changes to improve the usability of hr_timesheet_sheet application", "description": "Various changes to improve the usability of hr_timesheet_sheet application",
"summary": "", "summary": "",
"author": "Elabore", "author": "Elabore",