summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--addons/mass_mailing_sms/__init__.py7
-rw-r--r--addons/mass_mailing_sms/__manifest__.py32
-rw-r--r--addons/mass_mailing_sms/controllers/__init__.py4
-rw-r--r--addons/mass_mailing_sms/controllers/main.py105
-rw-r--r--addons/mass_mailing_sms/data/utm_data.xml8
-rw-r--r--addons/mass_mailing_sms/models/__init__.py9
-rw-r--r--addons/mass_mailing_sms/models/link_tracker.py33
-rw-r--r--addons/mass_mailing_sms/models/mailing_contact.py14
-rw-r--r--addons/mass_mailing_sms/models/mailing_list.py36
-rw-r--r--addons/mass_mailing_sms/models/mailing_mailing.py193
-rw-r--r--addons/mass_mailing_sms/models/mailing_trace.py80
-rw-r--r--addons/mass_mailing_sms/models/sms_sms.py47
-rw-r--r--addons/mass_mailing_sms/report/mailing_trace_report_views.xml71
-rw-r--r--addons/mass_mailing_sms/security/ir.model.access.csv1
-rw-r--r--addons/mass_mailing_sms/static/description/icon.pngbin0 -> 4338 bytes
-rw-r--r--addons/mass_mailing_sms/static/description/icon.svg70
-rw-r--r--addons/mass_mailing_sms/tests/__init__.py4
-rw-r--r--addons/mass_mailing_sms/tests/common.py73
-rw-r--r--addons/mass_mailing_sms/views/link_tracker_views.xml8
-rw-r--r--addons/mass_mailing_sms/views/mailing_contact_views.xml96
-rw-r--r--addons/mass_mailing_sms/views/mailing_list_views.xml22
-rw-r--r--addons/mass_mailing_sms/views/mailing_mailing_views.xml183
-rw-r--r--addons/mass_mailing_sms/views/mailing_sms_menus.xml63
-rw-r--r--addons/mass_mailing_sms/views/mailing_trace_views.xml121
-rw-r--r--addons/mass_mailing_sms/views/mass_mailing_sms_templates_portal.xml60
-rw-r--r--addons/mass_mailing_sms/wizard/__init__.py5
-rw-r--r--addons/mass_mailing_sms/wizard/mailing_sms_test.py40
-rw-r--r--addons/mass_mailing_sms/wizard/mailing_sms_test_views.xml30
-rw-r--r--addons/mass_mailing_sms/wizard/sms_composer.py99
-rw-r--r--addons/mass_mailing_sms/wizard/sms_composer_views.xml16
-rw-r--r--addons/test_mail_full/__manifest__.py3
-rw-r--r--addons/test_mail_full/tests/__init__.py1
-rw-r--r--addons/test_mail_full/tests/common.py12
-rw-r--r--addons/test_mail_full/tests/test_mass_sms.py152
-rw-r--r--addons/test_mail_full/tests/test_sms_performance.py4
-rw-r--r--addons/test_mail_full/tests/test_sms_sms.py109
36 files changed, 1807 insertions, 4 deletions
diff --git a/addons/mass_mailing_sms/__init__.py b/addons/mass_mailing_sms/__init__.py
new file mode 100644
index 00000000000..8a8a7a9c11b
--- /dev/null
+++ b/addons/mass_mailing_sms/__init__.py
@@ -0,0 +1,7 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import controllers
+from . import models
+from . import tests
+from . import wizard
diff --git a/addons/mass_mailing_sms/__manifest__.py b/addons/mass_mailing_sms/__manifest__.py
new file mode 100644
index 00000000000..a31778d1fd6
--- /dev/null
+++ b/addons/mass_mailing_sms/__manifest__.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+{
+ 'name': 'SMS Marketing',
+ 'summary': 'Design, send and track SMS',
+ 'description': '',
+ 'version': '1.0',
+ 'category': 'Marketing/Email Marketing',
+ 'depends': [
+ 'portal',
+ 'mass_mailing',
+ 'sms',
+ ],
+ 'data': [
+ 'data/utm_data.xml',
+ 'security/ir.model.access.csv',
+ 'views/mailing_sms_menus.xml',
+ 'views/mailing_list_views.xml',
+ 'views/mailing_contact_views.xml',
+ 'views/mailing_trace_views.xml',
+ 'views/mailing_mailing_views.xml',
+ 'views/link_tracker_views.xml',
+ 'views/mass_mailing_sms_templates_portal.xml',
+ 'report/mailing_trace_report_views.xml',
+ 'wizard/sms_composer_views.xml',
+ 'wizard/mailing_sms_test_views.xml',
+ ],
+ 'demo': [
+ ],
+ 'application': True,
+}
diff --git a/addons/mass_mailing_sms/controllers/__init__.py b/addons/mass_mailing_sms/controllers/__init__.py
new file mode 100644
index 00000000000..5d4b25db9c0
--- /dev/null
+++ b/addons/mass_mailing_sms/controllers/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import main
diff --git a/addons/mass_mailing_sms/controllers/main.py b/addons/mass_mailing_sms/controllers/main.py
new file mode 100644
index 00000000000..9f03af5c6d2
--- /dev/null
+++ b/addons/mass_mailing_sms/controllers/main.py
@@ -0,0 +1,105 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import werkzeug
+
+from odoo import http, _
+from odoo.addons.phone_validation.tools import phone_validation
+from odoo.http import request
+
+
+class MailingSMSController(http.Controller):
+
+ def _check_trace(self, mailing_id, trace_code):
+ try:
+ mailing = request.env['mailing.mailing'].sudo().search([('id', '=', mailing_id)])
+ except:
+ mailing = False
+ if not mailing:
+ return {'error': 'mailing_error'}
+ trace = request.env['mailing.trace'].sudo().search([
+ ('trace_type', '=', 'sms'),
+ ('sms_code', '=', trace_code),
+ ('mass_mailing_id', '=', mailing.id)
+ ])
+ if not trace:
+ return {'error': 'trace_error'}
+ return {'trace': trace}
+
+ @http.route(['/sms/<int:mailing_id>/<string:trace_code>'], type='http', website=True, auth='public')
+ def blacklist_page(self, mailing_id, trace_code, **post):
+ check_res = self._check_trace(mailing_id, trace_code)
+ if not check_res.get('trace'):
+ return werkzeug.utils.redirect('/web')
+ return request.render('mass_mailing_sms.blacklist_main', {
+ 'mailing_id': mailing_id,
+ 'trace_code': trace_code,
+ })
+
+ @http.route(['/sms/<int:mailing_id>/unsubscribe/<string:trace_code>'], type='http', website=True, auth='public')
+ def blacklist_number(self, mailing_id, trace_code, **post):
+ check_res = self._check_trace(mailing_id, trace_code)
+ if not check_res.get('trace'):
+ return werkzeug.utils.redirect('/web')
+ country_code = request.session.get('geoip', False) and request.session.geoip.get('country_code', False) if request.session.get('geoip') else None
+ trace = check_res['trace']
+ mailing_list_ids = trace.mass_mailing_id.contact_list_ids
+
+ # parse and validate number
+ sms_number = post.get('sms_number', '').strip(' ')
+ sanitize_res = phone_validation.phone_sanitize_numbers([sms_number], country_code, None)[sms_number]
+ tocheck_number = sanitize_res['sanitized'] or sms_number
+
+ # compute opt-out / blacklist information
+ lists_optout = request.env['mailing.list'].sudo()
+ lists_optin = request.env['mailing.list'].sudo()
+ unsubscribe_error = False
+ if tocheck_number and trace.sms_number == tocheck_number:
+ if mailing_list_ids:
+ subscriptions = request.env['mailing.contact.subscription'].sudo().search([
+ ('list_id', 'in', mailing_list_ids.ids),
+ ('contact_id.phone_sanitized', '=', tocheck_number),
+ ])
+ subscriptions.write({'opt_out': True})
+ lists_optout = subscriptions.mapped('list_id')
+ else:
+ blacklist_rec = request.env['phone.blacklist'].sudo().add(tocheck_number)
+ blacklist_rec._message_log(
+ body=_('Blacklist through mass sms unsubscribe (mailing ID: %s - model: %s)') %
+ (trace.mass_mailing_id.id, trace.mass_mailing_id.mailing_model_id.display_name))
+ lists_optin = request.env['mailing.contact.subscription'].sudo().search([
+ ('contact_id.phone_sanitized', '=', tocheck_number),
+ ('list_id', 'not in', mailing_list_ids.ids),
+ ('opt_out', '=', False),
+ ]).mapped('list_id')
+ elif tocheck_number and trace.sms_number != tocheck_number:
+ unsubscribe_error = _('Number %s not found' % tocheck_number)
+ else:
+ unsubscribe_error = sanitize_res['msg']
+
+ return request.render('mass_mailing_sms.blacklist_number', {
+ 'mailing_id': mailing_id,
+ 'trace_code': trace_code,
+ 'sms_number': sms_number,
+ 'lists_optin': lists_optin,
+ 'lists_optout': lists_optout,
+ 'unsubscribe_error': unsubscribe_error,
+ })
+
+ @http.route('/r/<string:code>/s/<int:sms_sms_id>', type='http', auth="public")
+ def sms_short_link_redirect(self, code, sms_sms_id, **post):
+ # don't assume geoip is set, it is part of the website module
+ # which mass_mailing doesn't depend on
+ country_code = request.session.get('geoip', False) and request.session.geoip.get('country_code', False)
+ if sms_sms_id:
+ trace_id = request.env['mailing.trace'].sudo().search([('sms_sms_id_int', '=', int(sms_sms_id))]).id
+ else:
+ trace_id = False
+
+ request.env['link.tracker.click'].sudo().add_click(
+ code,
+ ip=request.httprequest.remote_addr,
+ country_code=country_code,
+ mailing_trace_id=trace_id
+ )
+ return werkzeug.utils.redirect(request.env['link.tracker'].get_url_from_code(code), 301)
diff --git a/addons/mass_mailing_sms/data/utm_data.xml b/addons/mass_mailing_sms/data/utm_data.xml
new file mode 100644
index 00000000000..1b7cef28896
--- /dev/null
+++ b/addons/mass_mailing_sms/data/utm_data.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo><data noupdate="1">
+
+ <record model="utm.medium" id="utm_medium_sms">
+ <field name="name">SMS</field>
+ </record>
+
+</data></odoo>
diff --git a/addons/mass_mailing_sms/models/__init__.py b/addons/mass_mailing_sms/models/__init__.py
new file mode 100644
index 00000000000..5d4407a1729
--- /dev/null
+++ b/addons/mass_mailing_sms/models/__init__.py
@@ -0,0 +1,9 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import link_tracker
+from . import mailing_contact
+from . import mailing_list
+from . import mailing_mailing
+from . import mailing_trace
+from . import sms_sms
diff --git a/addons/mass_mailing_sms/models/link_tracker.py b/addons/mass_mailing_sms/models/link_tracker.py
new file mode 100644
index 00000000000..a8bee7d1546
--- /dev/null
+++ b/addons/mass_mailing_sms/models/link_tracker.py
@@ -0,0 +1,33 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import re
+import werkzeug
+
+from odoo import models
+from odoo.addons.mass_mailing_sms.models.sms_sms import TEXT_URL_REGEX
+
+
+class LinkTracker(models.Model):
+ _inherit = "link.tracker"
+
+ def _convert_links_text(self, body, vals, blacklist=None):
+ shortened_schema = self.env['ir.config_parameter'].sudo().get_param('web.base.url') + '/r/'
+ unsubscribe_schema = self.env['ir.config_parameter'].sudo().get_param('web.base.url') + '/sms/'
+ for match in re.findall(TEXT_URL_REGEX, body):
+ original_url = match[0]
+ # don't shorten already-shortened links or links towards unsubscribe page
+ if original_url.startswith(shortened_schema) or original_url.startswith(unsubscribe_schema):
+ continue
+ # support blacklist items in path, like /u/
+ parsed = werkzeug.urls.url_parse(original_url, scheme='http')
+ if blacklist and any(item in parsed.path for item in blacklist):
+ continue
+
+ vals['url'] = werkzeug.utils.unescape(original_url)
+ link = self.create(vals)
+ shortened_url = link.short_url
+ if shortened_url:
+ body = body.replace(original_url, shortened_url)
+
+ return body
diff --git a/addons/mass_mailing_sms/models/mailing_contact.py b/addons/mass_mailing_sms/models/mailing_contact.py
new file mode 100644
index 00000000000..68050b519d3
--- /dev/null
+++ b/addons/mass_mailing_sms/models/mailing_contact.py
@@ -0,0 +1,14 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models
+
+
+class MailingContact(models.Model):
+ _name = 'mailing.contact'
+ _inherit = ['mailing.contact', 'mail.thread.phone']
+
+ mobile = fields.Char(string='Mobile')
+
+ def _sms_get_number_fields(self):
+ return ['mobile']
diff --git a/addons/mass_mailing_sms/models/mailing_list.py b/addons/mass_mailing_sms/models/mailing_list.py
new file mode 100644
index 00000000000..56b96df552f
--- /dev/null
+++ b/addons/mass_mailing_sms/models/mailing_list.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import models
+
+
+class MailingList(models.Model):
+ _inherit = 'mailing.list'
+
+ def _compute_contact_nbr(self):
+ if self.env.context.get('mailing_sms'):
+ self.env.cr.execute('''
+select list_id, count(*)
+from mailing_contact_list_rel r
+left join mailing_contact c on (r.contact_id=c.id)
+left join phone_blacklist bl on c.phone_sanitized = bl.number and bl.active
+where
+ list_id in %s
+ AND COALESCE(r.opt_out,FALSE) = FALSE
+ AND c.phone_sanitized IS NOT NULL
+ AND bl.id IS NULL
+group by list_id''', (tuple(self.ids), ))
+ data = dict(self.env.cr.fetchall())
+ for mailing_list in self:
+ mailing_list.contact_nbr = data.get(mailing_list.id, 0)
+ return
+ return super(MailingList, self)._compute_contact_nbr()
+
+ def action_view_contacts(self):
+ if self.env.context.get('mailing_sms'):
+ action = self.env.ref('mass_mailing_sms.mailing_contact_action_sms').read()[0]
+ action['domain'] = [('list_ids', 'in', self.ids)]
+ context = dict(self.env.context, search_default_filter_valid_sms_recipient=1, default_list_ids=self.ids)
+ action['context'] = context
+ return action
+ return super(MailingList, self).action_view_contacts()
diff --git a/addons/mass_mailing_sms/models/mailing_mailing.py b/addons/mass_mailing_sms/models/mailing_mailing.py
new file mode 100644
index 00000000000..96e3eb4c8d1
--- /dev/null
+++ b/addons/mass_mailing_sms/models/mailing_mailing.py
@@ -0,0 +1,193 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import logging
+
+from odoo import api, fields, models, _
+from odoo.exceptions import UserError
+
+_logger = logging.getLogger(__name__)
+
+
+class Mailing(models.Model):
+ _inherit = 'mailing.mailing'
+
+ # mailing options
+ mailing_type = fields.Selection(selection_add=[('sms', 'SMS')])
+ # sms options
+ body_plaintext = fields.Text('SMS Body')
+ sms_template_id = fields.Many2one('sms.template', string='SMS Template', ondelete='set null')
+ sms_has_insufficient_credit = fields.Boolean(
+ 'Insufficient IAP credits', compute='_compute_sms_has_insufficient_credit',
+ help='UX Field to propose to buy IAP credits')
+ # opt_out_link
+ sms_allow_unsubscribe = fields.Boolean('Include opt-out link', default=True)
+
+ @api.onchange('mailing_type')
+ def _onchange_mailing_type(self):
+ if self.mailing_type == 'sms' and (not self.medium_id or self.medium_id == self.env.ref('utm.utm_medium_email')):
+ self.medium_id = self.env.ref('mass_mailing_sms.utm_medium_sms').id
+ elif self.mailing_type == 'mail' and (not self.medium_id or self.medium_id == self.env.ref('mass_mailing_sms.utm_medium_sms')):
+ self.medium_id = self.env.ref('utm.utm_medium_email').id
+
+ @api.onchange('sms_template_id', 'mailing_type')
+ def _onchange_sms_template_id(self):
+ if self.mailing_type == 'sms' and self.sms_template_id:
+ self.body_plaintext = self.sms_template_id.body
+
+ @api.depends('mailing_trace_ids.failure_type')
+ def _compute_sms_has_insufficient_credit(self):
+ mailing_ids = self.env['mailing.trace'].sudo().search([
+ ('mass_mailing_id', 'in', self.ids),
+ ('trace_type', '=', 'sms'),
+ ('failure_type', '=', 'sms_credit')
+ ]).mapped('mass_mailing_id')
+ for mailing in self:
+ mailing.sms_has_insufficient_credit = mailing in mailing_ids
+
+ # --------------------------------------------------
+ # CRUD
+ # --------------------------------------------------
+
+ @api.model
+ def create(self, values):
+ if values.get('mailing_type') == 'sms':
+ if not values.get('medium_id'):
+ values['medium_id'] = self.env.ref('mass_mailing_sms.utm_medium_sms').id
+ if values.get('sms_template_id') and not values.get('body_plaintext'):
+ values['body_plaintext'] = self.env['sms.template'].browse(values['sms_template_id']).body
+ return super(Mailing, self).create(values)
+
+ # --------------------------------------------------
+ # BUSINESS / VIEWS ACTIONS
+ # --------------------------------------------------
+
+ def action_put_in_queue_sms(self):
+ return self.action_put_in_queue()
+
+ def action_test(self):
+ if self.mailing_type == 'sms':
+ ctx = dict(self.env.context, default_mailing_id=self.id)
+ return {
+ 'name': _('Test Mailing'),
+ 'type': 'ir.actions.act_window',
+ 'view_mode': 'form',
+ 'res_model': 'mailing.sms.test',
+ 'target': 'new',
+ 'context': ctx,
+ }
+ return super(Mailing, self).action_test()
+
+ def _action_view_traces_filtered(self, view_filter):
+ action = super(Mailing, self)._action_view_traces_filtered(view_filter)
+ if self.mailing_type == 'sms':
+ action['views'] = [(self.env.ref('mass_mailing_sms.mailing_trace_view_tree_sms').id, 'tree'),
+ (self.env.ref('mass_mailing_sms.mailing_trace_view_form_sms').id, 'form')]
+ return action
+
+ def action_buy_sms_credits(self):
+ url = self.env['iap.account'].get_credits_url(service_name='sms')
+ return {
+ 'type': 'ir.actions.act_url',
+ 'url': url,
+ }
+
+ # --------------------------------------------------
+ # SMS SEND
+ # --------------------------------------------------
+
+ def _get_opt_out_list_sms(self):
+ """Returns a set of emails opted-out in target model"""
+ self.ensure_one()
+ opt_out = []
+ target = self.env[self.mailing_model_real]
+ if self.mailing_model_real == "mail.mass_mailing.contact":
+ # if user is opt_out on One list but not on another
+ # or if two user with same email address, one opted in and the other one opted out, send the mail anyway
+ # TODO DBE Fixme : Optimise the following to get real opt_out and opt_in
+ subscriptions = self.env['mailing.contact.subscription'].sudo().search(
+ [('list_id', 'in', self.contact_list_ids.ids)])
+ opt_out_contacts = subscriptions.filtered(lambda sub: sub.opt_out).mapped('contact_id')
+ opt_in_contacts = subscriptions.filtered(lambda sub: not sub.opt_out).mapped('contact_id')
+ opt_out = list(set(c.id for c in opt_out_contacts if c not in opt_in_contacts))
+
+ _logger.info("Mass SMS %s targets %s: optout: %s contacts", self, target._name, len(opt_out))
+ else:
+ _logger.info("Mass SMS %s targets %s: no opt out list available", self, target._name)
+ return opt_out
+
+ def _get_seen_list_sms(self):
+ """Returns a set of emails already targeted by current mailing/campaign (no duplicates)"""
+ self.ensure_one()
+ target = self.env[self.mailing_model_real]
+
+ if issubclass(type(target), self.pool['mail.thread.phone']):
+ phone_fields = ['phone_sanitized']
+ elif issubclass(type(target), self.pool['mail.thread']):
+ phone_fields = target._sms_get_number_fields()
+ else:
+ phone_fields = []
+ if 'mobile' in target._fields:
+ phone_fields.append('mobile')
+ if 'phone' in target._fields:
+ phone_fields.append('phone')
+ if not phone_fields:
+ raise UserError(_("Unsupported %s for mass SMS") % self.mailing_model_id.name)
+
+ query = """
+ SELECT %(select_query)s
+ FROM mailing_trace trace
+ JOIN %(target_table)s target ON (trace.res_id = target.id)
+ WHERE (%(where_query)s)
+ AND trace.mass_mailing_id = %%(mailing_id)s
+ AND trace.model = %%(target_model)s
+ """
+ query = query % {
+ 'select_query': 'target.id, ' + ', '.join('target.%s' % fname for fname in phone_fields),
+ 'where_query': ' OR '.join('target.%s IS NOT NULL' % fname for fname in phone_fields),
+ 'target_table': target._table
+ }
+ params = {'mailing_id': self.id, 'target_model': self.mailing_model_real}
+ self._cr.execute(query, params)
+ query_res = self._cr.fetchall()
+ seen_list = set(number for item in query_res for number in item[1:] if number)
+ seen_ids = set(item[0] for item in query_res)
+ _logger.info("Mass SMS %s targets %s: already reached %s SMS", self, target._name, len(seen_list))
+ return list(seen_ids), list(seen_list)
+
+ def _send_sms_get_composer_values(self, res_ids):
+ return {
+ # content
+ 'body': self.body_plaintext,
+ 'template_id': self.sms_template_id.id,
+ 'res_model': self.mailing_model_real,
+ 'res_ids': repr(res_ids),
+ # options
+ 'composition_mode': 'mass',
+ 'mailing_id': self.id,
+ 'mass_keep_log': False,
+ 'mass_sms_allow_unsubscribe': self.sms_allow_unsubscribe,
+ }
+
+ def action_send_mail(self, res_ids=None):
+ mass_sms = self.filtered(lambda m: m.mailing_type == 'sms')
+ if mass_sms:
+ mass_sms.action_send_sms(res_ids=res_ids)
+ return super(Mailing, self - mass_sms).action_send_mail(res_ids=res_ids)
+
+ def action_send_sms(self, res_ids=None):
+ for mailing in self:
+ if not res_ids:
+ res_ids = mailing._get_remaining_recipients()
+ if not res_ids:
+ raise UserError(_('There is no recipients selected.'))
+
+ composer = self.env['sms.composer'].create(mailing._send_sms_get_composer_values(res_ids))
+ # extra_context = self._get_mass_mailing_context()
+
+ # auto-commit except in testing mode
+ # auto_commit = not getattr(threading.currentThread(), 'testing', False)
+ # composer.send_mail(auto_commit=auto_commit)
+ composer._action_send_sms()
+ mailing.write({'state': 'done', 'sent_date': fields.Datetime.now()})
+ return True
diff --git a/addons/mass_mailing_sms/models/mailing_trace.py b/addons/mass_mailing_sms/models/mailing_trace.py
new file mode 100644
index 00000000000..56bb84b0eff
--- /dev/null
+++ b/addons/mass_mailing_sms/models/mailing_trace.py
@@ -0,0 +1,80 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import random
+import string
+
+from odoo import api, fields, models
+from odoo.osv import expression
+
+
+class MailingTrace(models.Model):
+ """ Improve statistics model to add SMS support. Main attributes of
+ statistics model are used, only some specific data is required. """
+ _inherit = 'mailing.trace'
+ CODE_SIZE = 3
+
+ trace_type = fields.Selection(selection_add=[('sms', 'SMS')])
+ sms_sms_id = fields.Many2one('sms.sms', string='SMS', index=True, ondelete='set null')
+ sms_sms_id_int = fields.Integer(
+ string='SMS ID (tech)',
+ help='ID of the related sms.sms. This field is an integer field because '
+ 'the related sms.sms can be deleted separately from its statistics. '
+ 'However the ID is needed for several action and controllers.',
+ index=True,
+ )
+ sms_number = fields.Char('Number')
+ sms_code = fields.Char('Code')
+ failure_type = fields.Selection(selection_add=[
+ ('sms_number_missing', 'Missing Number'),
+ ('sms_number_format', 'Wrong Number Format'),
+ ('sms_credit', 'Insufficient Credit'),
+ ('sms_server', 'Server Error'),
+ # mass mode specific codes
+ ('sms_blacklist', 'Blacklisted'),
+ ('sms_duplicate', 'Duplicate'),
+ ])
+
+ @api.model_create_multi
+ def create(self, values_list):
+ for values in values_list:
+ if 'sms_sms_id' in values:
+ values['sms_sms_id_int'] = values['sms_sms_id']
+ if values.get('trace_type') == 'sms' and not values.get('sms_code'):
+ values['sms_code'] = self._get_random_code()
+ return super(MailingTrace, self).create(values_list)
+
+ def _get_random_code(self):
+ """ Generate a random code for trace. Uniqueness is not really necessary
+ as it serves as obfuscation when unsubscribing. A valid trio
+ code / mailing_id / number will be requested. """
+ return ''.join(random.choice(string.ascii_letters + string.digits) for dummy in range(self.CODE_SIZE))
+
+ def _get_records_from_sms(self, sms_sms_ids=None, additional_domain=None):
+ if not self.ids and sms_sms_ids:
+ domain = [('sms_sms_id_int', 'in', sms_sms_ids)]
+ else:
+ domain = [('id', 'in', self.ids)]
+ if additional_domain:
+ domain = expression.AND([domain, additional_domain])
+ return self.search(domain)
+
+ def set_sms_sent(self, sms_sms_ids=None):
+ statistics = self._get_records_from_sms(sms_sms_ids, [('sent', '=', False)])
+ statistics.write({'sent': fields.Datetime.now()})
+ return statistics
+
+ def set_sms_clicked(self, sms_sms_ids=None):
+ statistics = self._get_records_from_sms(sms_sms_ids, [('clicked', '=', False)])
+ statistics.write({'clicked': fields.Datetime.now()})
+ return statistics
+
+ def set_sms_ignored(self, sms_sms_ids=None):
+ statistics = self._get_records_from_sms(sms_sms_ids, [('ignored', '=', False)])
+ statistics.write({'ignored': fields.Datetime.now()})
+ return statistics
+
+ def set_sms_exception(self, sms_sms_ids=None):
+ statistics = self._get_records_from_sms(sms_sms_ids, [('exception', '=', False)])
+ statistics.write({'exception': fields.Datetime.now()})
+ return statistics
diff --git a/addons/mass_mailing_sms/models/sms_sms.py b/addons/mass_mailing_sms/models/sms_sms.py
new file mode 100644
index 00000000000..2167faa46ef
--- /dev/null
+++ b/addons/mass_mailing_sms/models/sms_sms.py
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import re
+
+from odoo import fields, models
+
+TEXT_URL_REGEX = r'(https?:\/\/(www\.)?[a-zA-Z0-9@:%._\+~#=/-]{1,64})'
+
+
+class SmsSms(models.Model):
+ _inherit = ['sms.sms']
+
+ mailing_id = fields.Many2one('mailing.mailing', string='Mass Mailing')
+ mailing_trace_ids = fields.One2many('mailing.trace', 'sms_sms_id', string='Statistics')
+
+ def _update_body_short_links(self):
+ """ Override to tweak shortened URLs by adding statistics ids, allowing to
+ find customer back once clicked. """
+ shortened_schema = self.env['ir.config_parameter'].sudo().get_param('web.base.url') + '/r/'
+ res = dict.fromkeys(self.ids, False)
+ for sms in self:
+ if not sms.mailing_id or not sms.body:
+ res[sms.id] = sms.body
+ continue
+
+ body = sms.body
+ for match in re.findall(TEXT_URL_REGEX, body):
+ url = match[0]
+ if url.startswith(shortened_schema):
+ body = body.replace(url, url + '/s/%s' % sms.id)
+ res[sms.id] = body
+ return res
+
+ def _postprocess_iap_sent_sms(self, iap_results, failure_reason=None, delete_all=False):
+ all_sms_ids = [item['res_id'] for item in iap_results]
+ if any(sms.mailing_id for sms in self.env['sms.sms'].sudo().browse(all_sms_ids)):
+ for state in self.IAP_TO_SMS_STATE.keys():
+ sms_ids = [item['res_id'] for item in iap_results if item['state'] == state]
+ traces = self.env['mailing.trace'].sudo().search([
+ ('sms_sms_id_int', 'in', sms_ids)
+ ])
+ if traces and state == 'success':
+ traces.write({'sent': fields.Datetime.now(), 'exception': False})
+ elif traces:
+ traces.write({'exception': fields.Datetime.now(), 'failure_type': self.IAP_TO_SMS_STATE[state]})
+ return super(SmsSms, self)._postprocess_iap_sent_sms(iap_results, failure_reason=failure_reason, delete_all=delete_all)
diff --git a/addons/mass_mailing_sms/report/mailing_trace_report_views.xml b/addons/mass_mailing_sms/report/mailing_trace_report_views.xml
new file mode 100644
index 00000000000..2adf5953faf
--- /dev/null
+++ b/addons/mass_mailing_sms/report/mailing_trace_report_views.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <record id="mailing_trace_report_view_pivot" model="ir.ui.view">
+ <field name="name">mailing.trace.report.view.pivot</field>
+ <field name="model">mailing.trace.report</field>
+ <field name="arch" type="xml">
+ <pivot string="Mass Mailing Statistics" disable_linking="True">
+ <field name="campaign" type="row"/>
+ <field name="sent" type="measure"/>
+ <field name="delivered" type="measure"/>
+ <field name="opened" type="measure"/>
+ <field name="bounced" type="measure"/>
+ <field name="replied" type="measure"/>
+ <field name="clicked" type="measure"/>
+ </pivot>
+ </field>
+ </record>
+
+ <record id="mailing_trace_report_view_graph" model="ir.ui.view">
+ <field name="name">mailing.trace.report.view.graph</field>
+ <field name="model">mailing.trace.report</field>
+ <field name="arch" type="xml">
+ <graph string="Mass Mailing Statistics">
+ <field name="campaign"/>
+ <field name="sent" type="measure"/>
+ </graph>
+ </field>
+ </record>
+
+ <record id="mailing_trace_report_view_search" model="ir.ui.view">
+ <field name="name">mailing.trace.report.view.search</field>
+ <field name="model">mailing.trace.report</field>
+ <field name="arch" type="xml">
+ <search string="Mass Mailing Statistics">
+ <group expand="0" string="Extended Filters...">
+ <field name="scheduled_date"/>
+ </group>
+ <group expand="1" string="Group By...">
+ <filter string="Mass Mailing Campaign" name="mass_mailing_campaign"
+ domain="[]" context="{'group_by':'campaign'}"/>
+ <filter string="State" domain="[]" name="state"
+ context="{'group_by':'state'}"/>
+ <filter string="Sent By" domain="[]" name="sent_by"
+ context="{'group_by':'email_from'}"/>
+ <separator/>
+ <filter string="Scheduled Period" name="scheduled_date"
+ domain="[]" context="{'group_by':'scheduled_date'}"/>
+ </group>
+ </search>
+ </field>
+ </record>
+
+ <!-- Actions and Menuitems -->
+ <record id="mailing_trace_report_action_sms" model="ir.actions.act_window">
+ <field name="name">Mass SMS Analysis</field>
+ <field name="res_model">mailing.trace.report</field>
+ <field name="view_mode">graph,pivot</field>
+ <field name="domain">[('mailing_type', '=', 'sms')]</field>
+ <field name="help" type="html">
+ <p class="o_view_nocontent_smiling_face">
+ Mass SMS Statistics allows you to check different mailing related information like number of sent SMS or bounced SMS.
+ You can sort out your analysis by different groups to get accurate grained analysis.
+ </p>
+ </field>
+ </record>
+
+ <!-- Mass SMS / Reporting -->
+ <record id="mass_mailing_sms_menu_reporting" model="ir.ui.menu">
+ <field name="action" ref="mailing_trace_report_action_sms"/>
+ </record>
+</odoo>
diff --git a/addons/mass_mailing_sms/security/ir.model.access.csv b/addons/mass_mailing_sms/security/ir.model.access.csv
new file mode 100644
index 00000000000..4a08a4aa60d
--- /dev/null
+++ b/addons/mass_mailing_sms/security/ir.model.access.csv
@@ -0,0 +1 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
diff --git a/addons/mass_mailing_sms/static/description/icon.png b/addons/mass_mailing_sms/static/description/icon.png
new file mode 100644
index 00000000000..7dbb33addee
--- /dev/null
+++ b/addons/mass_mailing_sms/static/description/icon.png
Binary files differ
diff --git a/addons/mass_mailing_sms/static/description/icon.svg b/addons/mass_mailing_sms/static/description/icon.svg
new file mode 100644
index 00000000000..89055592c82
--- /dev/null
+++ b/addons/mass_mailing_sms/static/description/icon.svg
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 23.0.4, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 70 70" style="enable-background:new 0 0 70 70;" xml:space="preserve">
+<style type="text/css">
+ .st0{filter:url(#Adobe_OpacityMaskFilter);}
+ .st1{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}
+ .st2{mask:url(#icon-b_1_);}
+ .st3{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_1_);}
+ .st4{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;fill-opacity:0.383;}
+ .st5{fill-rule:evenodd;clip-rule:evenodd;fill-opacity:0.151;}
+ .st6{fill-rule:evenodd;clip-rule:evenodd;fill-opacity:0.383;}
+ .st7{opacity:0.5;}
+ .st8{fill:#FFFFFF;}
+ .st9{filter:url(#Adobe_OpacityMaskFilter_1_);}
+ .st10{mask:url(#icon-b_2_);}
+ .st11{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_2_);}
+ .st12{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_3_);}
+</style>
+<defs>
+ <filter id="Adobe_OpacityMaskFilter" filterUnits="userSpaceOnUse" x="-0.1" y="-0.2" width="70.1" height="70.2">
+ <feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
+ </filter>
+</defs>
+<mask maskUnits="userSpaceOnUse" x="-0.1" y="-0.2" width="70.1" height="70.2" id="icon-b_2_">
+ <g class="st0">
+ <path id="icon-a_2_" class="st1" d="M4,0c32.4,0,54.1,0,61,0c4,0,5,1,5,5c0,38,0,57.4,0,60c0,4-1,5-5,5S9,70,4,70c-3,0-4-1-4-5
+ c0-2.5,0-23.2,0-60C0,1,1,0,4,0z"/>
+ </g>
+</mask>
+<g class="st10">
+
+ <linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="-636.7537" y1="415.0251" x2="-637.7537" y2="414.0389" gradientTransform="matrix(70 0 0 -70 44643 29052)">
+ <stop offset="0" style="stop-color:#797C79"/>
+ <stop offset="1" style="stop-color:#545554"/>
+ </linearGradient>
+ <path class="st3" d="M0,0h70v70H0V0z"/>
+ <path class="st6" d="M4,69h61c2.7,0,4.3-1,5-3v4H0v-4C0.7,68,2,69,4,69z"/>
+
+ <linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="-636.7571" y1="415.0286" x2="-637.7571" y2="414.0286" gradientTransform="matrix(70 0 0 -70 44643 29052)">
+ <stop offset="0" style="stop-color:#94B6C8"/>
+ <stop offset="1" style="stop-color:#6A9EBA"/>
+ </linearGradient>
+ <path class="st11" d="M0,0h70v70H0V0z"/>
+ <path class="st4" d="M4,1h61c2.7,0,4.3,0.7,5,2c0-2.7,0-3.7,0-3H0c0-0.7,0,0.3,0,3C0.7,1.7,2,1,4,1z"/>
+ <path class="st6" d="M4,69h61c2.7,0,4.3-1,5-3v4H0v-4C0.7,68,2,69,4,69z"/>
+ <g transform="translate(0 22)">
+ <g>
+ <path id="icon-d_1_" class="st5" d="M4,46.9c-2,0-4-1-4-4.1V7l18.6-18.9h25.7l1.2,8.5l-1.9,1.9l11.1,7.6v8.3l-9.1,9.3l-0.7,11.7
+ L32.2,46.9H4z"/>
+ </g>
+ </g>
+ <path class="st7" d="M45.4,12.1c0-1.1-0.9-2-2-2H20.1c-1.1,0-2,0.9-2,2v44.6c0,1.1,0.9,2,2,2h23.3c1.1,0,2-0.9,2-2V42.7
+ c-0.7,0.5-1.5,1-2.3,1.4v9.3c0,0.2-0.1,0.3-0.3,0.3H20.7c-0.2,0-0.3-0.1-0.3-0.3v-37c0-0.2,0.1-0.3,0.3-0.3h22.1
+ c0.2,0,0.3,0.1,0.3,0.3v2.8c0.5,0,1.1-0.1,1.6-0.1c0.2,0,0.5,0,0.7,0V12.1z M31.8,54.5c0.8,0,1.5,0.7,1.5,1.5
+ c0,0.8-0.7,1.5-1.5,1.5c-0.8,0-1.5-0.7-1.5-1.5C30.3,55.2,31,54.5,31.8,54.5z M34.9,14.6h-6.1c-0.3,0-0.5-0.2-0.5-0.5
+ s0.2-0.5,0.5-0.5h6.1c0.3,0,0.5,0.2,0.5,0.5S35.1,14.6,34.9,14.6z"/>
+ <path class="st8" d="M44.7,41.7c-0.5,0-1.1,0-1.6-0.1v11.1c0,0.2-0.1,0.3-0.3,0.3H20.7c-0.2,0-0.3-0.1-0.3-0.3v-37
+ c0-0.2,0.1-0.3,0.3-0.3h22.1c0.2,0,0.3,0.1,0.3,0.3v2.9c0.5,0,1.1-0.1,1.6-0.1c0.2,0,0.5,0,0.7,0v-7.2c0-1.1-0.9-2-2-2H20.1
+ c-1.1,0-2,0.9-2,2V56c0,1.1,0.9,2,2,2h23.3c1.1,0,2-0.9,2-2V41.7C45.2,41.7,44.9,41.7,44.7,41.7z M28.7,13h6.1
+ c0.3,0,0.5,0.2,0.5,0.5S35.1,14,34.9,14h-6.1c-0.3,0-0.5-0.2-0.5-0.5S28.4,13,28.7,13z M31.8,56.9c-0.8,0-1.5-0.7-1.5-1.5
+ c0-0.8,0.7-1.5,1.5-1.5c0.8,0,1.5,0.7,1.5,1.5C33.3,56.3,32.6,56.9,31.8,56.9z"/>
+</g>
+<path id="icon-e" class="st7" d="M44.7,40.6c-2.2,0-4.3-0.4-6.1-1.1c-1.8,1.5-4.1,2.3-6.5,2.6c0,0,0,0-0.1,0c-0.2,0-0.4-0.1-0.5-0.4
+ c-0.1-0.2,0.1-0.4,0.3-0.5c0.9-0.9,1.9-1.5,2.4-4.4c-1.7-1.6-2.7-3.6-2.7-5.8c0-5.3,5.9-9.7,13.1-9.7s13.1,4.3,13.1,9.7
+ C57.8,36.3,51.9,40.6,44.7,40.6z"/>
+<path class="st8" d="M44.7,39.7c-2.2,0-4.3-0.4-6.1-1.1c-1.8,1.5-4.1,2.3-6.5,2.6c0,0,0,0-0.1,0c-0.2,0-0.4-0.1-0.5-0.4
+ c-0.1-0.2,0.1-0.4,0.3-0.5c0.9-0.9,1.9-1.5,2.4-4.4c-1.7-1.6-2.7-3.6-2.7-5.8c0-5.3,5.9-9.7,13.1-9.7s13.1,4.3,13.1,9.7
+ C57.8,35.4,51.9,39.7,44.7,39.7z"/>
+</svg>
diff --git a/addons/mass_mailing_sms/tests/__init__.py b/addons/mass_mailing_sms/tests/__init__.py
new file mode 100644
index 00000000000..511e649c9e2
--- /dev/null
+++ b/addons/mass_mailing_sms/tests/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import common
diff --git a/addons/mass_mailing_sms/tests/common.py b/addons/mass_mailing_sms/tests/common.py
new file mode 100644
index 00000000000..bffc117ac22
--- /dev/null
+++ b/addons/mass_mailing_sms/tests/common.py
@@ -0,0 +1,73 @@
+# -*- coding: utf-8 -*-
+
+from contextlib import contextmanager
+from unittest.mock import patch
+
+from odoo import exceptions, tools
+from odoo.addons.phone_validation.tools import phone_validation
+from odoo.addons.sms.tests import common as sms_common
+from odoo.tests import common
+from odoo.addons.sms.models.sms_api import SmsApi
+
+
+class MockSMS(sms_common.MockSMS):
+
+ def assertSMSOutgoingStatistics(self, partners, numbers, records, mailing):
+ found_sms = self.env['sms.sms'].sudo().search([
+ '|', ('partner_id', 'in', partners.ids), ('number', 'in', numbers),
+ ('state', '=', 'outgoing')
+ ])
+ self.assertEqual(found_sms.filtered(lambda s: s.partner_id).mapped('partner_id'), partners)
+ self.assertEqual(set(found_sms.filtered(lambda s: not s.partner_id).mapped('number')), set(numbers))
+
+ found_traces = self.env['mailing.trace'].sudo().search([
+ ('sms_sms_id_int', 'in', found_sms.ids),
+ ])
+ self.assertEqual(len(found_sms), len(found_traces))
+ self.assertTrue(all(s.state == 'outgoing' for s in found_traces))
+ self.assertTrue(all(s.res_model == records._name for s in found_traces))
+ self.assertEqual(set(found_traces.mapped('res_id')), set(records.ids))
+ self.assertTrue(all(s.mass_mailing_id == mailing for s in found_traces))
+
+ def assertSMSStatistics(self, recipients_info, mailing, records, check_sms=True):
+ """ Check content of notifications.
+
+ :param recipients_info: list[{
+ 'partner': res.partner record (may be empty),
+ 'number': number used for notification (may be empty, computed based on partner),
+ 'state': outgoing / sent / ignored / exception / opened (sent by default),
+ 'record: linked record,
+ 'failure_type': optional: sms_number_missing / sms_number_format / sms_credit / sms_server
+ }, { ... }]
+ """
+ traces = self.env['mailing.trace'].search([
+ ('mass_mailing_id', 'in', mailing.ids),
+ ('res_id', 'in', records.ids)
+ ])
+
+ self.assertTrue(all(s.model == records._name for s in traces))
+ # self.assertTrue(all(s.utm_campaign_id == mailing.campaign_id for s in traces))
+ self.assertEqual(set(s.res_id for s in traces), set(records.ids))
+
+ for recipient_info in recipients_info:
+ partner = recipient_info.get('partner', self.env['res.partner'])
+ number = recipient_info.get('number')
+ state = recipient_info.get('state', 'outgoing')
+ content = recipient_info.get('content', None)
+ if number is None and partner:
+ number = phone_validation.phone_get_sanitized_record_number(partner)
+
+ notif = traces.filtered(lambda s: s.sms_number == number and s.state == state)
+ self.assertTrue(notif, 'SMS: not found notification for number %s, (state: %s)' % (number, state))
+
+ if check_sms:
+ if state == 'sent':
+ self.assertSMSSent([number], content)
+ elif state == 'outgoing':
+ self.assertSMSOutgoing(partner, number, content)
+ elif state == 'exception':
+ self.assertSMSFailed(partner, number, recipient_info.get('failure_type'), content)
+ elif state == 'ignored':
+ self.assertSMSCanceled(partner, number, recipient_info.get('failure_type', False), content)
+ else:
+ raise NotImplementedError()
diff --git a/addons/mass_mailing_sms/views/link_tracker_views.xml b/addons/mass_mailing_sms/views/link_tracker_views.xml
new file mode 100644
index 00000000000..30ab3459e02
--- /dev/null
+++ b/addons/mass_mailing_sms/views/link_tracker_views.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <menuitem id="link_tracker_menu"
+ name="Link Tracker"
+ parent="mass_mailing_sms_menu_configuration"
+ sequence="2"
+ action="link_tracker.link_tracker_action"/>
+</odoo>
diff --git a/addons/mass_mailing_sms/views/mailing_contact_views.xml b/addons/mass_mailing_sms/views/mailing_contact_views.xml
new file mode 100644
index 00000000000..374191ec91f
--- /dev/null
+++ b/addons/mass_mailing_sms/views/mailing_contact_views.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <record id="mailing_contact_view_search" model="ir.ui.view">
+ <field name="name">mailing.contact.view.search.inherit.sms</field>
+ <field name="model">mailing.contact</field>
+ <field name="inherit_id" ref="mass_mailing.view_mail_mass_mailing_contact_search"/>
+ <field name="arch" type="xml">
+ <xpath expr="//field[@name='name']" position="after">
+ <field name="phone_sanitized"/>
+ </xpath>
+ <xpath expr="//filter[@name='filter_not_optout']" position="after">
+ <separator/>
+ <filter string="Valid SMS Recipients"
+ name="filter_valid_sms_recipient"
+ domain="[('opt_out', '=', False), ('phone_blacklisted', '=', False), ('phone_sanitized', '!=', False)]"
+ invisible="not context.get('default_list_ids')"/>
+ <separator/>
+ <filter string="Exclude Blacklisted Phone"
+ name="filter_not_phone_bl"
+ domain="[('phone_blacklisted', '=', False)]"/>
+ </xpath>
+ </field>
+ </record>
+
+ <record id="mailing_contact_view_tree_sms" model="ir.ui.view">
+ <field name="name">mailing.contact.view.tree.sms</field>
+ <field name="model">mailing.contact</field>
+ <field name="priority">20</field>
+ <field name="arch" type="xml">
+ <tree string="Contacts">
+ <field name="create_date"/>
+ <field name="name"/>
+ <field name="company_name"/>
+ <field name="phone_sanitized"/>
+ <field name="phone_blacklisted"/>
+ <field name="opt_out" invisible="'default_list_ids' not in context"/>
+ </tree>
+ </field>
+ </record>
+
+ <record id="mailing_contact_view_form" model="ir.ui.view">
+ <field name="name">mailing.contact.view.form.inherit.sms</field>
+ <field name="model">mailing.contact</field>
+ <field name="inherit_id" ref="mass_mailing.view_mail_mass_mailing_contact_form"/>
+ <field name="arch" type="xml">
+ <xpath expr="//label[@for='email']" position="attributes">
+ <attribute name="invisible">context.get('mailing_sms') == True</attribute>
+ </xpath>
+ <xpath expr="//div[@name='email_details']" position="attributes">
+ <attribute name="invisible">context.get('mailing_sms') == True</attribute>
+ </xpath>
+ <xpath expr="//div[@name='email_details']" position="after">
+ <label for="phone_sanitized" class="oe_inline"/>
+ <div class="o_row o_row_readonly" name="phone_details">
+ <i class="fa fa-ban" style="color: red;" role="img" title="This number is blacklisted for mass SMS"
+ aria-label="Phone Blacklisted" attrs="{'invisible': [('phone_blacklisted', '=', False)]}" groups="base.group_user"></i>
+ <field name="phone_sanitized" widget="phone"/>
+ <field name="phone_blacklisted" invisible="1"/>
+ </div>
+ </xpath>
+ </field>
+ </record>
+
+ <record id="mailing_contact_view_kanban" model="ir.ui.view">
+ <field name="name">mailing.contact.view.kanban.inherit.sms</field>
+ <field name="model">mailing.contact</field>
+ <field name="inherit_id" ref="mass_mailing.view_mail_mass_mailing_contact_kanban"/>
+ <field name="arch" type="xml">
+ <xpath expr="//field[@name='email']" position="after">
+ <field name="phone_sanitized"/>
+ </xpath>
+ <xpath expr="//t[@t-esc='record.email.value']" position="after">
+ <t t-esc="record.phone_sanitized.value"/>
+ </xpath>
+ </field>
+ </record>
+
+ <record id="mailing_contact_action_sms" model="ir.actions.act_window">
+ <field name="name">Contacts</field>
+ <field name="res_model">mailing.contact</field>
+ <field name="view_mode">tree,form</field>
+ <field name="context">{'mailing_sms': True, 'search_default_filter_not_phone_bl': 1, }</field>
+ <field name="view_id" ref="mailing_contact_view_tree_sms"/>
+ <field name="help" type="html">
+ <p class="o_view_nocontent_smiling_face">
+ Create a new contact
+ </p>
+ </field>
+ </record>
+
+ <!-- Mass SMS / Contacts Lists / Contacts Lists -->
+ <record id="mass_mailing_sms.mailing_contact_menu_sms" model="ir.ui.menu">
+ <field name="action" ref="mailing_contact_action_sms"/>
+ </record>
+
+</odoo>
diff --git a/addons/mass_mailing_sms/views/mailing_list_views.xml b/addons/mass_mailing_sms/views/mailing_list_views.xml
new file mode 100644
index 00000000000..d1b88669078
--- /dev/null
+++ b/addons/mass_mailing_sms/views/mailing_list_views.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <record id="mailing_list_action_sms" model="ir.actions.act_window">
+ <field name="name">Contact Lists</field>
+ <field name="res_model">mailing.list</field>
+ <field name="view_mode">kanban,tree,form</field>
+ <field name="context">{'mailing_sms': True}</field>
+ <field name="help" type="html">
+ <p class="o_view_nocontent_smiling_face">
+ Create a new contacts list
+ </p><p>
+ You don't need to import your contacts lists, you can easily
+ send SMS to any contact saved in other Odoo apps.
+ </p>
+ </field>
+ </record>
+
+ <!-- Mass SMS / Contacts Lists / Contacts Lists -->
+ <record id="mass_mailing_sms.mailing_list_menu_sms" model="ir.ui.menu">
+ <field name="action" ref="mailing_list_action_sms"/>
+ </record>
+</odoo>
diff --git a/addons/mass_mailing_sms/views/mailing_mailing_views.xml b/addons/mass_mailing_sms/views/mailing_mailing_views.xml
new file mode 100644
index 00000000000..d4517cf0b1f
--- /dev/null
+++ b/addons/mass_mailing_sms/views/mailing_mailing_views.xml
@@ -0,0 +1,183 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo><data>
+ <record id="mailing_mailing_view_form" model="ir.ui.view">
+ <field name="name">mailing.mailing.view.form.inherit.sms</field>
+ <field name="model">mailing.mailing</field>
+ <field name="inherit_id" ref="mass_mailing.view_mail_mass_mailing_form"/>
+ <field name="arch" type="xml">
+ <!-- Buttons / Actions -->
+ <xpath expr="//button[@name='action_put_in_queue']" position="attributes">
+ <attribute name="attrs">{'invisible': ['|', ('state', 'in', ('in_queue', 'done')), ('mailing_type', '!=', 'mail')]}</attribute>
+ </xpath>
+ <xpath expr="//button[@name='action_put_in_queue']" position="after">
+ <button name="action_put_in_queue" type="object"
+ class="oe_highlight" string="Send Now"
+ attrs="{'invisible': ['|', ('mailing_type', '=', 'mail'), ('state', 'in', ('in_queue', 'done'))]}"
+ confirm="This will send SMS to all recipients. Do you still want to proceed ?"/>
+ </xpath>
+ <!-- Headers / Warnings -->
+ <xpath expr="//header" position="after">
+ <field name="sms_has_insufficient_credit" invisible="1"/>
+ <div class="alert alert-warning text-center" attrs="{'invisible': [('sms_has_insufficient_credit', '=', False)]}" role="alert">
+ <p>
+ <button class="btn-link"
+ name="action_buy_sms_credits"
+ type="object">
+ <strong>
+ It appears you don't have enough IAP credits. Click here to buy credits.
+ </strong>
+ </button>
+ </p>
+ </div>
+ </xpath>
+ <xpath expr="//span[@name='ignored_text']" position="attributes">
+ <attribute name="attrs">{'invisible': [('mailing_type', '!=', 'mail')]}</attribute>
+ </xpath>
+ <xpath expr="//span[@name='ignored_text']" position="after">
+ <span name="ignored_text_sms" attrs="{'invisible': [('mailing_type', '!=', 'sms')]}">SMS Text Message have been ignored and will not be sent.</span>
+ </xpath>
+ <xpath expr="//span[@name='scheduled_text']" position="attributes">
+ <attribute name="attrs">{'invisible': [('mailing_type', '!=', 'mail')]}</attribute>
+ </xpath>
+ <xpath expr="//span[@name='scheduled_text']" position="after">
+ <span name="scheduled_text_sms" attrs="{'invisible': [('mailing_type', '!=', 'sms')]}">SMS Text Message are in queue and will be sent soon.</span>
+ </xpath>
+ <xpath expr="//span[@name='failed_text']" position="attributes">
+ <attribute name="attrs">{'invisible': [('mailing_type', '!=', 'mail')]}</attribute>
+ </xpath>
+ <xpath expr="//span[@name='failed_text']" position="after">
+ <span name="failed_text_sms" attrs="{'invisible': [('mailing_type', '!=', 'sms')]}">SMS Text Message could not be sent.</span>
+ </xpath>
+ <!-- Stat Buttons -->
+ <xpath expr="//button[@id='button_view_sent']" position="attributes">
+ <attribute name="attrs">{'invisible': ['|', '&amp;', ('sent', '=', 0), ('state', 'in', ('draft', 'test')), ('mailing_type', '!=', 'mail')]}</attribute>
+ </xpath>
+ <xpath expr="//button[@id='button_view_sent']" position="after">
+ <button name="action_view_sent"
+ type="object"
+ context="{'search_default_filter_sent': True}"
+ icon="fa-comment-o" class="oe_stat_button"
+ attrs="{'invisible': ['|', '&amp;', ('sent', '=', 0), ('state', 'in', ('draft', 'test')), ('mailing_type', '!=', 'sms')]}" >
+ <field name="sent" string="Sent" widget="statinfo"/>
+ </button>
+ </xpath>
+ <xpath expr="//button[@name='action_view_opened']" position="attributes">
+ <attribute name="attrs">{'invisible': [('mailing_type', '!=', 'mail')]}</attribute>
+ </xpath>
+ <xpath expr="//button[@name='action_view_replied']" position="attributes">
+ <attribute name="attrs">{'invisible': [('mailing_type', '!=', 'mail')]}</attribute>
+ </xpath>
+ <!-- Form -->
+ <xpath expr="//field[@name='subject']" position="attributes">
+ <attribute name="attrs">{'invisible': [('mailing_type', '!=', 'mail')]}</attribute>
+ </xpath>
+ <xpath expr="//page[@name='mail_body']" position="attributes">
+ <attribute name="attrs">{'invisible': [('mailing_type', '!=', 'mail')]}</attribute>
+ </xpath>
+ <xpath expr="//page[@name='mail_body']" position="after">
+ <page string="SMS Content" name="sms_body" attrs="{'invisible': [('mailing_type', '!=', 'sms')]}">
+ <field name="body_plaintext" widget="sms_widget"/>
+ <group>
+ <field name="sms_template_id" domain="[('model', '=', mailing_model_real)]"/>
+ <field name="sms_allow_unsubscribe"/>
+ </group>
+ </page>
+ </xpath>
+ <!-- Option page tweaks -->
+ <xpath expr="//field[@name='email_from']" position="attributes">
+ <attribute name="attrs">{'invisible': [('mailing_type', '!=', 'mail')]}</attribute>
+ </xpath>
+ <xpath expr="//label[@for='reply_to']" position="attributes">
+ <attribute name="attrs">{'invisible': [('mailing_type', '!=', 'mail')]}</attribute>
+ </xpath>
+ <xpath expr="//div[@name='reply_to_details']" position="attributes">
+ <attribute name="attrs">{'invisible': [('mailing_type', '!=', 'mail')]}</attribute>
+ </xpath>
+ <xpath expr="//label[@for='attachment_ids']" position="attributes">
+ <attribute name="attrs">{'invisible': [('mailing_type', '!=', 'mail')]}</attribute>
+ </xpath>
+ <xpath expr="//div[@name='attachment_ids_details']" position="attributes">
+ <attribute name="attrs">{'invisible': [('mailing_type', '!=', 'mail')]}</attribute>
+ </xpath>
+ <xpath expr="//field[@name='mail_server_id']" position="attributes">
+ <attribute name="attrs">{'invisible': [('mailing_type', '!=', 'mail')]}</attribute>
+ </xpath>
+ </field>
+ </record>
+
+ <record id="mailing_mailing_view_kanban" model="ir.ui.view">
+ <field name="name">mailing.mailing.view.kanban.inherit.sms</field>
+ <field name="model">mailing.mailing</field>
+ <field name="inherit_id" ref="mass_mailing.view_mail_mass_mailing_kanban"/>
+ <field name="arch" type="xml">
+ <xpath expr="//field[@name='active']" position="after">
+ <field name="sms_has_insufficient_credit"/>
+ </xpath>
+ <xpath expr="//h3" position="after">
+ <div class="alert alert-warning" role="alert" attrs="{'invisible': [('sms_has_insufficient_credit', '=', False)]}">
+ <a name="action_buy_sms_credits"
+ type="object">Insufficient credits</a>
+ </div>
+ </xpath>
+ <xpath expr="//div[@name='stat_opened']" position="attributes">
+ <attribute name="attrs">{'invisible': [('mailing_type', '!=', 'mail')]}</attribute>
+ </xpath>
+ <xpath expr="//div[@name='stat_replied']" position="attributes">
+ <attribute name="attrs">{'invisible': [('mailing_type', '!=', 'mail')]}</attribute>
+ </xpath>
+ </field>
+ </record>
+
+ <record id="mailing_mailing_view_tree_sms" model="ir.ui.view">
+ <field name="name">mailing.mailing.view.tree.sms</field>
+ <field name="model">mailing.mailing</field>
+ <field name="priority">20</field>
+ <field name="arch" type="xml">
+ <tree string="Mass SMS">
+ <field name="name"/>
+ <field name="mailing_type" invisible="1"/>
+ <field name="sent"/>
+ <field name="clicked"/>
+ <field name="bounced"/>
+ <field name="campaign_id" groups="mass_mailing.group_mass_mailing_campaign"/>
+ </tree>
+ </field>
+ </record>
+
+ <record id="mailing_mailing_action_sms" model="ir.actions.act_window">
+ <field name="name">Mass SMS</field>
+ <field name="res_model">mailing.mailing</field>
+ <field name="view_mode">kanban,tree,form,graph</field>
+ <field name="domain">[('mailing_type', '=', 'sms')]</field>
+ <field name="context">{
+ 'search_default_assigned_to_me': 1,
+ 'default_user_id': uid,
+ 'default_mailing_type': 'sms',
+ }</field>
+ <field name="help" type="html">
+ <p class="o_view_nocontent_smiling_face">
+ Create a new mass SMS
+ </p><p>
+ You can easily send SMS to any contact saved in other Odoo apps.
+ </p>
+ </field>
+ </record>
+ <record id="mailing_mailing_action_sms_view_kanban" model="ir.actions.act_window.view">
+ <field name="sequence">1</field>
+ <field name="view_mode">kanban</field>
+ <field name="view_id" ref="mass_mailing.view_mail_mass_mailing_kanban"/>
+ <field name="act_window_id" ref="mailing_mailing_action_sms"/>
+ </record>
+ <record id="mailing_mailing_action_sms_view_tree" model="ir.actions.act_window.view">
+ <field name="sequence">2</field>
+ <field name="view_mode">tree</field>
+ <field name="view_id" ref="mailing_mailing_view_tree_sms"/>
+ <field name="act_window_id" ref="mailing_mailing_action_sms"/>
+ </record>
+
+ <!-- Mass SMS / Mass SMS -->
+ <record id="mass_mailing_sms_menu_mass_sms" model="ir.ui.menu">
+ <field name="action" ref="mailing_mailing_action_sms"/>
+ </record>
+
+</data></odoo>
diff --git a/addons/mass_mailing_sms/views/mailing_sms_menus.xml b/addons/mass_mailing_sms/views/mailing_sms_menus.xml
new file mode 100644
index 00000000000..f7cc0f05edb
--- /dev/null
+++ b/addons/mass_mailing_sms/views/mailing_sms_menus.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <!-- Mass SMS -->
+ <menuitem id="mass_mailing_sms_menu_root"
+ name="Mass SMS"
+ sequence="60"
+ web_icon="mass_mailing_sms,static/description/icon.png"
+ groups="mass_mailing.group_mass_mailing_user"/>
+
+ <!-- Mass SMS / Mass SMS -->
+ <menuitem id="mass_mailing_sms_menu_mass_sms"
+ name="Mass SMS"
+ parent="mass_mailing_sms_menu_root"
+ sequence="1"
+ groups="mass_mailing.group_mass_mailing_user"/>
+
+ <!-- Mass SMS / Contacts Lists -->
+ <menuitem id="mass_mailing_sms_menu_contacts"
+ name="Contacts Lists"
+ parent="mass_mailing_sms_menu_root"
+ sequence="2"
+ groups="mass_mailing.group_mass_mailing_user"/>
+ <!-- Mass SMS / Contacts Lists / Contacts Lists -->
+ <menuitem id="mailing_list_menu_sms"
+ name="Contacts Lists"
+ parent="mass_mailing_sms_menu_contacts"
+ sequence="1"
+ groups="mass_mailing.group_mass_mailing_user"/>
+ <!-- Mass SMS / Contacts Lists / Contacts -->
+ <menuitem id="mailing_contact_menu_sms"
+ name="Contacts"
+ parent="mass_mailing_sms_menu_contacts"
+ sequence="2"
+ groups="mass_mailing.group_mass_mailing_user"/>
+
+ <!-- Mass SMS / Reporting -->
+ <menuitem id="mass_mailing_sms_menu_reporting"
+ name="Reporting"
+ parent="mass_mailing_sms_menu_root"
+ sequence="80"
+ groups="mass_mailing.group_mass_mailing_user"/>
+
+ <!-- Mass SMS / Configuration -->
+ <menuitem id="mass_mailing_sms_menu_configuration"
+ name="Configuration"
+ parent="mass_mailing_sms_menu_root"
+ sequence="100"
+ groups="mass_mailing.group_mass_mailing_user"/>
+ <!-- Mass SMS / Configuration / Blacklist -->
+ <menuitem id="phone_blacklist_menu"
+ name="Phone Blacklist"
+ parent="mass_mailing_sms_menu_configuration"
+ sequence="1"
+ action="phone_validation.phone_blacklist_action"
+ groups="mass_mailing.group_mass_mailing_user"/>
+ <!-- Mass SMS / Configuration / Link Tracker -->
+ <menuitem id="link_tracker_menu"
+ name="Link Tracker Blacklist"
+ parent="mass_mailing_sms_menu_configuration"
+ sequence="2"
+ action="link_tracker.link_tracker_action"
+ groups="mass_mailing.group_mass_mailing_user"/>
+</odoo> \ No newline at end of file
diff --git a/addons/mass_mailing_sms/views/mailing_trace_views.xml b/addons/mass_mailing_sms/views/mailing_trace_views.xml
new file mode 100644
index 00000000000..d109c86b8b4
--- /dev/null
+++ b/addons/mass_mailing_sms/views/mailing_trace_views.xml
@@ -0,0 +1,121 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <record id="mailing_trace_view_search" model="ir.ui.view">
+ <field name="name">mailing.trace.view.search.inherit.sms</field>
+ <field name="model">mailing.trace</field>
+ <field name="inherit_id" ref="mass_mailing.mailing_trace_view_search"/>
+ <field name="arch" type="xml">
+ <xpath expr="//field[@name='email']" position="after">
+ <field name="sms_sms_id_int"/>
+ <field name="sms_sms_id"/>
+ <field name="sms_number"/>
+ </xpath>
+ </field>
+ </record>
+
+ <record id="mailing_trace_view_tree" model="ir.ui.view">
+ <field name="name">mailing.trace.view.tree.inherit.sms</field>
+ <field name="model">mailing.trace</field>
+ <field name="inherit_id" ref="mass_mailing.mailing_trace_view_tree"/>
+ <field name="arch" type="xml">
+ <xpath expr="//field[@name='email']" position="before">
+ <field name="trace_type"/>
+ </xpath>
+ <xpath expr="//field[@name='email']" position="after">
+ <field name="sms_number"/>
+ </xpath>
+ </field>
+ </record>
+
+ <record id="mailing_trace_view_tree_sms" model="ir.ui.view">
+ <field name="name">mailing.trace.view.tree.sms</field>
+ <field name="model">mailing.trace</field>
+ <field name="priority">20</field>
+ <field name="arch" type="xml">
+ <tree string="SMS Traces" create="0">
+ <field name="sms_number"/>
+ <field name="state"/>
+ <field name="scheduled"/>
+ <field name="sent"/>
+ <field name="clicked"/>
+ <field name="bounced"/>
+ <field name="exception"/>
+ <field name="ignored"/>
+ <field name="failure_type"/>
+ </tree>
+ </field>
+ </record>
+
+ <record id="mailing_trace_view_form" model="ir.ui.view">
+ <field name="name">mailing.trace.view.form.inherit.sms</field>
+ <field name="model">mailing.trace</field>
+ <field name="inherit_id" ref="mass_mailing.mailing_trace_view_form"/>
+ <field name="arch" type="xml">
+ <xpath expr="//span[@name='trace_type_name_mail']" position="attributes">
+ <attribute name="attrs">{'invisible': [('trace_type', '!=', 'mail')]}</attribute>
+ </xpath>
+ <xpath expr="//span[@name='trace_type_name_mail']" position="after">
+ <span name="trace_type_name_sms" attrs="{'invisible': [('trace_type', '!=', 'sms')]}">This sms</span>
+ </xpath>
+ <xpath expr="//field[@name='email']" position="attributes">
+ <attribute name="attrs">{'invisible': [('trace_type', '!=', 'mail')]}</attribute>
+ </xpath>
+ <xpath expr="//field[@name='mail_mail_id_int']" position="attributes">
+ <attribute name="attrs">{'invisible': [('trace_type', '!=', 'mail')]}</attribute>
+ </xpath>
+ <xpath expr="//field[@name='message_id']" position="attributes">
+ <attribute name="attrs">{'invisible': [('trace_type', '!=', 'mail')]}</attribute>
+ </xpath>
+ <xpath expr="//field[@name='opened']" position="attributes">
+ <attribute name="attrs">{'invisible': [('trace_type', '=', 'sms')]}</attribute>
+ </xpath>
+ <xpath expr="//field[@name='replied']" position="attributes">
+ <attribute name="attrs">{'invisible': [('trace_type', '=', 'sms')]}</attribute>
+ </xpath>
+ <xpath expr="//field[@name='email']" position="after">
+ <field name="sms_number" attrs="{'invisible': [('trace_type', '!=', 'sms')]}"/>
+ <field name="sms_sms_id_int" attrs="{'invisible': [('trace_type', '!=', 'sms')]}"/>
+ <field name="sms_code" attrs="{'invisible': [('trace_type', '!=', 'sms')]}"/>
+ </xpath>
+ </field>
+ </record>
+
+ <record id="mailing_trace_view_form_sms" model="ir.ui.view">
+ <field name="name">mailing.trace.view.form.sms</field>
+ <field name="model">mailing.trace</field>
+ <field name="priority">20</field>
+ <field name="arch" type="xml">
+ <form string="SMS Trace" create="0">
+ <header>
+ <field name="state" widget="statusbar"/>
+ </header>
+ <sheet>
+ <div class="alert alert-info text-center" attrs="{'invisible': [('exception', '=', False)]}" role="alert">
+ <strong>This SMS could not be sent.</strong>
+ </div>
+ <div class="alert alert-info text-center" attrs="{'invisible': [('bounced', '=', False)]}" role="alert">
+ <strong>This number appears to be invalid.</strong>
+ </div>
+ <group>
+ <group>
+ <field name="sms_number"/>
+ <field name="mass_mailing_id"/>
+ <field name="campaign_id" groups="mass_mailing.group_mass_mailing_campaign"/>
+ <field name="sms_sms_id_int" groups="base.group_no_one"/>
+ <field name="model" groups="base.group_no_one"/>
+ <field name="res_id" groups="base.group_no_one"/>
+ </group>
+ <group>
+ <field name="scheduled"/>
+ <field name="sent"/>
+ <field name="clicked"/>
+ <field name="bounced"/>
+ <field name="exception"/>
+ <field name="ignored"/>
+ </group>
+ </group>
+ </sheet>
+ </form>
+ </field>
+ </record>
+</odoo>
diff --git a/addons/mass_mailing_sms/views/mass_mailing_sms_templates_portal.xml b/addons/mass_mailing_sms/views/mass_mailing_sms_templates_portal.xml
new file mode 100644
index 00000000000..bd1f927eb9d
--- /dev/null
+++ b/addons/mass_mailing_sms/views/mass_mailing_sms_templates_portal.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <template id="blacklist_main" name="Unsubscribed">
+ <t t-call="portal.frontend_layout">
+ <div class="container mb64">
+ <div class="row">
+ <div class="col-lg-6 offset-lg-3">
+ <h3>SMS Subscription</h3>
+
+ <form t-att-action="'/sms/%s/unsubscribe/%s' % (mailing_id, trace_code)" method="post">
+ <p>Please enter your phone number</p>
+ <div class="form-group row">
+ <label for="sms_number" class="col-sm-2 col-form-label">Number</label>
+ <div class="col-sm-10">
+ <input type="text" class="form-control" name="sms_number" id="sms_number"/>
+ </div>
+ </div>
+ <input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
+ <input type="hidden" name="trace_code" t-att-value="trace_code"/>
+ <input type="hidden" name="mailing_id" t-att-value="mailing_id"/>
+ <div class="form-group row">
+ <div class="col-sm-10 offset-sm-2">
+ <button type="submit" class="btn btn-primary">Unsubscribe me</button>
+ </div>
+ </div>
+ </form>
+ </div>
+ </div>
+ </div>
+ </t>
+ </template>
+
+ <template id="blacklist_number" name="Unsubscribed">
+ <t t-call="portal.frontend_layout">
+ <div class="container mb64">
+ <div class="row">
+ <div class="col-lg-6 offset-lg-3">
+ <h3>SMS Subscription</h3>
+
+ <div t-if="unsubscribe_error" class="alert alert-danger text-center" role="alert">
+ <p>There was an error when trying to unsubscribe <strong t-esc="sms_number"/></p>
+ <p t-esc="unsubscribe_error"/>
+ </div>
+ <div t-else="" class="alert alert-success text-center" role="status">
+ <t t-if="lists_optout">
+ <p><strong t-esc="sms_number"/> has been successfully removed from</p>
+ <t t-foreach="lists_optout" t-as="list_id">
+ <strong t-esc="list_id.name"/><br />
+ </t>
+ </t>
+ <p t-else="">
+ <strong t-esc="sms_number"/> has been successfully blacklisted
+ </p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </t>
+ </template>
+</odoo>
diff --git a/addons/mass_mailing_sms/wizard/__init__.py b/addons/mass_mailing_sms/wizard/__init__.py
new file mode 100644
index 00000000000..eecb3976fa5
--- /dev/null
+++ b/addons/mass_mailing_sms/wizard/__init__.py
@@ -0,0 +1,5 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import mailing_sms_test
+from . import sms_composer
diff --git a/addons/mass_mailing_sms/wizard/mailing_sms_test.py b/addons/mass_mailing_sms/wizard/mailing_sms_test.py
new file mode 100644
index 00000000000..72552715b59
--- /dev/null
+++ b/addons/mass_mailing_sms/wizard/mailing_sms_test.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, exceptions, fields, models, _
+from odoo.addons.phone_validation.tools import phone_validation
+
+
+class MassSMSTest(models.TransientModel):
+ _name = 'mailing.sms.test'
+ _description = 'Test SMS Mailing'
+
+ def _default_numbers(self):
+ return self.env.user.partner_id.phone_sanitized or ""
+
+ numbers = fields.Char(string='Number', required=True,
+ default=_default_numbers, help='Comma-separated list of phone numbers')
+ sanitized_numbers = fields.Char(compute='_compute_sanitized_numbers')
+ mailing_id = fields.Many2one('mailing.mailing', string='Mailing', required=True, ondelete='cascade')
+
+ @api.depends('numbers')
+ def _compute_sanitized_numbers(self):
+ if self.numbers:
+ numbers = [number.strip() for number in self.numbers.split(',')]
+ sanitize_res = phone_validation.phone_sanitize_numbers_w_record(numbers, self.env.user)
+ sanitized_numbers = [info['sanitized'] for info in sanitize_res.values() if info['sanitized']]
+ invalid_numbers = [number for number, info in sanitize_res.items() if info['code']]
+ if invalid_numbers:
+ raise exceptions.UserError(_('Following numbers are not correctly encoded: %s') % repr(invalid_numbers))
+ self.sanitized_numbers = ','.join(sanitized_numbers)
+ else:
+ self.sanitized_numbers = False
+
+ def action_send_sms(self):
+ self.ensure_one()
+ self.env['sms.api']._send_sms_batch([{
+ 'res_id': 0,
+ 'number': number,
+ 'content': self.mailing_id.body_plaintext,
+ } for number in self.sanitized_numbers.split(',')])
+ return True
diff --git a/addons/mass_mailing_sms/wizard/mailing_sms_test_views.xml b/addons/mass_mailing_sms/wizard/mailing_sms_test_views.xml
new file mode 100644
index 00000000000..97ed90df4e5
--- /dev/null
+++ b/addons/mass_mailing_sms/wizard/mailing_sms_test_views.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0"?>
+<odoo>
+ <record id="mailing_sms_test_view_form" model="ir.ui.view">
+ <field name="name">mailing.sms.test.view.form</field>
+ <field name="model">mailing.sms.test</field>
+ <field name="arch" type="xml">
+ <form string="Send a Sample SMS">
+ <p class="text-muted">
+ Send a sample SMS for testing purpose to the numbers below.
+ </p>
+ <group>
+ <field name="numbers"/>
+ <field name="sanitized_numbers" invisible="0"/>
+ <field name="mailing_id" invisible="1"/>
+ </group>
+ <footer>
+ <button string="Send" name="action_send_sms" type="object" class="btn-primary"/>
+ <button string="Cancel" class="btn btn-secondary" special="cancel"/>
+ </footer>
+ </form>
+ </field>
+ </record>
+
+ <record id="mailing_sms_test_action" model="ir.actions.act_window">
+ <field name="name">Test SMS Mailing</field>
+ <field name="res_model">mailing.sms.test</field>
+ <field name="view_mode">form</field>
+ <field name="target">new</field>
+ </record>
+</odoo>
diff --git a/addons/mass_mailing_sms/wizard/sms_composer.py b/addons/mass_mailing_sms/wizard/sms_composer.py
new file mode 100644
index 00000000000..584b15c66eb
--- /dev/null
+++ b/addons/mass_mailing_sms/wizard/sms_composer.py
@@ -0,0 +1,99 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import werkzeug.urls
+
+from odoo import fields, models
+
+
+class SMSComposer(models.TransientModel):
+ _inherit = 'sms.composer'
+
+ # mass mode with mass sms
+ mass_sms_allow_unsubscribe = fields.Boolean('Include opt-out link', default=True)
+ mailing_id = fields.Many2one('mailing.mailing', string='Mailing')
+ utm_campaign_id = fields.Many2one('utm.campaign', string='Campaign')
+
+ # ------------------------------------------------------------
+ # Mass mode specific
+ # ------------------------------------------------------------
+
+ def _get_unsubscribe_url(self, res_id, trace_code, number):
+ base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
+ return werkzeug.urls.url_join(
+ base_url,
+ '/sms/%s/%s' % (self.mailing_id.id, trace_code)
+ )
+
+ def _prepare_mass_sms_trace_values(self, record, sms_values):
+ trace_code = self.env['mailing.trace']._get_random_code()
+ trace_values = {
+ 'model': self.res_model,
+ 'res_id': record.id,
+ 'trace_type': 'sms',
+ 'mass_mailing_id': self.mailing_id.id,
+ 'sms_number': sms_values['number'],
+ 'sms_code': trace_code,
+ }
+ if sms_values['state'] == 'error':
+ if sms_values['error_code'] == 'sms_number_format':
+ trace_values['sent'] = fields.Datetime.now()
+ trace_values['bounced'] = fields.Datetime.now()
+ else:
+ trace_values['exception'] = fields.Datetime.now()
+ elif sms_values['state'] == 'canceled':
+ trace_values['ignored'] = fields.Datetime.now()
+ else:
+ if self.mass_sms_allow_unsubscribe:
+ sms_values['body'] = '%s %s' % ((sms_values['body'] or ''), self._get_unsubscribe_url(record.id, trace_code, sms_values['number']))
+ return trace_values
+
+ def _get_blacklist_record_ids(self, records, recipients_info):
+ """ Consider opt-outed contact as being blacklisted for that specific
+ mailing. """
+ res = super(SMSComposer, self)._get_blacklist_record_ids(records, recipients_info)
+ if self.mailing_id:
+ optout_res_ids = self.mailing_id._get_opt_out_list_sms()
+ res += optout_res_ids
+ return res
+
+ def _get_done_record_ids(self, records, recipients_info):
+ """ A/B testing could lead to records having been already mailed. """
+ res = super(SMSComposer, self)._get_done_record_ids(records, recipients_info)
+ if self.mailing_id:
+ seen_ids, seen_list = self.mailing_id._get_seen_list_sms()
+ res += seen_ids
+ return res
+
+ def _prepare_body_values(self, records):
+ all_bodies = super(SMSComposer, self)._prepare_body_values(records)
+ if self.mailing_id:
+ tracker_values = self.mailing_id._get_link_tracker_values()
+ for sms_id, body in all_bodies.items():
+ body = self.env['link.tracker'].sudo()._convert_links_text(body, tracker_values)
+ all_bodies[sms_id] = body
+ return all_bodies
+
+ def _prepare_mass_sms_values(self, records=None):
+ result = super(SMSComposer, self)._prepare_mass_sms_values(records=records)
+ if self.composition_mode == 'mass' and self.mailing_id:
+ records = records if records is not None else self._get_records()
+
+ # for record_id, sms_values in result.items():
+ for record in records:
+ sms_values = result[record.id]
+
+ trace_values = self._prepare_mass_sms_trace_values(record, sms_values)
+ sms_values.update({
+ 'mailing_id': self.mailing_id.id,
+ 'mailing_trace_ids': [(0, 0, trace_values)],
+ })
+ return result
+
+ def _action_send_sms_mass(self, records=None):
+ sms_all = super(SMSComposer, self)._action_send_sms_mass()
+ if self.mailing_id:
+ updated_bodies = sms_all._update_body_short_links()
+ for sms in sms_all:
+ sms.body = updated_bodies[sms.id]
+ return sms_all
diff --git a/addons/mass_mailing_sms/wizard/sms_composer_views.xml b/addons/mass_mailing_sms/wizard/sms_composer_views.xml
new file mode 100644
index 00000000000..139338c78d0
--- /dev/null
+++ b/addons/mass_mailing_sms/wizard/sms_composer_views.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <record id="sms_composer_view_form" model="ir.ui.view">
+ <field name="name">sms.composer.views.inherit.sms</field>
+ <field name="model">sms.composer</field>
+ <field name="inherit_id" ref="sms.sms_composer_view_form"/>
+ <field name="arch" type="xml">
+ <xpath expr="//field[@name='res_model']" position="after">
+ <field name="utm_campaign_id" groups="mass_mailing.group_mass_mailing_campaign"
+ invisible="1"/>
+ <field name="mailing_id" invisible="1"/>
+ </xpath>
+ </field>
+ </record>
+
+</odoo>
diff --git a/addons/test_mail_full/__manifest__.py b/addons/test_mail_full/__manifest__.py
index 3f6c16e5639..45ac06a9258 100644
--- a/addons/test_mail_full/__manifest__.py
+++ b/addons/test_mail_full/__manifest__.py
@@ -15,7 +15,8 @@ real applications. """,
'mail',
'mail_bot',
# 'snailmail',
- # 'mass_mailing',
+ 'mass_mailing',
+ 'mass_mailing_sms',
'phone_validation',
'sms',
],
diff --git a/addons/test_mail_full/tests/__init__.py b/addons/test_mail_full/tests/__init__.py
index 36523fca998..ce173406381 100644
--- a/addons/test_mail_full/tests/__init__.py
+++ b/addons/test_mail_full/tests/__init__.py
@@ -2,6 +2,7 @@
from . import common
from . import test_phone_blacklist
+from . import test_mass_sms
from . import test_sms_composer
from . import test_sms_management
from . import test_sms_performance
diff --git a/addons/test_mail_full/tests/common.py b/addons/test_mail_full/tests/common.py
index 0bd34a96277..5a7c3d88e07 100644
--- a/addons/test_mail_full/tests/common.py
+++ b/addons/test_mail_full/tests/common.py
@@ -2,6 +2,7 @@
from odoo.addons.phone_validation.tools import phone_validation
from odoo.addons.test_mail.tests import common as test_mail_common
+from odoo.addons.test_mail.tests.common import mail_new_test_user
class BaseFunctionalTest(test_mail_common.BaseFunctionalTest):
@@ -62,3 +63,14 @@ class TestRecipients(test_mail_common.TestRecipients):
phone_validation.phone_format(partner.mobile, partner.country_id.code, partner.country_id.phone_code, force_format='E164')
for partner in (cls.partner_1 | cls.partner_2)
]
+
+
+class MassSMSBaseFunctionalTest(BaseFunctionalTest):
+
+ @classmethod
+ def setUpClass(cls):
+ super(MassSMSBaseFunctionalTest, cls).setUpClass()
+ cls.user_marketing = mail_new_test_user(
+ cls.env, login='marketing',
+ groups='base.group_user,mass_mailing.group_mass_mailing_user',
+ name='Martial Marketing', signature='--\nMartial')
diff --git a/addons/test_mail_full/tests/test_mass_sms.py b/addons/test_mail_full/tests/test_mass_sms.py
new file mode 100644
index 00000000000..2dc589ec9d0
--- /dev/null
+++ b/addons/test_mail_full/tests/test_mass_sms.py
@@ -0,0 +1,152 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+
+from odoo.addons.phone_validation.tools import phone_validation
+from odoo.addons.mass_mailing_sms.tests import common as mass_mailing_sms_common
+from odoo.addons.test_mail.tests import common as test_mail_common
+from odoo.addons.test_mail_full.tests import common as test_mail_full_common
+from odoo.tools import mute_logger
+from odoo.tests import tagged
+
+
+@tagged('mass_mailing')
+class TestMassSMS(test_mail_full_common.MassSMSBaseFunctionalTest, mass_mailing_sms_common.MockSMS):
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestMassSMS, cls).setUpClass()
+ cls._test_body = 'Mass SMS in your face'
+
+ records = cls.env['mail.test.sms']
+ partners = cls.env['res.partner']
+ country_be_id = cls.env.ref('base.be').id,
+ country_us_id = cls.env.ref('base.us').id,
+
+ for x in range(10):
+ partners += cls.env['res.partner'].with_context(**cls._test_context).create({
+ 'name': 'Partner_%s' % (x),
+ 'email': '_test_partner_%s@example.com' % (x),
+ 'country_id': country_be_id,
+ 'mobile': '045600%s%s99' % (x, x)
+ })
+ records += cls.env['mail.test.sms'].with_context(**cls._test_context).create({
+ 'name': 'MassSMSTest_%s' % (x),
+ 'customer_id': partners[x].id,
+ 'phone_nbr': '045600%s%s44' % (x, x)
+ })
+ cls.records = cls._reset_mail_context(records)
+ cls.records_numbers = [phone_validation.phone_format(r.phone_nbr, 'BE', '32', force_format='E164') for r in cls.records]
+ cls.partners = partners
+
+ cls.sms_template = cls.env['sms.template'].create({
+ 'name': 'Test Template',
+ 'model_id': cls.env['ir.model']._get('mail.test.sms').id,
+ 'body': 'Dear ${object.display_name} this is a mass SMS.',
+ })
+
+ cls.partner_numbers = [
+ phone_validation.phone_format(partner.mobile, partner.country_id.code, partner.country_id.phone_code, force_format='E164')
+ for partner in partners
+ ]
+
+ cls.mailing = cls.env['mailing.mailing'].with_user(cls.user_marketing).create({
+ 'name': 'Xmas Spam',
+ 'subject': 'Xmas Spam',
+ 'mailing_model_id': cls.env['ir.model']._get('mail.test.sms').id,
+ 'mailing_type': 'sms',
+ 'mailing_domain': '%s' % repr([('name', 'ilike', 'MassSMSTest')]),
+ 'sms_template_id': cls.sms_template.id,
+ 'sms_allow_unsubscribe': False,
+ })
+
+ def test_mass_sms_internals(self):
+ with self.sudo('marketing'):
+ mailing = self.env['mailing.mailing'].create({
+ 'name': 'Xmas Spam',
+ 'subject': 'Xmas Spam',
+ 'mailing_model_id': self.env['ir.model']._get('mail.test.sms').id,
+ 'mailing_type': 'sms',
+ 'mailing_domain': '%s' % repr([('name', 'ilike', 'MassSMSTest')]),
+ 'sms_template_id': self.sms_template.id,
+ 'sms_allow_unsubscribe': False,
+ })
+
+ self.assertEqual(mailing.mailing_model_real, 'mail.test.sms')
+ self.assertEqual(mailing.medium_id, self.env.ref('mass_mailing_sms.utm_medium_sms'))
+ self.assertEqual(mailing.body_plaintext, self.sms_template.body)
+
+ remaining_res_ids = mailing._get_remaining_recipients()
+ self.assertEqual(set(remaining_res_ids), set(self.records.ids))
+
+ with self.mockSMSGateway():
+ mailing.action_send_sms()
+
+ self.assertSMSStatistics(
+ [{'partner': record.customer_id, 'number': self.records_numbers[i], 'content': 'Dear %s this is a mass SMS.' % record.display_name} for i, record in enumerate(self.records)],
+ mailing, self.records, check_sms=True
+ )
+
+ def test_mass_sms_internals_errors(self):
+ # same customer, specific different number on record -> should be valid
+ new_record_1 = self.env['mail.test.sms'].create({
+ 'name': 'MassSMSTest_nr1',
+ 'customer_id': self.partners[0].id,
+ 'phone_nbr': '0456999999',
+ })
+ # new customer, number already on record -> should be ignored
+ country_be_id = self.env.ref('base.be').id,
+ nr2_partner = self.env['res.partner'].create({
+ 'name': 'Partner_nr2',
+ 'country_id': country_be_id,
+ 'mobile': '0456449999',
+ })
+ new_record_2 = self.env['mail.test.sms'].create({
+ 'name': 'MassSMSTest_nr2',
+ 'customer_id': nr2_partner.id,
+ 'phone_nbr': self.records[0].phone_nbr,
+ })
+ records_numbers = self.records_numbers + ['+32456999999']
+
+ with self.sudo('marketing'):
+ with self.mockSMSGateway():
+ self.mailing.action_send_sms()
+
+ self.assertSMSStatistics(
+ [{'partner': record.customer_id, 'number': records_numbers[i], 'content': 'Dear %s this is a mass SMS.' % record.display_name} for i, record in enumerate(self.records | new_record_1)],
+ self.mailing, self.records | new_record_1, check_sms=True
+ )
+ self.assertSMSStatistics(
+ [{'partner': new_record_2.customer_id, 'number': self.records_numbers[0], 'content': 'Dear %s this is a mass SMS.' % new_record_2.display_name, 'state': 'ignored', 'failure_type': 'sms_duplicate'}],
+ self.mailing, new_record_2, check_sms=True
+ )
+
+ def test_mass_sms_internals_done_ids(self):
+ with self.sudo('marketing'):
+ with self.mockSMSGateway():
+ self.mailing.action_send_sms(res_ids=self.records[:5].ids)
+
+ traces = self.env['mailing.trace'].search([('mass_mailing_id', 'in', self.mailing.ids)])
+ self.assertEqual(len(traces), 5)
+ # new traces generated
+ self.assertSMSStatistics(
+ [{'partner': record.customer_id, 'number': self.records_numbers[i], 'content': 'Dear %s this is a mass SMS.' % record.display_name} for i, record in enumerate(self.records[:5])],
+ self.mailing, self.records[:5], check_sms=True
+ )
+
+ with self.sudo('marketing'):
+ with self.mockSMSGateway():
+ self.mailing.action_send_sms(res_ids=self.records.ids)
+
+ # delete old traces (for testing purpose: ease check by deleting old ones)
+ traces.unlink()
+ # new failed traces generated for duplicates
+ self.assertSMSStatistics(
+ [{'partner': record.customer_id, 'number': self.records_numbers[i], 'content': 'Dear %s this is a mass SMS.' % record.display_name, 'state': 'ignored', 'failure_type': 'sms_duplicate'} for i, record in enumerate(self.records[:5])],
+ self.mailing, self.records[:5], check_sms=True
+ )
+ # new traces generated
+ self.assertSMSStatistics(
+ [{'partner': record.customer_id, 'number': self.records_numbers[i+5], 'content': 'Dear %s this is a mass SMS.' % record.display_name} for i, record in enumerate(self.records[5:])],
+ self.mailing, self.records[5:], check_sms=True
+ )
diff --git a/addons/test_mail_full/tests/test_sms_performance.py b/addons/test_mail_full/tests/test_sms_performance.py
index 748d5184fd6..87ff9c7cdb5 100644
--- a/addons/test_mail_full/tests/test_sms_performance.py
+++ b/addons/test_mail_full/tests/test_sms_performance.py
@@ -142,8 +142,8 @@ class TestSMSMassPerformance(BaseMailPerformance, sms_common.MockSMS):
'mass_keep_log': False,
})
- with self.mockSMSGateway(), self.assertQueryCount(employee=107):
- composer.action_send_sms()
+ with self.mockSMSGateway(), self.assertQueryCount(employee=108):
+ composer.action_send_sms()
@mute_logger('odoo.addons.sms.models.sms_sms')
@users('employee')
diff --git a/addons/test_mail_full/tests/test_sms_sms.py b/addons/test_mail_full/tests/test_sms_sms.py
index 426e3872901..4bafe20ec89 100644
--- a/addons/test_mail_full/tests/test_sms_sms.py
+++ b/addons/test_mail_full/tests/test_sms_sms.py
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import werkzeug
+
from unittest.mock import patch
from unittest.mock import DEFAULT
@@ -8,9 +10,52 @@ from odoo import exceptions
from odoo.addons.sms.models.sms_sms import SmsSms as SmsSms
from odoo.addons.sms.tests import common as sms_common
from odoo.addons.test_mail_full.tests import common as test_mail_full_common
+from odoo.tests import common
+
+
+class LinkTrackerMock(common.BaseCase):
+
+ def setUp(self):
+ super(LinkTrackerMock, self).setUp()
+
+ def _compute_favicon():
+ # 1px to avoid real request
+ return 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg=='
+
+ def _get_title_from_url(u):
+ return "Test_TITLE"
+
+ self.env['ir.config_parameter'].sudo().set_param('web.base.url', 'https://test.odoo.com')
+
+ link_tracker_favicon_patch = patch('odoo.addons.link_tracker.models.link_tracker.LinkTracker._compute_favicon', wraps=_compute_favicon)
+ link_tracker_title_patch = patch('odoo.addons.link_tracker.models.link_tracker.LinkTracker._get_title_from_url', wraps=_get_title_from_url)
+ link_tracker_favicon_patch.start()
+ link_tracker_title_patch.start()
+ self.addCleanup(link_tracker_favicon_patch.stop)
+ self.addCleanup(link_tracker_title_patch.stop)
+
+ self.utm_c = self.env.ref('utm.utm_campaign_fall_drive')
+ self.utm_m = self.env.ref('mass_mailing_sms.utm_medium_sms')
+ self.tracker_values = {
+ 'campaign_id': self.utm_c.id,
+ 'medium_id': self.utm_m.id,
+ }
+ def assertLinkTracker(self, url, url_params):
+ links = self.env['link.tracker'].sudo().search([('url', '=', url)])
+ self.assertEqual(len(links), 1)
-class TestSMSPost(test_mail_full_common.BaseFunctionalTest, sms_common.MockSMS):
+ # check UTMS are correctly set on redirect URL
+ original_url = werkzeug.urls.url_parse(url)
+ redirect_url = werkzeug.urls.url_parse(links.redirected_url)
+ redirect_params = redirect_url.decode_query().to_dict(flat=True)
+ self.assertEqual(redirect_url.scheme, original_url.scheme)
+ self.assertEqual(redirect_url.decode_netloc(), original_url.decode_netloc())
+ self.assertEqual(redirect_url.path, original_url.path)
+ self.assertEqual(redirect_params, url_params)
+
+
+class TestSMSPost(test_mail_full_common.BaseFunctionalTest, sms_common.MockSMS, LinkTrackerMock):
@classmethod
def setUpClass(cls):
@@ -24,6 +69,68 @@ class TestSMSPost(test_mail_full_common.BaseFunctionalTest, sms_common.MockSMS):
'body': cls._test_body,
})
+ def test_body_link_shorten(self):
+ link = 'http://www.example.com'
+ self.env['link.tracker'].search([('url', '=', link)]).unlink()
+ new_body = self.env['link.tracker']._convert_links_text('Welcome to %s !' % link, self.tracker_values)
+ self.assertNotIn(link, new_body)
+ self.assertLinkTracker(link, {'utm_campaign': self.utm_c.name, 'utm_medium': self.utm_m.name})
+ link = self.env['link.tracker'].search([('url', '=', link)])
+ self.assertIn(link.short_url, new_body)
+
+ link = 'https://test.odoo.com/my/super_page'
+ self.env['link.tracker'].search([('url', '=', link)]).unlink()
+ new_body = self.env['link.tracker']._convert_links_text('Welcome to %s !' % link, self.tracker_values)
+ self.assertNotIn(link, new_body)
+ self.assertLinkTracker(link, {'utm_campaign': self.utm_c.name, 'utm_medium': self.utm_m.name})
+ link = self.env['link.tracker'].search([('url', '=', link)])
+ self.assertIn(link.short_url, new_body)
+
+ def test_body_link_shorten_wshort(self):
+ link = 'https://test.odoo.com/r/RAOUL'
+ self.env['link.tracker'].search([('url', '=', link)]).unlink()
+ new_body = self.env['link.tracker']._convert_links_text('Welcome to %s !' % link, self.tracker_values)
+ self.assertIn(link, new_body)
+ self.assertFalse(self.env['link.tracker'].search([('url', '=', link)]))
+
+ def test_body_link_shorten_wunsubscribe(self):
+ link = 'https://test.odoo.com/sms/3/'
+ self.env['link.tracker'].search([('url', '=', link)]).unlink()
+ new_body = self.env['link.tracker']._convert_links_text('Welcome to %s !' % link, self.tracker_values)
+ self.assertIn(link, new_body)
+ self.assertFalse(self.env['link.tracker'].search([('url', '=', link)]))
+
+ def test_sms_body_link_shorten_suffix(self):
+ mailing = self.env['mailing.mailing'].create({
+ 'name': 'Minimal mailing',
+ 'mailing_model_id': self.env['ir.model']._get('mail.test.sms').id,
+ 'mailing_type': 'sms',
+ })
+
+ sms_0 = self.env['sms.sms'].create({
+ 'body': 'Welcome to https://test.odoo.com',
+ 'number': '12',
+ 'mailing_id': mailing.id,
+ })
+ sms_1 = self.env['sms.sms'].create({
+ 'body': 'Welcome to https://test.odoo.com/r/RAOUL',
+ 'number': '12',
+ })
+ sms_2 = self.env['sms.sms'].create({
+ 'body': 'Welcome to https://test.odoo.com/r/RAOUL',
+ 'number': '12', 'mailing_id': mailing.id,
+ })
+ sms_3 = self.env['sms.sms'].create({
+ 'body': 'Welcome to https://test.odoo.com/leodagan/r/RAOUL',
+ 'number': '12', 'mailing_id': mailing.id,
+ })
+
+ res = (sms_0 | sms_1 | sms_2 | sms_3)._update_body_short_links()
+ self.assertEqual(res[sms_0.id], 'Welcome to https://test.odoo.com')
+ self.assertEqual(res[sms_1.id], 'Welcome to https://test.odoo.com/r/RAOUL')
+ self.assertEqual(res[sms_2.id], 'Welcome to https://test.odoo.com/r/RAOUL/s/%s' % sms_2.id)
+ self.assertEqual(res[sms_3.id], 'Welcome to https://test.odoo.com/leodagan/r/RAOUL')
+
def test_sms_send_batch_size(self):
self.count = 0