[WIP]hr_employee_stats_sheet
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from odoo import api, fields, models, _
|
from odoo import api, fields, models
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
@@ -130,10 +130,10 @@ class HrEmployeeStats(models.Model):
|
|||||||
return total_recovery_hours
|
return total_recovery_hours
|
||||||
|
|
||||||
def _get_total_leave_hours(self):
|
def _get_total_leave_hours(self):
|
||||||
leave = self.env["hr.leave"]
|
total_leave_hours = 0
|
||||||
for stat in self:
|
for stat in self:
|
||||||
if stat.date and stat.employee_id:
|
if stat.date and stat.employee_id:
|
||||||
leave_ids = leave.search(
|
leave_ids = self.env["hr.leave"].search(
|
||||||
[
|
[
|
||||||
("employee_id", "=", stat.employee_id.id),
|
("employee_id", "=", stat.employee_id.id),
|
||||||
("holiday_status_id", "!=", stat._get_holiday_status_id()),
|
("holiday_status_id", "!=", stat._get_holiday_status_id()),
|
||||||
@@ -141,20 +141,7 @@ class HrEmployeeStats(models.Model):
|
|||||||
("request_date_to", "<=", stat.date),
|
("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é
|
total_leave_hours = sum(leave_ids.mapped("number_of_hours_display"))
|
||||||
# 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
|
|
||||||
return total_leave_hours
|
return total_leave_hours
|
||||||
|
|
||||||
@api.depends("employee_id", "date")
|
@api.depends("employee_id", "date")
|
||||||
@@ -195,15 +182,8 @@ class HrEmployeeStats(models.Model):
|
|||||||
total_recovery_hours = stat._get_total_recovery_hours()
|
total_recovery_hours = stat._get_total_recovery_hours()
|
||||||
total_planned_hours = stat._get_total_planned_hours()
|
total_planned_hours = stat._get_total_planned_hours()
|
||||||
total_leave_hours = stat._get_total_leave_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_hours = total_hours
|
||||||
stat.total_planned_hours = total_planned_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.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_recovery_hours = total_recovery_hours
|
||||||
stat.total_leave_hours = total_leave_hours
|
stat.total_leave_hours = total_leave_hours
|
||||||
|
@@ -100,56 +100,36 @@ class HrTimesheetSheet(models.Model):
|
|||||||
sheet.recovery_allocation_ids.write({"state": "refuse"})
|
sheet.recovery_allocation_ids.write({"state": "refuse"})
|
||||||
return res
|
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 :
|
get the contracts 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
|
|
||||||
"""
|
"""
|
||||||
contract_in_progress_during_timesheet = self.env["hr.contract"].search(
|
contracts = self.env["hr.contract"].search(
|
||||||
[
|
[
|
||||||
("employee_id", "=", self.employee_id.id),
|
("employee_id", "=", self.employee_id.id),
|
||||||
("state", "in", ("open", "close")),
|
("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),
|
("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:
|
return contracts
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
def _get_calendar_in_progress_during_timesheet_time_period(self):
|
def _get_calendar_in_progress_during_timesheet_time_period(self):
|
||||||
"""
|
"""
|
||||||
get the ressource calendar which was used during the timesheet sheet time period
|
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
|
#checks if only one contract runs over the duration of the timesheet
|
||||||
contract_during_timesheet_sheet = self._get_contract_in_progress_during_timesheet_sheet_time_period()
|
contracts = self._get_contracts_in_progress_during_timesheet_sheet_time_period()
|
||||||
if contract_during_timesheet_sheet:
|
if len(contracts) > 1:
|
||||||
# check if a new contract start during timesheet sheet time period. If yes, raise an error
|
# 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
|
# 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:
|
elif self.employee_id.resource_calendar_id:
|
||||||
return self.employee_id.resource_calendar_id
|
return self.employee_id.resource_calendar_id
|
||||||
elif self.env.company.resource_calendar_id:
|
elif self.env.company.resource_calendar_id:
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
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
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
@tagged("post_install", "-at_install")
|
@tagged("post_install", "-at_install")
|
||||||
class TestHrEmployeeStatsRecovery(TransactionCase):
|
class TestHrEmployeeStatsRecovery(TransactionCase):
|
||||||
@@ -89,6 +90,11 @@ 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%
|
# 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_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",)
|
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):
|
def test_negative_recovery_hours(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) # lundi de la semaine dernière
|
||||||
@@ -103,6 +109,11 @@ class TestHrEmployeeStatsRecovery(TransactionCase):
|
|||||||
# La feuille de temps doit compter -5h de déficit et -5h de récupération
|
# 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_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)
|
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):
|
def test_recovery_hours_part_time_employee(self):
|
||||||
part_time_calendar = self.env['resource.calendar'].create({
|
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%
|
# 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_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):
|
||||||
|
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")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@@ -14,7 +14,9 @@
|
|||||||
name="action_generate_recovery_allocation"
|
name="action_generate_recovery_allocation"
|
||||||
string='Create recovery allocation'
|
string='Create recovery allocation'
|
||||||
type='object'
|
type='object'
|
||||||
attrs="{'invisible': ['|', ('can_review', '=', False), ('state', '!=', 'done')]}"
|
attrs="{'invisible': ['|', ('can_review', '=', False),
|
||||||
|
('state', '!=', 'done'),
|
||||||
|
('timesheet_sheet_recovery_hours', '=', 0)]}"
|
||||||
/>
|
/>
|
||||||
</group>
|
</group>
|
||||||
<xpath expr="//notebook" position="inside">
|
<xpath expr="//notebook" position="inside">
|
||||||
|
Reference in New Issue
Block a user