Files
odoo_source/addons/mass_mailing_sms/models/mailing_mailing.py
T
Thibault Delavallée 1b8f2608ce [FIX] mass_mailing_sms: support convert_links for sms mailings
Mailing model holds a convert_links method that converts its local links.
It is not used in standard flows as it has more used for external tools.
Indeed sending mailings effectively shorten links without going through
that method.

However sms mailings are not supported by this method. In this commit we
ensure calling this method effectively convert links for sms mailings.
Same as email mailings links in standard flows are already shortened.

This allows notably to fix demo data as links were not converted
correctly as it was calling convert_links in xml.

Task ID-2274770
PR odoo/odoo#56887

Related: odoo/upgrade#1744
Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
2020-12-03 12:46:51 +00:00

306 lines
14 KiB
Python

# -*- 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
from odoo.osv import expression
_logger = logging.getLogger(__name__)
class Mailing(models.Model):
_inherit = 'mailing.mailing'
@api.model
def default_get(self, fields):
res = super(Mailing, self).default_get(fields)
if fields is not None and 'keep_archives' in fields and res.get('mailing_type') == 'sms':
res['keep_archives'] = True
return res
# mailing options
mailing_type = fields.Selection(selection_add=[
('sms', 'SMS')
], ondelete={'sms': 'set default'})
# 'sms_subject' added to override 'subject' field (string attribute should be labelled "Title" when mailing_type == 'sms').
# 'sms_subject' should have the same helper as 'subject' field when 'mass_mailing_sms' installed.
# otherwise 'sms_subject' will get the old helper from 'mass_mailing' module.
# overriding 'subject' field helper in this model is not working, since the helper will keep the new value
# even when 'mass_mailing_sms' removed (see 'mailing_mailing_view_form_sms' for more details).
sms_subject = fields.Char('Title', help='For an email, the subject your recipients will see in their inbox.\n'
'For an SMS, the internal title of the message.',
related='subject', translate=True, readonly=False)
# sms options
body_plaintext = fields.Text('SMS Body', compute='_compute_body_plaintext', store=True, readonly=False)
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_iap_failure',
help='UX Field to propose to buy IAP credits')
sms_has_unregistered_account = fields.Boolean(
'Unregistered IAP account', compute='_compute_sms_has_iap_failure',
help='UX Field to propose to Register the SMS IAP account')
sms_force_send = fields.Boolean(
'Send Directly', help='Immediately send the SMS Mailing instead of queuing up. Use at your own risk.')
# opt_out_link
sms_allow_unsubscribe = fields.Boolean('Include opt-out link', default=False)
@api.depends('mailing_type')
def _compute_medium_id(self):
super(Mailing, self)._compute_medium_id()
for mailing in self:
if mailing.mailing_type == 'sms' and (not mailing.medium_id or mailing.medium_id == self.env.ref('utm.utm_medium_email')):
mailing.medium_id = self.env.ref('mass_mailing_sms.utm_medium_sms').id
elif mailing.mailing_type == 'mail' and (not mailing.medium_id or mailing.medium_id == self.env.ref('mass_mailing_sms.utm_medium_sms')):
mailing.medium_id = self.env.ref('utm.utm_medium_email').id
@api.depends('sms_template_id', 'mailing_type')
def _compute_body_plaintext(self):
for mailing in self:
if mailing.mailing_type == 'sms' and mailing.sms_template_id:
mailing.body_plaintext = mailing.sms_template_id.body
@api.depends('mailing_trace_ids.failure_type')
def _compute_sms_has_iap_failure(self):
failures = ['sms_acc', 'sms_credit']
if not self.ids:
self.sms_has_insufficient_credit = self.sms_has_unregistered_account = False
else:
traces = self.env['mailing.trace'].sudo().read_group([
('mass_mailing_id', 'in', self.ids),
('trace_type', '=', 'sms'),
('failure_type', 'in', failures)
], ['mass_mailing_id', 'failure_type'], ['mass_mailing_id', 'failure_type'], lazy=False)
trace_dict = dict.fromkeys(self.ids, {key: False for key in failures})
for t in traces:
trace_dict[t['mass_mailing_id'][0]][t['failure_type']] = t['__count'] and True or False
for mail in self:
mail.sms_has_insufficient_credit = trace_dict[mail.id]['sms_credit']
mail.sms_has_unregistered_account = trace_dict[mail.id]['sms_acc']
# --------------------------------------------------
# ORM OVERRIDES
# --------------------------------------------------
@api.model
def create(self, values):
# Get subject from "sms_subject" field when SMS installed (used to build the name of record in the super 'create' method)
if values.get('mailing_type') == 'sms' and values.get('sms_subject'):
values['subject'] = values['sms_subject']
return super(Mailing, self).create(values)
# --------------------------------------------------
# BUSINESS / VIEWS ACTIONS
# --------------------------------------------------
def action_retry_failed(self):
mass_sms = self.filtered(lambda m: m.mailing_type == 'sms')
if mass_sms:
mass_sms.action_retry_failed_sms()
return super(Mailing, self - mass_sms).action_retry_failed()
def action_retry_failed_sms(self):
failed_sms = self.env['sms.sms'].sudo().search([
('mailing_id', 'in', self.ids),
('state', '=', 'error')
])
failed_sms.mapped('mailing_trace_ids').unlink()
failed_sms.unlink()
self.write({'state': 'in_queue'})
def action_test(self):
if self.mailing_type == 'sms':
ctx = dict(self.env.context, default_mailing_id=self.id)
return {
'name': _('Test SMS marketing'),
'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 == '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': self.keep_archives,
'mass_force_send': self.sms_force_send,
'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 are no recipients selected.'))
composer = self.env['sms.composer'].with_context(active_id=False).create(mailing._send_sms_get_composer_values(res_ids))
composer._action_send_sms()
mailing.write({
'state': 'done',
'sent_date': fields.Datetime.now(),
'kpi_mail_required': not mailing.sent_date,
})
return True
# ------------------------------------------------------
# STATISTICS
# ------------------------------------------------------
def _prepare_statistics_email_values(self):
"""Return some statistics that will be displayed in the mailing statistics email.
Each item in the returned list will be displayed as a table, with a title and
1, 2 or 3 columns.
"""
values = super(Mailing, self)._prepare_statistics_email_values()
if self.mailing_type == 'sms':
mailing_type = self._get_pretty_mailing_type()
values['title'] = _('24H Stats of %(mailing_type)s "%(mailing_name)s"',
mailing_type=mailing_type,
mailing_name=self.subject
)
values['kpi_data'][0] = {
'kpi_fullname': _('Report for %(expected)i %(mailing_type)s Sent',
expected=self.expected,
mailing_type=mailing_type
),
'kpi_col1': {
'value': f'{self.received_ratio}%',
'col_subtitle': _('RECEIVED (%i)', self.delivered),
},
'kpi_col2': {
'value': f'{self.clicks_ratio}%',
'col_subtitle': _('CLICKED (%i)', self.clicked),
},
'kpi_col3': {
'value': f'{self.bounced_ratio}%',
'col_subtitle': _('BOUNCED (%i)', self.bounced),
},
'kpi_action': None,
}
return values
def _get_pretty_mailing_type(self):
if self.mailing_type == 'sms':
return _('SMS Text Message')
return super(Mailing, self)._get_pretty_mailing_type()
# --------------------------------------------------
# TOOLS
# --------------------------------------------------
def _get_default_mailing_domain(self):
mailing_domain = super(Mailing, self)._get_default_mailing_domain()
if self.mailing_type == 'sms' and 'phone_sanitized_blacklisted' in self.env[self.mailing_model_name]._fields:
mailing_domain = expression.AND([mailing_domain, [('phone_sanitized_blacklisted', '=', False)]])
return mailing_domain
def convert_links(self):
sms_mailings = self.filtered(lambda m: m.mailing_type == 'sms')
res = {}
for mailing in sms_mailings:
tracker_values = mailing._get_link_tracker_values()
body = mailing._shorten_links_text(mailing.body_plaintext, tracker_values)
res[mailing.id] = body
res.update(super(Mailing, self - sms_mailings).convert_links())
return res