[ADD] mass_mailing_sms: support SMS in mass mailing
PURPOSE
SMS are a powerful marketing tool. For instance it is perfect to announce a
sale or to communicate a coupon code, to welcome a new customer in a fidelity
program, ...
Purpose of this task is to integrate SMS sending in batch in mass mailing. It
will use same mailing objects but sending SMS instead of emails. Some metrics
and flows will have to be slightly updated at the same time.
SPECIFICATIONS
Create a new application called "Mass SMS" that follows the structure of
mass_mailing application.
General guidelines
* use the fa-comment icon for this module;
* add a "Notification type" field to the mailings: email or sms. Set it
invisible and set as domain from context when choosing mass mailing or
mass sms application (menus / action specific);
* adapt the metrics from Mass Mailing to SMS
* sent = received = whether the SMS was sent or not;
* opened => we don't have this information;
* replied => we don't have this information;
* clicks = from link tracker (no change of behavior);
* bounced = SMS that failed to be delivered because the format is wrong;
* exception = any issue when sending SMS;
* keep the Mailing Lists menu, hide email related fields on contact model;
* settings: hide the "Specific Mail Server" feature
Mailing form view
* add an "SMS Content" tab between Mail Body and Options. In there, we should
have the following fields:
* content
* opt-out link: provide a link to the Unsubscribe page. Make it as short
as possible;
* SMS Template
* add an "SMS Text Message" Medium;
Mailing actions
* add an "SMS Sent" smart button. Use the fa-comment-o icon and it should
redirect to the same list views as the "Email sent" smart button and list
the contacts to which an SMS was sent;
* add a "Mobile" field in the list views of the "Email sent" smart button:
* adapt the TEST button > Test Mailing modal to SMS (we should have 1 button
for Email and 1 for SMS); update description;
* default value for the Recipients field should be Name of the current
user, work mobile/phone number;
* If there are no Work Mobile/Phone numbers set on res.users, display
(123)-456-7890 instead;
* rename the Send Sample Mail button into Send Sample SMS;
* adapt the SEND NOW button > Confirmation modal to SMS. Change the message
into "This will send the SMS Text Message to all recipients. Do you still
want to proceed?"
* hide the following elements: Subject, From, Reply to, Attachments, Mail
Server, Mail Body tab; Opened, Replied;
* adapt the label of the following "warnings" to SMS
* x SMS Text Message(s) have been ignored and will not be sent.
* x SMS Text Message(s) are in queue and will be sent soon.
* x SMS Text Message(s) could not be sent.
Mailing kanban view
* hide the following stats: https://nimb.ws/grvBn0: Opened, Replied;
Mailing list / contact management
* following fields should be left empty: https://nimb.ws/3HBeJL: Opened,
Replied;
* when browsing through mailing lsits and contacts through the Mass SMS
application, update actions and views to hide fields related to
email and display only phone-related information;
UTM Campaign
* form view > Related Mailings tab: the following fields should be left
empty if notification type = SMS: https://nimb.ws/nfvr1O: Opened, Replied
Mass Mailing > Reporting (mail.trace.report model)
* adapt the reporting based on the fact that we do not have the following
metrics for SMS: Opened, Replied;
Mass Mailing > Configuration > Blacklist
* do not display any email information (phone blacklist is about phone);
* support phone blacklist. Phone blacklist is therefore a separate model
from mail.blacklist to simplify management. It is included in sms sending
process like mail blacklist when mailing in mass;
Opt-out and blacklist handling
* we do support mailing lists because they could come from different sources
and are a way to communicate with a specific audience (and not just a list
we bought), e.g. subscribe to this list to be kept updated whenever this
product is back in stock, or to receive results of this ...
* if the SMSing is from a list, opt-out removes ourselves from the list;
* if it is from the contact ==> Sent to blacklist;
* allow to attach unsubscription links in sent SMS, redirecting to a new
route in mass mailing SMS allowing quick unsubscribe and/or blacklist
of phone numbers;
Error management
* if the user clicks on Send Now / Schedule / Test and does not have enough
credits, open the "Insufficient credits" modal in a lazy option (aka when
having failed statistics linked to insufficient credits);
* on the mailing, we however display this failure as :
* a message on the mailing kanban view
* a message on the top of the form view + same behavior as the mailing and
"Could not be sent" https://www.screencast.com/t/BExgd1Gc;
LINKS
Task 1997464
PR #34424
Original SMS addition: Task 1922163 (4287481)
This commit is contained in:
@@ -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
|
||||
@@ -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,
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import main
|
||||
@@ -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)
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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']
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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>
|
||||
@@ -0,0 +1 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
|
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -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>
|
||||
|
After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import common
|
||||
@@ -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()
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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': ['|', '&', ('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': ['|', '&', ('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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -15,7 +15,8 @@ real applications. """,
|
||||
'mail',
|
||||
'mail_bot',
|
||||
# 'snailmail',
|
||||
# 'mass_mailing',
|
||||
'mass_mailing',
|
||||
'mass_mailing_sms',
|
||||
'phone_validation',
|
||||
'sms',
|
||||
],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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')
|
||||
|
||||
@@ -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 TestSMSPost(test_mail_full_common.BaseFunctionalTest, sms_common.MockSMS):
|
||||
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)
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user