Skip to content

Commit

Permalink
[FIX] hr_holidays: correct the timezone for Public Time Off
Browse files Browse the repository at this point in the history
When we create a public time off,
we save the start date and the end date in utc.
Unfortunately, to calculate the utc, we don't use
the calendar timezone linked to these public time off.

Issue:
For example, with the "timesheet_grid" module,
the timesheets created for these time offs
will not be correct for employees who have a calendar
in a different timezone than the user who creates the public time off.

Solution:
Convert the dates received in the calendar timezone.

opw-3302925

closes #122748

X-original-commit: 63688d3
Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
Signed-off-by: Lefebvre Thomas (thle) <thle@odoo.com>
  • Loading branch information
thle-odoo committed May 31, 2023
1 parent e0cd650 commit 96fbd6d
Show file tree
Hide file tree
Showing 2 changed files with 80 additions and 1 deletion.
50 changes: 50 additions & 0 deletions addons/hr_holidays/models/resource.py
Expand Up @@ -4,6 +4,8 @@
from odoo import fields, models, api, _
from odoo.exceptions import ValidationError
from odoo.osv import expression
import pytz
from datetime import datetime

class CalendarLeaves(models.Model):
_inherit = "resource.calendar.leaves"
Expand Down Expand Up @@ -80,8 +82,56 @@ def _reevaluate_leaves(self, time_domain_dict):

leaves_to_cancel._force_cancel(_("a new public holiday completely overrides this leave."), 'mail.mt_comment')

def _convert_timezone(self, utc_naive_datetime, tz_from, tz_to):
"""
Convert a naive date to another timezone that initial timezone
used to generate the date.
:param utc_naive_datetime: utc date without tzinfo
:type utc_naive_datetime: datetime
:param tz_from: timezone used to obtained `utc_naive_datetime`
:param tz_to: timezone in which we want the date
:return: datetime converted into tz_to without tzinfo
:rtype: datetime
"""
naive_datetime_from = utc_naive_datetime.astimezone(tz_from).replace(tzinfo=None)
aware_datetime_to = tz_to.localize(naive_datetime_from)
utc_naive_datetime_to = aware_datetime_to.astimezone(pytz.utc).replace(tzinfo=None)
return utc_naive_datetime_to

def _ensure_datetime(self, datetime_representation, date_format=None):
"""
Be sure to get a datetime object if we have the necessary information.
:param datetime_reprentation: object which should represent a datetime
:rtype: datetime if a correct datetime_represtion, None otherwise
"""
if isinstance(datetime_representation, datetime):
return datetime_representation
elif isinstance(datetime_representation, str) and date_format:
return datetime.strptime(datetime_representation, date_format)
else:
return None

def _prepare_public_holidays_values(self, vals_list):
for vals in vals_list:
# Manage the case of create a Public Time Off in another timezone
# The datetime created has to be in UTC for the calendar's timezone
if not vals.get('calendar_id') or vals.get('resource_id') or \
not isinstance(vals.get('date_from'), (datetime, str)) or \
not isinstance(vals.get('date_to'), (datetime, str)):
continue
user_tz = pytz.timezone(self.env.user.tz) if self.env.user.tz else pytz.utc
calendar_tz = pytz.timezone(self.env['resource.calendar'].browse(vals['calendar_id']).tz)
if user_tz != calendar_tz:
datetime_from = self._ensure_datetime(vals['date_from'], '%Y-%m-%d %H:%M:%S')
datetime_to = self._ensure_datetime(vals['date_to'], '%Y-%m-%d %H:%M:%S')
if datetime_from and datetime_to:
vals['date_from'] = self._convert_timezone(datetime_from, user_tz, calendar_tz)
vals['date_to'] = self._convert_timezone(datetime_to, user_tz, calendar_tz)
return vals_list

@api.model_create_multi
def create(self, vals_list):
vals_list = self._prepare_public_holidays_values(vals_list)
res = super().create(vals_list)
time_domain_dict = res._get_time_domain_dict()
self._reevaluate_leaves(time_domain_dict)
Expand Down
31 changes: 30 additions & 1 deletion addons/hr_holidays/tests/test_global_leaves.py
@@ -1,9 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from datetime import date
from datetime import date, datetime
from odoo.addons.hr_holidays.tests.common import TestHrHolidaysCommon
from odoo.exceptions import ValidationError
from freezegun import freeze_time

from odoo.tests import tagged

Expand Down Expand Up @@ -101,3 +102,31 @@ def test_leave_on_calendar_leave(self):
'date_to': date(2022, 3, 8),
'calendar_id': self.calendar_1.id,
})

@freeze_time('2023-05-12')
def test_global_leave_timezone(self):
"""
It is necessary to use the timezone of the calendar
for the global leaves (without resource).
"""
calendar_asia = self.env['resource.calendar'].create({
'name': 'Asia calendar',
'tz': 'Asia/Calcutta', # UTC +05:30
'hours_per_day': 8.0,
'attendance_ids': []
})
self.env.user.tz = 'Europe/Brussels'
global_leave = self.env['resource.calendar.leaves'].with_user(self.env.user).create({
'name': 'Public holiday',
'date_from': "2023-05-15 06:00:00", # utc from 8:00:00 for Europe/Brussels (UTC +02:00)
'date_to': "2023-05-15 15:00:00", # utc from 17:00:00 for Europe/Brussels (UTC +02:00)
'calendar_id': calendar_asia.id,
})
# Expectation:
# 6:00:00 in UTC (data from the browser) --> 8:00:00 for Europe/Brussel (UTC +02:00)
# 8:00:00 for Asia/Calcutta (UTC +05:30) --> 2:30:00 in UTC
self.assertEqual(global_leave.date_from, datetime(2023, 5, 15, 2, 30))
self.assertEqual(global_leave.date_to, datetime(2023, 5, 15, 11, 30))
# Note:
# The user in Europe/Brussels timezone see 4:30 and not 2:30 because he is in UTC +02:00.
# The user in Asia/Calcutta timezone (determined via the browser) see 8:00 because he is in UTC +05:30

0 comments on commit 96fbd6d

Please sign in to comment.