summaryrefslogtreecommitdiff
path: root/addons/sms/static
diff options
context:
space:
mode:
Diffstat (limited to 'addons/sms/static')
-rw-r--r--addons/sms/static/img/sms_failure.svg1
-rw-r--r--addons/sms/static/src/bugfix/bugfix.js10
-rw-r--r--addons/sms/static/src/bugfix/bugfix.scss6
-rw-r--r--addons/sms/static/src/bugfix/bugfix.xml11
-rw-r--r--addons/sms/static/src/bugfix/bugfix_tests.js18
-rw-r--r--addons/sms/static/src/components/message/message.xml20
-rw-r--r--addons/sms/static/src/components/message/message_tests.js197
-rw-r--r--addons/sms/static/src/components/notification_group/notification_group.js27
-rw-r--r--addons/sms/static/src/components/notification_group/notification_group.xml12
-rw-r--r--addons/sms/static/src/components/notification_list/notification_list_notification_group_tests.js309
-rw-r--r--addons/sms/static/src/js/fields_phone_widget.js100
-rw-r--r--addons/sms/static/src/js/fields_sms_widget.js185
-rw-r--r--addons/sms/static/src/models/message/message.js33
-rw-r--r--addons/sms/static/src/models/notification_group/notification_group.js62
-rw-r--r--addons/sms/static/tests/sms_widget_test.js229
15 files changed, 1220 insertions, 0 deletions
diff --git a/addons/sms/static/img/sms_failure.svg b/addons/sms/static/img/sms_failure.svg
new file mode 100644
index 00000000..d03ee04b
--- /dev/null
+++ b/addons/sms/static/img/sms_failure.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="512" height="512"><defs><path id="a" d="M91.974 123.535v-17.07c0-.839-.283-1.542-.851-2.111-.568-.57-1.24-.854-2.017-.854H71.894c-.777 0-1.449.285-2.017.854-.568.569-.851 1.272-.851 2.11v17.071c0 .839.283 1.542.851 2.111.568.57 1.24.854 2.017.854h17.212c.777 0 1.449-.285 2.017-.854.568-.569.851-1.272.851-2.11zm-.179-33.601l1.614-41.239c0-.718-.3-1.287-.897-1.707-.777-.659-1.494-.988-2.151-.988H70.639c-.657 0-1.374.33-2.151.988-.598.42-.897 1.048-.897 1.887l1.524 41.059c0 .599.3 1.093.897 1.482.597.39 1.314.584 2.151.584h16.584c.837 0 1.54-.195 2.107-.584.568-.39.881-.883.941-1.482zM90.54 6.02l68.846 126.5c2.092 3.773 2.032 7.546-.179 11.32a11.276 11.276 0 0 1-4.168 4.133 11.192 11.192 0 0 1-5.693 1.527H11.654c-2.032 0-3.93-.51-5.693-1.527a11.276 11.276 0 0 1-4.168-4.133c-2.211-3.774-2.271-7.547-.18-11.32L70.46 6.02a11.462 11.462 0 0 1 4.213-4.403A11.105 11.105 0 0 1 80.5 0c2.092 0 4.034.54 5.827 1.617A11.462 11.462 0 0 1 90.54 6.02z"/><path id="c" d="M91.974 123.535v-17.07c0-.839-.283-1.542-.851-2.111-.568-.57-1.24-.854-2.017-.854H71.894c-.777 0-1.449.285-2.017.854-.568.569-.851 1.272-.851 2.11v17.071c0 .839.283 1.542.851 2.111.568.57 1.24.854 2.017.854h17.212c.777 0 1.449-.285 2.017-.854.568-.569.851-1.272.851-2.11zm-.179-33.601l1.614-41.239c0-.718-.3-1.287-.897-1.707-.777-.659-1.494-.988-2.151-.988H70.639c-.657 0-1.374.33-2.151.988-.598.42-.897 1.048-.897 1.887l1.524 41.059c0 .599.3 1.093.897 1.482.597.39 1.314.584 2.151.584h16.584c.837 0 1.54-.195 2.107-.584.568-.39.881-.883.941-1.482zM90.54 6.02l68.846 126.5c2.092 3.773 2.032 7.546-.179 11.32a11.276 11.276 0 0 1-4.168 4.133 11.192 11.192 0 0 1-5.693 1.527H11.654c-2.032 0-3.93-.51-5.693-1.527a11.276 11.276 0 0 1-4.168-4.133c-2.211-3.774-2.271-7.547-.18-11.32L70.46 6.02a11.462 11.462 0 0 1 4.213-4.403A11.105 11.105 0 0 1 80.5 0c2.092 0 4.034.54 5.827 1.617A11.462 11.462 0 0 1 90.54 6.02z"/></defs><g fill="none" fill-rule="evenodd"><circle cx="256" cy="253" r="256" fill="#FDA20C"/><path fill="#000" fill-opacity=".3" fill-rule="nonzero" d="M361 98.292C361 90.982 353.978 85 345.396 85H163.604C155.022 85 148 90.981 148 98.292v296.416c0 7.31 7.022 13.292 15.604 13.292h181.792c8.582 0 15.604-5.981 15.604-13.292v-64.636c-5.462 3.323-11.703 6.646-17.945 9.305v33.399c0 1.329-.78 1.994-2.34 1.994h-172.43c-1.56 0-2.34-.665-2.34-1.994V126.87c0-1.329.78-1.993 2.34-1.993h172.43c1.56 0 2.34.664 2.34 1.993v63.113c3.901 0 8.582-.664 12.483-.664H361V98.292zM254.5 380c5.067 0 9.5 4.433 9.5 9.5s-4.433 9.5-9.5 9.5-9.5-4.433-9.5-9.5 4.433-9.5 9.5-9.5zm24.577-266.907h-47.593c-2.341 0-3.902-1.56-3.902-3.9s1.56-3.899 3.902-3.899h47.593c2.34 0 3.901 1.56 3.901 3.9 0 2.339-2.34 3.899-3.901 3.899z"/><path fill="#FFF" fill-rule="nonzero" d="M355.538 321.966c-3.9 0-8.582 0-12.483-.665v45.438c0 1.33-.78 1.996-2.34 1.996h-172.43c-1.56 0-2.34-.665-2.34-1.996V120.58c0-1.33.78-1.996 2.34-1.996h172.43c1.56 0 2.34.665 2.34 1.996v63.81c3.901 0 8.582-.666 12.483-.666H361V91.306C361 83.988 353.978 78 345.396 78H163.604C155.022 78 148 83.988 148 91.306v297.388c0 7.318 7.022 13.306 15.604 13.306h181.792c8.582 0 15.604-5.988 15.604-13.306v-66.728h-5.462zM230.703 98.871h47.594c2.34 0 3.9 1.56 3.9 3.901 0 2.34-1.56 3.902-3.12 3.902h-47.593c-2.341 0-3.902-1.561-3.902-3.902 0-2.34.78-3.901 3.121-3.901zM254.5 394c-5.067 0-9.5-4.433-9.5-9.5s4.433-9.5 9.5-9.5 9.5 4.433 9.5 9.5c0 5.7-4.433 9.5-9.5 9.5z"/><g opacity=".437" transform="translate(217 160)"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><g fill="#2F3136" mask="url(#b)"><path d="M0 0H161V161H0z"/></g></g><g transform="translate(217 149)"><mask id="d" fill="#fff"><use xlink:href="#c"/></mask><g fill="#FFF" mask="url(#d)"><path d="M0 0H161V161H0z"/></g></g></g></svg> \ No newline at end of file
diff --git a/addons/sms/static/src/bugfix/bugfix.js b/addons/sms/static/src/bugfix/bugfix.js
new file mode 100644
index 00000000..ee604b8b
--- /dev/null
+++ b/addons/sms/static/src/bugfix/bugfix.js
@@ -0,0 +1,10 @@
+/**
+ * This file allows introducing new JS modules without contaminating other files.
+ * This is useful when bug fixing requires adding such JS modules in stable
+ * versions of Odoo. Any module that is defined in this file should be isolated
+ * in its own file in master.
+ */
+odoo.define('sms/static/src/bugfix/bugfix.js', function (require) {
+'use strict';
+
+});
diff --git a/addons/sms/static/src/bugfix/bugfix.scss b/addons/sms/static/src/bugfix/bugfix.scss
new file mode 100644
index 00000000..c4272e52
--- /dev/null
+++ b/addons/sms/static/src/bugfix/bugfix.scss
@@ -0,0 +1,6 @@
+/**
+* This file allows introducing new styles without contaminating other files.
+* This is useful when bug fixing requires adding new components for instance in
+* stable versions of Odoo. Any style that is defined in this file should be isolated
+* in its own file in master.
+*/
diff --git a/addons/sms/static/src/bugfix/bugfix.xml b/addons/sms/static/src/bugfix/bugfix.xml
new file mode 100644
index 00000000..c17906f7
--- /dev/null
+++ b/addons/sms/static/src/bugfix/bugfix.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates xml:space="preserve">
+
+<!--
+ This file allows introducing new static templates without contaminating other files.
+ This is useful when bug fixing requires adding new components for instance in stable
+ versions of Odoo. Any template that is defined in this file should be isolated
+ in its own file in master.
+-->
+
+</templates>
diff --git a/addons/sms/static/src/bugfix/bugfix_tests.js b/addons/sms/static/src/bugfix/bugfix_tests.js
new file mode 100644
index 00000000..938426c6
--- /dev/null
+++ b/addons/sms/static/src/bugfix/bugfix_tests.js
@@ -0,0 +1,18 @@
+odoo.define('sms/static/src/bugfix/bugfix_tests.js', function (require) {
+'use strict';
+
+/**
+ * This file allows introducing new QUnit test modules without contaminating
+ * other test files. This is useful when bug fixing requires adding new
+ * components for instance in stable versions of Odoo. Any test that is defined
+ * in this file should be isolated in its own file in master.
+ */
+QUnit.module('sms', {}, function () {
+QUnit.module('bugfix', {}, function () {
+QUnit.module('bugfix_tests.js', {
+
+});
+});
+});
+
+});
diff --git a/addons/sms/static/src/components/message/message.xml b/addons/sms/static/src/components/message/message.xml
new file mode 100644
index 00000000..b8b6a18d
--- /dev/null
+++ b/addons/sms/static/src/components/message/message.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates xml:space="preserve">
+
+ <t t-inherit="mail.Message" t-inherit-mode="extension">
+ <xpath expr="//*[@name='failureIcon']" position="replace">
+ <t t-if="message.message_type === 'sms'">
+ <i class="o_Message_notificationIcon fa fa-mobile"/> SMS
+ </t>
+ <t t-else="">$0</t>
+ </xpath>
+
+ <xpath expr="//*[@name='notificationIcon']" position="replace">
+ <t t-if="message.message_type === 'sms'">
+ <i class="o_Message_notificationIcon fa fa-mobile"/> SMS
+ </t>
+ <t t-else="">$0</t>
+ </xpath>
+ </t>
+
+</templates>
diff --git a/addons/sms/static/src/components/message/message_tests.js b/addons/sms/static/src/components/message/message_tests.js
new file mode 100644
index 00000000..57bc21ab
--- /dev/null
+++ b/addons/sms/static/src/components/message/message_tests.js
@@ -0,0 +1,197 @@
+odoo.define('sms/static/src/components/message/message_tests.js', function (require) {
+'use strict';
+
+const components = {
+ Message: require('mail/static/src/components/message/message.js'),
+};
+const { makeDeferred } = require('mail/static/src/utils/deferred/deferred.js');
+const {
+ afterEach,
+ afterNextRender,
+ beforeEach,
+ createRootComponent,
+ start,
+} = require('mail/static/src/utils/test_utils.js');
+
+const Bus = require('web.Bus');
+
+QUnit.module('sms', {}, function () {
+QUnit.module('components', {}, function () {
+QUnit.module('message', {}, function () {
+QUnit.module('message_tests.js', {
+ beforeEach() {
+ beforeEach(this);
+
+ this.createMessageComponent = async (message, otherProps) => {
+ const props = Object.assign({ messageLocalId: message.localId }, otherProps);
+ await createRootComponent(this, components.Message, {
+ props,
+ target: this.widget.el,
+ });
+ };
+
+ this.start = async params => {
+ const { env, widget } = await start(Object.assign({}, params, {
+ data: this.data,
+ }));
+ this.env = env;
+ this.widget = widget;
+ };
+ },
+ afterEach() {
+ afterEach(this);
+ },
+});
+
+QUnit.test('Notification Sent', async function (assert) {
+ assert.expect(9);
+
+ await this.start();
+ const threadViewer = this.env.models['mail.thread_viewer'].create({
+ hasThreadView: true,
+ thread: [['create', {
+ id: 11,
+ model: 'mail.channel',
+ }]],
+ });
+ const message = this.env.models['mail.message'].create({
+ id: 10,
+ message_type: 'sms',
+ notifications: [['insert', {
+ id: 11,
+ notification_status: 'sent',
+ notification_type: 'sms',
+ partner: [['insert', { id: 12, name: "Someone" }]],
+ }]],
+ originThread: [['link', threadViewer.thread]]
+ });
+ await this.createMessageComponent(message, {
+ threadViewLocalId: threadViewer.threadView.localId
+ });
+
+ assert.containsOnce(
+ document.body,
+ '.o_Message',
+ "should display a message component"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_Message_notificationIconClickable',
+ "should display the notification icon container"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_Message_notificationIcon',
+ "should display the notification icon"
+ );
+ assert.hasClass(
+ document.querySelector('.o_Message_notificationIcon'),
+ 'fa-mobile',
+ "icon should represent sms"
+ );
+
+ await afterNextRender(() => {
+ document.querySelector('.o_Message_notificationIconClickable').click();
+ });
+ assert.containsOnce(
+ document.body,
+ '.o_NotificationPopover',
+ "notification popover should be open"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_NotificationPopover_notificationIcon',
+ "popover should have one icon"
+ );
+ assert.hasClass(
+ document.querySelector('.o_NotificationPopover_notificationIcon'),
+ 'fa-check',
+ "popover should have the sent icon"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_NotificationPopover_notificationPartnerName',
+ "popover should have the partner name"
+ );
+ assert.strictEqual(
+ document.querySelector('.o_NotificationPopover_notificationPartnerName').textContent.trim(),
+ "Someone",
+ "partner name should be correct"
+ );
+});
+
+QUnit.test('Notification Error', async function (assert) {
+ assert.expect(8);
+
+ const openResendActionDef = makeDeferred();
+ const bus = new Bus();
+ bus.on('do-action', null, payload => {
+ assert.step('do_action');
+ assert.strictEqual(
+ payload.action,
+ 'sms.sms_resend_action',
+ "action should be the one to resend sms"
+ );
+ assert.strictEqual(
+ payload.options.additional_context.default_mail_message_id,
+ 10,
+ "action should have correct message id"
+ );
+ openResendActionDef.resolve();
+ });
+
+ await this.start({ env: { bus } });
+ const threadViewer = this.env.models['mail.thread_viewer'].create({
+ hasThreadView: true,
+ thread: [['create', {
+ id: 11,
+ model: 'mail.channel',
+ }]],
+ });
+ const message = this.env.models['mail.message'].create({
+ id: 10,
+ message_type: 'sms',
+ notifications: [['insert', {
+ id: 11,
+ notification_status: 'exception',
+ notification_type: 'sms',
+ }]],
+ originThread: [['link', threadViewer.thread]]
+ });
+ await this.createMessageComponent(message, {
+ threadViewLocalId: threadViewer.threadView.localId
+ });
+
+ assert.containsOnce(
+ document.body,
+ '.o_Message',
+ "should display a message component"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_Message_notificationIconClickable',
+ "should display the notification icon container"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_Message_notificationIcon',
+ "should display the notification icon"
+ );
+ assert.hasClass(
+ document.querySelector('.o_Message_notificationIcon'),
+ 'fa-mobile',
+ "icon should represent sms"
+ );
+ document.querySelector('.o_Message_notificationIconClickable').click();
+ await openResendActionDef;
+ assert.verifySteps(
+ ['do_action'],
+ "should do an action to display the resend sms dialog"
+ );
+});
+
+});
+});
+});
+
+});
diff --git a/addons/sms/static/src/components/notification_group/notification_group.js b/addons/sms/static/src/components/notification_group/notification_group.js
new file mode 100644
index 00000000..053fedc5
--- /dev/null
+++ b/addons/sms/static/src/components/notification_group/notification_group.js
@@ -0,0 +1,27 @@
+odoo.define('sms/static/src/components/notification_group/notification_group.js', function (require) {
+'use strict';
+
+const components = {
+ NotificationGroup: require('mail/static/src/components/notification_group/notification_group.js'),
+};
+
+const { patch } = require('web.utils');
+
+patch(components.NotificationGroup, 'sms/static/src/components/notification_group/notification_group.js', {
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ image() {
+ if (this.group.notification_type === 'sms') {
+ return '/sms/static/img/sms_failure.svg';
+ }
+ return this._super(...arguments);
+ },
+});
+
+});
diff --git a/addons/sms/static/src/components/notification_group/notification_group.xml b/addons/sms/static/src/components/notification_group/notification_group.xml
new file mode 100644
index 00000000..c5f5a8db
--- /dev/null
+++ b/addons/sms/static/src/components/notification_group/notification_group.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates xml:space="preserve">
+
+ <t t-inherit="mail.NotificationGroup" t-inherit-mode="extension">
+ <xpath expr="//*[hasclass('o_NotificationGroup_inlineText')]" position="inside">
+ <t t-if="group.notification_type === 'sms'">
+ An error occurred when sending an SMS.
+ </t>
+ </xpath>
+ </t>
+
+</templates>
diff --git a/addons/sms/static/src/components/notification_list/notification_list_notification_group_tests.js b/addons/sms/static/src/components/notification_list/notification_list_notification_group_tests.js
new file mode 100644
index 00000000..bd5d8402
--- /dev/null
+++ b/addons/sms/static/src/components/notification_list/notification_list_notification_group_tests.js
@@ -0,0 +1,309 @@
+odoo.define('sms/static/src/components/notification_list/notification_list_notification_group_tests.js', function (require) {
+'use strict';
+
+const components = {
+ NotificationList: require('mail/static/src/components/notification_list/notification_list.js'),
+};
+
+const {
+ afterEach,
+ beforeEach,
+ createRootComponent,
+ start,
+} = require('mail/static/src/utils/test_utils.js');
+
+const Bus = require('web.Bus');
+
+QUnit.module('sms', {}, function () {
+QUnit.module('components', {}, function () {
+QUnit.module('notification_list', {}, function () {
+QUnit.module('notification_list_notification_group_tests.js', {
+ beforeEach() {
+ beforeEach(this);
+
+ /**
+ * @param {Object} param0
+ * @param {string} [param0.filter='all']
+ */
+ this.createNotificationListComponent = async ({ filter = 'all' } = {}) => {
+ await createRootComponent(this, components.NotificationList, {
+ props: { filter },
+ target: this.widget.el,
+ });
+ };
+
+ this.start = async params => {
+ const { env, widget } = await start(Object.assign({}, params, {
+ data: this.data,
+ }));
+ this.env = env;
+ this.widget = widget;
+ };
+ },
+ afterEach() {
+ afterEach(this);
+ },
+});
+
+QUnit.test('mark as read', async function (assert) {
+ assert.expect(6);
+
+ this.data['mail.message'].records.push(
+ // message that is expected to have a failure
+ {
+ id: 11, // random unique id, will be used to link failure to message
+ message_type: 'sms', // message must be sms (goal of the test)
+ model: 'mail.channel', // expected value to link message to channel
+ res_id: 31, // id of a random channel
+ }
+ );
+ this.data['mail.notification'].records.push(
+ // failure that is expected to be used in the test
+ {
+ mail_message_id: 11, // id of the related message
+ notification_status: 'exception', // necessary value to have a failure
+ notification_type: 'sms', // expected failure type for sms message
+ }
+ );
+ const bus = new Bus();
+ bus.on('do-action', null, payload => {
+ assert.step('do_action');
+ assert.strictEqual(
+ payload.action,
+ 'sms.sms_cancel_action',
+ "action should be the one to cancel sms"
+ );
+ assert.strictEqual(
+ payload.options.additional_context.default_model,
+ 'mail.channel',
+ "action should have the group model as default_model"
+ );
+ assert.strictEqual(
+ payload.options.additional_context.unread_counter,
+ 1,
+ "action should have the group notification length as unread_counter"
+ );
+ });
+
+ await this.start({ env: { bus } });
+ await this.createNotificationListComponent();
+
+ assert.containsOnce(
+ document.body,
+ '.o_NotificationGroup_markAsRead',
+ "should have 1 mark as read button"
+ );
+
+ document.querySelector('.o_NotificationGroup_markAsRead').click();
+ assert.verifySteps(
+ ['do_action'],
+ "should do an action to display the cancel sms dialog"
+ );
+});
+
+QUnit.test('notifications grouped by notification_type', async function (assert) {
+ assert.expect(11);
+
+ this.data['mail.message'].records.push(
+ // first message that is expected to have a failure
+ {
+ id: 11, // random unique id, will be used to link failure to message
+ message_type: 'email', // different type from second message
+ model: 'res.partner', // same model as second message (and not `mail.channel`)
+ res_id: 31, // same res_id as second message
+ res_model_name: "Partner", // random related model name
+ },
+ // second message that is expected to have a failure
+ {
+ id: 12, // random unique id, will be used to link failure to message
+ message_type: 'sms', // different type from first message
+ model: 'res.partner', // same model as first message (and not `mail.channel`)
+ res_id: 31, // same res_id as first message
+ res_model_name: "Partner", // same related model name for consistency
+ }
+ );
+ this.data['mail.notification'].records.push(
+ // first failure that is expected to be used in the test
+ {
+ mail_message_id: 11, // id of the related first message
+ notification_status: 'exception', // necessary value to have a failure
+ notification_type: 'email', // different type from second failure
+ },
+ // second failure that is expected to be used in the test
+ {
+ mail_message_id: 12, // id of the related second message
+ notification_status: 'exception', // necessary value to have a failure
+ notification_type: 'sms', // different type from first failure
+ }
+ );
+ await this.start();
+ await this.createNotificationListComponent();
+
+ assert.containsN(
+ document.body,
+ '.o_NotificationGroup',
+ 2,
+ "should have 2 notifications group"
+ );
+ const groups = document.querySelectorAll('.o_NotificationGroup');
+ assert.containsOnce(
+ groups[0],
+ '.o_NotificationGroup_name',
+ "should have 1 group name in first group"
+ );
+ assert.strictEqual(
+ groups[0].querySelector('.o_NotificationGroup_name').textContent,
+ "Partner",
+ "should have model name as group name"
+ );
+ assert.containsOnce(
+ groups[0],
+ '.o_NotificationGroup_counter',
+ "should have 1 group counter in first group"
+ );
+ assert.strictEqual(
+ groups[0].querySelector('.o_NotificationGroup_counter').textContent.trim(),
+ "(1)",
+ "should have 1 notification in first group"
+ );
+ assert.strictEqual(
+ groups[0].querySelector('.o_NotificationGroup_inlineText').textContent.trim(),
+ "An error occurred when sending an email.",
+ "should have the group text corresponding to email"
+ );
+ assert.containsOnce(
+ groups[1],
+ '.o_NotificationGroup_name',
+ "should have 1 group name in second group"
+ );
+ assert.strictEqual(
+ groups[1].querySelector('.o_NotificationGroup_name').textContent,
+ "Partner",
+ "should have second model name as group name"
+ );
+ assert.containsOnce(
+ groups[1],
+ '.o_NotificationGroup_counter',
+ "should have 1 group counter in second group"
+ );
+ assert.strictEqual(
+ groups[1].querySelector('.o_NotificationGroup_counter').textContent.trim(),
+ "(1)",
+ "should have 1 notification in second group"
+ );
+ assert.strictEqual(
+ groups[1].querySelector('.o_NotificationGroup_inlineText').textContent.trim(),
+ "An error occurred when sending an SMS.",
+ "should have the group text corresponding to sms"
+ );
+});
+
+QUnit.test('grouped notifications by document model', async function (assert) {
+ // If all failures linked to a document model refers to different documents,
+ // a single notification should group all failures that are linked to this
+ // document model.
+ assert.expect(12);
+
+ this.data['mail.message'].records.push(
+ // first message that is expected to have a failure
+ {
+ id: 11, // random unique id, will be used to link failure to message
+ message_type: 'sms', // message must be sms (goal of the test)
+ model: 'res.partner', // same model as second message (and not `mail.channel`)
+ res_id: 31, // different res_id from second message
+ res_model_name: "Partner", // random related model name
+ },
+ // second message that is expected to have a failure
+ {
+ id: 12, // random unique id, will be used to link failure to message
+ message_type: 'sms', // message must be sms (goal of the test)
+ model: 'res.partner', // same model as first message (and not `mail.channel`)
+ res_id: 32, // different res_id from first message
+ res_model_name: "Partner", // same related model name for consistency
+ }
+ );
+ this.data['mail.notification'].records.push(
+ // first failure that is expected to be used in the test
+ {
+ mail_message_id: 11, // id of the related first message
+ notification_status: 'exception', // necessary value to have a failure
+ notification_type: 'sms', // expected failure type for sms message
+ },
+ // second failure that is expected to be used in the test
+ {
+ mail_message_id: 12, // id of the related second message
+ notification_status: 'exception', // necessary value to have a failure
+ notification_type: 'sms', // expected failure type for sms message
+ }
+ );
+ const bus = new Bus();
+ bus.on('do-action', null, payload => {
+ assert.step('do_action');
+ assert.strictEqual(
+ payload.action.name,
+ "SMS Failures",
+ "action should have 'SMS Failures' as name",
+ );
+ assert.strictEqual(
+ payload.action.type,
+ 'ir.actions.act_window',
+ "action should have the type act_window"
+ );
+ assert.strictEqual(
+ payload.action.view_mode,
+ 'kanban,list,form',
+ "action should have 'kanban,list,form' as view_mode"
+ );
+ assert.strictEqual(
+ JSON.stringify(payload.action.views),
+ JSON.stringify([[false, 'kanban'], [false, 'list'], [false, 'form']]),
+ "action should have correct views"
+ );
+ assert.strictEqual(
+ payload.action.target,
+ 'current',
+ "action should have 'current' as target"
+ );
+ assert.strictEqual(
+ payload.action.res_model,
+ 'res.partner',
+ "action should have the group model as res_model"
+ );
+ assert.strictEqual(
+ JSON.stringify(payload.action.domain),
+ JSON.stringify([['message_has_sms_error', '=', true]]),
+ "action should have 'message_has_sms_error' as domain"
+ );
+ });
+
+ await this.start({ env: { bus } });
+ await this.createNotificationListComponent();
+
+ assert.containsOnce(
+ document.body,
+ '.o_NotificationGroup',
+ "should have 1 notification group"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_NotificationGroup_counter',
+ "should have 1 group counter"
+ );
+ assert.strictEqual(
+ document.querySelector('.o_NotificationGroup_counter').textContent.trim(),
+ "(2)",
+ "should have 2 notifications in the group"
+ );
+
+ document.querySelector('.o_NotificationGroup').click();
+ assert.verifySteps(
+ ['do_action'],
+ "should do an action to display the related records"
+ );
+});
+
+});
+});
+});
+
+});
diff --git a/addons/sms/static/src/js/fields_phone_widget.js b/addons/sms/static/src/js/fields_phone_widget.js
new file mode 100644
index 00000000..82b8e5b3
--- /dev/null
+++ b/addons/sms/static/src/js/fields_phone_widget.js
@@ -0,0 +1,100 @@
+odoo.define('sms.fields', function (require) {
+"use strict";
+
+var basic_fields = require('web.basic_fields');
+var core = require('web.core');
+var session = require('web.session');
+
+var _t = core._t;
+
+/**
+ * Override of FieldPhone to add a button calling SMS composer if option activated (default)
+ */
+
+var Phone = basic_fields.FieldPhone;
+Phone.include({
+ /**
+ * By default, enable_sms is activated
+ *
+ * @override
+ */
+ init() {
+ this._super.apply(this, arguments);
+ this.enableSMS = 'enable_sms' in this.attrs.options ? this.attrs.options.enable_sms : true;
+ // reinject in nodeOptions (and thus in this.attrs) to signal the property
+ this.attrs.options.enable_sms = this.enableSMS;
+ },
+ /**
+ * When the send SMS button is displayed, $el becomes a div wrapping
+ * the original links.
+ * This method makes sure we always focus the phone number
+ *
+ * @override
+ */
+ getFocusableElement() {
+ if (this.enableSMS && this.mode === 'readonly') {
+ return this.$el.filter('.' + this.className);
+ }
+ return this._super.apply(this, arguments);
+ },
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Open SMS composer wizard
+ *
+ * @private
+ */
+ _onClickSMS: function (ev) {
+ ev.preventDefault();
+ ev.stopPropagation();
+
+ var context = session.user_context;
+ context = _.extend({}, context, {
+ default_res_model: this.model,
+ default_res_id: parseInt(this.res_id),
+ default_number_field_name: this.name,
+ default_composition_mode: 'comment',
+ });
+ var self = this;
+ return this.do_action({
+ title: _t('Send SMS Text Message'),
+ type: 'ir.actions.act_window',
+ res_model: 'sms.composer',
+ target: 'new',
+ views: [[false, 'form']],
+ context: context,
+ }, {
+ on_close: function () {
+ self.trigger_up('reload');
+ }});
+ },
+
+ /**
+ * Add a button to call the composer wizard
+ *
+ * @override
+ * @private
+ */
+ _renderReadonly: function () {
+ var def = this._super.apply(this, arguments);
+ if (this.enableSMS && this.value) {
+ var $composerButton = $('<a>', {
+ title: _t('Send SMS Text Message'),
+ href: '',
+ class: 'ml-3 d-inline-flex align-items-center o_field_phone_sms',
+ html: $('<small>', {class: 'font-weight-bold ml-1', html: 'SMS'}),
+ });
+ $composerButton.prepend($('<i>', {class: 'fa fa-mobile'}));
+ $composerButton.on('click', this._onClickSMS.bind(this));
+ this.$el = this.$el.add($composerButton);
+ }
+
+ return def;
+ },
+});
+
+return Phone;
+
+});
diff --git a/addons/sms/static/src/js/fields_sms_widget.js b/addons/sms/static/src/js/fields_sms_widget.js
new file mode 100644
index 00000000..5a52d68e
--- /dev/null
+++ b/addons/sms/static/src/js/fields_sms_widget.js
@@ -0,0 +1,185 @@
+odoo.define('sms.sms_widget', function (require) {
+"use strict";
+
+var core = require('web.core');
+var fieldRegistry = require('web.field_registry');
+var FieldTextEmojis = require('mail.field_text_emojis');
+
+var _t = core._t;
+/**
+ * SmsWidget is a widget to display a textarea (the body) and a text representing
+ * the number of SMS and the number of characters. This text is computed every
+ * time the user changes the body.
+ */
+var SmsWidget = FieldTextEmojis.extend({
+ className: 'o_field_text',
+ enableEmojis: false,
+ /**
+ * @constructor
+ */
+ init: function () {
+ this._super.apply(this, arguments);
+ this.nbrChar = 0;
+ this.nbrSMS = 0;
+ this.encoding = 'GSM7';
+ this.enableEmojis = !!this.nodeOptions.enable_emojis;
+ },
+
+ /**
+ * @override
+ *"This will add the emoji dropdown to a target field (controlled by the "enableEmojis" attribute)
+ */
+ on_attach_callback: function () {
+ if (this.enableEmojis) {
+ this._super.apply(this, arguments);
+ }
+ },
+
+ //--------------------------------------------------------------------------
+ // Private: override widget
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ * @override
+ */
+ _renderEdit: function () {
+ var def = this._super.apply(this, arguments);
+
+ this._compute();
+ $('.o_sms_container').remove();
+ var $sms_container = $('<div class="o_sms_container"/>');
+ $sms_container.append(this._renderSMSInfo());
+ $sms_container.append(this._renderIAPButton());
+ this.$el = this.$el.add($sms_container);
+
+ return def;
+ },
+
+ //--------------------------------------------------------------------------
+ // Private: SMS
+ //--------------------------------------------------------------------------
+
+ /**
+ * Compute the number of characters and sms
+ * @private
+ */
+ _compute: function () {
+ var content = this._getValue();
+ this.encoding = this._extractEncoding(content);
+ this.nbrChar = content.length;
+ this.nbrChar += (content.match(/\n/g) || []).length;
+ this.nbrSMS = this._countSMS(this.nbrChar, this.encoding);
+ },
+
+ /**
+ * Count the number of SMS of the content
+ * @private
+ * @returns {integer} Number of SMS
+ */
+ _countSMS: function () {
+ if (this.nbrChar === 0) {
+ return 0;
+ }
+ if (this.encoding === 'UNICODE') {
+ if (this.nbrChar <= 70) {
+ return 1;
+ }
+ return Math.ceil(this.nbrChar / 67);
+ }
+ if (this.nbrChar <= 160) {
+ return 1;
+ }
+ return Math.ceil(this.nbrChar / 153);
+ },
+
+ /**
+ * Extract the encoding depending on the characters in the content
+ * @private
+ * @param {String} content Content of the SMS
+ * @returns {String} Encoding of the content (GSM7 or UNICODE)
+ */
+ _extractEncoding: function (content) {
+ if (String(content).match(RegExp("^[@£$¥èéùìòÇ\\nØø\\rÅåΔ_ΦΓΛΩΠΨΣΘΞÆæßÉ !\\\"#¤%&'()*+,-./0123456789:;<=>?¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ¿abcdefghijklmnopqrstuvwxyzäöñüà]*$"))) {
+ return 'GSM7';
+ }
+ return 'UNICODE';
+ },
+
+ /**
+ * Render the IAP button to redirect to IAP pricing
+ * @private
+ */
+ _renderIAPButton: function () {
+ return $('<a>', {
+ 'href': 'https://iap-services.odoo.com/iap/sms/pricing',
+ 'target': '_blank',
+ 'title': _t('SMS Pricing'),
+ 'aria-label': _t('SMS Pricing'),
+ 'class': 'fa fa-lg fa-info-circle',
+ });
+ },
+
+ /**
+ * Render the number of characters, sms and the encoding.
+ * @private
+ */
+ _renderSMSInfo: function () {
+ var string = _.str.sprintf(_t('%s characters, fits in %s SMS (%s) '), this.nbrChar, this.nbrSMS, this.encoding);
+ var $span = $('<span>', {
+ 'class': 'text-muted o_sms_count',
+ });
+ $span.text(string);
+ return $span;
+ },
+
+ /**
+ * Update widget SMS information with re-computed info about length, ...
+ * @private
+ */
+ _updateSMSInfo: function () {
+ this._compute();
+ var string = _.str.sprintf(_t('%s characters, fits in %s SMS (%s) '), this.nbrChar, this.nbrSMS, this.encoding);
+ this.$('.o_sms_count').text(string);
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ * @private
+ */
+ _onBlur: function () {
+ var content = this._getValue();
+ if( !content.trim().length && content.length > 0) {
+ this.do_warn(_t("Your SMS Text Message must include at least one non-whitespace character"));
+ this.$input.val(content.trim());
+ this._updateSMSInfo();
+ }
+ },
+
+ /**
+ * @override
+ * @private
+ */
+ _onChange: function () {
+ this._super.apply(this, arguments);
+ this._updateSMSInfo();
+ },
+
+ /**
+ * @override
+ * @private
+ */
+ _onInput: function () {
+ this._super.apply(this, arguments);
+ this._updateSMSInfo();
+ },
+});
+
+fieldRegistry.add('sms_widget', SmsWidget);
+
+return SmsWidget;
+});
diff --git a/addons/sms/static/src/models/message/message.js b/addons/sms/static/src/models/message/message.js
new file mode 100644
index 00000000..2f468cc0
--- /dev/null
+++ b/addons/sms/static/src/models/message/message.js
@@ -0,0 +1,33 @@
+odoo.define('sms/static/src/models/message/message.js', function (require) {
+'use strict';
+
+const {
+ registerInstancePatchModel,
+} = require('mail/static/src/model/model_core.js');
+
+registerInstancePatchModel('mail.message', 'sms/static/src/models/message/message.js', {
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ openResendAction() {
+ if (this.message_type === 'sms') {
+ this.env.bus.trigger('do-action', {
+ action: 'sms.sms_resend_action',
+ options: {
+ additional_context: {
+ default_mail_message_id: this.id,
+ },
+ },
+ });
+ } else {
+ this._super(...arguments);
+ }
+ },
+});
+
+});
diff --git a/addons/sms/static/src/models/notification_group/notification_group.js b/addons/sms/static/src/models/notification_group/notification_group.js
new file mode 100644
index 00000000..887d9a7d
--- /dev/null
+++ b/addons/sms/static/src/models/notification_group/notification_group.js
@@ -0,0 +1,62 @@
+odoo.define('sms/static/src/models/notification_group/notification_group.js', function (require) {
+'use strict';
+
+const {
+ registerInstancePatchModel,
+} = require('mail/static/src/model/model_core.js');
+
+registerInstancePatchModel('mail.notification_group', 'sms/static/src/models/notification_group/notification_group.js', {
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ openCancelAction() {
+ if (this.notification_type !== 'sms') {
+ return this._super(...arguments);
+ }
+ this.env.bus.trigger('do-action', {
+ action: 'sms.sms_cancel_action',
+ options: {
+ additional_context: {
+ default_model: this.res_model,
+ unread_counter: this.notifications.length,
+ },
+ },
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ _openDocuments() {
+ if (this.notification_type !== 'sms') {
+ return this._super(...arguments);
+ }
+ this.env.bus.trigger('do-action', {
+ action: {
+ name: this.env._t("SMS Failures"),
+ type: 'ir.actions.act_window',
+ view_mode: 'kanban,list,form',
+ views: [[false, 'kanban'], [false, 'list'], [false, 'form']],
+ target: 'current',
+ res_model: this.res_model,
+ domain: [['message_has_sms_error', '=', true]],
+ },
+ });
+ if (this.env.messaging.device.isMobile) {
+ // messaging menu has a higher z-index than views so it must
+ // be closed to ensure the visibility of the view
+ this.env.messaging.messagingMenu.close();
+ }
+ },
+});
+
+});
diff --git a/addons/sms/static/tests/sms_widget_test.js b/addons/sms/static/tests/sms_widget_test.js
new file mode 100644
index 00000000..3da34dcd
--- /dev/null
+++ b/addons/sms/static/tests/sms_widget_test.js
@@ -0,0 +1,229 @@
+odoo.define('sms.sms_widget_tests', function (require) {
+"use strict";
+
+var config = require('web.config');
+var FormView = require('web.FormView');
+var ListView = require('web.ListView');
+var testUtils = require('web.test_utils');
+
+var createView = testUtils.createView;
+
+QUnit.module('fields', {
+ beforeEach: function () {
+ this.data = {
+ partner: {
+ fields: {
+ message: {string: "message", type: "text"},
+ foo: {string: "Foo", type: "char", default: "My little Foo Value"},
+ mobile: {string: "mobile", type: "text"},
+ },
+ records: [{
+ id: 1,
+ message: "",
+ foo: 'yop',
+ mobile: "+32494444444",
+ }, {
+ id: 2,
+ message: "",
+ foo: 'bayou',
+ }]
+ },
+ visitor: {
+ fields: {
+ mobile: {string: "mobile", type: "text"},
+ },
+ records: [{
+ id: 1,
+ mobile: "+32494444444",
+ }]
+ },
+ };
+ }
+}, function () {
+
+ QUnit.module('SmsWidget');
+
+ QUnit.test('Sms widgets are correctly rendered', async function (assert) {
+ assert.expect(9);
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form><sheet><field name="message" widget="sms_widget"/></sheet></form>',
+ });
+
+ assert.containsOnce(form, '.o_sms_count', "Should have a sms counter");
+ assert.strictEqual(form.$('.o_sms_count').text(), '0 characters, fits in 0 SMS (GSM7) ',
+ 'Should be "0 characters, fits in 0 SMS (GSM7) " by default');
+ // GSM-7
+ await testUtils.fields.editAndTrigger(form.$('.o_input'), "Hello from Odoo", 'input');
+ assert.strictEqual(form.$('.o_sms_count').text(), '15 characters, fits in 1 SMS (GSM7) ',
+ 'Should be "15 characters, fits in 1 SMS (GSM7) " for "Hello from Odoo"');
+ // GSM-7 with \n => this one count as 2 characters
+ form.$('.o_input').val("Hello from Odoo\n").trigger('input');
+ assert.strictEqual(form.$('.o_sms_count').text(), '17 characters, fits in 1 SMS (GSM7) ',
+ 'Should be "17 characters, fits in 1 SMS (GSM7) " for "Hello from Odoo\\n"');
+ // Unicode => ê
+ form.$('.o_input').val("Hêllo from Odoo").trigger('input');
+ assert.strictEqual(form.$('.o_sms_count').text(), '15 characters, fits in 1 SMS (UNICODE) ',
+ 'Should be "15 characters, fits in 1 SMS (UNICODE) " for "Hêllo from Odoo"');
+ // GSM-7 with 160c
+ var text = Array(161).join('a');
+ await testUtils.fields.editAndTrigger(form.$('.o_input'), text, 'input');
+ assert.strictEqual(form.$('.o_sms_count').text(), '160 characters, fits in 1 SMS (GSM7) ',
+ 'Should be "160 characters, fits in 1 SMS (GSM7) " for 160 x "a"');
+ // GSM-7 with 161c
+ text = Array(162).join('a');
+ await testUtils.fields.editAndTrigger(form.$('.o_input'), text, 'input');
+ assert.strictEqual(form.$('.o_sms_count').text(), '161 characters, fits in 2 SMS (GSM7) ',
+ 'Should be "161 characters, fits in 2 SMS (GSM7) " for 161 x "a"');
+ // Unicode with 70c
+ text = Array(71).join('ê');
+ await testUtils.fields.editAndTrigger(form.$('.o_input'), text, 'input');
+ assert.strictEqual(form.$('.o_sms_count').text(), '70 characters, fits in 1 SMS (UNICODE) ',
+ 'Should be "70 characters, fits in 1 SMS (UNICODE) " for 70 x "ê"');
+ // Unicode with 71c
+ text = Array(72).join('ê');
+ await testUtils.fields.editAndTrigger(form.$('.o_input'), text, 'input');
+ assert.strictEqual(form.$('.o_sms_count').text(), '71 characters, fits in 2 SMS (UNICODE) ',
+ 'Should be "71 characters, fits in 2 SMS (UNICODE) " for 71 x "ê"');
+
+ form.destroy();
+ });
+
+ QUnit.test('Sms widgets with non-empty initial value', async function (assert) {
+ assert.expect(1);
+ var form = await createView({
+ View: FormView,
+ model: 'visitor',
+ data: this.data,
+ arch: `<form><sheet><field name="mobile" widget="sms_widget"/></sheet></form>`,
+ res_id: 1,
+ res_ids: [1],
+ });
+
+ assert.strictEqual(form.$('.o_field_text').text(), '+32494444444',
+ 'Should have the initial value');
+
+ form.destroy();
+ });
+
+ QUnit.test('Sms widgets with empty initial value', async function (assert) {
+ assert.expect(1);
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `<form><sheet><field name="message" widget="sms_widget"/></sheet></form>`,
+ res_id: 1,
+ res_ids: [1],
+ });
+
+ assert.strictEqual(form.$('.o_field_text').text(), '',
+ 'Should have the empty initial value');
+
+ form.destroy();
+ });
+
+ QUnit.module('PhoneWidget');
+
+ QUnit.test('phone field in editable list view on normal screens', async function (assert) {
+ assert.expect(11);
+ var doActionCount = 0;
+
+ var list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ debug:true,
+ arch: '<tree editable="bottom"><field name="foo" widget="phone"/></tree>',
+ intercepts: {
+ do_action(ev) {
+ assert.equal(ev.data.action.res_model, 'sms.composer',
+ 'The action to send an SMS should have been executed');
+ doActionCount += 1;
+ }
+ }
+ });
+
+ assert.containsN(list, 'tbody td:not(.o_list_record_selector)', 4);
+ assert.strictEqual(list.$('tbody td:not(.o_list_record_selector)').first().text(), 'yopSMS',
+ "value should be displayed properly with a link to send SMS");
+
+ assert.containsN(list, 'a.o_field_widget.o_form_uri', 2,
+ "should have the correct classnames");
+
+ // Edit a line and check the result
+ var $cell = list.$('tbody td:not(.o_list_record_selector)').first();
+ await testUtils.dom.click($cell);
+ assert.hasClass($cell.parent(),'o_selected_row', 'should be set as edit mode');
+ assert.strictEqual($cell.find('input').val(), 'yop',
+ 'should have the corect value in internal input');
+ await testUtils.fields.editInput($cell.find('input'), 'new');
+
+ // save
+ await testUtils.dom.click(list.$buttons.find('.o_list_button_save'));
+ $cell = list.$('tbody td:not(.o_list_record_selector)').first();
+ assert.doesNotHaveClass($cell.parent(), 'o_selected_row', 'should not be in edit mode anymore');
+ assert.strictEqual(list.$('tbody td:not(.o_list_record_selector)').first().text(), 'newSMS',
+ "value should be properly updated");
+ assert.containsN(list, 'a.o_field_widget.o_form_uri', 2,
+ "should still have links with correct classes");
+
+ await testUtils.dom.click(list.$('tbody td:not(.o_list_record_selector) .o_field_phone_sms').first());
+ assert.equal(doActionCount, 1, 'Only one action should have been executed');
+ assert.containsNone(list, '.o_selected_row',
+ 'None of the list element should have been activated');
+
+ list.destroy();
+ });
+
+ QUnit.test('readonly sms phone field is properly rerendered after been changed by onchange', async function (assert) {
+ assert.expect(4);
+
+ const NEW_PHONE = '+32595555555';
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="foo" on_change="1"/>' + // onchange to update mobile in readonly mode directly
+ '<field name="mobile" widget="phone" readonly="1"/>' + // readonly only, we don't want to go through write mode
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {mode: 'edit'},
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ return Promise.resolve({
+ value: {
+ mobile: NEW_PHONE, // onchange to update mobile in readonly mode directly
+ },
+ });
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+ // check initial rendering
+ assert.strictEqual(form.$('.o_field_phone').text(), "+32494444444",
+ 'Initial Phone text should be set');
+ assert.strictEqual(form.$('.o_field_phone_sms').text(), 'SMS',
+ 'SMS button label should be rendered');
+
+ // trigger the onchange to update phone field, but still in readonly mode
+ await testUtils.fields.editInput($('input[name="foo"]'), 'someOtherFoo');
+
+ // check rendering after changes
+ assert.strictEqual(form.$('.o_field_phone').text(), NEW_PHONE,
+ 'Phone text should be updated');
+ assert.strictEqual(form.$('.o_field_phone_sms').text(), 'SMS',
+ 'SMS button label should not be changed');
+
+ form.destroy();
+ });
+});
+});