diff --git a/addons/calendar/models/res_users.py b/addons/calendar/models/res_users.py index 5c70a4eaad6..6cd185de0e0 100644 --- a/addons/calendar/models/res_users.py +++ b/addons/calendar/models/res_users.py @@ -11,26 +11,56 @@ class Users(models.Model): _inherit = 'res.users' def _systray_get_calendar_event_domain(self): + # Determine the domain for which the users should be notified. This method sends notification to + # events occurring between now and the end of the day. Note that "now" needs to be computed in the + # user TZ and converted into UTC to compare with the records values and "the end of the day" needs + # also conversion. Otherwise TZ diverting a lot from UTC would send notification for events occurring + # tomorrow. + # The user is notified if the start is occurring between now and the end of the day + # if the event is not finished. + # | | + # |===========|===> DAY A (`start_dt`): now in the user TZ + # | | + # | | <--- `start_dt_utc`: now is on the right if the user lives + # | | in West Longitude (America for example) + # | | + # | ------- | <--- `start`: the start of the event (in UTC) + # | | event | | + # | ------- | <--- `stop`: the stop of the event (in UTC) + # | | + # | | + # | | <--- `stop_dt_utc` = `stop_dt` if user lives in an area of East longitude (positive shift compared to UTC, Belgium for example) + # | | + # | | + # |-----------| <--- `stop_dt` = end of the day for DAY A from user point of view (23:59 in this TZ) + # |===========|===> DAY B + # | | + # | | <--- `stop_dt_utc` = `stop_dt` if user lives in an area of West longitude (positive shift compared to UTC, America for example) + # | | + now_utc = datetime.datetime.utcnow() + start_dt_utc = start_dt = now_utc.replace(tzinfo=UTC) + stop_dt_utc = datetime.datetime.combine(now_utc.date(), datetime.time.max).replace(tzinfo=UTC) + + tz = self.env.user.tz + if tz: + user_tz = timezone(tz) + start_dt = start_dt_utc.astimezone(user_tz) + stop_dt = datetime.datetime.combine(start_dt.date(), datetime.time.max).replace(tzinfo=user_tz) + stop_dt_utc = stop_dt.astimezone(UTC) + + start_date = start_dt.date() + current_user_non_declined_attendee_ids = self.env['calendar.attendee']._search([ ('partner_id', '=', self.env.user.partner_id.id), ('state', '!=', 'declined'), ]) - tz = self.env.user.tz - start_dt = datetime.datetime.utcnow() - if tz: - start_date = timezone(tz).localize(start_dt).astimezone(UTC).date() - else: - start_date = datetime.date.today() - end_dt = datetime.datetime.combine(start_date, datetime.time.max) - if tz: - end_dt = timezone(tz).localize(end_dt).astimezone(UTC) return ['&', '|', '&', '|', - ['start', '>=', fields.Datetime.to_string(start_dt)], - ['stop', '>=', fields.Datetime.to_string(start_dt)], - ['start', '<=', fields.Datetime.to_string(end_dt)], + ['start', '>=', fields.Datetime.to_string(start_dt_utc)], + ['stop', '>=', fields.Datetime.to_string(start_dt_utc)], + ['start', '<=', fields.Datetime.to_string(stop_dt_utc)], '&', ['allday', '=', True], ['start_date', '=', fields.Date.to_string(start_date)], diff --git a/addons/calendar/tests/test_event_notifications.py b/addons/calendar/tests/test_event_notifications.py index 954090e90fe..e696226203c 100644 --- a/addons/calendar/tests/test_event_notifications.py +++ b/addons/calendar/tests/test_event_notifications.py @@ -267,3 +267,55 @@ class TestEventNotifications(TransactionCase, MailCase, CronMixinCase): }).with_context(mail_notrack=True) self.env.flush_all() self.assertEqual(len(capt.records), 1) + + def test_notification_event_timezone(self): + """ + Check the domain that decides when calendar events should be notified to the user. + """ + def search_event(): + return self.env['calendar.event'].search(self.env['res.users']._systray_get_calendar_event_domain()) + + self.env.user.tz = 'Europe/Brussels' # UTC +1 15th November 2023 + event = self.env['calendar.event'].create({ + 'name': "Meeting", + 'start': datetime(2023, 11, 15, 18, 0), # 19:00 + 'stop': datetime(2023, 11, 15, 19, 0), # 20:00 + }).with_context(mail_notrack=True) + with freeze_time('2023-11-15 17:30:00'): # 18:30 before event + self.assertEqual(search_event(), event) + with freeze_time('2023-11-15 18:00:00'): # 19:00 during event + self.assertEqual(search_event(), event) + with freeze_time('2023-11-15 18:30:00'): # 19:30 during event + self.assertEqual(search_event(), event) + with freeze_time('2023-11-15 19:00:00'): # 20:00 during event + self.assertEqual(search_event(), event) + with freeze_time('2023-11-15 19:30:00'): # 20:30 after event + self.assertEqual(len(search_event()), 0) + event.unlink() + + self.env.user.tz = 'America/Lima' # UTC -5 15th November 2023 + event = self.env['calendar.event'].create({ + 'name': "Meeting", + 'start': datetime(2023, 11, 16, 0, 0), # 19:00 15th November + 'stop': datetime(2023, 11, 16, 1, 0), # 20:00 15th November + }).with_context(mail_notrack=True) + with freeze_time('2023-11-15 23:30:00'): # 18:30 before event + self.assertEqual(search_event(), event) + with freeze_time('2023-11-16 00:00:00'): # 19:00 during event + self.assertEqual(search_event(), event) + with freeze_time('2023-11-16 00:30:00'): # 19:30 during event + self.assertEqual(search_event(), event) + with freeze_time('2023-11-16 01:00:00'): # 20:00 during event + self.assertEqual(search_event(), event) + with freeze_time('2023-11-16 01:30:00'): # 20:30 after event + self.assertEqual(len(search_event()), 0) + event.unlink() + + event = self.env['calendar.event'].create({ + 'name': "Meeting", + 'start': datetime(2023, 11, 16, 21, 0), # 16:00 16th November + 'stop': datetime(2023, 11, 16, 22, 0), # 27:00 16th November + }).with_context(mail_notrack=True) + with freeze_time('2023-11-15 19:00:00'): # 14:00 the day before event + self.assertEqual(len(search_event()), 0) + event.unlink()