361 lines
16 KiB
Python
361 lines
16 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', related='subject',
|
|
readonly=False, translate=False,
|
|
help='For an email, the subject your recipients will see in their inbox.\n'
|
|
'For an SMS, the internal title of the message.')
|
|
# 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') # used to propose buying IAP credits
|
|
sms_has_unregistered_account = fields.Boolean(
|
|
'Unregistered IAP account', compute='_compute_sms_has_iap_failure') # used 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)
|
|
# A/B Testing
|
|
ab_testing_sms_winner_selection = fields.Selection(
|
|
related="campaign_id.ab_testing_sms_winner_selection",
|
|
default="clicks_ratio", readonly=False, copy=True)
|
|
ab_testing_mailings_sms_count = fields.Integer(related="campaign_id.ab_testing_mailings_sms_count")
|
|
|
|
@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):
|
|
self.sms_has_insufficient_credit = self.sms_has_unregistered_account = False
|
|
traces = self.env['mailing.trace'].sudo()._read_group([
|
|
('mass_mailing_id', 'in', self.ids),
|
|
('trace_type', '=', 'sms'),
|
|
('failure_type', 'in', ['sms_acc', 'sms_credit'])
|
|
], ['mass_mailing_id', 'failure_type'])
|
|
|
|
for mass_mailing, failure_type in traces:
|
|
if failure_type == 'sms_credit':
|
|
mass_mailing.sms_has_insufficient_credit = True
|
|
elif failure_type == 'sms_acc':
|
|
mass_mailing.sms_has_unregistered_account = True
|
|
|
|
# --------------------------------------------------
|
|
# ORM OVERRIDES
|
|
# --------------------------------------------------
|
|
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
for vals in vals_list:
|
|
# Get subject from "sms_subject" field when SMS installed (used to
|
|
# build the name of record in the super 'create' method)
|
|
if vals.get('mailing_type') == 'sms' and vals.get('sms_subject'):
|
|
vals['subject'] = vals['sms_subject']
|
|
return super().create(vals_list)
|
|
|
|
# --------------------------------------------------
|
|
# 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.action_put_in_queue()
|
|
|
|
def action_test(self):
|
|
if self.mailing_type == 'sms':
|
|
ctx = dict(self.env.context, default_mailing_id=self.id, dialog_size='medium')
|
|
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):
|
|
""" Give list of opt-outed records, depending on specific model-based
|
|
computation if available.
|
|
|
|
:return list: opt-outed record IDs
|
|
"""
|
|
self.ensure_one()
|
|
opt_out = []
|
|
target = self.env[self.mailing_model_real]
|
|
if hasattr(self.env[self.mailing_model_name], '_mailing_get_opt_out_list_sms'):
|
|
opt_out = self.env[self.mailing_model_name]._mailing_get_opt_out_list_sms(self)
|
|
_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]
|
|
|
|
partner_fields = []
|
|
if isinstance(target, self.pool['mail.thread.phone']):
|
|
phone_fields = ['phone_sanitized']
|
|
else:
|
|
phone_fields = [
|
|
fname for fname in target._phone_get_number_fields()
|
|
if fname in target._fields and target._fields[fname].store
|
|
]
|
|
partner_fields = target._mail_get_partner_fields()
|
|
partner_field = next(
|
|
(fname for fname in partner_fields if target._fields[fname].store and target._fields[fname].type == 'many2one'),
|
|
False
|
|
)
|
|
if not phone_fields and not partner_field:
|
|
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)
|
|
%(join_add_query)s
|
|
WHERE (%(where_query)s)
|
|
AND trace.mass_mailing_id = %%(mailing_id)s
|
|
AND trace.model = %%(target_model)s
|
|
"""
|
|
if phone_fields:
|
|
# phone fields are checked on target mailed model
|
|
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)
|
|
join_add_query = ''
|
|
else:
|
|
# phone fields are checked on res.partner model
|
|
partner_phone_fields = ['mobile', 'phone']
|
|
select_query = 'target.id, ' + ', '.join('partner.%s' % fname for fname in partner_phone_fields)
|
|
where_query = ' OR '.join('partner.%s IS NOT NULL' % fname for fname in partner_phone_fields)
|
|
join_add_query = 'JOIN res_partner partner ON (target.%s = partner.id)' % partner_field
|
|
|
|
query = query % {
|
|
'select_query': select_query,
|
|
'where_query': where_query,
|
|
'target_table': target._table,
|
|
'join_add_query': join_add_query,
|
|
}
|
|
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 res_ids:
|
|
composer = self.env['sms.composer'].with_context(active_id=False).create(mailing._send_sms_get_composer_values(res_ids))
|
|
composer._action_send_sms()
|
|
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,
|
|
'kpi_name': self.mailing_type,
|
|
}
|
|
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
|
|
|
|
# ------------------------------------------------------
|
|
# A/B Test Override
|
|
# ------------------------------------------------------
|
|
|
|
def _get_ab_testing_description_modifying_fields(self):
|
|
fields_list = super()._get_ab_testing_description_modifying_fields()
|
|
return fields_list + ['ab_testing_sms_winner_selection']
|
|
|
|
def _get_ab_testing_description_values(self):
|
|
values = super()._get_ab_testing_description_values()
|
|
if self.mailing_type == 'sms':
|
|
values.update({
|
|
'ab_testing_count': self.ab_testing_mailings_sms_count,
|
|
'ab_testing_winner_selection': self.ab_testing_sms_winner_selection,
|
|
})
|
|
return values
|
|
|
|
def _get_ab_testing_winner_selection(self):
|
|
result = super()._get_ab_testing_winner_selection()
|
|
if self.mailing_type == 'sms':
|
|
ab_testing_winner_selection_description = dict(
|
|
self._fields.get('ab_testing_sms_winner_selection').related_field.selection
|
|
).get(self.ab_testing_sms_winner_selection)
|
|
result.update({
|
|
'value': self.campaign_id.ab_testing_sms_winner_selection,
|
|
'description': ab_testing_winner_selection_description
|
|
})
|
|
return result
|
|
|
|
def _get_ab_testing_siblings_mailings(self):
|
|
mailings = super()._get_ab_testing_siblings_mailings()
|
|
if self.mailing_type == 'sms':
|
|
mailings = self.campaign_id.mailing_sms_ids.filtered('ab_testing_enabled')
|
|
return mailings
|
|
|
|
def _get_default_ab_testing_campaign_values(self, values=None):
|
|
campaign_values = super()._get_default_ab_testing_campaign_values(values)
|
|
values = values or dict()
|
|
if self.mailing_type == 'sms':
|
|
sms_subject = values.get('sms_subject') or self.sms_subject
|
|
if sms_subject:
|
|
campaign_values['name'] = _("A/B Test: %s", sms_subject)
|
|
campaign_values['ab_testing_sms_winner_selection'] = self.ab_testing_sms_winner_selection
|
|
return campaign_values
|