diff options
| -rw-r--r-- | addons/event/models/event_mail.py | 27 | ||||
| -rw-r--r-- | addons/event/models/event_registration.py | 2 | ||||
| -rw-r--r-- | addons/event/tests/test_event_mail_schedule.py | 284 | ||||
| -rw-r--r-- | addons/event_sms/models/event_mail.py | 22 | ||||
| -rw-r--r-- | addons/mail/tests/common.py | 4 |
5 files changed, 270 insertions, 69 deletions
diff --git a/addons/event/models/event_mail.py b/addons/event/models/event_mail.py index 58fc24af9ed..9d9df8faf06 100644 --- a/addons/event/models/event_mail.py +++ b/addons/event/models/event_mail.py @@ -1,6 +1,10 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +import logging +import random +import threading + from datetime import datetime from dateutil.relativedelta import relativedelta @@ -8,8 +12,6 @@ from odoo import api, fields, models, tools from odoo.tools import exception_to_unicode from odoo.tools.translate import _ -import random -import logging _logger = logging.getLogger(__name__) _INTERVALS = { @@ -117,10 +119,11 @@ class EventMailScheduler(models.Model): if lines: mail.write({'mail_registration_ids': lines}) # execute scheduler on registrations - mail.mail_registration_ids.filtered(lambda reg: reg.scheduled_date and reg.scheduled_date <= now).execute() + mail.mail_registration_ids.execute() else: # Do not send emails if the mailing was scheduled before the event but the event is over - if not mail.mail_sent and (mail.interval_type != 'before_event' or mail.event_id.date_end > now) and mail.notification_type == 'mail': + if not mail.mail_sent and mail.scheduled_date <= now and mail.notification_type == 'mail' and \ + (mail.interval_type != 'before_event' or mail.event_id.date_end > now): mail.event_id.mail_attendees(mail.template_id.id) mail.write({'mail_sent': True}) return True @@ -176,7 +179,7 @@ You receive this email because you are: self.invalidate_cache() self._warn_template_error(scheduler, e) else: - if autocommit: + if autocommit and not getattr(threading.currentThread(), 'testing', False): self.env.cr.commit() return True @@ -193,10 +196,16 @@ class EventMailRegistration(models.Model): mail_sent = fields.Boolean('Mail Sent') def execute(self): - for mail in self: - if mail.registration_id.state in ['open', 'done'] and not mail.mail_sent and mail.scheduler_id.notification_type == 'mail': - mail.scheduler_id.template_id.send_mail(mail.registration_id.id) - mail.write({'mail_sent': True}) + now = fields.Datetime.now() + todo = self.filtered(lambda reg_mail: + not reg_mail.mail_sent and \ + reg_mail.registration_id.state in ['open', 'done'] and \ + (reg_mail.scheduled_date and reg_mail.scheduled_date <= now) and \ + reg_mail.scheduler_id.notification_type == 'mail' + ) + for reg_mail in todo: + reg_mail.scheduler_id.template_id.send_mail(reg_mail.registration_id.id) + todo.write({'mail_sent': True}) @api.depends('registration_id', 'scheduler_id.interval_unit', 'scheduler_id.interval_type') def _compute_scheduled_date(self): diff --git a/addons/event/models/event_registration.py b/addons/event/models/event_registration.py index 58960460622..5b14153be03 100644 --- a/addons/event/models/event_registration.py +++ b/addons/event/models/event_registration.py @@ -160,7 +160,7 @@ class EventRegistration(models.Model): if vals.get('state') == 'open': # auto-trigger after_sub (on subscribe) mail schedulers, if needed onsubscribe_schedulers = self.mapped('event_id.event_mail_ids').filtered(lambda s: s.interval_type == 'after_sub') - onsubscribe_schedulers.execute() + onsubscribe_schedulers.sudo().execute() return ret diff --git a/addons/event/tests/test_event_mail_schedule.py b/addons/event/tests/test_event_mail_schedule.py index 42e1d18017d..896a5873205 100644 --- a/addons/event/tests/test_event_mail_schedule.py +++ b/addons/event/tests/test_event_mail_schedule.py @@ -1,76 +1,254 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from datetime import datetime from dateutil.relativedelta import relativedelta +from freezegun import freeze_time -from odoo import fields from odoo.addons.event.tests.common import TestEventCommon -from odoo.tools import mute_logger +from odoo.addons.mail.tests.common import MockEmail +from odoo.tools import formataddr, mute_logger -class TestMailSchedule(TestEventCommon): +class TestMailSchedule(TestEventCommon, MockEmail): @mute_logger('odoo.addons.base.models.ir_model', 'odoo.models') def test_event_mail_schedule(self): """ Test mail scheduling for events """ - now = fields.Datetime.now() - event_date_begin = now + relativedelta(days=1) - event_date_end = now + relativedelta(days=3) - - test_event = self.env['event.event'].with_user(self.user_eventmanager).create({ - 'name': 'TestEventMail', - 'auto_confirm': True, - 'date_begin': event_date_begin, - 'date_end': event_date_end, - 'event_mail_ids': [ - (0, 0, { # right at subscription - 'interval_unit': 'now', - 'interval_type': 'after_sub', - 'template_id': self.env['ir.model.data'].xmlid_to_res_id('event.event_subscription')}), - (0, 0, { # 1 days before event - 'interval_nbr': 1, - 'interval_unit': 'days', - 'interval_type': 'before_event', - 'template_id': self.env['ir.model.data'].xmlid_to_res_id('event.event_reminder')}), - ] - }) + event_cron_id = self.env.ref('event.event_mail_scheduler') - # create some registrations - self.env['event.registration'].with_user(self.user_eventuser).create({ - 'event_id': test_event.id, - 'name': 'Reg0', - 'email': 'reg0@example.com', - }) - self.env['event.registration'].with_user(self.user_eventuser).create({ - 'event_id': test_event.id, - 'name': 'Reg1', - 'email': 'reg1@example.com', - }) + # deactivate other schedulers to avoid messing with crons + self.env['event.mail'].search([]).unlink() + + # freeze some datetimes, and ensure more than 1D+1H before event starts + # to ease time-based scheduler check + now = datetime(2021, 3, 20, 14, 30, 15) + event_date_begin = datetime(2021, 3, 22, 8, 0, 0) + event_date_end = datetime(2021, 3, 24, 18, 0, 0) + + with freeze_time(now): + test_event = self.env['event.event'].with_user(self.user_eventmanager).create({ + 'name': 'TestEventMail', + 'auto_confirm': True, + 'date_begin': event_date_begin, + 'date_end': event_date_end, + 'event_mail_ids': [ + (0, 0, { # right at subscription + 'interval_unit': 'now', + 'interval_type': 'after_sub', + 'template_id': self.env['ir.model.data'].xmlid_to_res_id('event.event_subscription')}), + (0, 0, { # one day after subscription + 'interval_nbr': 1, + 'interval_unit': 'hours', + 'interval_type': 'after_sub', + 'template_id': self.env['ir.model.data'].xmlid_to_res_id('event.event_subscription')}), + (0, 0, { # 1 days before event + 'interval_nbr': 1, + 'interval_unit': 'days', + 'interval_type': 'before_event', + 'template_id': self.env['ir.model.data'].xmlid_to_res_id('event.event_reminder')}), + (0, 0, { # immediately after event + 'interval_nbr': 1, + 'interval_unit': 'hours', + 'interval_type': 'after_event', + 'template_id': self.env['ir.model.data'].xmlid_to_res_id('event.event_reminder')}), + ] + }) # check subscription scheduler - schedulers = self.env['event.mail'].search([('event_id', '=', test_event.id), ('interval_type', '=', 'after_sub')]) - self.assertEqual(len(schedulers), 1, 'event: wrong scheduler creation') - self.assertEqual(schedulers[0].scheduled_date, test_event.create_date, 'event: incorrect scheduled date for checking controller') + after_sub_scheduler = self.env['event.mail'].search([('event_id', '=', test_event.id), ('interval_type', '=', 'after_sub'), ('interval_unit', '=', 'now')]) + self.assertEqual(len(after_sub_scheduler), 1, 'event: wrong scheduler creation') + self.assertEqual(after_sub_scheduler.scheduled_date, test_event.create_date) + self.assertTrue(after_sub_scheduler.done) + after_sub_scheduler_2 = self.env['event.mail'].search([('event_id', '=', test_event.id), ('interval_type', '=', 'after_sub'), ('interval_unit', '=', 'hours')]) + self.assertEqual(len(after_sub_scheduler_2), 1, 'event: wrong scheduler creation') + self.assertEqual(after_sub_scheduler_2.scheduled_date, test_event.create_date + relativedelta(hours=1)) + self.assertTrue(after_sub_scheduler_2.done) + # check before event scheduler + event_prev_scheduler = self.env['event.mail'].search([('event_id', '=', test_event.id), ('interval_type', '=', 'before_event')]) + self.assertEqual(len(event_prev_scheduler), 1, 'event: wrong scheduler creation') + self.assertEqual(event_prev_scheduler.scheduled_date, event_date_begin + relativedelta(days=-1)) + self.assertFalse(event_prev_scheduler.done) + # check after event scheduler + event_next_scheduler = self.env['event.mail'].search([('event_id', '=', test_event.id), ('interval_type', '=', 'after_event')]) + self.assertEqual(len(event_next_scheduler), 1, 'event: wrong scheduler creation') + self.assertEqual(event_next_scheduler.scheduled_date, event_date_end + relativedelta(hours=1)) + self.assertFalse(event_next_scheduler.done) + + # create some registrations + with freeze_time(now), self.mock_mail_gateway(): + reg1 = self.env['event.registration'].with_user(self.user_eventuser).create({ + 'event_id': test_event.id, + 'name': 'Reg1', + 'email': 'reg1@example.com', + }) + reg2 = self.env['event.registration'].with_user(self.user_eventuser).create({ + 'event_id': test_event.id, + 'name': 'Reg2', + 'email': 'reg2@example.com', + }) + + # REGISTRATIONS / PRE SCHEDULERS + # -------------------------------------------------- + + # check registration state + self.assertTrue(all(reg.state == 'open' for reg in reg1 + reg2), 'Registrations: should be auto-confirmed') + self.assertTrue(all(reg.date_open == now for reg in reg1 + reg2), 'Registrations: should have open date set to confirm date') # verify that subscription scheduler was auto-executed after each registration - self.assertEqual(len(schedulers[0].mail_registration_ids), 2, 'event: incorrect number of mail scheduled date') + self.assertEqual(len(after_sub_scheduler.mail_registration_ids), 2, 'event: should have 2 scheduled communication (1 / registration)') + for mail_registration in after_sub_scheduler.mail_registration_ids: + self.assertEqual(mail_registration.scheduled_date, now) + self.assertTrue(mail_registration.mail_sent, 'event: registration mail should be sent at registration creation') + self.assertTrue(after_sub_scheduler.done, 'event: all subscription mails should have been sent') - mails = self.env['mail.mail'].sudo().search([('subject', 'ilike', 'registration'), ('date', '>=', now)], order='date DESC', limit=3) - self.assertEqual(len(mails), 2, 'event: wrong number of registration mail sent') + # check emails effectively sent + self.assertEqual(len(self._new_mails), 2, 'event: should have 2 scheduled emails (1 / registration)') + self.assertMailMailWEmails( + [formataddr((reg1.name, reg1.email)), formataddr((reg2.name, reg2.email))], + 'outgoing', '', + fields_values={'subject': 'Your registration at %s' % test_event.name, + 'email_from': self.user_eventmanager.company_id.email_formatted, + }) - for registration in schedulers[0].mail_registration_ids: - self.assertTrue(registration.mail_sent, 'event: wrongly confirmed mailing on registration') + # same for second scheduler: scheduled but not sent + self.assertEqual(len(after_sub_scheduler_2.mail_registration_ids), 2, 'event: should have 2 scheduled communication (1 / registration)') + for mail_registration in after_sub_scheduler_2.mail_registration_ids: + self.assertEqual(mail_registration.scheduled_date, now + relativedelta(hours=1)) + self.assertFalse(mail_registration.mail_sent, 'event: registration mail should be scheduled, not sent') + self.assertFalse(after_sub_scheduler_2.done, 'event: all subscription mails should be scheduled, not sent') - # check before event scheduler - schedulers = self.env['event.mail'].search([('event_id', '=', test_event.id), ('interval_type', '=', 'before_event')]) - self.assertEqual(len(schedulers), 1, 'event: wrong scheduler creation') - self.assertEqual(schedulers[0].scheduled_date, event_date_begin + relativedelta(days=-1), 'event: incorrect scheduled date') + # execute event reminder scheduler explicitly, before scheduled date -> should not do anything + with freeze_time(now), self.mock_mail_gateway(): + after_sub_scheduler_2.execute() + self.assertFalse(any(mail_reg.mail_sent for mail_reg in after_sub_scheduler_2.mail_registration_ids)) + self.assertFalse(after_sub_scheduler_2.done) + self.assertEqual(len(self._new_mails), 0, 'event: should not send mails before scheduled date') + + # execute event reminder scheduler explicitly, right at scheduled date -> should sent mails + now_registration = now + relativedelta(hours=1) + with freeze_time(now_registration), self.mock_mail_gateway(): + after_sub_scheduler_2.execute() + + # verify that subscription scheduler was auto-executed after each registration + self.assertEqual(len(after_sub_scheduler_2.mail_registration_ids), 2, 'event: should have 2 scheduled communication (1 / registration)') + self.assertTrue(all(mail_reg.mail_sent for mail_reg in after_sub_scheduler_2.mail_registration_ids)) + # FIXME: field not updated + # self.assertTrue(after_sub_scheduler_2.done, 'event: all subscription mails should have been sent') + + # check emails effectively sent + self.assertEqual(len(self._new_mails), 2, 'event: should have 2 scheduled emails (1 / registration)') + self.assertMailMailWEmails( + [formataddr((reg1.name, reg1.email)), formataddr((reg2.name, reg2.email))], + 'outgoing', '', + fields_values={'subject': 'Your registration at %s' % test_event.name, + 'email_from': self.user_eventmanager.company_id.email_formatted, + }) + + # PRE SCHEDULERS (MOVE FORWARD IN TIME) + # -------------------------------------------------- + + self.assertFalse(event_prev_scheduler.mail_sent) + self.assertFalse(event_prev_scheduler.done) + + # execute event reminder scheduler explicitly, before scheduled date -> should not do anything + now_start = event_date_begin + relativedelta(hours=-25) + with freeze_time(now_start), self.mock_mail_gateway(): + event_prev_scheduler.execute() + + self.assertFalse(event_prev_scheduler.mail_sent) + self.assertFalse(event_prev_scheduler.done) + self.assertEqual(len(self._new_mails), 0) + + # execute cron to run schedulers + now_start = event_date_begin + relativedelta(hours=-23) + with freeze_time(now_start), self.mock_mail_gateway(): + event_cron_id.method_direct_trigger() + + # check that scheduler is finished + self.assertTrue(event_prev_scheduler.mail_sent, 'event: reminder scheduler should have run') + self.assertTrue(event_prev_scheduler.done, 'event: reminder scheduler should have run') + + # check emails effectively sent + self.assertEqual(len(self._new_mails), 2, 'event: should have scheduled 2 mails (1 / registration)') + self.assertMailMailWEmails( + [formataddr((reg1.name, reg1.email)), formataddr((reg2.name, reg2.email))], + 'outgoing', '', + fields_values={'subject': '%s: tomorrow' % test_event.name, + 'email_from': self.user_eventmanager.company_id.email_formatted, + }) + + # NEW REGISTRATION EFFECT ON SCHEDULERS + # -------------------------------------------------- + + test_event.write({'auto_confirm': False}) + with freeze_time(now_start), self.mock_mail_gateway(): + reg3 = self.env['event.registration'].with_user(self.user_eventuser).create({ + 'event_id': test_event.id, + 'name': 'Reg3', + 'email': 'reg3@example.com', + }) + + # no more seats + self.assertEqual(reg3.state, 'draft') + + # schedulers state untouched + self.assertTrue(event_prev_scheduler.mail_sent) + self.assertTrue(event_prev_scheduler.mail_sent) + self.assertFalse(event_next_scheduler.mail_sent) + self.assertFalse(event_next_scheduler.done) + self.assertFalse(after_sub_scheduler.done, 'event: scheduler registrations should be lower than effective registrations') + self.assertFalse(after_sub_scheduler_2.done, 'event: scheduler registrations should be lower than effective registrations') + + # confirm registration -> should trigger registration schedulers + # NOTE: currently all schedulers are based on date_open which equals create_date + # meaning several communications may be sent in the time time + with freeze_time(now_start + relativedelta(hours=1)), self.mock_mail_gateway(): + reg3.action_confirm() + + # verify that subscription scheduler was auto-executed after new registration confirmed + self.assertEqual(len(after_sub_scheduler.mail_registration_ids), 3, 'event: should have 3 scheduled communication (1 / registration)') + new_mail_reg = after_sub_scheduler.mail_registration_ids.filtered(lambda mail_reg: mail_reg.registration_id == reg3) + self.assertEqual(new_mail_reg.scheduled_date, now_start) + self.assertTrue(new_mail_reg.mail_sent, 'event: registration mail should be sent at registration creation') + self.assertTrue(after_sub_scheduler.done, 'event: all subscription mails should have been sent') + + # verify that subscription scheduler was auto-executed after new registration confirmed + self.assertEqual(len(after_sub_scheduler_2.mail_registration_ids), 3, 'event: should have 3 scheduled communication (1 / registration)') + new_mail_reg = after_sub_scheduler_2.mail_registration_ids.filtered(lambda mail_reg: mail_reg.registration_id == reg3) + self.assertEqual(new_mail_reg.scheduled_date, now_start + relativedelta(hours=1)) + self.assertTrue(new_mail_reg.mail_sent, 'event: registration mail should be sent at registration creation') + self.assertTrue(after_sub_scheduler_2.done, 'event: all subscription mails should have been sent') + + # check emails effectively sent + self.assertEqual(len(self._new_mails), 2, 'event: should have 1 scheduled emails (new registration only)') + # manual check because 2 identical mails are sent and mail tools do not support it easily + for mail in self._new_mails: + self.assertEqual(mail.email_from, self.user_eventmanager.company_id.email_formatted) + self.assertEqual(mail.subject, 'Your registration at %s' % test_event.name) + self.assertEqual(mail.state, 'outgoing') + self.assertEqual(mail.email_to, formataddr((reg3.name, reg3.email))) + + # POST SCHEDULERS (MOVE FORWARD IN TIME) + # -------------------------------------------------- + + self.assertFalse(event_next_scheduler.mail_sent) + self.assertFalse(event_next_scheduler.done) - # execute event reminder scheduler explicitly - schedulers[0].execute() + # execute event reminder scheduler explicitly after its schedule date + new_end = event_date_end + relativedelta(hours=2) + with freeze_time(new_end), self.mock_mail_gateway(): + event_cron_id.method_direct_trigger() - self.assertTrue(schedulers[0].mail_sent, 'event: reminder scheduler should have sent an email') - self.assertTrue(schedulers[0].done, 'event: reminder scheduler should be done') + # check that scheduler is finished + self.assertTrue(event_next_scheduler.mail_sent, 'event: reminder scheduler should should have run') + self.assertTrue(event_next_scheduler.done, 'event: reminder scheduler should have run') - mails = self.env['mail.mail'].sudo().search([('subject', 'ilike', 'TestEventMail'), ('date', '>=', now)], order='date DESC', limit=3) - self.assertEqual(len(mails), 3, 'event: wrong number of reminders in outgoing mail queue') + # check emails effectively sent + self.assertEqual(len(self._new_mails), 3, 'event: should have scheduled 3 mails, one for each registration') + self.assertMailMailWEmails( + [formataddr((reg1.name, reg1.email)), formataddr((reg2.name, reg2.email)), formataddr((reg3.name, reg3.email))], + 'outgoing', '', + fields_values={'subject': '%s: today' % test_event.name, + 'email_from': self.user_eventmanager.company_id.email_formatted, + }) diff --git a/addons/event_sms/models/event_mail.py b/addons/event_sms/models/event_mail.py index 53562875f10..3e56a0552a7 100644 --- a/addons/event_sms/models/event_mail.py +++ b/addons/event_sms/models/event_mail.py @@ -36,7 +36,9 @@ class EventMailScheduler(models.Model): now = fields.Datetime.now() if mail.interval_type != 'after_sub': # Do not send SMS if the communication was scheduled before the event but the event is over - if not mail.mail_sent and (mail.interval_type != 'before_event' or mail.event_id.date_end > now) and mail.notification_type == 'sms' and mail.sms_template_id: + if not mail.mail_sent and mail.scheduled_date <= now and mail.notification_type == 'sms' and \ + (mail.interval_type != 'before_event' or mail.event_id.date_end > now) and \ + mail.sms_template_id: self.env['event.registration']._message_sms_schedule_mass( template=mail.sms_template_id, active_domain=[('event_id', '=', mail.event_id.id), ('state', '!=', 'cancel')], @@ -50,8 +52,18 @@ class EventMailRegistration(models.Model): _inherit = 'event.mail.registration' def execute(self): - for record in self: - if record.registration_id.state in ['open', 'done'] and not record.mail_sent and record.scheduler_id.notification_type == 'sms': - record.registration_id._message_sms_schedule_mass(template=record.scheduler_id.sms_template_id, mass_keep_log=True) - record.write({'mail_sent': True}) + now = fields.Datetime.now() + todo = self.filtered(lambda reg_mail: + not reg_mail.mail_sent and \ + reg_mail.registration_id.state in ['open', 'done'] and \ + (reg_mail.scheduled_date and reg_mail.scheduled_date <= now) and \ + reg_mail.scheduler_id.notification_type == 'sms' + ) + for reg_mail in todo: + reg_mail.registration_id._message_sms_schedule_mass( + template=reg_mail.scheduler_id.sms_template_id, + mass_keep_log=True + ) + todo.write({'mail_sent': True}) + return super(EventMailRegistration, self).execute() diff --git a/addons/mail/tests/common.py b/addons/mail/tests/common.py index d4dbfb2f949..3e1dd62994d 100644 --- a/addons/mail/tests/common.py +++ b/addons/mail/tests/common.py @@ -240,7 +240,9 @@ class MockEmail(common.BaseCase): if content: self.assertIn(content, sent_mail.body_html) for fname, fvalue in (fields_values or {}).items(): - self.assertEqual(sent_mail[fname], fvalue) + self.assertEqual( + sent_mail[fname], fvalue, + 'Mail: expected %s for %s, got %s' % (fvalue, fname, sent_mail[fname])) def assertNoMail(self, author, recipients, mail_message): try: |
