[FIX] hr_holidays: make _onchange_request_parameters handle timezones

At the end it constructs date_{from,to} (Datetime) by combining:

- request_date_{from,to} (Date) local to the user
- UTC time from date_{from,to} (extracted using time() into hour_{from,to})

This doesn't work in all cases, e.g.:

A leave in US/Pacific for one day (with request_unit_custom = True):
- request_date_from: 1st of Jan
- request_date_to:   1st of Jan
- hour_from:         15:00 (7:00 local)
- hour_to:           03:00 (19:00 local) <-- this happens on the 2nd of Jan in UTC

Before this patch it resulted in:
date_from: 1st of Jan 15:00
date_to:   1st of Jan 03:00

The correct value for date_to is:
date_to:   2nd of Jan 03:00

_adjust_date_based_on_tz takes care of adjusting the date based on the
hour and the user's timezone.

opw-2169863

closes odoo/odoo#44534

closes odoo/odoo#45746

X-original-commit: 523fd6e3c4ea3a4c153616bf6bdba9573501127f
Original-signed-off-by: jorenvo <jorenvo@users.noreply.github.com>
Signed-off-by: Nicolas Martinelli (nim) <nim@odoo.com>
This commit is contained in:
Joren Van Onder
2020-02-20 01:14:03 +00:00
parent 2e490f1c95
commit 81f3c4e81d
2 changed files with 74 additions and 16 deletions
+31 -3
View File
@@ -8,7 +8,7 @@ import math
from collections import namedtuple
from datetime import datetime, time
from datetime import datetime, date, timedelta, time
from dateutil.rrule import rrule, DAILY
from pytz import timezone, UTC
@@ -356,6 +356,9 @@ class HolidaysRequest(models.Model):
# find last attendance coming before last_day
attendance_to = next((att for att in reversed(attendances) if int(att.dayofweek) <= self.request_date_to.weekday()), attendances[-1] if attendances else default_value)
compensated_request_date_from = self.request_date_from
compensated_request_date_to = self.request_date_to
if self.request_unit_half:
if self.request_date_from_period == 'am':
hour_from = float_to_time(attendance_from.hour_from)
@@ -369,12 +372,15 @@ class HolidaysRequest(models.Model):
elif self.request_unit_custom:
hour_from = self.date_from.time()
hour_to = self.date_to.time()
compensated_request_date_from = self._adjust_date_based_on_tz(self.request_date_from, hour_from)
compensated_request_date_to = self._adjust_date_based_on_tz(self.request_date_to, hour_to)
else:
hour_from = float_to_time(attendance_from.hour_from)
hour_to = float_to_time(attendance_to.hour_to)
self.date_from = timezone(self.tz).localize(datetime.combine(self.request_date_from, hour_from)).astimezone(UTC).replace(tzinfo=None)
self.date_to = timezone(self.tz).localize(datetime.combine(self.request_date_to, hour_to)).astimezone(UTC).replace(tzinfo=None)
self.date_from = timezone(self.tz).localize(datetime.combine(compensated_request_date_from, hour_from)).astimezone(UTC).replace(tzinfo=None)
self.date_to = timezone(self.tz).localize(datetime.combine(compensated_request_date_to, hour_to)).astimezone(UTC).replace(tzinfo=None)
self._onchange_leave_dates()
@api.onchange('request_unit_half')
@@ -571,6 +577,28 @@ class HolidaysRequest(models.Model):
return {'days': hours / (today_hours or HOURS_PER_DAY), 'hours': hours}
def _adjust_date_based_on_tz(self, leave_date, hour):
""" request_date_{from,to} are local to the user's tz but hour_{from,to} are in UTC.
In some cases they are combined (assuming they are in the same tz) as a datetime. When
that happens it's possible we need to adjust one of the dates. This function adjust the
date, so that it can be passed to datetime().
E.g. a leave in US/Pacific for one day:
- request_date_from: 1st of Jan
- request_date_to: 1st of Jan
- hour_from: 15:00 (7:00 local)
- hour_to: 03:00 (19:00 local) <-- this happens on the 2nd of Jan in UTC
"""
user_tz = timezone(self.env.user.tz if self.env.user.tz else 'UTC')
request_date_to_utc = UTC.localize(datetime.combine(leave_date, hour)).astimezone(user_tz).replace(tzinfo=None)
if request_date_to_utc.date() < leave_date:
return leave_date + timedelta(days=1)
elif request_date_to_utc.date() > leave_date:
return leave_date - timedelta(days=1)
else:
return leave_date
####################################################
# ORM Overrides methods
####################################################
+43 -13
View File
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime, date
from dateutil.relativedelta import relativedelta
from pytz import timezone, UTC
from odoo import fields
from odoo.exceptions import ValidationError
@@ -366,23 +367,52 @@ class TestLeaveRequests(TestHrHolidaysCommon):
leave1.action_validate()
self.assertEqual(leave1.number_of_hours_display, 24)
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_defaults_with_timezones(self):
""" Make sure that leaves start with correct defaults for non-UTC timezones """
self.user_employee.tz = 'US/Pacific'
def _test_leave_with_tz(self, tz, local_date_from, local_date_to, number_of_days):
self.user_employee.tz = tz
tz = timezone(tz)
# Mimic what is done by the calendar widget when clicking on a day. It
# will take the local datetime from 7:00 to 19:00 and then convert it
# to UTC before sending it. Values here are for PST (UTC -8) and
# represent a leave on 2019/1/1 from 7:00 to 19:00 local time.
webclient_values = {
'date_from': datetime(2019, 1, 1, 15, 0, 0),
'date_to': datetime(2019, 1, 2, 3, 0), # note that this is the next day in UTC
values = {
'date_from': tz.localize(local_date_from).astimezone(UTC).replace(tzinfo=None),
'date_to': tz.localize(local_date_to).astimezone(UTC).replace(tzinfo=None), # note that this can be the next day in UTC
}
values = self.env['hr.leave'].sudo(self.user_employee_id)._default_get_request_parameters(webclient_values)
values.update(self.env['hr.leave'].with_user(self.user_employee_id)._default_get_request_parameters(values))
# Dates should be local to the user, they do not and cannot be
# converted using the user's timezone.
first_of_january = date(2019, 1, 1)
self.assertEqual(values['request_date_from'], first_of_january)
self.assertEqual(values['request_date_to'], first_of_january)
# Dates should be local to the user
self.assertEqual(values['request_date_from'], local_date_from.date())
self.assertEqual(values['request_date_to'], local_date_to.date())
values.update({
'name': 'Test',
'employee_id': self.employee_emp_id,
'holiday_status_id': self.holidays_type_1.id,
})
leave = self.env['hr.leave'].with_user(self.user_employee_id).new(values)
leave._onchange_request_parameters()
self.assertEqual(leave.number_of_days, number_of_days)
@mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail')
def test_leave_defaults_with_timezones(self):
""" Make sure that leaves start with correct defaults for non-UTC timezones """
timezones_to_test = ('UTC', 'Pacific/Midway', 'US/Pacific', 'Asia/Taipei', 'Pacific/Kiritimati') # UTC, UTC -11, UTC -8, UTC +8, UTC +14
# January 2020
# Su Mo Tu We Th Fr Sa
# 1 2 3 4
# 5 6 7 8 9 10 11
# 12 13 14 15 16 17 18
# 19 20 21 22 23 24 25
# 26 27 28 29 30 31
local_date_from = datetime(2019, 1, 1, 7, 0, 0)
local_date_to = datetime(2019, 1, 1, 19, 0, 0)
for tz in timezones_to_test:
self._test_leave_with_tz(tz, local_date_from, local_date_to, 1)
# We, Th, Fr, Mo, Tu, We => 6 days
local_date_from = datetime(2019, 1, 1, 7, 0, 0)
local_date_to = datetime(2019, 1, 8, 19, 0, 0)
for tz in timezones_to_test:
self._test_leave_with_tz(tz, local_date_from, local_date_to, 6)