Skip to content

Commit 71a2e6f

Browse files
[FIX]hr_employee_calendar_planning:incorrect hours_per_day
Always using the topmost hr.employee.calendar for hours per day may lead to unexpected and incorrect behaviour. For example if the date_start and date_end of the topmost calendar are in the future. Then the hours_per_day from the future calendar are taken not from the currently used one. The more specific calendars in regard of date_start and date_end are considered earlier than the less specific ones.
1 parent 949f24f commit 71a2e6f

File tree

5 files changed

+143
-5
lines changed

5 files changed

+143
-5
lines changed

hr_employee_calendar_planning/__manifest__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
33
{
44
"name": "Employee Calendar Planning",
5-
"version": "16.0.1.1.9",
5+
"version": "16.0.1.1.10",
66
"category": "Human Resources",
77
"website": "https://github.com/OCA/hr",
88
"author": "Tecnativa,Odoo Community Association (OCA)",
@@ -11,6 +11,7 @@
1111
"depends": ["hr"],
1212
"data": [
1313
"security/ir.model.access.csv",
14+
"data/data.xml",
1415
"views/hr_employee_views.xml",
1516
"views/resource_calendar_views.xml",
1617
],
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?xml version="1.0" encoding="UTF-8" ?>
2+
<odoo noupdate="1">
3+
<record id="ir_cron_create_time_reports" model="ir.cron">
4+
<field name="name">Hr Employee: Recompute hours per day</field>
5+
<field name="model_id" ref="hr_employee_calendar_planning.model_hr_employee" />
6+
<field name="state">code</field>
7+
<field name="code">model.cron_recompute_hours_per_day()</field>
8+
<field name="interval_type">days</field>
9+
<field name="interval_number">1</field>
10+
<field name="numbercall">-1</field>
11+
<field
12+
name="nextcall"
13+
eval="datetime.now().replace(hour=1, minute=0, second=0, microsecond=0) + timedelta(days=1)"
14+
/>
15+
<field name="doall" eval="True" />
16+
<field name="active" eval="True" />
17+
</record>
18+
</odoo>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from openupgradelib import openupgrade
2+
3+
4+
@openupgrade.migrate()
5+
def migrate(env, version):
6+
env["hr.employee"].cron_recompute_hours_per_day()

hr_employee_calendar_planning/models/hr_employee.py

+50-4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
# Copyright 2022-2023 Tecnativa - Víctor Martínez
33
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
44

5+
from datetime import date
6+
57
from odoo import _, api, fields, models
68
from odoo.exceptions import UserError
79
from odoo.tools import config
@@ -58,6 +60,43 @@ def default_get(self, fields):
5860
]
5961
return vals
6062

63+
def _get_current_hours_per_day(self):
64+
"""
65+
Checks all calendars and uses the most specific first (date_start and date_end are set).
66+
If no calendar matches it checks for calendars where no date_end is set.
67+
If no calendar matches it checks for calendars where no date_start is set.
68+
If no calendar matches it checks for calenders with neither date_start nor date_end.
69+
It returns the hours_per_day of the first matching resource calendar.
70+
If no calendar matches or no calendar exists -1 is returned.
71+
:return: the current valid hours per day
72+
"""
73+
if not self.calendar_ids:
74+
return -1
75+
today = date.today()
76+
relevant_calendars = self.calendar_ids.filtered(
77+
lambda x: x.date_start
78+
and x.date_end
79+
and x.date_start <= today <= x.date_end
80+
)
81+
if relevant_calendars:
82+
return relevant_calendars[0].calendar_id.hours_per_day
83+
relevant_calendars = self.calendar_ids.filtered(
84+
lambda x: x.date_start and not x.date_end and x.date_start <= today
85+
)
86+
if relevant_calendars:
87+
return relevant_calendars[0].calendar_id.hours_per_day
88+
relevant_calendars = self.calendar_ids.filtered(
89+
lambda x: not x.date_start and x.date_end and today <= x.date_end
90+
)
91+
if relevant_calendars:
92+
return relevant_calendars[0].calendar_id.hours_per_day
93+
relevant_calendars = self.calendar_ids.filtered(
94+
lambda x: not x.date_start and not x.date_end
95+
)
96+
if relevant_calendars:
97+
return relevant_calendars[0].calendar_id.hours_per_day
98+
return -1
99+
61100
def _regenerate_calendar(self):
62101
self.ensure_one()
63102
vals_list = []
@@ -115,11 +154,11 @@ def _regenerate_calendar(self):
115154
)
116155
else:
117156
self.resource_calendar_id.attendance_ids = vals_list
118-
# Set the hours per day to the last (top date end) calendar line to apply
157+
# Set the hours per day to the value of the current resource calendar
158+
current_hours_per_day = self._get_current_hours_per_day()
159+
if current_hours_per_day >= 0:
160+
self.resource_id.calendar_id.hours_per_day = current_hours_per_day
119161
if self.calendar_ids:
120-
self.resource_id.calendar_id.hours_per_day = self.calendar_ids[
121-
0
122-
].calendar_id.hours_per_day
123162
# set global leaves
124163
self.copy_global_leaves()
125164

