From eeae211176dfc2a373db0a46c2a554c82c585c98 Mon Sep 17 00:00:00 2001 From: "Lucas Perais (lpe)" Date: Tue, 30 Oct 2018 15:07:15 +0000 Subject: [PATCH] [FIX] calendar: recurring event around DST Be in timezone Europe/Brussels, where, in 2018, the DST switch happens on 2018-10-28. Create a recurring event starting on 2018-10-27 until 2018-10-29 at say, 8am For the days before the 28, the time of the event will be correct Before this commit though, for the days after the 28, the meeting time will be shifted by one hour This is because the rrule took a TZ aware datetime, and generated recurring event dates with that same TZ, i.e. the one in STD time After, this commit, all the meetings are well scheduled at 8am, OPW 1895315 closes odoo/odoo#27985 --- addons/calendar/models/calendar.py | 20 +++++++++++++------- addons/calendar/tests/test_calendar.py | 22 ++++++++++++++++++++++ 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/addons/calendar/models/calendar.py b/addons/calendar/models/calendar.py index 8ec5dcb195c..3a1fc072b76 100644 --- a/addons/calendar/models/calendar.py +++ b/addons/calendar/models/calendar.py @@ -609,13 +609,16 @@ class Meeting(models.Model): event_date = datetime.now() use_naive_datetime = self.allday and self.rrule and 'UNTIL' in self.rrule and 'Z' not in self.rrule - if use_naive_datetime: - rset1 = rrule.rrulestr(str(self.rrule), dtstart=event_date.replace(tzinfo=None), forceset=True, ignoretz=True) - else: + if not use_naive_datetime: # Convert the event date to saved timezone (or context tz) as it'll # define the correct hour/day asked by the user to repeat for recurrence. - event_date = event_date.astimezone(timezone) # transform "+hh:mm" timezone - rset1 = rrule.rrulestr(str(self.rrule), dtstart=event_date, forceset=True, tzinfos={}) + event_date = event_date.astimezone(timezone) + + # The start date is naive + # the timezone will be applied, if necessary, at the very end of the process + # to allow for DST timezone reevaluation + rset1 = rrule.rrulestr(str(self.rrule), dtstart=event_date.replace(tzinfo=None), forceset=True, ignoretz=True) + recurring_meetings = self.search([('recurrent_id', '=', self.id), '|', ('active', '=', False), ('active', '=', True)]) # We handle a maximum of 50,000 meetings at a time, and clear the cache at each step to @@ -629,12 +632,15 @@ class Meeting(models.Model): if use_naive_datetime: recurring_date = recurring_date.replace(tzinfo=None) else: - recurring_date = todate(meeting.recurrent_id_date) + recurring_date = todate(meeting.recurrent_id_date).replace(tzinfo=None) if date_field == "stop": recurring_date += timedelta(hours=self.duration) rset1.exdate(recurring_date) invalidate = True - return [d.astimezone(pytz.UTC) if d.tzinfo else d for d in rset1 if d.year < MAXYEAR] + + def naive_tz_to_utc(d): + return timezone.localize(d).astimezone(pytz.UTC) + return [naive_tz_to_utc(d) if not use_naive_datetime else d for d in rset1 if d.year < MAXYEAR] @api.multi def _get_recurrency_end_date(self): diff --git a/addons/calendar/tests/test_calendar.py b/addons/calendar/tests/test_calendar.py index e04866f1f68..b764a4a9463 100644 --- a/addons/calendar/tests/test_calendar.py +++ b/addons/calendar/tests/test_calendar.py @@ -6,6 +6,7 @@ from datetime import datetime, timedelta from odoo import fields from odoo.tests.common import TransactionCase +import pytz class TestCalendar(TransactionCase): @@ -292,3 +293,24 @@ class TestCalendar(TransactionCase): self.assertEqual(event.start, '2018-10-16 08:00:00') self.assertEqual(event.stop, '2018-10-18 18:00:00') + + def test_recurring_around_dst(self): + m = self.CalendarEvent.create({ + 'name': "wheee", + 'start': '2018-10-27 14:30:00', + 'allday': False, + 'rrule': u'FREQ=DAILY;INTERVAL=1;COUNT=4', + 'duration': 2, + 'stop': '2018-10-27 16:30:00', + }) + + start_recurring_dates = m.with_context({'tz': 'Europe/Brussels'})._get_recurrent_date_by_event() + self.assertEqual(len(start_recurring_dates), 4) + + for d in start_recurring_dates: + self.assertEqual(d.tzinfo, pytz.UTC) + if d.day < 28: # DST switch happens between 2018-10-27 and 2018-10-28 + self.assertEqual(d.hour, 14) + else: + self.assertEqual(d.hour, 15) + self.assertEqual(d.minute, 30)