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):