[ADD] mail, mass_mailing: allow a user to resend a mail with failures
When a mail is in failure state, a red envelope appears next to the message in a thread but it was difficult to send the mail again. This tasks will allow users to send mail again easily, or mark notification as cancelled if the user want to ignore this failure. A notification will appear in sender systray while mail are in failure. Task: #46158 PR: #24628
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
'data': [
|
||||
'wizard/invite_view.xml',
|
||||
'wizard/mail_compose_message_view.xml',
|
||||
'wizard/mail_resend_message_views.xml',
|
||||
'views/mail_message_subtype_views.xml',
|
||||
'views/mail_tracking_views.xml',
|
||||
'views/mail_message_views.xml',
|
||||
|
||||
@@ -224,6 +224,7 @@ class MailController(http.Controller):
|
||||
'needaction_inbox_counter': request.env['res.partner'].get_needaction_count(),
|
||||
'starred_counter': request.env['res.partner'].get_starred_count(),
|
||||
'channel_slots': request.env['mail.channel'].channel_fetch_slot(),
|
||||
'mail_failures': request.env['mail.message'].message_fetch_failed(),
|
||||
'commands': request.env['mail.channel'].get_mention_commands(),
|
||||
'mention_partner_suggestions': request.env['res.partner'].get_static_mention_suggestions(),
|
||||
'shortcodes': request.env['mail.shortcode'].sudo().search_read([], ['source', 'substitution', 'description']),
|
||||
|
||||
@@ -123,7 +123,7 @@ class MailMail(models.Model):
|
||||
return res
|
||||
|
||||
@api.multi
|
||||
def _postprocess_sent_message(self, mail_sent=True):
|
||||
def _postprocess_sent_message(self, success_pids, failure_reason=False, failure_type='NONE'):
|
||||
"""Perform any post-processing necessary after sending ``mail``
|
||||
successfully, including deleting it completely along with its
|
||||
attachment if the ``auto_delete`` flag of the mail was set.
|
||||
@@ -131,20 +131,29 @@ class MailMail(models.Model):
|
||||
|
||||
:return: True
|
||||
"""
|
||||
mail_message_notif_ids = [mail.mail_message_id.id for mail in self if mail.notification]
|
||||
if mail_message_notif_ids:
|
||||
notif_mails_ids = [mail.id for mail in self if mail.notification]
|
||||
if notif_mails_ids:
|
||||
notifications = self.env['mail.notification'].search([
|
||||
('mail_message_id', 'in', mail_message_notif_ids),
|
||||
('is_email', '=', True)])
|
||||
if notifications and mail_sent:
|
||||
notifications.write({
|
||||
('is_email', '=', True),
|
||||
('mail_id', 'in', notif_mails_ids),
|
||||
('email_status', 'not in', ('sent', 'canceled'))
|
||||
])
|
||||
if notifications:
|
||||
#find all notification linked to a failure
|
||||
failed = self.env['mail.notification']
|
||||
if failure_type != 'NONE':
|
||||
failed = notifications.filtered(lambda notif: notif.res_partner_id not in success_pids)
|
||||
failed.write({
|
||||
'email_status': 'exception',
|
||||
'failure_type': failure_type,
|
||||
'failure_reason': failure_reason,
|
||||
})
|
||||
messages = notifications.mapped('mail_message_id').filtered(lambda m: m.res_id and m.model)
|
||||
messages._notify_failure_update() # notify user that we have a failure
|
||||
(notifications - failed).write({
|
||||
'email_status': 'sent',
|
||||
})
|
||||
elif notifications:
|
||||
notifications.write({
|
||||
'email_status': 'exception',
|
||||
})
|
||||
if mail_sent:
|
||||
if failure_type in ('NONE', 'RECIPIENT'): # if we have another error, we want to keep the mail.
|
||||
mail_to_delete_ids = [mail.id for mail in self if mail.auto_delete]
|
||||
self.browse(mail_to_delete_ids).sudo().unlink()
|
||||
return True
|
||||
@@ -225,7 +234,9 @@ class MailMail(models.Model):
|
||||
# exceptions, it is encapsulated into an Odoo MailDeliveryException
|
||||
raise MailDeliveryException(_('Unable to connect to SMTP Server'), exc)
|
||||
else:
|
||||
self.browse(batch_ids).write({'state': 'exception', 'failure_reason': exc})
|
||||
batch = self.browse(batch_ids)
|
||||
batch.write({'state': 'exception', 'failure_reason': exc})
|
||||
batch._postprocess_sent_message(success_pids=[], failure_type="SMTP")
|
||||
else:
|
||||
self.browse(batch_ids)._send(
|
||||
auto_commit=auto_commit,
|
||||
@@ -242,13 +253,16 @@ class MailMail(models.Model):
|
||||
def _send(self, auto_commit=False, raise_exception=False, smtp_session=None):
|
||||
IrMailServer = self.env['ir.mail_server']
|
||||
for mail_id in self.ids:
|
||||
success_pids = []
|
||||
failure_type = 'NONE'
|
||||
processing_pid = None
|
||||
mail = None
|
||||
try:
|
||||
mail = self.browse(mail_id)
|
||||
if mail.state != 'outgoing':
|
||||
if mail.state != 'exception' and mail.auto_delete:
|
||||
mail.sudo().unlink()
|
||||
continue
|
||||
|
||||
# load attachment binary data with a separate read(), as prefetching all
|
||||
# `datas` (binary field) could bloat the browse cache, triggerring
|
||||
# soft/hard mem limits with temporary data.
|
||||
@@ -260,7 +274,10 @@ class MailMail(models.Model):
|
||||
if mail.email_to:
|
||||
email_list.append(mail._send_prepare_values())
|
||||
for partner in mail.recipient_ids:
|
||||
email_list.append(mail._send_prepare_values(partner=partner))
|
||||
values = mail._send_prepare_values(partner=partner)
|
||||
values['partner_id'] = partner
|
||||
email_list.append(values)
|
||||
|
||||
|
||||
# headers
|
||||
headers = {}
|
||||
@@ -285,8 +302,6 @@ class MailMail(models.Model):
|
||||
'state': 'exception',
|
||||
'failure_reason': _('Error without exception. Probably due do sending an email without computed recipients.'),
|
||||
})
|
||||
mail_sent = False
|
||||
|
||||
# build an RFC2822 email.message.Message object and send it without queuing
|
||||
res = None
|
||||
for email in email_list:
|
||||
@@ -305,11 +320,16 @@ class MailMail(models.Model):
|
||||
subtype='html',
|
||||
subtype_alternative='plain',
|
||||
headers=headers)
|
||||
processing_pid = email.pop("partner_id", None)
|
||||
try:
|
||||
res = IrMailServer.send_email(
|
||||
msg, mail_server_id=mail.mail_server_id.id, smtp_session=smtp_session)
|
||||
if processing_pid:
|
||||
success_pids.append(processing_pid)
|
||||
processing_pid = None
|
||||
except AssertionError as error:
|
||||
if str(error) == IrMailServer.NO_VALID_RECIPIENT:
|
||||
failure_type = "RECIPIENT"
|
||||
# No valid recipient found for this particular
|
||||
# mail item -> ignore error to avoid blocking
|
||||
# delivery to next recipients, if any. If this is
|
||||
@@ -318,21 +338,19 @@ class MailMail(models.Model):
|
||||
mail.message_id, email.get('email_to'))
|
||||
else:
|
||||
raise
|
||||
if res:
|
||||
if res: # mail has been sent at least once, no major exception occured
|
||||
mail.write({'state': 'sent', 'message_id': res, 'failure_reason': False})
|
||||
mail_sent = True
|
||||
|
||||
# /!\ can't use mail.state here, as mail.refresh() will cause an error
|
||||
# see revid:odo@openerp.com-20120622152536-42b2s28lvdv3odyr in 6.1
|
||||
if mail_sent:
|
||||
_logger.info('Mail with ID %r and Message-Id %r successfully sent', mail.id, mail.message_id)
|
||||
mail._postprocess_sent_message(mail_sent=mail_sent)
|
||||
# /!\ can't use mail.state here, as mail.refresh() will cause an error
|
||||
# see revid:odo@openerp.com-20120622152536-42b2s28lvdv3odyr in 6.1
|
||||
mail._postprocess_sent_message(success_pids=success_pids, failure_type=failure_type)
|
||||
except MemoryError:
|
||||
# prevent catching transient MemoryErrors, bubble up to notify user or abort cron job
|
||||
# instead of marking the mail as failed
|
||||
_logger.exception(
|
||||
'MemoryError while processing mail with ID %r and Msg-Id %r. Consider raising the --limit-memory-hard startup option',
|
||||
mail.id, mail.message_id)
|
||||
# mail status will stay on ongoing since transaction will be rollback
|
||||
raise
|
||||
except psycopg2.Error:
|
||||
# If an error with the database occurs, chances are that the cursor is unusable.
|
||||
@@ -344,7 +362,7 @@ class MailMail(models.Model):
|
||||
failure_reason = tools.ustr(e)
|
||||
_logger.exception('failed sending mail (id: %s) due to %s', mail.id, failure_reason)
|
||||
mail.write({'state': 'exception', 'failure_reason': failure_reason})
|
||||
mail._postprocess_sent_message(mail_sent=False)
|
||||
mail._postprocess_sent_message(success_pids=success_pids, failure_reason=failure_reason, failure_type='UNKNOWN')
|
||||
if raise_exception:
|
||||
if isinstance(e, AssertionError):
|
||||
# get the args of the original error, wrap into a value and throw a MailDeliveryException
|
||||
|
||||
@@ -4,11 +4,13 @@
|
||||
import logging
|
||||
import re
|
||||
|
||||
from operator import itemgetter
|
||||
from email.utils import formataddr
|
||||
|
||||
from odoo import _, api, fields, models, modules, SUPERUSER_ID, tools
|
||||
from odoo.exceptions import UserError, AccessError
|
||||
from odoo.osv import expression
|
||||
from odoo.tools import groupby
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
_image_dataurl = re.compile(r'(data:image/[a-z]+?);base64,([a-z0-9+/\n]{3,}=*)\n*([\'"])', re.I)
|
||||
@@ -81,6 +83,9 @@ class Message(models.Model):
|
||||
needaction = fields.Boolean(
|
||||
'Need Action', compute='_get_needaction', search='_search_needaction',
|
||||
help='Need Action')
|
||||
has_error = fields.Boolean(
|
||||
'Has error', compute='_compute_has_error', search='_search_has_error',
|
||||
help='Has error')
|
||||
channel_ids = fields.Many2many(
|
||||
'mail.channel', 'mail_message_mail_channel_rel', string='Channels')
|
||||
# notifications
|
||||
@@ -114,6 +119,9 @@ class Message(models.Model):
|
||||
('rejected', 'Rejected')], string="Moderation Status", index=True)
|
||||
moderator_id = fields.Many2one('res.users', string="Moderated By", index=True)
|
||||
need_moderation = fields.Boolean('Need moderation', compute='_compute_need_moderation', search='_search_need_moderation')
|
||||
#keep notification layout informations to be able to generate mail again
|
||||
layout = fields.Char('Layout', copy=False) # xml id of layout
|
||||
layout_values = fields.Char('Notification values', copy=False)
|
||||
|
||||
@api.multi
|
||||
def _get_needaction(self):
|
||||
@@ -131,6 +139,20 @@ class Message(models.Model):
|
||||
return ['&', ('notification_ids.res_partner_id', '=', self.env.user.partner_id.id), ('notification_ids.is_read', '=', False)]
|
||||
return ['&', ('notification_ids.res_partner_id', '=', self.env.user.partner_id.id), ('notification_ids.is_read', '=', True)]
|
||||
|
||||
@api.multi
|
||||
def _compute_has_error(self):
|
||||
error_from_notification = self.env['mail.notification'].sudo().search([
|
||||
('mail_message_id', 'in', self.ids),
|
||||
('email_status', 'in', ('bounce', 'exception'))]).mapped('mail_message_id')
|
||||
for message in self:
|
||||
message.has_error = message in error_from_notification
|
||||
|
||||
@api.multi
|
||||
def _search_has_error(self, operator, operand):
|
||||
if operator == '=' and operand:
|
||||
return [('notification_ids.email_status', 'in', ('bounce', 'exception'))]
|
||||
return ['!', ('notification_ids.email_status', 'in', ('bounce', 'exception'))] # this wont work and will be equivalent to "not in" beacause of orm restrictions. Dont use "has_error = False"
|
||||
|
||||
@api.depends('starred_partner_ids')
|
||||
def _get_starred(self):
|
||||
""" Compute if the message is starred by the current user. """
|
||||
@@ -350,7 +372,7 @@ class Message(models.Model):
|
||||
if partner.id in partner_tree]
|
||||
|
||||
customer_email_data = []
|
||||
for notification in message.notification_ids.filtered(lambda notif: notif.res_partner_id.partner_share and notif.res_partner_id.active):
|
||||
for notification in message.notification_ids.filtered(lambda notif: notif.email_status in ('bounce', 'exception', 'canceled') or (notif.res_partner_id.partner_share and notif.res_partner_id.active)):
|
||||
customer_email_data.append((partner_tree[notification.res_partner_id.id][0], partner_tree[notification.res_partner_id.id][1], notification.email_status))
|
||||
|
||||
attachment_ids = []
|
||||
@@ -375,6 +397,11 @@ class Message(models.Model):
|
||||
|
||||
return True
|
||||
|
||||
@api.multi
|
||||
def message_fetch_failed(self):
|
||||
messages = self.search([('has_error', '=', True), ('author_id.id', '=', self.env.user.partner_id.id), ('res_id', '!=', 0), ('model', '!=', False)])
|
||||
return messages._format_mail_failures()
|
||||
|
||||
@api.model
|
||||
def message_fetch(self, domain, limit=20):
|
||||
return self.search(domain, limit=limit).message_format()
|
||||
@@ -461,6 +488,37 @@ class Message(models.Model):
|
||||
message['module_icon'] = modules.module.get_module_icon(self.env[message['model']]._original_module)
|
||||
return message_values
|
||||
|
||||
@api.multi
|
||||
def _format_mail_failures(self):
|
||||
"""
|
||||
A shorter message to notify a failure update
|
||||
"""
|
||||
failures_infos = []
|
||||
# for each channel, build the information header and include the logged partner information
|
||||
for message in self:
|
||||
info = {
|
||||
'message_id': message.id,
|
||||
'record_name': message.record_name,
|
||||
'model_name': self.env['ir.model']._get(message.model).display_name,
|
||||
'uuid': message.message_id,
|
||||
'res_id': message.res_id,
|
||||
'model': message.model,
|
||||
'last_message_date': message.date,
|
||||
'module_icon': '/mail/static/src/img/smiley/mailfailure.jpg',
|
||||
'notifications': dict((notif.res_partner_id.id, (notif.email_status, notif.res_partner_id.name)) for notif in message.notification_ids)
|
||||
}
|
||||
failures_infos.append(info)
|
||||
return failures_infos
|
||||
|
||||
@api.multi
|
||||
def _notify_failure_update(self):
|
||||
authors = {}
|
||||
for author, author_messages in groupby(self, itemgetter('author_id')):
|
||||
self.env['bus.bus'].sendone(
|
||||
(self._cr.dbname, 'res.partner', author.id),
|
||||
{'type': 'mail_failure', 'elements': self.env['mail.message'].concat(*author_messages)._format_mail_failures()}
|
||||
)
|
||||
|
||||
#------------------------------------------------------
|
||||
# mail_message internals
|
||||
#------------------------------------------------------
|
||||
@@ -939,8 +997,16 @@ class Message(models.Model):
|
||||
# remove author from notified partners
|
||||
if not self._context.get('mail_notify_author', False) and self_sudo.author_id:
|
||||
partners_sudo = partners_sudo - self_sudo.author_id
|
||||
# update message, with maybe custom values
|
||||
|
||||
# list channels and partner by notification type
|
||||
email_channels = channels_sudo.filtered(lambda channel: channel.email_send)
|
||||
notif_partners = partners_sudo.filtered(lambda partner: 'inbox' in partner.mapped('user_ids.notification_type'))
|
||||
email_partner = partners_sudo - notif_partners
|
||||
|
||||
#update message, with maybe custom values
|
||||
message_values = {}
|
||||
if email_partner:
|
||||
message_values = {'layout': layout, 'layout_values': repr(values)}
|
||||
if channels_sudo:
|
||||
message_values['channel_ids'] = [(6, 0, channels_sudo.ids)]
|
||||
if partners_sudo:
|
||||
@@ -953,13 +1019,10 @@ class Message(models.Model):
|
||||
# notify partners and channels
|
||||
# those methods are called as SUPERUSER because portal users posting messages
|
||||
# have no access to partner model. Maybe propagating a real uid could be necessary.
|
||||
email_channels = channels_sudo.filtered(lambda channel: channel.email_send)
|
||||
notif_partners = partners_sudo.filtered(lambda partner: 'inbox' in partner.mapped('user_ids.notification_type'))
|
||||
|
||||
if email_channels or partners_sudo - notif_partners:
|
||||
if email_channels or email_partner:
|
||||
partners_sudo.search([
|
||||
'|',
|
||||
('id', 'in', (partners_sudo - notif_partners).ids),
|
||||
('id', 'in', (email_partner).ids),
|
||||
('channel_ids', 'in', email_channels.ids),
|
||||
('email', '!=', self_sudo.author_id.email or self_sudo.email_from),
|
||||
])._notify(self, layout=layout, force_send=force_send, send_after_commit=send_after_commit, values=values)
|
||||
|
||||
@@ -20,11 +20,35 @@ class Notification(models.Model):
|
||||
('ready', 'Ready to Send'),
|
||||
('sent', 'Sent'),
|
||||
('bounce', 'Bounced'),
|
||||
('exception', 'Exception')], 'Email Status',
|
||||
('exception', 'Exception'),
|
||||
('canceled', 'Canceled')], 'Email Status',
|
||||
default='ready', index=True)
|
||||
mail_id = fields.Many2one('mail.mail', 'Mail', index=True)
|
||||
# it would be technically possible to find notification from mail without adding a mail_id field on notification,
|
||||
# comparing partner_ids and message_ids, but this will involve to search notifications one by one since we want to match
|
||||
# bot value. Working with set inclusion, we could have a notif matching message from mail 1 and partner from mail 2, we dont want that.
|
||||
# The solution would be to iterate over mail or to filter mail after search,... or add a mail_id field on notification to KISS
|
||||
failure_type = fields.Selection(selection=[
|
||||
("NONE", "No error"),
|
||||
("SMTP", "Error while connecting to smtp server"),
|
||||
("RECIPIENT", "Invalid email adress"),
|
||||
("BOUNCE", "Email address not found"),
|
||||
("UNKNOWN", "Unknown error occured"),
|
||||
], default='NONE', string='Failure type')
|
||||
failure_reason = fields.Text('Failure reason', copy=False)
|
||||
|
||||
@api.model_cr
|
||||
def init(self):
|
||||
self._cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('mail_notification_res_partner_id_is_read_email_status_mail_message_id',))
|
||||
if not self._cr.fetchone():
|
||||
self._cr.execute('CREATE INDEX mail_notification_res_partner_id_is_read_email_status_mail_message_id ON mail_message_res_partner_needaction_rel (res_partner_id, is_read, email_status, mail_message_id)')
|
||||
|
||||
@api.multi
|
||||
def format_failure_reason(self):
|
||||
self.ensure_one()
|
||||
if self.failure_type != 'UNKNOWN':
|
||||
return dict(type(self).failure_type.selection).get(self.failure_type)
|
||||
else:
|
||||
return "Unknow error occured: %s" % (self.failure_reason)
|
||||
|
||||
|
||||
|
||||
@@ -102,6 +102,12 @@ class MailThread(models.AbstractModel):
|
||||
message_needaction_counter = fields.Integer(
|
||||
'Number of Actions', compute='_get_message_needaction',
|
||||
help="Number of messages which requires an action")
|
||||
message_has_error = fields.Boolean(
|
||||
'Message Delivery error', compute='_compute_message_has_error', search='_search_message_has_error',
|
||||
help="If checked, some messages have a delivery error.")
|
||||
message_has_error_counter = fields.Integer(
|
||||
'Number of error', compute='_compute_message_has_error',
|
||||
help="Number of messages with delivery error")
|
||||
|
||||
@api.one
|
||||
@api.depends('message_follower_ids')
|
||||
@@ -208,6 +214,26 @@ class MailThread(models.AbstractModel):
|
||||
def _search_message_needaction(self, operator, operand):
|
||||
return [('message_ids.needaction', operator, operand)]
|
||||
|
||||
@api.multi
|
||||
def _compute_message_has_error(self):
|
||||
self._cr.execute(""" SELECT msg.res_id, COUNT(msg.res_id) FROM mail_message msg
|
||||
RIGHT JOIN mail_message_res_partner_needaction_rel rel
|
||||
ON rel.mail_message_id = msg.id AND rel.email_status in ('exception','bounce')
|
||||
WHERE msg.author_id = %s AND msg.model = %s AND msg.res_id in %s
|
||||
GROUP BY msg.res_id""",
|
||||
(self.env.user.partner_id.id, self._name, tuple(self.ids),))
|
||||
res = dict()
|
||||
for result in self._cr.fetchall():
|
||||
res[result[0]] = result[1]
|
||||
|
||||
for record in self:
|
||||
record.message_has_error_counter = res.get(record.id, 0)
|
||||
record.message_has_error = bool(record.message_has_error_counter)
|
||||
|
||||
@api.model
|
||||
def _search_message_has_error(self, operator, operand):
|
||||
return [('message_ids.has_error', operator, operand)]
|
||||
|
||||
# ------------------------------------------------------
|
||||
# CRUD overrides for automatic subscription and logging
|
||||
# ------------------------------------------------------
|
||||
|
||||
@@ -140,6 +140,7 @@ class Partner(models.Model):
|
||||
('mail_message_id', '=', email.mail_message_id.id),
|
||||
('res_partner_id', 'in', email.recipient_ids.ids)])
|
||||
notifications.write({
|
||||
'mail_id': email.id,
|
||||
'is_email': True,
|
||||
'is_read': True, # handle by email discards Inbox notification
|
||||
'email_status': 'ready',
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
@@ -119,6 +119,7 @@ var ChatManager = AbstractService.extend({
|
||||
|
||||
this.messages = [];
|
||||
this.channels = [];
|
||||
this.messageFailures = [];
|
||||
this.channelsPreviewDef;
|
||||
this.channelDefs = {};
|
||||
this.unreadConversationCounter = 0;
|
||||
@@ -285,7 +286,8 @@ var ChatManager = AbstractService.extend({
|
||||
var self = this;
|
||||
var channelsPreview = _.map(channels, function (channel) {
|
||||
var info;
|
||||
if (channel.channel_ids && _.contains(channel.channel_ids,"channel_inbox")) {
|
||||
if ((channel.channel_ids && _.contains(channel.channel_ids, "channel_inbox")) || channel.id === "mail_failure") {
|
||||
// this is a hack to preview messages and mail failure
|
||||
// map inbox(mail_message) data with existing channel/chat template
|
||||
info = _.pick(channel,
|
||||
'id', 'body', 'avatar_src', 'res_id', 'model', 'module_icon',
|
||||
@@ -299,7 +301,9 @@ var ChatManager = AbstractService.extend({
|
||||
info.name = info.record_name || info.subject || info.displayed_author;
|
||||
info.image_src = info.module_icon || info.avatar_src;
|
||||
info.message_id = info.id;
|
||||
info.id = 'channel_inbox';
|
||||
if (channel.id !== "mail_failure") {
|
||||
info.id = 'channel_inbox';
|
||||
}
|
||||
return info;
|
||||
}
|
||||
info = _.pick(channel, 'id', 'is_chat', 'name', 'status', 'unread_counter');
|
||||
@@ -420,6 +424,14 @@ var ChatManager = AbstractService.extend({
|
||||
}
|
||||
return result;
|
||||
},
|
||||
/**
|
||||
* Returns a list of mail failures
|
||||
*
|
||||
* @return {Object[]} list of channels
|
||||
*/
|
||||
getMessageFailures: function () {
|
||||
return this.messageFailures;
|
||||
},
|
||||
/**
|
||||
* Get all listeners of a channel.
|
||||
*
|
||||
@@ -1215,6 +1227,7 @@ var ChatManager = AbstractService.extend({
|
||||
_.each(result.channel_slots, function (channels) {
|
||||
_.each(channels, self._addChannel.bind(self));
|
||||
});
|
||||
self.messageFailures = result.mail_failures;
|
||||
self.needactionCounter = result.needaction_inbox_counter || 0;
|
||||
self.starredCounter = result.starred_counter || 0;
|
||||
self.moderationCounter = result.moderation_counter;
|
||||
@@ -1635,6 +1648,42 @@ var ChatManager = AbstractService.extend({
|
||||
});
|
||||
this.chatBus.trigger('update_moderation_counter');
|
||||
},
|
||||
/**
|
||||
* Add or remove failure when receiving a failure update message
|
||||
* @private
|
||||
* @param {Object} data
|
||||
*/
|
||||
_manageMessageFailureNotification: function (data) {
|
||||
var self = this;
|
||||
_.each(data.elements, function (updateMessage) {
|
||||
|
||||
var isAddFailure = _.some(updateMessage.notifications, function(notif) {
|
||||
return notif[0] === 'exception' || notif[0] === 'bounce';
|
||||
});
|
||||
var res = _.find(self.messageFailures, {'message_id': updateMessage.message_id});
|
||||
if (res) {
|
||||
var index = _.findIndex(self.messageFailures, res);
|
||||
if (isAddFailure) {
|
||||
self.messageFailures[index] = updateMessage;
|
||||
} else {
|
||||
self.messageFailures.splice(index, 1);
|
||||
}
|
||||
} else if (isAddFailure) {
|
||||
self.messageFailures.push(updateMessage);
|
||||
}
|
||||
var message = _.findWhere(self.messages, { id: updateMessage.message_id });
|
||||
if (message) {
|
||||
if (isAddFailure) {
|
||||
message.customer_email_status = "exception";
|
||||
} else{
|
||||
message.customer_email_status = "sent";
|
||||
}
|
||||
self._updateMessageNotificationStatus(updateMessage, message);
|
||||
self.chatBus.trigger('update_message', message);
|
||||
}
|
||||
});
|
||||
this.chatBus.trigger('update_needaction', this.needactionCounter);
|
||||
},
|
||||
/**
|
||||
* Updates channel_inbox when a message has marked as read.
|
||||
*
|
||||
@@ -1738,6 +1787,8 @@ var ChatManager = AbstractService.extend({
|
||||
this._manageTransientMessageNotification(data);
|
||||
} else if (data.type === 'activity_updated') {
|
||||
this._manageActivityUpdateNotification(data);
|
||||
} else if (data.type === 'mail_failure') {
|
||||
this._manageMessageFailureNotification(data);
|
||||
} else {
|
||||
this._manageChatSessionNotification(data);
|
||||
}
|
||||
@@ -1883,7 +1934,28 @@ var ChatManager = AbstractService.extend({
|
||||
channel.unread_counter = counter;
|
||||
this.chatBus.trigger("update_channel_unread_counter", channel);
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the message notification status of message based on update_message
|
||||
*
|
||||
* @private
|
||||
* @param {Object} updateMessage
|
||||
* @param {Object[]} updateMessage.notifications
|
||||
*/
|
||||
_updateMessageNotificationStatus: function (updateMessage, message) {
|
||||
_.each(updateMessage.notifications, function (notif, id, list) {
|
||||
var partnerName = notif[1];
|
||||
var notifStatus = notif[0];
|
||||
var res = _.find(message.customer_email_data, function(entry){
|
||||
return entry[0] === parseInt(id);
|
||||
});
|
||||
if (res) {
|
||||
res[2] = notifStatus;
|
||||
} else {
|
||||
message.customer_email_data.push([parseInt(id), partnerName, notifStatus]);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
@@ -5,8 +5,8 @@ var config = require('web.config');
|
||||
var core = require('web.core');
|
||||
var session = require('web.session');
|
||||
var SystrayMenu = require('web.SystrayMenu');
|
||||
var time = require('web.time');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
var QWeb = core.qweb;
|
||||
|
||||
/**
|
||||
@@ -34,19 +34,23 @@ var MessagingMenu = Widget.extend({
|
||||
this.$filter_buttons = this.$('.o_filter_button');
|
||||
this.$channels_preview = this.$('.o_mail_navbar_dropdown_channels');
|
||||
this.filter = false;
|
||||
var chatBus = this.call('chat_manager', 'getChatBus');
|
||||
chatBus.on("update_needaction", this, this.update_counter);
|
||||
chatBus.on("update_channel_unread_counter", this, this.update_counter);
|
||||
this.call('chat_manager', 'isReady').then(this.update_counter.bind(this));
|
||||
var self = this;
|
||||
this.call('chat_manager', 'isReady').then(function () {
|
||||
self.update_counter();
|
||||
var chatBus = self.call('chat_manager', 'getChatBus');
|
||||
chatBus.on('update_needaction', self, self.update_counter);
|
||||
chatBus.on('update_channel_unread_counter', self, self.update_counter);
|
||||
});
|
||||
return this._super();
|
||||
},
|
||||
is_open: function () {
|
||||
return this.$el.hasClass('open');
|
||||
},
|
||||
update_counter: function () {
|
||||
var messageFailures = this.call('chat_manager', 'getMessageFailures');
|
||||
var needactionCounter = this.call('chat_manager', 'getNeedactionCounter');
|
||||
var unreadConversationCounter = this.call('chat_manager', 'getUnreadConversationCounter');
|
||||
var counter = needactionCounter + unreadConversationCounter;
|
||||
var counter = needactionCounter + unreadConversationCounter + messageFailures.length;
|
||||
this.$('.o_notification_counter').text(counter);
|
||||
this.$el.toggleClass('o_no_notification', !counter);
|
||||
if (this.is_open()) {
|
||||
@@ -55,7 +59,6 @@ var MessagingMenu = Widget.extend({
|
||||
},
|
||||
update_channels_preview: function () {
|
||||
var self = this;
|
||||
|
||||
// Display spinner while waiting for channels preview
|
||||
this.$channels_preview.html(QWeb.render('Spinner'));
|
||||
this.call('chat_manager', 'isReady').then(function () {
|
||||
@@ -69,6 +72,28 @@ var MessagingMenu = Widget.extend({
|
||||
return channel.type !== 'static';
|
||||
}
|
||||
});
|
||||
var formatedFailures = [];
|
||||
var messageFailures = self.call('chat_manager', 'getMessageFailures');
|
||||
_.each(messageFailures, function (messageFailure) {
|
||||
var model = messageFailure.model;
|
||||
var existingFailure = _.findWhere(formatedFailures, { model: model });
|
||||
if (existingFailure) {
|
||||
if (existingFailure.res_id !== messageFailure.res_id) {
|
||||
existingFailure.res_id = null;
|
||||
existingFailure.record_name = messageFailure.model_name;//for display, will be used as subject
|
||||
}
|
||||
existingFailure.unread_counter ++;
|
||||
} else {
|
||||
messageFailure = _.clone(messageFailure);
|
||||
messageFailure.unread_counter = 1;
|
||||
messageFailure.id = "mail_failure";
|
||||
messageFailure.body = "An error occured sending an email";
|
||||
messageFailure.date = moment(time.str_to_datetime(messageFailure.last_message_date));
|
||||
messageFailure.displayed_author = "";
|
||||
formatedFailures.push(messageFailure);
|
||||
}
|
||||
});
|
||||
channels = _.union(channels, formatedFailures);
|
||||
self.call('chat_manager', 'getMessages', {channelID: 'channel_inbox'})
|
||||
.then(function (messages) {
|
||||
var res = [];
|
||||
@@ -115,8 +140,10 @@ var MessagingMenu = Widget.extend({
|
||||
this.call('chat_window_manager', 'openChat');
|
||||
},
|
||||
|
||||
// Handlers
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
/**
|
||||
* When a channel is clicked on, we want to open chat/channel window
|
||||
*
|
||||
@@ -126,7 +153,7 @@ var MessagingMenu = Widget.extend({
|
||||
_onClickChannel: function (event) {
|
||||
var self = this;
|
||||
var channelID = $(event.currentTarget).data('channel_id');
|
||||
if (channelID === 'channel_inbox') {
|
||||
if (channelID === 'channel_inbox' || channelID === 'mail_failure') {
|
||||
var resID = $(event.currentTarget).data('res_id');
|
||||
var resModel = $(event.currentTarget).data('res_model');
|
||||
if (resModel && resID) {
|
||||
@@ -136,6 +163,16 @@ var MessagingMenu = Widget.extend({
|
||||
views: [[false, 'form']],
|
||||
res_id: resID
|
||||
});
|
||||
} else if (resModel) {
|
||||
this.do_action({
|
||||
name: "Mail failures",
|
||||
type: 'ir.actions.act_window',
|
||||
view_mode: 'kanban,list,form',
|
||||
views: [[false, 'kanban'], [false, 'list'], [false, 'form']],
|
||||
target: 'current',
|
||||
res_model: resModel,
|
||||
domain: [['message_has_error', '=', true]],
|
||||
});
|
||||
} else {
|
||||
this.do_action('mail.mail_channel_action_client_chat', {clear_breadcrumbs: true})
|
||||
.then(function () {
|
||||
|
||||
@@ -35,15 +35,16 @@ var Thread = Widget.extend({
|
||||
"click .o_attachment_download": "_onAttachmentDownload",
|
||||
"click .o_attachment_view": "_onAttachmentView",
|
||||
"click .o_thread_message_needaction": function (event) {
|
||||
var message_id = $(event.currentTarget).data('message-id');
|
||||
this.trigger("mark_as_read", message_id);
|
||||
var messageId = $(event.currentTarget).data('message-id');
|
||||
this.trigger("mark_as_read", messageId);
|
||||
},
|
||||
"click .o_thread_message_moderation": "_onClickMessageModeration",
|
||||
"change .moderation_checkbox": "_onChangeModerationCheckbox",
|
||||
"click .o_thread_message_star": function (event) {
|
||||
var message_id = $(event.currentTarget).data('message-id');
|
||||
this.trigger("toggle_star_status", message_id);
|
||||
var messageId = $(event.currentTarget).data('message-id');
|
||||
this.trigger("toggle_star_status", messageId);
|
||||
},
|
||||
"click .o_thread_message_email_exception": "_onClickMailException",
|
||||
"click .o_thread_message_reply": function (event) {
|
||||
this.selected_id = $(event.currentTarget).data('message-id');
|
||||
this.$('.o_thread_message').removeClass('o_thread_selected_message');
|
||||
@@ -414,6 +415,18 @@ var Thread = Widget.extend({
|
||||
var decision = $button.data('decision');
|
||||
this.trigger_up('message_moderation', { messageID: messageID, decision: decision });
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
_onClickMailException: function (event) {
|
||||
var messageId = $(event.currentTarget).data('message-id');
|
||||
this.do_action('mail.mail_resend_message_action', {
|
||||
additional_context: {
|
||||
mail_message_to_resend: messageId
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
Thread.ORDER = ORDER;
|
||||
|
||||
@@ -161,6 +161,7 @@
|
||||
&.o_thread_message_email_exception {
|
||||
color: red;
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
&.o_thread_message_email_bounce {
|
||||
color: red;
|
||||
|
||||
@@ -231,7 +231,9 @@
|
||||
<span class="fa fa-mail-reply"/> You:
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-esc="channel.last_message.displayed_author"/>:
|
||||
<t t-if="channel.last_message.displayed_author">
|
||||
<t t-esc="channel.last_message.displayed_author"/>:
|
||||
</t>
|
||||
</t>
|
||||
<t t-raw="channel.last_message_preview"/>
|
||||
</div>
|
||||
|
||||
@@ -175,14 +175,19 @@
|
||||
(from <a t-att-data-oe-id="message.origin_id" href="#">#<t t-esc="message.origin_name"/></a>)
|
||||
</t>
|
||||
<span t-if="options.display_email_icon and message.customer_email_data and message.customer_email_data.length" class="o_thread_tooltip_container">
|
||||
<i t-att-class="'o_thread_tooltip o_thread_message_email o_thread_message_email_' + message.customer_email_status + ' fa fa-envelope-o'"/>
|
||||
<t t-set="fatype" t-value="''"/>
|
||||
<t t-if="message.customer_email_status === 'sent' or message.customer_email_status === 'ready'">
|
||||
<t t-set="fatype" t-value="'-o'"/>
|
||||
</t>
|
||||
<i t-att-class="'o_thread_tooltip o_thread_message_email o_thread_message_email_' + message.customer_email_status + ' fa fa-envelope'+ fatype" t-att-data-message-id="message.id"/>
|
||||
<span class="o_thread_tooltip_content">
|
||||
<t t-foreach="message.customer_email_data" t-as="customer">
|
||||
<span>
|
||||
<t t-if="customer[2] == 'sent'"><i class='fa fa-check'/></t>
|
||||
<t t-if="customer[2] == 'bounce'"><i class='fa fa-exclamation'/></t>
|
||||
<t t-if="customer[2] == 'exception'"><i class='fa fa-exclamation'/></t>
|
||||
<t t-if="customer[2] == 'ready'"><i class='fa fa-send-o'/></t>
|
||||
<t t-if="customer[2] === 'sent'"><i class='fa fa-check'/></t>
|
||||
<t t-if="customer[2] === 'bounce'"><i class='fa fa-exclamation'/></t>
|
||||
<t t-if="customer[2] === 'exception'"><i class='fa fa-exclamation'/></t>
|
||||
<t t-if="customer[2] === 'ready'"><i class='fa fa-send-o'/></t>
|
||||
<t t-if="customer[2] === 'canceled'"><i class='fa fa-trash-o'/></t>
|
||||
<t t-esc="customer[1]"/>
|
||||
</span>
|
||||
<br />
|
||||
|
||||
@@ -56,6 +56,7 @@ MockServer.include({
|
||||
'mention_partner_suggestions': [],
|
||||
'shortcodes': [],
|
||||
'menu_id': false,
|
||||
'mail_failures': [],
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
from . import invite
|
||||
from . import mail_compose_message
|
||||
from . import mail_resend_message
|
||||
from . import email_template_preview
|
||||
from . import base_module_uninstall
|
||||
from . import base_partner_merge
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from ast import literal_eval
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
class MailResendMessage(models.TransientModel):
|
||||
_name = 'mail.resend.message'
|
||||
_description = 'Email resend wizard'
|
||||
|
||||
mail_message_id = fields.Many2one('mail.message', 'Message', readonly=True)
|
||||
partner_ids = fields.One2many('mail.resend.partner', 'resend_wizard_id', string='Recipients')
|
||||
notification_ids = fields.Many2many('mail.notification', string='Notifications', readonly=True)
|
||||
has_cancel = fields.Boolean(compute='_compute_has_cancel')
|
||||
partner_readonly = fields.Boolean(compute='_compute_partner_readonly')
|
||||
|
||||
@api.depends("partner_ids")
|
||||
def _compute_has_cancel(self):
|
||||
self.has_cancel = self.partner_ids.filtered(lambda p: not p.resend)
|
||||
|
||||
def _compute_partner_readonly(self):
|
||||
self.partner_readonly = not self.env['res.partner'].check_access_rights('write', raise_exception=False)
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
rec = super(MailResendMessage, self).default_get(fields)
|
||||
message_id = self._context.get('mail_message_to_resend')
|
||||
if message_id:
|
||||
mail_message_id = self.env['mail.message'].browse(message_id)
|
||||
notification_ids = mail_message_id.notification_ids.filtered(lambda notif: notif.email_status in ('exception', 'bounce'))
|
||||
partner_readonly = not self.env['res.partner'].check_access_rights('write', raise_exception=False)
|
||||
partner_ids = [(0, 0,
|
||||
{
|
||||
"partner_id": notif.res_partner_id.id,
|
||||
"name": notif.res_partner_id.name,
|
||||
"email": notif.res_partner_id.email,
|
||||
"resend": True,
|
||||
"message": notif.format_failure_reason(),
|
||||
}
|
||||
) for notif in notification_ids]
|
||||
rec['partner_readonly'] = partner_readonly
|
||||
rec['notification_ids'] = [(6, 0, notification_ids.ids)]
|
||||
rec['mail_message_id'] = mail_message_id.id
|
||||
rec['partner_ids'] = partner_ids
|
||||
else:
|
||||
raise UserError('No message_id found in context')
|
||||
return rec
|
||||
|
||||
@api.multi
|
||||
def resend_mail_action(self):
|
||||
""" Process the wizard content and proceed with sending the related
|
||||
email(s), rendering any template patterns on the fly if needed. """
|
||||
for wizard in self:
|
||||
"If a partner disappeared from partner list, we cancel the notification"
|
||||
to_cancel = wizard.partner_ids.filtered(lambda p: not p.resend).mapped("partner_id")
|
||||
to_send = wizard.partner_ids.filtered(lambda p: p.resend).mapped("partner_id")
|
||||
notif_to_cancel = wizard.notification_ids.filtered(lambda notif: notif.res_partner_id in to_cancel and notif.email_status in ('exception', 'bounce'))
|
||||
notif_to_cancel.sudo().write({'email_status': 'canceled'})
|
||||
to_send.sudo()._notify(self.mail_message_id, self.mail_message_id.layout, force_send=True, send_after_commit=False, values=literal_eval(self.mail_message_id.layout_values))
|
||||
self.mail_message_id._notify_failure_update()
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
||||
@api.multi
|
||||
def cancel_mail_action(self):
|
||||
for wizard in self:
|
||||
for notif in wizard.notification_ids:
|
||||
notif.filtered(lambda notif: notif.email_status in ('exception', 'bounce')).sudo().write({'email_status': 'canceled'})
|
||||
wizard.mail_message_id._notify_failure_update()
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
||||
|
||||
|
||||
class PartnerResend(models.TransientModel):
|
||||
_name = 'mail.resend.partner'
|
||||
_description = 'Partner with additionnal information for mail resend'
|
||||
|
||||
partner_id = fields.Many2one('res.partner', string='Partner', required=True, ondelete='cascade')
|
||||
name = fields.Char(related="partner_id.name")
|
||||
email = fields.Char(related="partner_id.email")
|
||||
resend = fields.Boolean(string="Send Again", default=True)
|
||||
resend_wizard_id = fields.Many2one('mail.resend.message', string="Resend wizard")
|
||||
message = fields.Char(string="Help message")
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="mail_resend_message_view_form" model="ir.ui.view">
|
||||
<field name="name">mail.resend.message.form</field>
|
||||
<field name="model">mail.resend.message</field>
|
||||
<field name="groups_id" eval="[(4,ref('base.group_user'))]"/>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Edit Partners">
|
||||
<field name="mail_message_id" invisible="1"/>
|
||||
<field name="notification_ids" invisible="1"/>
|
||||
<field name="has_cancel" invisible="1"/>
|
||||
<field name="partner_readonly" invisible="1"/>
|
||||
<p>Please, select the action to do on each mail and correct the email address if needed. The modified address will be saved on the corresponding contact.</p>
|
||||
<field name="partner_ids">
|
||||
<tree string="Recipient" editable="top" create="0" delete="0">
|
||||
<field name="name" readonly="1"/>
|
||||
<field name="email" attrs="{'readonly': [('parent.partner_readonly', '=', True)]}"/>
|
||||
<field name="message" readonly="1"/>
|
||||
<field name="resend" widget="boolean_toggle"/>
|
||||
</tree>
|
||||
</field>
|
||||
<p attrs="{'invisible':[('has_cancel', '==', False)]}"> <span class="fa fa-info-circle"/> Caution: It won't be possible to send this mail again to the recipients you did not select.</p>
|
||||
<footer>
|
||||
<button string="Resend to selected" name="resend_mail_action" type="object" class="btn-primary o_mail_send"/>
|
||||
<button string="Ignore all failure" name="cancel_mail_action" type="object" class="btn-default" />
|
||||
<button string="Cancel" class="btn-default" special="cancel" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="mail_resend_message_action" model="ir.actions.act_window">
|
||||
<field name="name">Resend mail</field>
|
||||
<field name="res_model">mail.resend.message</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -95,11 +95,13 @@ class MailMail(models.Model):
|
||||
return res
|
||||
|
||||
@api.multi
|
||||
def _postprocess_sent_message(self, mail_sent=True):
|
||||
def _postprocess_sent_message(self, failure_type='NONE', **kwargs):
|
||||
mail_sent = failure_type == 'NONE' # we consider that a recipient error is a failure with mass mailling and show them as failed
|
||||
for mail in self:
|
||||
if mail.mailing_id:
|
||||
if mail_sent is True and mail.statistics_ids:
|
||||
mail.statistics_ids.write({'sent': fields.Datetime.now(), 'exception': False})
|
||||
elif mail_sent is False and mail.statistics_ids:
|
||||
mail.statistics_ids.write({'exception': fields.Datetime.now()})
|
||||
return super(MailMail, self)._postprocess_sent_message(mail_sent=mail_sent)
|
||||
return super(MailMail, self)._postprocess_sent_message(failure_type=failure_type, **kwargs)
|
||||
|
||||
@@ -4,6 +4,7 @@ from . import test_mail_activity
|
||||
from . import test_mail_followers
|
||||
from . import test_mail_message
|
||||
from . import test_mail_mail
|
||||
from . import test_mail_resend
|
||||
from . import test_mail_channel
|
||||
from . import test_mail_gateway
|
||||
from . import test_mail_template
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import itertools
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo.addons.test_mail.tests import common
|
||||
from odoo.tests import tagged
|
||||
from odoo.tools import mute_logger
|
||||
from odoo import api
|
||||
from odoo.addons.base.models.ir_mail_server import IrMailServer
|
||||
|
||||
|
||||
@tagged('resend_test')
|
||||
class TestMailResend(common.BaseFunctionalTest, common.MockEmails):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestMailResend, cls).setUpClass()
|
||||
#Two users
|
||||
cls.user1 = cls.env['res.users'].with_context(cls._quick_create_user_ctx).create({
|
||||
'name': 'Employee 1',
|
||||
'login': 'e1',
|
||||
'email': 'e1', # invalid email
|
||||
'notification_type': 'email',
|
||||
'groups_id': [(6, 0, [cls.env.ref('base.group_user').id])]
|
||||
})
|
||||
cls.user2 = cls.env['res.users'].with_context(cls._quick_create_user_ctx).create({
|
||||
'name': 'Employee 2',
|
||||
'login': 'e2',
|
||||
'email': 'e2@example.com',
|
||||
'notification_type': 'email',
|
||||
'groups_id': [(6, 0, [cls.env.ref('base.group_user').id])]
|
||||
})
|
||||
#Two partner
|
||||
cls.partner1 = cls.env['res.partner'].with_context(cls._quick_create_user_ctx).create({
|
||||
'name': 'Partner 1',
|
||||
'email': 'p1' # invalid email
|
||||
})
|
||||
cls.partner2 = cls.env['res.partner'].with_context(cls._quick_create_user_ctx).create({
|
||||
'name': 'Partner 2',
|
||||
'email': 'p2@example.com'
|
||||
})
|
||||
|
||||
@api.model
|
||||
def send_email(self, message, *args, **kwargs):
|
||||
assert '@' in message['To'], self.NO_VALID_RECIPIENT
|
||||
return message['Message-Id']
|
||||
cls.bus_update_failure = []
|
||||
def sendone(self, channel, message):
|
||||
if 'type' in message and message['type'] == 'mail_failure':
|
||||
cls.bus_update_failure.append((channel, message))
|
||||
cls.env['ir.mail_server']._patch_method('send_email', send_email)
|
||||
cls.env['bus.bus']._patch_method('sendone', sendone)
|
||||
cls.partners = cls.env['res.partner'].concat(cls.user1.partner_id, cls.user2.partner_id, cls.partner1, cls.partner2)
|
||||
cls.invalid_email_partners = cls.env['res.partner'].concat(cls.user1.partner_id, cls.partner1)
|
||||
|
||||
def setUp(self):
|
||||
super(TestMailResend, self).setUp()
|
||||
TestMailResend.bus_update_failure = []
|
||||
|
||||
def assertNotifStates(self, states, message):
|
||||
notif = self.env['mail.notification'].search([('mail_message_id', '=', message.id)], order="res_partner_id asc")
|
||||
self.assertEquals(tuple(notif.mapped('email_status')), states)
|
||||
return notif
|
||||
|
||||
def assertBusMessage(self, partners):
|
||||
partner_ids = [elem[0][2] for elem in self.bus_update_failure]
|
||||
self.assertEquals(partner_ids, [partner.id for partner in partners])
|
||||
self.bus_update_failure.clear()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
# Remove mocks
|
||||
cls.env['ir.mail_server']._revert_method('send_email')
|
||||
cls.env['bus.bus']._revert_method('sendone')
|
||||
super(TestMailResend, cls).tearDownClass()
|
||||
|
||||
def assertEmails(self, *args, **kwargs):
|
||||
res = super(TestMailResend, self).assertEmails(*args, **kwargs)
|
||||
self._mails.clear()
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_mail_resend_workflow(self):
|
||||
cls = TestMailResend
|
||||
#missconfigured server
|
||||
@api.model
|
||||
def connect_failure(**kwargs):
|
||||
raise Exception
|
||||
with patch.object(IrMailServer, 'connect', side_effect=connect_failure):
|
||||
message = self.test_record.sudo().message_post(partner_ids=self.partners.ids, subtype='mail.mt_comment', message_type='notification')
|
||||
self.assertBusMessage([self.partner_admin])
|
||||
self.assertEmails(self.partner_admin, [])
|
||||
self.assertNotifStates(('exception', 'exception', 'exception', 'exception'), message)
|
||||
wizard = self.env['mail.resend.message'].with_context({'mail_message_to_resend': message.id}).create({})
|
||||
self.assertEqual(wizard.notification_ids.mapped('res_partner_id'), self.partners, "wizard should manage notifications for each failed partner")
|
||||
wizard.resend_mail_action()
|
||||
self.assertBusMessage([self.partner_admin] * 3) # three more failure sent on bus, one for each mail in failure and one for resend
|
||||
self.assertEmails(self.partner_admin, self.partners)
|
||||
self.assertNotifStates(('exception', 'sent', 'exception', 'sent'), message)
|
||||
self.user1.write({"email": 'u1@example.com'})
|
||||
self.env['mail.resend.message'].with_context({'mail_message_to_resend': message.id}).create({}).resend_mail_action()
|
||||
self.assertBusMessage([self.partner_admin] * 2) # two more failure update sent on bus, one for failed mail and one for resend
|
||||
self.assertEmails(self.partner_admin, self.invalid_email_partners)
|
||||
self.assertNotifStates(('sent', 'sent', 'exception', 'sent'), message)
|
||||
self.partner1.write({"email": 'p1@example.com'})
|
||||
self.env['mail.resend.message'].with_context({'mail_message_to_resend': message.id}).create({}).resend_mail_action()
|
||||
self.assertBusMessage([self.partner_admin]) # A success update should be sent on bus once the email has no more failure
|
||||
self.assertEmails(self.partner_admin, self.partner1)
|
||||
self.assertNotifStates(('sent', 'sent', 'sent', 'sent'), message)
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_mail_send_no_failure(self):
|
||||
self.user1.write({"email": 'u1@example.com'})
|
||||
self.partner1.write({"email": 'p1@example.com'})
|
||||
message = self.test_record.sudo().message_post(partner_ids=self.partners.ids, subtype='mail.mt_comment', message_type='notification')
|
||||
self.assertBusMessage([]) # one update for cancell
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_remove_mail_become_canceled(self):
|
||||
message = self.test_record.sudo().message_post(partner_ids=self.partners.ids, subtype='mail.mt_comment', message_type='notification')
|
||||
self.assertEmails(self.partner_admin, self.partners)
|
||||
self.assertBusMessage([self.partner_admin] * 2) # two failure sent on bus, one for each mail
|
||||
wizard = self.env['mail.resend.message'].with_context({'mail_message_to_resend': message.id}).create({})
|
||||
partners = wizard.partner_ids.mapped("partner_id")
|
||||
self.assertEqual(self.invalid_email_partners, partners)
|
||||
wizard.partner_ids.filtered(lambda p: p.partner_id == self.partner1).write({"resend": False})
|
||||
wizard.resend_mail_action()
|
||||
self.assertEmails(self.partner_admin, self.user1)
|
||||
self.assertBusMessage([self.partner_admin] * 2) # two more failure sent on bus, one for failure, one for resend
|
||||
self.assertNotifStates(('exception', 'sent', 'canceled', 'sent'), message)
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_cancel_all(self):
|
||||
message = self.test_record.sudo().message_post(partner_ids=self.partners.ids, subtype='mail.mt_comment', message_type='notification')
|
||||
self.assertNotifStates(('exception', 'sent', 'exception', 'sent'), message)
|
||||
self.assertBusMessage([self.partner_admin] * 2)
|
||||
wizard = self.env['mail.resend.message'].with_context({'mail_message_to_resend': message.id}).create({})
|
||||
wizard.cancel_mail_action()
|
||||
self.assertNotifStates(('canceled', 'sent', 'canceled', 'sent'), message)
|
||||
self.assertBusMessage([self.partner_admin]) # one update for cancell
|
||||
Reference in New Issue
Block a user