diff --git a/addons/sms/__manifest__.py b/addons/sms/__manifest__.py index 820b8b5a711..9cf60447885 100644 --- a/addons/sms/__manifest__.py +++ b/addons/sms/__manifest__.py @@ -24,7 +24,7 @@ The service is provided by the In App Purchase Odoo platform. 'security/ir.model.access.csv', ], 'qweb': [ - 'static/src/xml/sms_widget.xml', + 'static/src/xml/thread.xml', ], 'installable': True, 'auto_install': True, diff --git a/addons/sms/models/mail_message.py b/addons/sms/models/mail_message.py index 0581b9248c6..92fda5e8a83 100644 --- a/addons/sms/models/mail_message.py +++ b/addons/sms/models/mail_message.py @@ -15,6 +15,24 @@ class MailMessage(models.Model): _inherit = 'mail.message' message_type = fields.Selection(selection_add=[('sms', 'SMS')]) + has_sms_error = fields.Boolean( + 'Has SMS error', compute='_compute_has_sms_error', search='_search_has_sms_error', + help='Has error') + + @api.multi + def _compute_has_sms_error(self): + sms_error_from_notification = self.env['mail.notification'].sudo().search([ + ('notification_type', '=', 'sms'), + ('mail_message_id', 'in', self.ids), + ('notification_status', '=', 'exception')]).mapped('mail_message_id') + for message in self: + message.has_error = message in sms_error_from_notification + + @api.multi + def _search_has_sms_error(self, operator, operand): + if operator == '=' and operand: + return ['&', ('notification_ids.notification_status', '=', 'exception'), ('notification_ids.notification_type', '=', True)] + raise NotImplementedError() @api.multi def _format_mail_failures(self): @@ -46,11 +64,13 @@ class MailMessage(models.Model): @api.multi def _notify_sms_update(self): + """ Send bus notifications to update status of notifications in chatter. + Purpose is to send the updated status per author. + + TDE FIXME: author_id strategy seems curious, check with JS """ messages = self.env['mail.message'] for message in self: - # Check if user has access to the record before displaying a notification about it. - # In case the user switches from one company to another, it might happen that he doesn't - # have access to the record related to the notification. In this case, we skip it. + # YTI FIXME: check allowed_company_ids if necessary if message.model and message.res_id: record = self.env[message.model].browse(message.res_id) try: @@ -85,6 +105,6 @@ class MailMessage(models.Model): msgid_to_notif[notif.mail_message_id.id] += notif for message in message_values: - customer_sms_data = [(notif.id, notif.res_partner_id.display_name, notif.notification_status) for notif in msgid_to_notif.get(message['id'], [])] + customer_sms_data = [(notif.id, notif.res_partner_id.display_name or notif.sms_number, notif.notification_status) for notif in msgid_to_notif.get(message['id'], [])] message['sms_ids'] = customer_sms_data return message_values diff --git a/addons/sms/models/mail_thread.py b/addons/sms/models/mail_thread.py index b088309cbfe..a432d29a2d3 100644 --- a/addons/sms/models/mail_thread.py +++ b/addons/sms/models/mail_thread.py @@ -3,7 +3,7 @@ import logging -from odoo import api, models +from odoo import api, models, fields from odoo.addons.phone_validation.tools import phone_validation from odoo.tools import html2plaintext @@ -13,6 +13,29 @@ _logger = logging.getLogger(__name__) class MailThread(models.AbstractModel): _inherit = 'mail.thread' + message_has_sms_error = fields.Boolean( + 'SMS Delivery error', compute='_compute_message_has_sms_error', search='_search_message_has_sms_error', + help="If checked, some messages have a delivery error.") + + @api.multi + def _compute_message_has_sms_error(self): + res = {} + if self.ids: + 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.notification_type = 'sms' AND rel.notification_status in ('exception') + WHERE msg.author_id = %s AND msg.model = %s AND msg.res_id in %s AND msg.message_type != 'user_notification' + GROUP BY msg.res_id""", + (self.env.user.partner_id.id, self._name, tuple(self.ids),)) + res.update(self._cr.fetchall()) + + for record in self: + record.message_has_sms_error = bool(res.get(record._origin.id, 0)) + + @api.model + def _search_message_has_sms_error(self, operator, operand): + return ['&', ('message_ids.has_sms_error', operator, operand), ('message_ids.author_id', '=', self.env.user.partner_id.id)] + def _sms_get_default_partners(self): """ This method will likely need to be overriden by inherited models. :returns partners: recordset of res.partner diff --git a/addons/sms/static/src/js/mail_failure.js b/addons/sms/static/src/js/mail_failure.js new file mode 100644 index 00000000000..0acb8cad76a --- /dev/null +++ b/addons/sms/static/src/js/mail_failure.js @@ -0,0 +1,29 @@ +odoo.define('sms.model.MailFailure', function (require) { +'use strict'; + +var MailFailure = require('mail.model.MailFailure'); +var core = require('web.core'); +var _t = core._t; + +MailFailure.include({ + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + */ + getPreview: function () { + var preview = this._super.apply(this, arguments); + if (this._failureType === 'sms') { + _.extend(preview, { + body: _t('An error occurred when sending SMS'), + id: 'sms_failure', + }); + } + return preview; + }, +}); + +}); diff --git a/addons/sms/static/src/js/message.js b/addons/sms/static/src/js/message.js new file mode 100644 index 00000000000..f800563fd72 --- /dev/null +++ b/addons/sms/static/src/js/message.js @@ -0,0 +1,73 @@ +odoo.define('sms.model.Message', function (require) { +"use strict"; + +var Message = require('mail.model.Message'); + +Message.include({ + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Retrieves the list of SMS + * + * @returns {Array} Array of Array(smsID, recipientName, smsStatus) + */ + getSmsIds: function () { + return this._smsIds; + }, + /** + * Retrieves the SMS status + * + * @return {string} + */ + getSmsStatus: function () { + var self = this; + this._smsStatus = 'sent'; + _.each(this._smsIds, function (sms) { + if (sms[2] === 'bounce' || sms[2] === 'exception') { + self._smsStatus = 'error'; + } + }); + return this._smsStatus; + }, + /** + * Does the message contains at least one SMS failure + * + * @returns {boolean} + */ + isError: function () { + return this.getSmsStatus() === 'error'; + }, + /** + * Update the status of a SMS + * + * @param {integer} smsID ID of the SMS to update + * @param {string} smsStatus New status of the SMS + */ + setSmsStatus: function (smsID, smsStatus) { + var self = this; + var sms = _.find(self._smsIds, function (sms) { + return sms[0] === smsID; + }); + sms[2] = smsStatus; + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + * @private + */ + _setInitialData: function (data) { + this._super.apply(this, arguments); + this._smsIds = data.sms_ids; + if (this._smsStatus === false) { + this._smsStatus = 'sent'; + } + }, +}); +}); diff --git a/addons/sms/static/src/js/sms_notification_manager.js b/addons/sms/static/src/js/sms_notification_manager.js new file mode 100644 index 00000000000..7d93d96fe51 --- /dev/null +++ b/addons/sms/static/src/js/sms_notification_manager.js @@ -0,0 +1,64 @@ +odoo.define('sms.NotificationManager', function (require) { +"use strict"; + +var MailManager = require('mail.Manager'); +var MailFailure = require('mail.model.MailFailure'); + +MailManager.include({ + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @override + * @param {Object} data structure depending on the type + * @param {integer} data.id + */ + _handlePartnerNotification: function (data) { + if (data.type === 'sms_update') { + this._handleSMSUpdateNotification(data); + } else { + this._super.apply(this, arguments); + } + }, + /** + * Updates message in thread when there's an update in a SMS letter + * + * @private + * @param {Object} datas + * @param {Object[]} datas.elements list of SMS failure data + * @param {string} datas.elements[].message_id ID of related message that + * has a sms failure. + */ + _handleSMSUpdateNotification: function (datas) { + var self = this; + _.each(datas.elements, function (data) { + var isNewFailure = data.sms_status === 'error'; + var matchedFailure = _.find(self._mailFailures, function (failure) { + return failure.getMessageID() === data.message_id; + }); + + if (matchedFailure) { + var index = _.findIndex(self._mailFailures, matchedFailure); + if (isNewFailure) { + self._mailFailures[index] = new MailFailure(self, data); + } else { + self._mailFailures.splice(index, 1); + } + } else if (isNewFailure) { + self._mailFailures.push(new MailFailure(self, data)); + } + var message = _.find(self._messages, function (msg) { + return msg.getID() === data.message_id; + }); + if (message) { + message.setSmsStatus(data.sms_id, data.sms_status); + self._mailBus.trigger('update_message', message); + } + }); + this._mailBus.trigger('update_needaction', this.needactionCounter); + }, +}); + +}); diff --git a/addons/sms/static/src/js/systray_messaging_menu.js b/addons/sms/static/src/js/systray_messaging_menu.js new file mode 100644 index 00000000000..80987af37e6 --- /dev/null +++ b/addons/sms/static/src/js/systray_messaging_menu.js @@ -0,0 +1,80 @@ +odoo.define('sms.systray.MessagingMenu', function (require) { +"use strict"; + +var core = require('web.core'); +var MessagingMenu = require('mail.systray.MessagingMenu'); + +var _t = core._t; + +MessagingMenu.include({ + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Called when clicking on a preview related to a snailmail failure + * + * @private + * @param {$.Element} $target DOM of preview element clicked + */ + _clickSMSFailurePreview: function ($target) { + var documentID = $target.data('document-id'); + var documentModel = $target.data('document-model'); + if (documentModel && documentID) { + this._openDocument(documentModel, documentID); + } else if (documentModel !== 'mail.channel') { + // preview of SMS failures grouped to different document of same model + this.do_action({ + name: _t('SMS Failures'), + type: 'ir.actions.act_window', + view_mode: 'kanban,list,form', + views: [[false, 'kanban'], [false, 'list'], [false, 'form']], + target: 'current', + res_model: documentModel, + domain: [['message_has_sms_error', '=', true]], + }); + } + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @override + */ + _onClickPreview: function (ev) { + var $target = $(ev.currentTarget); + var previewID = $target.data('preview-id'); + if (previewID === 'sms_failure') { + this._clickSMSFailurePreview($target); + } else { + this._super.apply(this, arguments); + } + }, + /** + * @private + * @override + */ + _onClickPreviewMarkAsRead: function (ev) { + ev.stopPropagation(); + var $preview = $(ev.currentTarget).closest('.o_mail_preview'); + var previewID = $preview.data('preview-id'); + if (previewID === 'sms_failure') { + var documentModel = $preview.data('document-model'); + var unreadCounter = $preview.data('unread-counter'); + this.do_action('sms.sms_cancel_action', { + additional_context: { + default_model: documentModel, + unread_counter: unreadCounter + } + }); + } else { + this._super.apply(this, arguments); + } + }, +}); + +}); diff --git a/addons/sms/static/src/js/thread_widget.js b/addons/sms/static/src/js/thread_widget.js new file mode 100644 index 00000000000..4655732b34d --- /dev/null +++ b/addons/sms/static/src/js/thread_widget.js @@ -0,0 +1,89 @@ +odoo.define('sms.widget.Thread', function (require) { +"use strict"; + +var ThreadWidget = require('mail.widget.Thread'); +var core = require('web.core'); + +var QWeb = core.qweb; + +ThreadWidget.include({ + events: _.extend({}, ThreadWidget.prototype.events, { + 'click .o_thread_message_sms_error': '_onClickSMSError' + }), + /** + * @override + */ + init: function () { + this._super.apply(this, arguments); + this._enabledOptions = _.defaults(this._enabledOptions, { + displaySmsIcons: true, + }); + this._disabledOptions = _.defaults(this._disabledOptions, { + displaySmsIcons: false, + }); + }, + /** + * @override + */ + render: function (thread, options) { + this._super.apply(this, arguments); + var messages = _.clone(thread.getMessages({domain: options.domain || []})); + this._renderMessageSmsPopover(messages); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Render the popover when mouse-hovering on the mail icon of a message + * in the thread. There is at most one such popover at any given time. + * + * @private + * @param {mail.model.AbstractMessage[]} messages list of messages in the + * rendered thread, for which popover on mouseover interaction is + * permitted. + */ + _renderMessageSmsPopover: function (messages) { + if (this._messageSmsPopover) { + this._messageSmsPopover.popover('hide'); + } + if (!this.$('.o_thread_sms_tooltip').length) { + return; + } + this._messageSmsPopover = this.$('.o_thread_sms_tooltip').popover({ + html: true, + boundary: 'viewport', + placement: 'auto', + trigger: 'hover', + offset: '0, 1', + content: function () { + var messageID = $(this).data('message-id'); + var message = _.find(messages, function (message) { + return message.getID() === messageID; + }); + return QWeb.render('sms.widget.Thread.Message.SmsTooltip', { + data: message.getSmsIds() + }); + }, + }); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {MouseEvent} ev + */ + _onClickSMSError: function (ev) { + var messageID = $(ev.currentTarget).data('message-id'); + this.do_action('sms.sms_resend_action', { + additional_context: { + default_mail_message_id: messageID + } + }); + }, +}); +}); diff --git a/addons/sms/static/src/scss/thread.scss b/addons/sms/static/src/scss/thread.scss new file mode 100644 index 00000000000..ba4e54386c7 --- /dev/null +++ b/addons/sms/static/src/scss/thread.scss @@ -0,0 +1,5 @@ +i.o_thread_message_sms.o_thread_message_sms_error { + color: red; + opacity: 1; + cursor: pointer; +} diff --git a/addons/sms/static/src/xml/sms_widget.xml b/addons/sms/static/src/xml/sms_widget.xml deleted file mode 100644 index 62d356e3a70..00000000000 --- a/addons/sms/static/src/xml/sms_widget.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/addons/sms/static/src/xml/thread.xml b/addons/sms/static/src/xml/thread.xml new file mode 100644 index 00000000000..b01524ba609 --- /dev/null +++ b/addons/sms/static/src/xml/thread.xml @@ -0,0 +1,26 @@ + + diff --git a/addons/sms/views/assets.xml b/addons/sms/views/assets.xml index b3fdf38d74a..ba7f5a5914f 100644 --- a/addons/sms/views/assets.xml +++ b/addons/sms/views/assets.xml @@ -5,6 +5,12 @@ + + + + + + diff --git a/addons/test_mail_full/tests/test_sms_post.py b/addons/test_mail_full/tests/test_sms_post.py index 686ed941415..489250fde1c 100644 --- a/addons/test_mail_full/tests/test_sms_post.py +++ b/addons/test_mail_full/tests/test_sms_post.py @@ -8,6 +8,10 @@ from odoo.addons.test_mail_full.tests import common as test_mail_full_common class TestSMSPost(test_mail_full_common.BaseFunctionalTest, sms_common.MockSMS, test_mail_common.MockEmails, test_mail_common.TestRecipients): + """ TODO + + * add tests for new mail.message and mail.thread fields; + """ @classmethod def setUpClass(cls):