From df52fedee6c8262337214dd540e1a352653262e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 18 Feb 2021 16:56:05 +0000 Subject: [PATCH 1/6] [IMP] base: add missing views for ir.cron.trigger model Purpose of this commit is to help debugging processes involving cron triggers by adding views and a menu in Odoo. It allows to see triggers having been created. Menu has been added under Scheduled Actions (aka Crons), in Technical menu. Task ID-2414658 COM PR odoo/odoo#63093 --- odoo/addons/base/__manifest__.py | 1 + .../base/views/ir_cron_trigger_views.xml | 51 +++++++++++++++++++ odoo/addons/base/views/ir_cron_views.xml | 5 +- 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 odoo/addons/base/views/ir_cron_trigger_views.xml diff --git a/odoo/addons/base/__manifest__.py b/odoo/addons/base/__manifest__.py index 1a3658ceaef..2e7f552f237 100644 --- a/odoo/addons/base/__manifest__.py +++ b/odoo/addons/base/__manifest__.py @@ -30,6 +30,7 @@ The kernel of Odoo, needed for all installation. 'views/ir_actions_views.xml', 'views/ir_config_parameter_views.xml', 'views/ir_cron_views.xml', + 'views/ir_cron_trigger_views.xml', 'views/ir_filters_views.xml', 'views/ir_mail_server_views.xml', 'views/ir_model_views.xml', diff --git a/odoo/addons/base/views/ir_cron_trigger_views.xml b/odoo/addons/base/views/ir_cron_trigger_views.xml new file mode 100644 index 00000000000..bd19351dbb0 --- /dev/null +++ b/odoo/addons/base/views/ir_cron_trigger_views.xml @@ -0,0 +1,51 @@ + + + + ir.cron.trigger.view.form + ir.cron.trigger + +
+ + + + + + +
+
+
+ + + ir.cron.trigger.view.tree + ir.cron.trigger + + + + + + + + + + ir.cron.trigger.view.search + ir.cron.trigger + + + + + + + + + + Scheduled Actions Triggers + ir.cron.trigger + tree,form + + + + +
diff --git a/odoo/addons/base/views/ir_cron_views.xml b/odoo/addons/base/views/ir_cron_views.xml index 62d04e9d7c1..039a7adb94c 100644 --- a/odoo/addons/base/views/ir_cron_views.xml +++ b/odoo/addons/base/views/ir_cron_views.xml @@ -91,6 +91,9 @@ - + From db8f68f68377edf2794ff432271fc32ba9b0690c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 9 Feb 2021 10:17:41 +0000 Subject: [PATCH 2/6] [MOV] event: cleanup registration code location Purpose is to ease understanding of code. Ranging chaussettes in the correct tiroir is important. Task ID-2414658 COM PR odoo/odoo#63093 --- addons/event/__manifest__.py | 1 + addons/event/data/event_data.xml | 13 ----- addons/event/data/ir_cron_data.xml | 15 ++++++ addons/event/models/event_registration.py | 58 +++++++++++++---------- 4 files changed, 49 insertions(+), 38 deletions(-) create mode 100644 addons/event/data/ir_cron_data.xml diff --git a/addons/event/__manifest__.py b/addons/event/__manifest__.py index 005e66e6a98..3dc6be1a116 100644 --- a/addons/event/__manifest__.py +++ b/addons/event/__manifest__.py @@ -27,6 +27,7 @@ Key Features 'views/event_stage_views.xml', 'report/event_event_templates.xml', 'report/event_event_reports.xml', + 'data/ir_cron_data.xml', 'data/mail_template_data.xml', 'data/event_data.xml', 'views/res_config_settings_views.xml', diff --git a/addons/event/data/event_data.xml b/addons/event/data/event_data.xml index e08d0744b74..bcaeb997ffe 100644 --- a/addons/event/data/event_data.xml +++ b/addons/event/data/event_data.xml @@ -1,19 +1,6 @@ - - - Event: Mail Scheduler - - code - model.run(True) - - 1 - hours - -1 - - - Ticketing diff --git a/addons/event/data/ir_cron_data.xml b/addons/event/data/ir_cron_data.xml new file mode 100644 index 00000000000..d0ffda9d0ae --- /dev/null +++ b/addons/event/data/ir_cron_data.xml @@ -0,0 +1,15 @@ + + + + + Event: Mail Scheduler + + code + model.run(True) + + 1 + hours + -1 + + + diff --git a/addons/event/models/event_registration.py b/addons/event/models/event_registration.py index 5b14153be03..94988477c2a 100644 --- a/addons/event/models/event_registration.py +++ b/addons/event/models/event_registration.py @@ -208,6 +208,36 @@ class EventRegistration(models.Model): def action_cancel(self): self.write({'state': 'cancel'}) + def action_send_badge_email(self): + """ Open a window to compose an email, with the template - 'event_badge' + message loaded by default + """ + self.ensure_one() + template = self.env.ref('event.event_registration_mail_template_badge') + compose_form = self.env.ref('mail.email_compose_message_wizard_form') + ctx = dict( + default_model='event.registration', + default_res_id=self.id, + default_use_template=bool(template), + default_template_id=template.id, + default_composition_mode='comment', + custom_layout="mail.mail_notification_light", + ) + return { + 'name': _('Compose Email'), + 'type': 'ir.actions.act_window', + 'view_mode': 'form', + 'res_model': 'mail.compose.message', + 'views': [(compose_form.id, 'form')], + 'view_id': compose_form.id, + 'target': 'new', + 'context': ctx, + } + + # ------------------------------------------------------------ + # MAILING / GATEWAY + # ------------------------------------------------------------ + def _message_get_suggested_recipients(self): recipients = super(EventRegistration, self)._message_get_suggested_recipients() public_users = self.env['res.users'].sudo() @@ -248,31 +278,9 @@ class EventRegistration(models.Model): ]).write({'partner_id': new_partner.id}) return super(EventRegistration, self)._message_post_after_hook(message, msg_vals) - def action_send_badge_email(self): - """ Open a window to compose an email, with the template - 'event_badge' - message loaded by default - """ - self.ensure_one() - template = self.env.ref('event.event_registration_mail_template_badge') - compose_form = self.env.ref('mail.email_compose_message_wizard_form') - ctx = dict( - default_model='event.registration', - default_res_id=self.id, - default_use_template=bool(template), - default_template_id=template.id, - default_composition_mode='comment', - custom_layout="mail.mail_notification_light", - ) - return { - 'name': _('Compose Email'), - 'type': 'ir.actions.act_window', - 'view_mode': 'form', - 'res_model': 'mail.compose.message', - 'views': [(compose_form.id, 'form')], - 'view_id': compose_form.id, - 'target': 'new', - 'context': ctx, - } + # ------------------------------------------------------------ + # TOOLS + # ------------------------------------------------------------ def get_date_range_str(self): self.ensure_one() From 214d8fe589fba05ec4945c01fb976f47ddcc3fd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Debauche?= Date: Fri, 19 Mar 2021 14:22:10 +0000 Subject: [PATCH 3/6] [FIX] event: fix done field definition Add missing trigger for done Do not copy done field on event.mail model. Task ID-2414658 COM PR odoo/odoo#63093 --- addons/event/models/event_mail.py | 4 ++-- addons/event/tests/test_event_mail_schedule.py | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/addons/event/models/event_mail.py b/addons/event/models/event_mail.py index 9d9df8faf06..448101636be 100644 --- a/addons/event/models/event_mail.py +++ b/addons/event/models/event_mail.py @@ -85,9 +85,9 @@ class EventMailScheduler(models.Model): scheduled_date = fields.Datetime('Scheduled Sent Mail', compute='_compute_scheduled_date', store=True) mail_registration_ids = fields.One2many('event.mail.registration', 'scheduler_id') mail_sent = fields.Boolean('Mail Sent on Event', copy=False) - done = fields.Boolean('Sent', compute='_compute_done', store=True) + done = fields.Boolean('Sent', compute='_compute_done', store=True, copy=False) - @api.depends('mail_sent', 'interval_type', 'event_id.registration_ids', 'mail_registration_ids') + @api.depends('mail_sent', 'interval_type', 'event_id.registration_ids', 'mail_registration_ids.mail_sent') def _compute_done(self): for mail in self: if mail.interval_type in ['before_event', 'after_event']: diff --git a/addons/event/tests/test_event_mail_schedule.py b/addons/event/tests/test_event_mail_schedule.py index 896a5873205..4f913309fb9 100644 --- a/addons/event/tests/test_event_mail_schedule.py +++ b/addons/event/tests/test_event_mail_schedule.py @@ -133,8 +133,7 @@ class TestMailSchedule(TestEventCommon, MockEmail): # 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') + 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)') From e47725abd88e04dd4594c915b61aca800e83a609 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 24 Mar 2021 08:57:30 +0000 Subject: [PATCH 4/6] [FIX] event: fix missing trigger for scheduled mails based on event end date Scheduled date is not correctly triggered if event date_end changes. It should update its scheduled communications. Task ID-2414658 COM PR odoo/odoo#63093 --- addons/event/models/event_mail.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/addons/event/models/event_mail.py b/addons/event/models/event_mail.py index 448101636be..11ff8fbff64 100644 --- a/addons/event/models/event_mail.py +++ b/addons/event/models/event_mail.py @@ -95,17 +95,17 @@ class EventMailScheduler(models.Model): else: mail.done = len(mail.mail_registration_ids) == len(mail.event_id.registration_ids) and all(mail.mail_sent for mail in mail.mail_registration_ids) - @api.depends('event_id.date_begin', 'interval_type', 'interval_unit', 'interval_nbr') + @api.depends('event_id.date_begin', 'event_id.date_end', 'interval_type', 'interval_unit', 'interval_nbr') def _compute_scheduled_date(self): - for mail in self: - if mail.interval_type == 'after_sub': - date, sign = mail.event_id.create_date, 1 - elif mail.interval_type == 'before_event': - date, sign = mail.event_id.date_begin, -1 + for scheduler in self: + if scheduler.interval_type == 'after_sub': + date, sign = scheduler.event_id.create_date, 1 + elif scheduler.interval_type == 'before_event': + date, sign = scheduler.event_id.date_begin, -1 else: - date, sign = mail.event_id.date_end, 1 + date, sign = scheduler.event_id.date_end, 1 - mail.scheduled_date = date + _INTERVALS[mail.interval_unit](sign * mail.interval_nbr) if date else False + scheduler.scheduled_date = date + _INTERVALS[scheduler.interval_unit](sign * scheduler.interval_nbr) if date else False def execute(self): for mail in self: From 69df7d3d8aed916ad62a67dbbb399d8a62c98cbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Debauche=20St=C3=A9phane?= Date: Fri, 10 Jan 2020 15:01:19 +0000 Subject: [PATCH 5/6] [IMP] event: improve event mail state display Purpose ======= Show an icon representing the state of the event mail and number of contacted registrations instead of the simple checkbox "sent". Specifications ============== Instead of the checkbox "sent", an event mail can have 3 values 1. Sent, if all the mails has been sent; 2. Scheduled, if scheduled but no attendee have been contacted; 3. Running, if not all attendees have been contacted; Note that communication targeting "after registration" are always considered as being "running" as any new registration triggers a new communication. A new widget is added that displays an icon depending on the state. A new field holding contact count is added. When scheduler send emails it is updated to keep a count of sent communications. LINKS Task ID-2414658 COM PR odoo/odoo#63093 UPG PR odoo/upgrade#2014 --- addons/event/__manifest__.py | 5 +++- addons/event/models/event_mail.py | 21 ++++++++++++++ .../static/src/js/field_icon_selection.js | 29 +++++++++++++++++++ .../static/src/xml/field_icon_selection.xml | 6 ++++ addons/event/views/event_templates.xml | 1 + addons/event/views/event_views.xml | 12 +++++--- addons/event_sms/models/event_mail.py | 3 ++ 7 files changed, 72 insertions(+), 5 deletions(-) create mode 100644 addons/event/static/src/js/field_icon_selection.js create mode 100644 addons/event/static/src/xml/field_icon_selection.xml diff --git a/addons/event/__manifest__.py b/addons/event/__manifest__.py index 3dc6be1a116..bd221a9962d 100644 --- a/addons/event/__manifest__.py +++ b/addons/event/__manifest__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- { 'name': 'Events Organization', - 'version': '1.4', + 'version': '1.5', 'website': 'https://www.odoo.com/page/events', 'category': 'Marketing/Events', 'summary': 'Trainings, Conferences, Meetings, Exhibitions, Registrations', @@ -44,4 +44,7 @@ Key Features ], 'installable': True, 'auto_install': False, + 'qweb': [ + 'static/src/xml/field_icon_selection.xml', + ], } diff --git a/addons/event/models/event_mail.py b/addons/event/models/event_mail.py index 11ff8fbff64..57ac14ff55b 100644 --- a/addons/event/models/event_mail.py +++ b/addons/event/models/event_mail.py @@ -86,6 +86,10 @@ class EventMailScheduler(models.Model): mail_registration_ids = fields.One2many('event.mail.registration', 'scheduler_id') mail_sent = fields.Boolean('Mail Sent on Event', copy=False) done = fields.Boolean('Sent', compute='_compute_done', store=True, copy=False) + mail_state = fields.Selection( + [('running', 'Running'), ('scheduled', 'Scheduled'), ('sent', 'Sent')], + string='Global communication Status', compute='_compute_mail_state') + mail_count_done = fields.Integer('# Sent', copy=False, readonly=True) @api.depends('mail_sent', 'interval_type', 'event_id.registration_ids', 'mail_registration_ids.mail_sent') def _compute_done(self): @@ -107,6 +111,20 @@ class EventMailScheduler(models.Model): scheduler.scheduled_date = date + _INTERVALS[scheduler.interval_unit](sign * scheduler.interval_nbr) if date else False + @api.depends('interval_type', 'scheduled_date', 'event_mail_sent') + def _compute_mail_state(self): + for scheduler in self: + # registrations based + if scheduler.interval_type == 'after_sub': + scheduler.mail_state = 'running' + # global event based + elif scheduler.event_mail_sent: + scheduler.mail_state = 'sent' + elif scheduler.scheduled_date: + scheduler.mail_state = 'scheduled' + else: + scheduler.mail_state = 'running' + def execute(self): for mail in self: now = fields.Datetime.now() @@ -120,12 +138,15 @@ class EventMailScheduler(models.Model): mail.write({'mail_registration_ids': lines}) # execute scheduler on registrations mail.mail_registration_ids.execute() + mail.mail_count_done = len(mail.mail_registration_ids.filtered(lambda reg: reg.mail_sent)) 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.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}) + mail.mail_count_done = len(mail.event_id.registration_ids.filtered(lambda reg: reg.state != 'cancel')) + return True @api.model diff --git a/addons/event/static/src/js/field_icon_selection.js b/addons/event/static/src/js/field_icon_selection.js new file mode 100644 index 00000000000..95740ea62d9 --- /dev/null +++ b/addons/event/static/src/js/field_icon_selection.js @@ -0,0 +1,29 @@ +odoo.define('event.field_icon_selection', function (require) { +"use strict"; + +var core = require('web.core'); +var registry = require('web.field_registry'); +var AbstractField = require('web.AbstractField'); +var QWeb = core.qweb; + +var IconSelection = AbstractField.extend({ + supportedFieldTypes: ['char', 'text', 'selection'], + + /** + * @override + * @private + */ + _render: function () { + this._super.apply(this, arguments); + this.icon = this.nodeOptions[this.value]; + this.title = this.value.charAt(0).toUpperCase() + this.value.slice(1); + this.$el.empty().append(QWeb.render('event.IconSelection', {'widget': this})); + }, + +}); + +registry.add('icon_selection', IconSelection); + +return IconSelection; + +}); diff --git a/addons/event/static/src/xml/field_icon_selection.xml b/addons/event/static/src/xml/field_icon_selection.xml new file mode 100644 index 00000000000..399be360311 --- /dev/null +++ b/addons/event/static/src/xml/field_icon_selection.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/addons/event/views/event_templates.xml b/addons/event/views/event_templates.xml index c8e320e5515..54cbdb0b684 100644 --- a/addons/event/views/event_templates.xml +++ b/addons/event/views/event_templates.xml @@ -4,6 +4,7 @@ diff --git a/addons/event/views/event_views.xml b/addons/event/views/event_views.xml index da2acc446e6..eaa9c2bba50 100644 --- a/addons/event/views/event_views.xml +++ b/addons/event/views/event_views.xml @@ -266,7 +266,10 @@ - + + + @@ -750,7 +753,7 @@ - + diff --git a/addons/event_sms/models/event_mail.py b/addons/event_sms/models/event_mail.py index 3e56a0552a7..3bdf5230c31 100644 --- a/addons/event_sms/models/event_mail.py +++ b/addons/event_sms/models/event_mail.py @@ -45,6 +45,9 @@ class EventMailScheduler(models.Model): mass_keep_log=True ) mail.write({'mail_sent': True}) + + mail.mail_count_done = len(mail.event_id.registration_ids.filtered(lambda reg: reg.state != 'cancel')) + return super(EventMailScheduler, self).execute() From 2520bfe625dde6869ad80b3e81d00827f533c9da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 9 Feb 2021 10:13:47 +0000 Subject: [PATCH 6/6] [REF] event: manually update completion of mail schedulers Purpose of this commit is to remove the "done" field computation. Indeed it is based on either * mail_sent field if scheduler is global to the event (before or after event). This computation is light as this field changes only once when emails are scheduled and sent; * status of event registrations compared to all sent communication on those registrations. This is costly as adding a new registration changes In this commit we therefore * rename ``done`` to ``mail_done`` to ease grep and understanding; * remove ``mail_sent`` as it is integrated within ``done``; * manually update ``mail_done`` when updating schedulers instead of doing it through a compute method; Some code cleaning is performed to make it clearer and easier to understand. LINKS Task ID-2414658 COM PR odoo/odoo#63093 UPG PR odoo/upgrade#2014 --- addons/event/data/ir_cron_data.xml | 2 +- addons/event/models/event_event.py | 2 +- addons/event/models/event_mail.py | 91 ++++++++++++------- addons/event/models/event_registration.py | 33 ++++++- .../event/tests/test_event_mail_schedule.py | 84 ++++++++++------- addons/event/views/event_views.xml | 2 +- addons/event_sms/models/event_mail.py | 25 +++-- addons/event_sms/tests/test_sms_schedule.py | 25 +++-- 8 files changed, 171 insertions(+), 93 deletions(-) diff --git a/addons/event/data/ir_cron_data.xml b/addons/event/data/ir_cron_data.xml index d0ffda9d0ae..3c6493cc536 100644 --- a/addons/event/data/ir_cron_data.xml +++ b/addons/event/data/ir_cron_data.xml @@ -5,7 +5,7 @@ Event: Mail Scheduler code - model.run(True) + model.schedule_communications(autocommit=True) 1 hours diff --git a/addons/event/models/event_event.py b/addons/event/models/event_event.py index ca113423f2e..d51a720d3ef 100644 --- a/addons/event/models/event_event.py +++ b/addons/event/models/event_event.py @@ -398,7 +398,7 @@ class EventEvent(models.Model): # lines to keep: those with already sent emails or registrations mails_to_remove = event.event_mail_ids.filtered( - lambda mail: not(mail._origin.mail_sent or mail._origin.mail_registration_ids) + lambda mail: not(mail._origin.mail_done) and not(mail._origin.mail_registration_ids) ) command = [Command.unlink(mail.id) for mail in mails_to_remove] if event.event_type_id.use_mail_schedule: diff --git a/addons/event/models/event_mail.py b/addons/event/models/event_mail.py index 57ac14ff55b..70207bd8ac9 100644 --- a/addons/event/models/event_mail.py +++ b/addons/event/models/event_mail.py @@ -82,23 +82,17 @@ class EventMailScheduler(models.Model): 'mail.template', string='Email Template', domain=[('model', '=', 'event.registration')], ondelete='restrict', help='This field contains the template of the mail that will be automatically sent') - scheduled_date = fields.Datetime('Scheduled Sent Mail', compute='_compute_scheduled_date', store=True) - mail_registration_ids = fields.One2many('event.mail.registration', 'scheduler_id') - mail_sent = fields.Boolean('Mail Sent on Event', copy=False) - done = fields.Boolean('Sent', compute='_compute_done', store=True, copy=False) + scheduled_date = fields.Datetime('Schedule Date', compute='_compute_scheduled_date', store=True) + # contact and status + mail_registration_ids = fields.One2many( + 'event.mail.registration', 'scheduler_id', + help='Communication related to event registrations') + mail_done = fields.Boolean("Sent", copy=False, readonly=True) mail_state = fields.Selection( [('running', 'Running'), ('scheduled', 'Scheduled'), ('sent', 'Sent')], string='Global communication Status', compute='_compute_mail_state') mail_count_done = fields.Integer('# Sent', copy=False, readonly=True) - @api.depends('mail_sent', 'interval_type', 'event_id.registration_ids', 'mail_registration_ids.mail_sent') - def _compute_done(self): - for mail in self: - if mail.interval_type in ['before_event', 'after_event']: - mail.done = mail.mail_sent - else: - mail.done = len(mail.mail_registration_ids) == len(mail.event_id.registration_ids) and all(mail.mail_sent for mail in mail.mail_registration_ids) - @api.depends('event_id.date_begin', 'event_id.date_end', 'interval_type', 'interval_unit', 'interval_nbr') def _compute_scheduled_date(self): for scheduler in self: @@ -111,14 +105,14 @@ class EventMailScheduler(models.Model): scheduler.scheduled_date = date + _INTERVALS[scheduler.interval_unit](sign * scheduler.interval_nbr) if date else False - @api.depends('interval_type', 'scheduled_date', 'event_mail_sent') + @api.depends('interval_type', 'scheduled_date', 'mail_done') def _compute_mail_state(self): for scheduler in self: # registrations based if scheduler.interval_type == 'after_sub': scheduler.mail_state = 'running' # global event based - elif scheduler.event_mail_sent: + elif scheduler.mail_done: scheduler.mail_state = 'sent' elif scheduler.scheduled_date: scheduler.mail_state = 'scheduled' @@ -126,29 +120,48 @@ class EventMailScheduler(models.Model): scheduler.mail_state = 'running' def execute(self): - for mail in self: + for scheduler in self: now = fields.Datetime.now() - if mail.interval_type == 'after_sub': - # update registration lines - lines = [ - (0, 0, {'registration_id': registration.id}) - for registration in (mail.event_id.registration_ids - mail.mapped('mail_registration_ids.registration_id')) - ] - if lines: - mail.write({'mail_registration_ids': lines}) - # execute scheduler on registrations - mail.mail_registration_ids.execute() - mail.mail_count_done = len(mail.mail_registration_ids.filtered(lambda reg: reg.mail_sent)) - 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.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}) - mail.mail_count_done = len(mail.event_id.registration_ids.filtered(lambda reg: reg.state != 'cancel')) + if scheduler.interval_type == 'after_sub': + new_registrations = scheduler.event_id.registration_ids.filtered_domain( + [('state', 'not in', ('cancel', 'draft'))] + ) - scheduler.mail_registration_ids.registration_id + scheduler._create_missing_mail_registrations(new_registrations) + # execute scheduler on registrations + scheduler.mail_registration_ids.execute() + total_sent = len(scheduler.mail_registration_ids.filtered(lambda reg: reg.mail_sent)) + scheduler.update({ + 'mail_done': total_sent >= (scheduler.event_id.seats_reserved + scheduler.event_id.seats_used), + 'mail_count_done': total_sent, + }) + else: + # before or after event -> one shot email + if scheduler.mail_done or scheduler.notification_type != 'mail': + continue + # no template -> ill configured, skip and avoid crash + if not scheduler.template_id: + continue + # do not send emails if the mailing was scheduled before the event but the event is over + if scheduler.scheduled_date <= now and (scheduler.interval_type != 'before_event' or scheduler.event_id.date_end > now): + scheduler.event_id.mail_attendees(scheduler.template_id.id) + scheduler.update({ + 'mail_done': True, + 'mail_count_done': scheduler.event_id.seats_reserved + scheduler.event_id.seats_used, + }) return True + def _create_missing_mail_registrations(self, registrations): + new = [] + for scheduler in self: + new += [{ + 'registration_id': registration.id, + 'scheduler_id': scheduler.id, + } for registration in registrations] + if new: + return self.env['event.mail.registration'].create(new) + return self.env['event.mail.registration'] + @api.model def _warn_template_error(self, scheduler, exception): # We warn ~ once by hour ~ instead of every 10 min if the interval unit is more than 'hours'. @@ -189,7 +202,17 @@ You receive this email because you are: @api.model def run(self, autocommit=False): - schedulers = self.search([('done', '=', False), ('scheduled_date', '<=', datetime.strftime(fields.datetime.now(), tools.DEFAULT_SERVER_DATETIME_FORMAT))]) + """ Backward compatible method, notably if crons are not updated when + migrating for some reason. """ + return self.schedule_communications(autocommit=autocommit) + + @api.model + def schedule_communications(self, autocommit=False): + schedulers = self.search([ + ('mail_done', '=', False), + ('scheduled_date', '<=', fields.Datetime.now()) + ]) + for scheduler in schedulers: try: with self.env.cr.savepoint(): diff --git a/addons/event/models/event_registration.py b/addons/event/models/event_registration.py index 94988477c2a..6e14e7f7c66 100644 --- a/addons/event/models/event_registration.py +++ b/addons/event/models/event_registration.py @@ -149,18 +149,25 @@ class EventRegistration(models.Model): @api.model_create_multi def create(self, vals_list): registrations = super(EventRegistration, self).create(vals_list) + + # auto_confirm if possible; if not automatically confirmed, call mail schedulers in case + # some were created already open if registrations._check_auto_confirmation(): registrations.sudo().action_confirm() + else: + registrations._update_mail_schedulers() return registrations def write(self, vals): + pre_draft = self.env['event.registration'] + if vals.get('state') == 'open': + pre_draft = self.filtered(lambda registration: registration.state == 'draft') + ret = super(EventRegistration, self).write(vals) 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.sudo().execute() + pre_draft._update_mail_schedulers() return ret @@ -234,6 +241,26 @@ class EventRegistration(models.Model): 'context': ctx, } + def _update_mail_schedulers(self): + """ Update schedulers to set them as running again, and cron to be called + as soon as possible. """ + open_registrations = self.filtered(lambda registration: registration.state == 'open') + if not open_registrations: + return + + onsubscribe_schedulers = self.env['event.mail'].sudo().search([ + ('event_id', 'in', open_registrations.event_id.ids), + ('interval_type', '=', 'after_sub') + ]) + if not onsubscribe_schedulers: + return + + onsubscribe_schedulers.update({'mail_done': False}) + # we could simply call _create_missing_mail_registrations and let cron do their job + # but it currently leads to several delays. We therefore call execute until + # cron triggers are correctly used + onsubscribe_schedulers.execute() + # ------------------------------------------------------------ # MAILING / GATEWAY # ------------------------------------------------------------ diff --git a/addons/event/tests/test_event_mail_schedule.py b/addons/event/tests/test_event_mail_schedule.py index 4f913309fb9..7b9bb735c42 100644 --- a/addons/event/tests/test_event_mail_schedule.py +++ b/addons/event/tests/test_event_mail_schedule.py @@ -27,8 +27,11 @@ class TestMailSchedule(TestEventCommon, MockEmail): 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({ + # create with admin to force create_date + test_event = self.env['event.event'].create({ 'name': 'TestEventMail', + 'create_date': now, + 'user_id': self.user_eventmanager.id, 'auto_confirm': True, 'date_begin': event_date_begin, 'date_end': event_date_end, @@ -54,26 +57,35 @@ class TestMailSchedule(TestEventCommon, MockEmail): 'template_id': self.env['ir.model.data'].xmlid_to_res_id('event.event_reminder')}), ] }) + self.assertEqual(test_event.create_date, now) # check subscription scheduler 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) + self.assertFalse(after_sub_scheduler.mail_done) + self.assertEqual(after_sub_scheduler.mail_state, 'running') + self.assertEqual(after_sub_scheduler.mail_count_done, 0) 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) + self.assertFalse(after_sub_scheduler_2.mail_done) + self.assertEqual(after_sub_scheduler_2.mail_state, 'running') + self.assertEqual(after_sub_scheduler_2.mail_count_done, 0) # 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) + self.assertFalse(event_prev_scheduler.mail_done) + self.assertEqual(event_prev_scheduler.mail_state, 'scheduled') + self.assertEqual(event_prev_scheduler.mail_count_done, 0) # 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) + self.assertFalse(event_next_scheduler.mail_done) + self.assertEqual(event_next_scheduler.mail_state, 'scheduled') + self.assertEqual(event_next_scheduler.mail_count_done, 0) # create some registrations with freeze_time(now), self.mock_mail_gateway(): @@ -100,7 +112,9 @@ class TestMailSchedule(TestEventCommon, MockEmail): 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') + self.assertTrue(after_sub_scheduler.mail_done, 'event: all subscription mails should have been sent') + self.assertEqual(after_sub_scheduler.mail_state, 'running') + self.assertEqual(after_sub_scheduler.mail_count_done, 2) # check emails effectively sent self.assertEqual(len(self._new_mails), 2, 'event: should have 2 scheduled emails (1 / registration)') @@ -116,13 +130,15 @@ class TestMailSchedule(TestEventCommon, MockEmail): 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') + self.assertFalse(after_sub_scheduler_2.mail_done, 'event: all subscription mails should be scheduled, not sent') + self.assertEqual(after_sub_scheduler_2.mail_count_done, 0) # 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.assertFalse(after_sub_scheduler_2.mail_done) + self.assertEqual(after_sub_scheduler_2.mail_count_done, 0) 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 @@ -133,7 +149,9 @@ class TestMailSchedule(TestEventCommon, MockEmail): # 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)) - self.assertTrue(after_sub_scheduler_2.done, 'event: all subscription mails should have been sent') + self.assertTrue(after_sub_scheduler_2.mail_done, 'event: all subscription mails should have been sent') + self.assertEqual(after_sub_scheduler_2.mail_state, 'running') + self.assertEqual(after_sub_scheduler_2.mail_count_done, 2) # check emails effectively sent self.assertEqual(len(self._new_mails), 2, 'event: should have 2 scheduled emails (1 / registration)') @@ -147,26 +165,27 @@ class TestMailSchedule(TestEventCommon, MockEmail): # PRE SCHEDULERS (MOVE FORWARD IN TIME) # -------------------------------------------------- - self.assertFalse(event_prev_scheduler.mail_sent) - self.assertFalse(event_prev_scheduler.done) + self.assertFalse(event_prev_scheduler.mail_done) + self.assertEqual(event_prev_scheduler.mail_state, 'scheduled') - # execute event reminder scheduler explicitly, before scheduled date -> should not do anything + # simulate cron running 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() + event_cron_id.method_direct_trigger() - self.assertFalse(event_prev_scheduler.mail_sent) - self.assertFalse(event_prev_scheduler.done) + self.assertFalse(event_prev_scheduler.mail_done) + self.assertEqual(event_prev_scheduler.mail_state, 'scheduled') + self.assertEqual(event_prev_scheduler.mail_count_done, 0) self.assertEqual(len(self._new_mails), 0) - # execute cron to run schedulers + # execute cron to run schedulers after scheduled date 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') + self.assertTrue(event_prev_scheduler.mail_done, 'event: reminder scheduler should have run') + self.assertEqual(event_prev_scheduler.mail_state, 'sent', '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)') @@ -192,12 +211,10 @@ class TestMailSchedule(TestEventCommon, MockEmail): 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') + self.assertTrue(event_prev_scheduler.mail_done) + self.assertFalse(event_next_scheduler.mail_done) + self.assertTrue(after_sub_scheduler.mail_done, 'event: scheduler on registration not updated next to draft registration') + self.assertTrue(after_sub_scheduler_2.mail_done, 'event: scheduler on registration not updated next to draft registration') # confirm registration -> should trigger registration schedulers # NOTE: currently all schedulers are based on date_open which equals create_date @@ -205,19 +222,20 @@ class TestMailSchedule(TestEventCommon, MockEmail): 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 + # 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.assertTrue(after_sub_scheduler.mail_done, 'event: all subscription mails should have been sent') + self.assertEqual(after_sub_scheduler.mail_count_done, 3) + # 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') + self.assertTrue(after_sub_scheduler_2.mail_done, 'event: all subscription mails should have been sent') + self.assertEqual(after_sub_scheduler_2.mail_count_done, 3) # check emails effectively sent self.assertEqual(len(self._new_mails), 2, 'event: should have 1 scheduled emails (new registration only)') @@ -231,8 +249,7 @@ class TestMailSchedule(TestEventCommon, MockEmail): # POST SCHEDULERS (MOVE FORWARD IN TIME) # -------------------------------------------------- - self.assertFalse(event_next_scheduler.mail_sent) - self.assertFalse(event_next_scheduler.done) + self.assertFalse(event_next_scheduler.mail_done) # execute event reminder scheduler explicitly after its schedule date new_end = event_date_end + relativedelta(hours=2) @@ -240,8 +257,9 @@ class TestMailSchedule(TestEventCommon, MockEmail): event_cron_id.method_direct_trigger() # 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') + self.assertTrue(event_next_scheduler.mail_done, 'event: reminder scheduler should should have run') + self.assertEqual(event_next_scheduler.mail_state, 'sent', 'event: reminder scheduler should have run') + self.assertEqual(event_next_scheduler.mail_count_done, 3) # check emails effectively sent self.assertEqual(len(self._new_mails), 3, 'event: should have scheduled 3 mails, one for each registration') diff --git a/addons/event/views/event_views.xml b/addons/event/views/event_views.xml index eaa9c2bba50..cf0df9c32bc 100644 --- a/addons/event/views/event_views.xml +++ b/addons/event/views/event_views.xml @@ -266,7 +266,7 @@ - + diff --git a/addons/event_sms/models/event_mail.py b/addons/event_sms/models/event_mail.py index 3bdf5230c31..f3ff5ca6ccf 100644 --- a/addons/event_sms/models/event_mail.py +++ b/addons/event_sms/models/event_mail.py @@ -32,21 +32,26 @@ class EventMailScheduler(models.Model): help='This field contains the template of the SMS that will be automatically sent') def execute(self): - for mail in self: + for scheduler in self: now = fields.Datetime.now() - if mail.interval_type != 'after_sub': + if scheduler.interval_type != 'after_sub' and scheduler.notification_type == 'sms': + # before or after event -> one shot email + if scheduler.mail_done: + continue + # no template -> ill configured, skip and avoid crash + if not scheduler.sms_template_id: + continue # Do not send SMS if the communication was scheduled before the event but the event is over - 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: + if scheduler.scheduled_date <= now and (scheduler.interval_type != 'before_event' or scheduler.event_id.date_end > now): self.env['event.registration']._message_sms_schedule_mass( - template=mail.sms_template_id, - active_domain=[('event_id', '=', mail.event_id.id), ('state', '!=', 'cancel')], + template=scheduler.sms_template_id, + active_domain=[('event_id', '=', scheduler.event_id.id), ('state', '!=', 'cancel')], mass_keep_log=True ) - mail.write({'mail_sent': True}) - - mail.mail_count_done = len(mail.event_id.registration_ids.filtered(lambda reg: reg.state != 'cancel')) + scheduler.update({ + 'mail_done': True, + 'mail_count_done': scheduler.event_id.seats_reserved + scheduler.event_id.seats_used, + }) return super(EventMailScheduler, self).execute() diff --git a/addons/event_sms/tests/test_sms_schedule.py b/addons/event_sms/tests/test_sms_schedule.py index 3b592d61b5a..87251c47812 100644 --- a/addons/event_sms/tests/test_sms_schedule.py +++ b/addons/event_sms/tests/test_sms_schedule.py @@ -47,33 +47,38 @@ class TestSMSSchedule(TestEventCommon, SMSCase): self._create_registrations(self.event_0, 3) # check subscription scheduler - schedulers = self.env['event.mail'].search([('event_id', '=', self.event_0.id), ('interval_type', '=', 'after_sub')]) - self.assertEqual(len(schedulers), 1) - self.assertEqual(schedulers.scheduled_date, self.event_0.create_date, 'event: incorrect scheduled date for checking controller') + sub_scheduler = self.env['event.mail'].search([('event_id', '=', self.event_0.id), ('interval_type', '=', 'after_sub')]) + self.assertEqual(len(sub_scheduler), 1) + self.assertEqual(sub_scheduler.scheduled_date, self.event_0.create_date, 'event: incorrect scheduled date for checking controller') # verify that subscription scheduler was auto-executed after each registration - self.assertEqual(len(schedulers.mail_registration_ids), 3) - self.assertTrue(all(m.mail_sent is True for m in schedulers.mail_registration_ids)) - self.assertEqual(schedulers.mapped('mail_registration_ids.registration_id'), self.event_0.registration_ids) + self.assertEqual(len(sub_scheduler.mail_registration_ids), 3) + self.assertTrue(all(m.mail_sent is True for m in sub_scheduler.mail_registration_ids)) + self.assertEqual(sub_scheduler.mapped('mail_registration_ids.registration_id'), self.event_0.registration_ids) + sanitized_numbers = [] for registration in self.event_0.registration_ids: reg_sanitized_number = phone_validation.phone_format(registration.phone, 'BE', '32', force_format='E164') sanitized_numbers.append(reg_sanitized_number) self.assertSMSOutgoing(self.env['res.partner'], reg_sanitized_number, '%s registration confirmation.' % self.event_0.organizer_id.name) + self.assertTrue(sub_scheduler.mail_done) + self.assertEqual(sub_scheduler.mail_count_done, 3) # clear notification queue to avoid conflicts when checking next notifications self.env['mail.notification'].search([('sms_number', 'in', sanitized_numbers)]).unlink() self.env['sms.sms'].search([('number', 'in', sanitized_numbers)]).unlink() # check before event scheduler - schedulers = self.env['event.mail'].search([('event_id', '=', self.event_0.id), ('interval_type', '=', 'before_event')]) - self.assertEqual(len(schedulers), 1, 'event: wrong scheduler creation') - self.assertEqual(schedulers[0].scheduled_date, self.event_0.date_begin + relativedelta(days=-3)) + before_scheduler = self.env['event.mail'].search([('event_id', '=', self.event_0.id), ('interval_type', '=', 'before_event')]) + self.assertEqual(len(before_scheduler), 1, 'event: wrong scheduler creation') + self.assertEqual(before_scheduler.scheduled_date, self.event_0.date_begin + relativedelta(days=-3)) # execute event reminder scheduler explicitly - schedulers.execute() + before_scheduler.execute() # verify that subscription scheduler was auto-executed after each registration for registration in self.event_0.registration_ids: reg_sanitized_number = phone_validation.phone_format(registration.phone, 'BE', '32', force_format='E164') self.assertSMSOutgoing(self.env['res.partner'], reg_sanitized_number, '%s reminder' % self.event_0.organizer_id.name) + self.assertTrue(before_scheduler.mail_done) + self.assertEqual(before_scheduler.mail_count_done, 3)