Skip to content

Commit

Permalink
[FIX] calendar: no duplicated alarm in daily recurrences
Browse files Browse the repository at this point in the history
Before this commit, alarms were always being set up when there was no 'next_date' specified, creating an undeterministic behavior since inside the 'get_next_alarm_date' function we check if the 'call_at' attribute from the trigger happens before 'now'. If this condition is false, i.e. if there is already an alarm set up for the future, we were returning a falsy value for the next alarm date and setting up the alarm anyway (using the current date inside '_setup_alarms', since the context was None), thus generating a new alarm with the wrong call date.

After this commit, we make an extra check for recurrences in the 'get_next_alarm_date' function in order to get the correct next date for the next alarm from the next recurrent event. Additionally, if there is no next date to be called in the in the future, we skip the 'send_reminder' function which was wrongly creating another alarm using 'now' as time.

closes #163851

Issue-from: #147914
X-original-commit: 90d5c99
Signed-off-by: Arnaud Joset (arj) <arj@odoo.com>
Signed-off-by: Gabriel de Paula Felix (gdpf) <gdpf@odoo.com>
  • Loading branch information
geflx committed Apr 30, 2024
1 parent d629ba6 commit b09bb8c
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 1 deletion.
5 changes: 4 additions & 1 deletion addons/calendar/models/calendar_alarm_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,10 @@ def _send_reminder(self):
for event in events:
if event.recurrence_id:
next_date = event.get_next_alarm_date(events_by_alarm)
event.recurrence_id.with_context(date=next_date)._setup_alarms()
# In cron, setup alarm only when there is a next date on the target. Otherwise the 'now()'
# check in the call below can generate undeterministic behavior and setup random alarms.
if next_date:
event.recurrence_id.with_context(date=next_date)._setup_alarms()

@api.model
def get_next_notif(self):
Expand Down
9 changes: 9 additions & 0 deletions addons/calendar/models/calendar_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,15 @@ def get_next_alarm_date(self, events_by_alarm):
next_date = self.start - timedelta(minutes=sorted_alarms[0].duration_minutes) \
if event_has_future_alarms \
else self.start
# For recurrent events, when there is no next_date and no trigger in the recurence, set the next
# date as the date of the next event. This keeps the single alarm alive in the recurrence.
recurrence_has_no_trigger = self.recurrence_id and not self.recurrence_id.trigger_id
if recurrence_has_no_trigger and not next_date and len(sorted_alarms) > 0:
future_recurrent_events = self.recurrence_id.calendar_event_ids.filtered(lambda ev: ev.start > self.start)
if future_recurrent_events:
# The next event (minus the alarm duration) will be the next date.
next_recurrent_event = future_recurrent_events.sorted("start")[0]
next_date = next_recurrent_event.start - timedelta(minutes=sorted_alarms[0].duration_minutes)
return next_date

# ------------------------------------------------------------
Expand Down
38 changes: 38 additions & 0 deletions addons/calendar/tests/test_event_notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,44 @@ def test_email_alarm_recurrence(self):
self.env.flush_all()
self.assertEqual(len(capt.records), 1)

with self.capture_triggers('calendar.ir_cron_scheduler_alarm') as capt:
# Create alarm with one hour interval.
alarm_hour = self.env['calendar.alarm'].create({
'name': 'Alarm',
'alarm_type': 'email',
'interval': 'hours',
'duration': 1,
})
# Create monthly recurrence, ensure the next alarm is set to the first event
# and then one month later must be set one hour before to the last event.
with freeze_time('2024-04-16 10:00+0000'):
now = fields.Datetime.now()
self.env['calendar.event'].create({
'name': "Single Doom's day",
'start': now + relativedelta(hours=2),
'stop': now + relativedelta(hours=3),
'recurrency': True,
'rrule_type': 'monthly',
'count': 2,
'day': 16,
'alarm_ids': [fields.Command.link(alarm_hour.id)],
}).with_context(mail_notrack=True)
self.env.flush_all()
# Ensure that there is only one alarm set, exactly for one hour previous the event.
self.assertEqual(len(capt.records), 1, "Only one trigger must be created for the entire recurrence.")
self.assertEqual(capt.records.mapped('call_at'), [datetime(2024, 4, 16, 11, 0)], "Alarm must be one hour before the first event.")

# Garbage-collect the previous trigger from the cron.
with freeze_time('2024-05-10 11:00+0000'):
self.env['ir.cron.trigger']._gc_cron_triggers()

with freeze_time('2024-04-22 10:00+0000'):
# The next alarm will be set through the next_date selection for the next event.
# Ensure that there is only one alarm set, exactly for one hour previous the event.
self.env['calendar.alarm_manager']._send_reminder()
self.assertEqual(len(capt.records), 1, "Only one trigger must be created for the entire recurrence.")
self.assertEqual(capt.records.mapped('call_at'), [datetime(2024, 5, 16, 11, 0)], "Alarm must be one hour before the second event.")

def test_email_alarm_daily_recurrence(self):
# test email alarm is sent correctly on daily recurrence
alarm = self.env['calendar.alarm'].create({
Expand Down

0 comments on commit b09bb8c

Please sign in to comment.