diff --git a/addons/hr_holidays/models/resource.py b/addons/hr_holidays/models/resource.py index 9cb072b27bf2d5ee2a67e282f5d8b5a5d2e4b4d3..8c400744fc84a49464a261713b6ce5333cd0179c 100644 --- a/addons/hr_holidays/models/resource.py +++ b/addons/hr_holidays/models/resource.py @@ -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" @@ -80,8 +82,56 @@ class CalendarLeaves(models.Model): 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) diff --git a/addons/hr_holidays/tests/test_global_leaves.py b/addons/hr_holidays/tests/test_global_leaves.py index fc57e08556037509f3ffbe2a8997010787c6afc0..ef1c8d5a4d3bf8aa95568ffa8e4355e6824d74f8 100644 --- a/addons/hr_holidays/tests/test_global_leaves.py +++ b/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 @@ -96,3 +97,31 @@ class TestGlobalLeaves(TestHrHolidaysCommon): '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