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)