diff options
| author | XavierDo <xdo@odoo.com> | 2018-04-27 16:50:03 +0200 |
|---|---|---|
| committer | XavierDo <xdo@odoo.com> | 2018-06-07 11:46:28 +0200 |
| commit | 133eeb1bbf52e9a9ee58f83be8e7f0aba3c6d1ad (patch) | |
| tree | cd7c6b0a3c931705158c9af590206130480426f6 /addons | |
| parent | aa7fb8c72317e330bbeb6ce29b4aace8627ef1d7 (diff) | |
[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
Diffstat (limited to 'addons')
21 files changed, 593 insertions, 57 deletions
diff --git a/addons/mail/__manifest__.py b/addons/mail/__manifest__.py index 1638baae47e..1385da9f61e 100644 --- a/addons/mail/__manifest__.py +++ b/addons/mail/__manifest__.py @@ -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', diff --git a/addons/mail/controllers/main.py b/addons/mail/controllers/main.py index f4b6f6b8d5d..949a715cddb 100644 --- a/addons/mail/controllers/main.py +++ b/addons/mail/controllers/main.py @@ -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']), diff --git a/addons/mail/models/mail_mail.py b/addons/mail/models/mail_mail.py index fb4f9b76096..d8abbd1f06f 100644 --- a/addons/mail/models/mail_mail.py +++ b/addons/mail/models/mail_mail.py @@ -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 diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py index d98c98fb1d9..c5d3abda206 100644 --- a/addons/mail/models/mail_message.py +++ b/addons/mail/models/mail_message.py @@ -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) diff --git a/addons/mail/models/mail_notification.py b/addons/mail/models/mail_notification.py index e14754b7e8f..66f18379565 100644 --- a/addons/mail/models/mail_notification.py +++ b/addons/mail/models/mail_notification.py @@ -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) + + diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 4cde154e1cb..4f89da3b69e 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -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 # ------------------------------------------------------ diff --git a/addons/mail/models/res_partner.py b/addons/mail/models/res_partner.py index f6472eafd0b..07bee0bb6e6 100644 --- a/addons/mail/models/res_partner.py +++ b/addons/mail/models/res_partner.py @@ -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', diff --git a/addons/mail/static/src/img/smiley/mailfailure.jpg b/addons/mail/static/src/img/smiley/mailfailure.jpg Binary files differnew file mode 100644 index 00000000000..6f0ec91ad80 --- /dev/null +++ b/addons/mail/static/src/img/smiley/mailfailure.jpg diff --git a/addons/mail/static/src/js/services/chat_manager.js b/addons/mail/static/src/js/services/chat_manager.js index eaedb9cd11e..8a90fc57482 100644 --- a/addons/mail/static/src/js/services/chat_manager.js +++ b/addons/mail/static/src/js/services/chat_manager.js @@ -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'); @@ -421,6 +425,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. * * @param {Object} 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; @@ -1636,6 +1649,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. * * @private @@ -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 //-------------------------------------------------------------------------- diff --git a/addons/mail/static/src/js/systray.js b/addons/mail/static/src/js/systray.js index c62b964472f..f11531b6eab 100644 --- a/addons/mail/static/src/js/systray.js +++ b/addons/mail/static/src/js/systray.js @@ -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 () { diff --git a/addons/mail/static/src/js/thread.js b/addons/mail/static/src/js/thread.js index 39d179a715a..54a36e169e4 100644 --- a/addons/mail/static/src/js/thread.js +++ b/addons/mail/static/src/js/thread.js @@ -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; diff --git a/addons/mail/static/src/scss/thread.scss b/addons/mail/static/src/scss/thread.scss index 0529238a36e..6d8eb8b8254 100644 --- a/addons/mail/static/src/scss/thread.scss +++ b/addons/mail/static/src/scss/thread.scss @@ -161,6 +161,7 @@ &.o_thread_message_email_exception { color: red; opacity: 1; + cursor: pointer; } &.o_thread_message_email_bounce { color: red; diff --git a/addons/mail/static/src/xml/discuss.xml b/addons/mail/static/src/xml/discuss.xml index 1ec08a23635..1eda1dfec1b 100644 --- a/addons/mail/static/src/xml/discuss.xml +++ b/addons/mail/static/src/xml/discuss.xml @@ -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> diff --git a/addons/mail/static/src/xml/thread.xml b/addons/mail/static/src/xml/thread.xml index 20049e89cf6..ca8ae9b8319 100644 --- a/addons/mail/static/src/xml/thread.xml +++ b/addons/mail/static/src/xml/thread.xml @@ -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 /> diff --git a/addons/mail/static/tests/helpers/mock_server.js b/addons/mail/static/tests/helpers/mock_server.js index 22c1783f5a3..773e4570152 100644 --- a/addons/mail/static/tests/helpers/mock_server.js +++ b/addons/mail/static/tests/helpers/mock_server.js @@ -56,6 +56,7 @@ MockServer.include({ 'mention_partner_suggestions': [], 'shortcodes': [], 'menu_id': false, + 'mail_failures': [], }); }, diff --git a/addons/mail/wizard/__init__.py b/addons/mail/wizard/__init__.py index 819ae4154f4..e6ba124f4ae 100644 --- a/addons/mail/wizard/__init__.py +++ b/addons/mail/wizard/__init__.py @@ -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 diff --git a/addons/mail/wizard/mail_resend_message.py b/addons/mail/wizard/mail_resend_message.py new file mode 100644 index 00000000000..b4480220e4f --- /dev/null +++ b/addons/mail/wizard/mail_resend_message.py @@ -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")
+
diff --git a/addons/mail/wizard/mail_resend_message_views.xml b/addons/mail/wizard/mail_resend_message_views.xml new file mode 100644 index 00000000000..85af6dc4b09 --- /dev/null +++ b/addons/mail/wizard/mail_resend_message_views.xml @@ -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>
diff --git a/addons/mass_mailing/models/mail_mail.py b/addons/mass_mailing/models/mail_mail.py index 2de2b3ec4b1..beb4952da1f 100644 --- a/addons/mass_mailing/models/mail_mail.py +++ b/addons/mass_mailing/models/mail_mail.py @@ -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) +
\ No newline at end of file diff --git a/addons/test_mail/tests/__init__.py b/addons/test_mail/tests/__init__.py index 411d73d5694..2d63b95f205 100644 --- a/addons/test_mail/tests/__init__.py +++ b/addons/test_mail/tests/__init__.py @@ -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 diff --git a/addons/test_mail/tests/test_mail_resend.py b/addons/test_mail/tests/test_mail_resend.py new file mode 100644 index 00000000000..21790062488 --- /dev/null +++ b/addons/test_mail/tests/test_mail_resend.py @@ -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
|