@@ -162,6 +201,13 @@ def copy_global_leaves(self):
162201
to_unlink.unlink()
163202
return self.env["resource.calendar.leaves"].create(new_vals).ids
164203

204+
def cron_recompute_hours_per_day(self):
205+
employees = self.search([])
206+
for employee in employees:
207+
current_hours_per_day = employee._get_current_hours_per_day()
208+
if current_hours_per_day >= 0:
209+
employee.resource_id.calendar_id.hours_per_day = current_hours_per_day
210+
165211
def regenerate_calendar(self):
166212
for item in self:
167213
item._regenerate_calendar()

hr_employee_calendar_planning/tests/test_hr_employee_calendar_planning.py

+67
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
# Copyright 2021-2023 Tecnativa - Víctor Martínez
33
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
44

5+
from datetime import date, timedelta
6+
57
from odoo import exceptions, fields
68
from odoo.tests import common, new_test_user
79

@@ -454,3 +456,68 @@ def test_create_employee_multi(self):
454456
]
455457
)
456458
self.assertEqual(len(employees), 2)
459+
460+
def test_get_current_hours_per_day(self):
461+
employee_calendar1 = self.env["hr.employee.calendar"].create(
462+
{
463+
"date_start": date.today() - timedelta(days=2),
464+
"date_end": date.today() + timedelta(days=2),
465+
"employee_id": self.employee.id,
466+
"calendar_id": self.calendar1.id,
467+
}
468+
)
469+
self.assertEqual(
470+
self.employee.resource_calendar_id.hours_per_day,
471+
self.calendar1.hours_per_day,
472+
)
473+
self.calendar2.attendance_ids[
474+
-1
475+
].unlink() # Make hours_per_day different for calendar 1 and 2
476+
employee_calendar2 = self.env["hr.employee.calendar"].create(
477+
{
478+
"date_start": date.today() + timedelta(days=3),
479+
"date_end": date.today() + timedelta(days=5),
480+
"employee_id": self.employee.id,
481+
"calendar_id": self.calendar2.id,
482+
}
483+
)
484+
self.assertEqual(
485+
self.employee.resource_calendar_id.hours_per_day,
486+
self.calendar1.hours_per_day,
487+
)
488+
employee_calendar1.write(
489+
{
490+
"date_start": employee_calendar1.date_start + timedelta(days=5),
491+
"date_end": employee_calendar1.date_start + timedelta(days=7),
492+
}
493+
)
494+
employee_calendar2.write(
495+
{
496+
"date_start": date.today() - timedelta(days=2),
497+
"date_end": None,
498+
}
499+
)
500+
self.assertEqual(
501+
self.employee.resource_calendar_id.hours_per_day,
502+
self.calendar2.hours_per_day,
503+
)
504+
employee_calendar1.write(
505+
{
506+
"date_start": None,
507+
"date_end": None,
508+
}
509+
)
510+
self.assertEqual(
511+
self.employee.resource_calendar_id.hours_per_day,
512+
self.calendar2.hours_per_day,
513+
)
514+
employee_calendar2.write(
515+
{
516+
"date_start": date.today() + timedelta(days=2),
517+
"date_end": None,
518+
}
519+
)
520+
self.assertEqual(
521+
self.employee.resource_calendar_id.hours_per_day,
522+
self.calendar1.hours_per_day,
523+
)

0 commit comments

Comments
 (0)