[MERGE] event: improve state, display and performance of event mail schedulers
PURPOSE
Improve event mail scheduled communications: add a state and a contacted
count. Improve performances and code readability.
SPECIFICATIONS: STATE
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.
SPECIFICATIONS: MAIL COMPUTATION IMPROVEMENT
Purpose of this merge 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 merge 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
Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
@@ -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',
|
||||
@@ -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',
|
||||
@@ -43,4 +44,7 @@ Key Features
|
||||
],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
'qweb': [
|
||||
'static/src/xml/field_icon_selection.xml',
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1,19 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<!-- Event Mail Scheduler-->
|
||||
<record model="ir.cron" forcecreate="True" id="event_mail_scheduler">
|
||||
<field name="name">Event: Mail Scheduler</field>
|
||||
<field name="model_id" ref="model_event_mail"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model.run(True)</field>
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">hours</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="doall" eval="False" />
|
||||
</record>
|
||||
|
||||
<!-- Event Categories -->
|
||||
<record id="event_type_data_ticket" model="event.type">
|
||||
<field name="name">Ticketing</field>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo><data noupdate="1">
|
||||
<!-- Event Mail Scheduler-->
|
||||
<record model="ir.cron" forcecreate="True" id="event_mail_scheduler">
|
||||
<field name="name">Event: Mail Scheduler</field>
|
||||
<field name="model_id" ref="model_event_mail"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model.schedule_communications(autocommit=True)</field>
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">hours</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="doall" eval="False" />
|
||||
</record>
|
||||
</data></odoo>
|
||||
@@ -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:
|
||||
|
||||
@@ -82,52 +82,86 @@ 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)
|
||||
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')
|
||||
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', '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
|
||||
|
||||
@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.mail_done:
|
||||
scheduler.mail_state = 'sent'
|
||||
elif scheduler.scheduled_date:
|
||||
scheduler.mail_state = 'scheduled'
|
||||
else:
|
||||
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})
|
||||
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
|
||||
mail.mail_registration_ids.execute()
|
||||
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:
|
||||
# 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})
|
||||
# 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'.
|
||||
@@ -168,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():
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -208,6 +215,56 @@ 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,
|
||||
}
|
||||
|
||||
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
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def _message_get_suggested_recipients(self):
|
||||
recipients = super(EventRegistration, self)._message_get_suggested_recipients()
|
||||
public_users = self.env['res.users'].sudo()
|
||||
@@ -248,31 +305,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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="event.IconSelection">
|
||||
<i t-att-class="widget.icon" t-att-title="widget.title"/>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -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,8 +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))
|
||||
# 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.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)')
|
||||
@@ -148,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)')
|
||||
@@ -193,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
|
||||
@@ -206,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)')
|
||||
@@ -232,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)
|
||||
@@ -241,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')
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<template id="assets_backend" name="event assets" inherit_id="web.assets_backend">
|
||||
<xpath expr="." position="inside">
|
||||
<link rel="stylesheet" type="text/scss" href="/event/static/src/scss/event.scss"/>
|
||||
<script type="text/javascript" src="/event/static/src/js/field_icon_selection.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -266,7 +266,10 @@
|
||||
<field name="interval_nbr" attrs="{'readonly':[('interval_unit','=','now')]}"/>
|
||||
<field name="interval_unit"/>
|
||||
<field name="interval_type"/>
|
||||
<field name="done"/>
|
||||
<field name="scheduled_date" groups="base.group_no_one"/>
|
||||
<field name="mail_count_done"/>
|
||||
<field name="mail_state" widget="icon_selection" string=" "
|
||||
options="{'sent': 'fa fa-check', 'scheduled': 'fa fa-hourglass-half', 'running': 'fa fa-cogs'}"/>
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
@@ -750,7 +753,7 @@
|
||||
<field name="event_id"/>
|
||||
<field name="notification_type" invisible="1"/>
|
||||
<field name="template_id" attrs="{'required': [('notification_type', '=', 'mail')]}"/>
|
||||
<field name="mail_sent"/>
|
||||
<field name="mail_state"/>
|
||||
</group>
|
||||
<group>
|
||||
<label for="interval_nbr"/>
|
||||
@@ -787,8 +790,9 @@
|
||||
<field name="notification_type" invisible="1"/>
|
||||
<field name="template_id" attrs="{'required': [('notification_type', '=', 'mail')]}"/>
|
||||
<field name="scheduled_date"/>
|
||||
<field name="mail_sent" string="Sent"/>
|
||||
<field name="done"/>
|
||||
<field name="mail_count_done"/>
|
||||
<field name="mail_state" widget="icon_selection" string=" "
|
||||
options="{'sent': 'fa fa-check', 'scheduled': 'fa fa-hourglass-half', 'running': 'fa fa-cogs'}"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -32,19 +32,27 @@ 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})
|
||||
scheduler.update({
|
||||
'mail_done': True,
|
||||
'mail_count_done': scheduler.event_id.seats_reserved + scheduler.event_id.seats_used,
|
||||
})
|
||||
|
||||
return super(EventMailScheduler, self).execute()
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="ir_cron_trigger_view_form" model="ir.ui.view">
|
||||
<field name="name">ir.cron.trigger.view.form</field>
|
||||
<field name="model">ir.cron.trigger</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Cron Trigger">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="cron_id"/>
|
||||
<field name="call_at"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="ir_cron_trigger_view_tree" model="ir.ui.view">
|
||||
<field name="name">ir.cron.trigger.view.tree</field>
|
||||
<field name="model">ir.cron.trigger</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Cron Triggers">
|
||||
<field name="cron_id"/>
|
||||
<field name="call_at"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="ir_cron_trigger_view_search" model="ir.ui.view">
|
||||
<field name="name">ir.cron.trigger.view.search</field>
|
||||
<field name="model">ir.cron.trigger</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Cron Triggers">
|
||||
<field name="cron_id"/>
|
||||
<field name="call_at"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="ir_cron_trigger_action" model="ir.actions.act_window">
|
||||
<field name="name">Scheduled Actions Triggers</field>
|
||||
<field name="res_model">ir.cron.trigger</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="ir_cron_trigger_menu"
|
||||
action="ir_cron_trigger_action"
|
||||
parent="base.menu_automation"
|
||||
sequence="3"/>
|
||||
|
||||
</odoo>
|
||||
@@ -91,6 +91,9 @@
|
||||
<field name="view_id" ref="ir_cron_view_tree"/>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_ir_cron_act" action="ir_cron_act" parent="base.menu_automation"/>
|
||||
<menuitem id="menu_ir_cron_act"
|
||||
parent="base.menu_automation"
|
||||
action="ir_cron_act"
|
||||
sequence="2"/>
|
||||
|
||||
</odoo>
|
||||
|
||||
Reference in New Issue
Block a user