diff --git a/addons/mass_mailing/models/mailing.py b/addons/mass_mailing/models/mailing.py index fa25860b6c4..f3b8aafbfcc 100644 --- a/addons/mass_mailing/models/mailing.py +++ b/addons/mass_mailing/models/mailing.py @@ -329,15 +329,19 @@ class MassMailing(models.Model): self.browse(row.pop('mailing_id')).update(row) def _compute_next_departure(self): - cron_next_call = self.env.ref('mass_mailing.ir_cron_mass_mailing_queue').sudo().nextcall - str2dt = fields.Datetime.from_string - cron_time = str2dt(cron_next_call) + # Schedule_date should only be False if schedule_type = "now" or + # mass_mailing is canceled. + # A cron.trigger is created when mailing is put "in queue" + # so we can reasonably expect that the cron worker will + # execute this based on the cron.trigger's call_at which should + # be now() when clicking "Send" or schedule_date if scheduled + for mass_mailing in self: if mass_mailing.schedule_date: - schedule_date = str2dt(mass_mailing.schedule_date) - mass_mailing.next_departure = max(schedule_date, cron_time) + # max in case the user schedules a date in the past + mass_mailing.next_departure = max(mass_mailing.schedule_date, fields.datetime.now()) else: - mass_mailing.next_departure = cron_time + mass_mailing.next_departure = fields.datetime.now() @api.depends('email_from', 'mail_server_id') def _compute_warning_message(self): @@ -931,7 +935,7 @@ class MassMailing(models.Model): JOIN %(target)s t ON (s.res_id = t.id) %(join_domain)s WHERE substring(t.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)') IS NOT NULL - %(where_domain)s + %(where_domain)s """ # Apply same 'get email field' rule from mail_thread.message_get_default_recipients @@ -942,9 +946,9 @@ class MassMailing(models.Model): FROM mailing_trace s JOIN %(target)s t ON (s.res_id = t.id) JOIN res_partner p ON (t.partner_id = p.id) - %(join_domain)s + %(join_domain)s WHERE substring(p.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)') IS NOT NULL - %(where_domain)s + %(where_domain)s """ elif issubclass(type(target), self.pool['mail.thread.blacklist']): mail_field = 'email_normalized' diff --git a/addons/mass_mailing/tests/test_mailing_mailing_schedule_date.py b/addons/mass_mailing/tests/test_mailing_mailing_schedule_date.py index 815cdb24a69..77891aaa802 100644 --- a/addons/mass_mailing/tests/test_mailing_mailing_schedule_date.py +++ b/addons/mass_mailing/tests/test_mailing_mailing_schedule_date.py @@ -2,16 +2,118 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from datetime import datetime +from freezegun import freeze_time +from odoo.addons.base.tests.test_ir_cron import CronMixinCase from odoo.addons.mass_mailing.tests.common import MassMailCommon from odoo.tests import users, Form from odoo.tools import mute_logger -class TestMailingScheduleDateWizard(MassMailCommon): +class TestMailingScheduleDateWizard(MassMailCommon, CronMixinCase): @mute_logger('odoo.addons.mail.models.mail_mail') @users('user_marketing') + def test_mailing_next_departure(self): + # test if mailing.mailing.next_departure is correctly set taking into account + # presence of implicitly created cron triggers (since odoo v15). These should + # launch cron job before its schedule nextcall datetime (if scheduled_date < nextcall) + + cron_job = self.env.ref('mass_mailing.ir_cron_mass_mailing_queue').sudo() + cron_job.write({'nextcall' : datetime(2023, 2, 18, 9, 0)}) + cron_job_id = cron_job.id + + # case where user click on "Send" button (action_launch) + with freeze_time(datetime(2023, 2, 17, 9, 0)): + with self.capture_triggers(cron_job_id) as capt: + mailing = self.env['mailing.mailing'].create({ + 'name': 'mailing', + 'subject': 'some subject', + 'mailing_model_id': self.env['ir.model']._get('res.partner').id, + 'state' : 'draft' + }) + mailing.action_launch() + capt.records.ensure_one() + + # assert that the schedule_date and schedule_type fields are correct and that the mailing is put in queue + self.assertEqual(mailing.next_departure, datetime(2023, 2, 17, 9, 0)) + self.assertIsNot(mailing.schedule_date, cron_job.nextcall) + self.assertEqual(mailing.schedule_type, 'now') + self.assertEqual(mailing.state, 'in_queue') + self.assertEqual(capt.records.call_at, datetime(2023, 2, 17, 9, 0)) #verify that cron.trigger exists + + # case where client uses schedule wizard to chose a date between now and cron.job nextcall + with freeze_time(datetime(2023, 2, 17, 9, 0)): + with self.capture_triggers(cron_job_id) as capt: + mailing = self.env['mailing.mailing'].create({ + 'name': 'mailing', + 'subject': 'some subject', + 'mailing_model_id': self.env['ir.model']._get('res.partner').id, + 'state' : 'draft', + 'schedule_date' : datetime(2023, 2, 17, 11, 0), + 'schedule_type' : 'scheduled' + }) + mailing.action_schedule() + capt.records.ensure_one() + + self.assertEqual(mailing.schedule_date, datetime(2023, 2, 17, 11, 0)) + self.assertEqual(mailing.next_departure, datetime(2023, 2, 17, 11, 0)) + self.assertEqual(mailing.schedule_type, 'scheduled') + self.assertEqual(mailing.state, 'in_queue') + self.assertEqual(capt.records.call_at, datetime(2023, 2, 17, 11, 0)) #verify that cron.trigger exists + + # case where client uses schedule wizard to chose a date after cron.job nextcall + # which means mails will get send after that date (datetime(2023, 2, 18, 9, 0)) + with freeze_time(datetime(2023, 2, 17, 9, 0)): + with self.capture_triggers(cron_job_id) as capt: + mailing = self.env['mailing.mailing'].create({ + 'name': 'mailing', + 'subject': 'some subject', + 'mailing_model_id': self.env['ir.model']._get('res.partner').id, + 'state' : 'draft', + 'schedule_date' : datetime(2024, 2, 17, 11, 0), + 'schedule_type' : 'scheduled' + }) + mailing.action_schedule() + capt.records.ensure_one() + + self.assertEqual(mailing.schedule_date, datetime(2024, 2, 17, 11, 0)) + self.assertEqual(mailing.next_departure, datetime(2024, 2, 17, 11, 0)) + self.assertEqual(mailing.schedule_type, 'scheduled') + self.assertEqual(mailing.state, 'in_queue') + self.assertEqual(capt.records.call_at, datetime(2024, 2, 17, 11, 0)) #verify that cron.trigger exists + + # case where client uses schedule wizard to chose a date in the past + with freeze_time(datetime(2023, 2, 17, 9, 0)): + with self.capture_triggers(cron_job_id) as capt: + mailing = self.env['mailing.mailing'].create({ + 'name': 'mailing', + 'subject': 'some subject', + 'mailing_model_id': self.env['ir.model']._get('res.partner').id, + 'state' : 'draft', + 'schedule_date' : datetime(2024, 2, 17, 11, 0), + 'schedule_type' : 'scheduled' + }) + # create a schedule date wizard + # Have to use wizard for this case to simulate schedule date in the past + # Otherwise "state" doesn't get update from draft to'in_queue' + # in test env vs production env (see mailing.mailing.schedule.date wizard) + + wizard_form = Form( + self.env['mailing.mailing.schedule.date'].with_context(default_mass_mailing_id=mailing.id)) + + # set a schedule date + wizard_form.schedule_date = datetime(2022, 2, 17, 11, 0) + wizard = wizard_form.save() + wizard.action_schedule_date() + capt.records.ensure_one() + + self.assertEqual(mailing.schedule_date, datetime(2022, 2, 17, 11, 0)) + self.assertEqual(mailing.next_departure, datetime(2023, 2, 17, 9, 0)) #now + self.assertEqual(mailing.schedule_type, 'scheduled') + self.assertEqual(mailing.state, 'in_queue') + self.assertEqual(capt.records.call_at, datetime(2022, 2, 17, 11, 0)) #verify that cron.trigger exists + def test_mailing_schedule_date(self): mailing = self.env['mailing.mailing'].create({ 'name': 'mailing',