From 29976fb9a4322fd82a116f8d46cdb6de51d9c3df Mon Sep 17 00:00:00 2001 From: Laetitia Da Costa Date: Mon, 15 Sep 2025 19:55:37 +0200 Subject: [PATCH] [WIP]hr_employee_stats_sheet --- .../models/hr_employee_stats.py | 28 +---- .../models/hr_timesheet_sheet.py | 56 +++------ .../tests/test_employee_stats.py | 112 +++++++++++++++++- .../views/hr_timesheet_sheet.xml | 4 +- 4 files changed, 136 insertions(+), 64 deletions(-) diff --git a/hr_employee_stats_sheet/models/hr_employee_stats.py b/hr_employee_stats_sheet/models/hr_employee_stats.py index 62fa623..64d2211 100644 --- a/hr_employee_stats_sheet/models/hr_employee_stats.py +++ b/hr_employee_stats_sheet/models/hr_employee_stats.py @@ -1,6 +1,6 @@ import logging -from odoo import api, fields, models, _ +from odoo import api, fields, models from datetime import timedelta _logger = logging.getLogger(__name__) @@ -130,10 +130,10 @@ class HrEmployeeStats(models.Model): return total_recovery_hours def _get_total_leave_hours(self): - leave = self.env["hr.leave"] + total_leave_hours = 0 for stat in self: if stat.date and stat.employee_id: - leave_ids = leave.search( + leave_ids = self.env["hr.leave"].search( [ ("employee_id", "=", stat.employee_id.id), ("holiday_status_id", "!=", stat._get_holiday_status_id()), @@ -141,20 +141,7 @@ class HrEmployeeStats(models.Model): ("request_date_to", "<=", stat.date), ] ) - # retire des congés les jours où j'ai travaillé (car présence dans l'app présence) alors que j'étais noté en congé - # TODO faire pareil avec les feuilles de temps? - intersect_hours = sum(leave_ids.mapped("number_of_hours_display")) - # for leave_id in leave_ids: - # for attendance_id in stat.attendance_ids: - # intersect_hours -= stat._get_intersects( - # leave_id.date_from, - # leave_id.date_to, - # attendance_id.check_in, - # attendance_id.check_out, - # ) - total_leave_hours = intersect_hours - else: - total_leave_hours = 0 + total_leave_hours = sum(leave_ids.mapped("number_of_hours_display")) return total_leave_hours @api.depends("employee_id", "date") @@ -195,15 +182,8 @@ class HrEmployeeStats(models.Model): total_recovery_hours = stat._get_total_recovery_hours() total_planned_hours = stat._get_total_planned_hours() total_leave_hours = stat._get_total_leave_hours() - # balance = ( - # total_hours - # + total_recovery_hours - # + total_leave_hours - # - total_planned_hours - # ) stat.total_hours = total_hours stat.total_planned_hours = total_planned_hours - #stat.gap_hours = balance stat.gap_hours = stat._get_gap_hours(total_hours, total_recovery_hours, total_leave_hours, total_planned_hours) stat.total_recovery_hours = total_recovery_hours stat.total_leave_hours = total_leave_hours diff --git a/hr_employee_stats_sheet/models/hr_timesheet_sheet.py b/hr_employee_stats_sheet/models/hr_timesheet_sheet.py index 13d8bdb..ca82407 100644 --- a/hr_employee_stats_sheet/models/hr_timesheet_sheet.py +++ b/hr_employee_stats_sheet/models/hr_timesheet_sheet.py @@ -100,56 +100,36 @@ class HrTimesheetSheet(models.Model): sheet.recovery_allocation_ids.write({"state": "refuse"}) return res - def _get_contract_in_progress_during_timesheet_sheet_time_period(self): + def _get_contracts_in_progress_during_timesheet_sheet_time_period(self): """ - get the contract which was in progress during the timesheet sheet range time : - - the contrat start date must be before the timesheet sheet start date - - the contrat end date can be not defined or must be after the timesheet sheet end date - - the state can be closed today or still open + get the contracts which was in progress during the timesheet sheet range time """ - contract_in_progress_during_timesheet = self.env["hr.contract"].search( + contracts = self.env["hr.contract"].search( [ - ("employee_id", "=", self.employee_id.id), - ("state", "in", ("open","close")), - ("date_start", "<=", self.date_start), - ("date_end", ">=", self.date_end or None), - ], - order="date_start desc", - limit=1, - ) - return contract_in_progress_during_timesheet - - def _check_new_contract_starting_during_timesheet(self): - """ - check if there was a contract starting during the timesheet sheet range time - :return: raise error if there is a contract starting during the timesheet sheet range time - """ - contract_starting_during_timesheet_ids = self.env["hr.contract"].search( - [ - ("employee_id", "=", self.employee_id.id), - ("state", "in", ("open","close")), - ("date_start", ">", self.date_start), - ("date_start", "<=", self.date_end), + ("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 ], ) - if contract_starting_during_timesheet_ids: - 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 - ) + 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 """ - # get work contrat in progress for the employe during the timesheet sheet time period - contract_during_timesheet_sheet = self._get_contract_in_progress_during_timesheet_sheet_time_period() - if contract_during_timesheet_sheet: + #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 - self._check_new_contract_starting_during_timesheet() + 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 - return contract_during_timesheet_sheet.resource_calendar_id elif self.employee_id.resource_calendar_id: return self.employee_id.resource_calendar_id elif self.env.company.resource_calendar_id: diff --git a/hr_employee_stats_sheet/tests/test_employee_stats.py b/hr_employee_stats_sheet/tests/test_employee_stats.py index c9ce571..0aff8fe 100644 --- a/hr_employee_stats_sheet/tests/test_employee_stats.py +++ b/hr_employee_stats_sheet/tests/test_employee_stats.py @@ -1,6 +1,7 @@ from odoo.tests.common import TransactionCase from odoo.tests import tagged from datetime import date, timedelta +from odoo.exceptions import UserError @tagged("post_install", "-at_install") class TestHrEmployeeStatsRecovery(TransactionCase): @@ -89,7 +90,12 @@ class TestHrEmployeeStatsRecovery(TransactionCase): # 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.78125, "The recovery allocation should be for 6.25h/8h = 0.78125 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) @@ -103,6 +109,11 @@ class TestHrEmployeeStatsRecovery(TransactionCase): # 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.625, "The recovery allocation should be for -5/8 hours = ") def test_recovery_hours_part_time_employee(self): part_time_calendar = self.env['resource.calendar'].create({ @@ -132,3 +143,102 @@ class TestHrEmployeeStatsRecovery(TransactionCase): # 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_contract(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'}), + ], + }) + #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.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', + }) + self.env['hr.contract'].create({ + 'name': 'Contract 2', + '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, + }) + 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) + #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({ + '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 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)) + + timesheet_sheet_1.action_generate_recovery_allocation() + 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") + + + + + + + + + + + diff --git a/hr_employee_stats_sheet/views/hr_timesheet_sheet.xml b/hr_employee_stats_sheet/views/hr_timesheet_sheet.xml index 27cfaaf..cfd6a75 100644 --- a/hr_employee_stats_sheet/views/hr_timesheet_sheet.xml +++ b/hr_employee_stats_sheet/views/hr_timesheet_sheet.xml @@ -14,7 +14,9 @@ name="action_generate_recovery_allocation" string='Create recovery allocation' type='object' - attrs="{'invisible': ['|', ('can_review', '=', False), ('state', '!=', 'done')]}" + attrs="{'invisible': ['|', ('can_review', '=', False), + ('state', '!=', 'done'), + ('timesheet_sheet_recovery_hours', '=', 0)]}" />