From 6d684daf589597c4bb9504f8a4ab2619e86619d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Taymans?= Date: Thu, 19 Apr 2018 09:53:38 +0200 Subject: [PATCH] [FIX] website_shift: Wrong time when crossing DST The issue: There was a time difference of one hour when the fictive shifts cross the DST (Daylight Saving Time). Explanation: Python works with two types of datetime. The datetime without a timezone called *naive* and the one with a timezone called *non-naive*. `timedelta()` is used to add a number of day to a datetime. However, changing a non-naive datetime with `timedelta` just change the date but leave the timezone unchanged. That's no correct because the new date may not be in the same timezone because of the DST. If the new date is after a change of time, due to DST, the timezone of the non-naive datetime should be updated. Python don't do that because `timedelta` doesn't realy add days but add the corresponding hours ! So when adding hours it's consistent to not change the timezone (you can use `normalize()` to get your datetime in the right timezone after a cross of DST). To fix this, after adding a `timedelta` the timezone of the datetime should be set to none and the relocalized with the `localize()` function of the `pytz` corresponding timezone. --- beesdoo_website_shift/controllers/main.py | 45 +++++++++++++++++++---- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/beesdoo_website_shift/controllers/main.py b/beesdoo_website_shift/controllers/main.py index 0a5d8a7..76fc93c 100644 --- a/beesdoo_website_shift/controllers/main.py +++ b/beesdoo_website_shift/controllers/main.py @@ -7,10 +7,10 @@ from ast import literal_eval from datetime import datetime, timedelta from itertools import groupby +from pytz import timezone, utc from openerp import http, fields from openerp.http import request -from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT as DATETIME_FORMAT from openerp.addons.beesdoo_shift.models.planning import float_to_time from openerp.addons.beesdoo_shift.models.cooperative_status import PERIOD @@ -33,6 +33,34 @@ class WebsiteShiftController(http.Controller): working_mode = user.partner_id.working_mode return working_mode == 'exempt' + def add_days(self, datetime, days): + """ + Add the number of days to datetime. This take the DST in + account, meaning that the UTC time will be correct even if the + new datetime has cross the DST boundary. + + :param datetime: a naive datetime expressed in UTC + :return: a naive datetime expressed in UTC with the added days + """ + # Ensure that the datetime given is without a timezone + assert datetime.tzinfo is None + # Get current user and user timezone + cur_user = request.env['res.users'].browse(request.uid) + user_tz = timezone(cur_user.tz) + # Convert to UTC + dt_utc = utc.localize(datetime, is_dst=False) + # Convert to user TZ + dt_local = dt_utc.astimezone(user_tz) + # Add the number of days + newdt_local = dt_local + timedelta(days=days) + # If the newdt_local has cross the DST boundary, its tzinfo is + # no longer correct. So it will be replaced by the correct one. + newdt_local = user_tz.localize(newdt_local.replace(tzinfo=None)) + # Now the newdt_local has the right DST so it can be converted + # to UTC. + newdt_utc = newdt_local.astimezone(utc) + return newdt_utc.replace(tzinfo=None) + @http.route('/my/shift', auth='user', website=True) def my_shift(self, **kw): """ @@ -283,11 +311,6 @@ class WebsiteShiftController(http.Controller): 'beesdoo_website_shift.regular_next_shift_limit')) for i in range(nb_subscribed_shifts, regular_next_shift_limit): - # Compute the new date for the created shift - start_time = fields.Datetime.from_string(main_shift.start_time) - start_time = (start_time + timedelta(days=i*PERIOD)).strftime(DATETIME_FORMAT) - end_time = fields.Datetime.from_string(main_shift.end_time) - end_time = (end_time + timedelta(days=i*PERIOD)).strftime(DATETIME_FORMAT) # Create the fictive shift shift = main_shift.new() shift.name = main_shift.name @@ -302,8 +325,14 @@ class WebsiteShiftController(http.Controller): shift.replaced_id = main_shift.replaced_id shift.revert_info = main_shift.revert_info # Set new date - shift.start_time = start_time - shift.end_time = end_time + shift.start_time = self.add_days( + fields.Datetime.from_string(main_shift.start_time), + days=i*PERIOD + ) + shift.end_time = self.add_days( + fields.Datetime.from_string(main_shift.end_time), + days=i*PERIOD + ) # Add the fictive shift to the list of shift subscribed_shifts.append(shift)