1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375
|
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from ast import literal_eval
from uuid import uuid4
from odoo import api, fields, models, _
from odoo.addons.sms.tools.sms_tools import sms_content_to_rendered_html
from odoo.exceptions import UserError
class SendSMS(models.TransientModel):
_name = 'sms.composer'
_description = 'Send SMS Wizard'
@api.model
def default_get(self, fields):
result = super(SendSMS, self).default_get(fields)
result['res_model'] = result.get('res_model') or self.env.context.get('active_model')
if not result.get('res_ids'):
if not result.get('res_id') and self.env.context.get('active_ids') and len(self.env.context.get('active_ids')) > 1:
result['res_ids'] = repr(self.env.context.get('active_ids'))
if not result.get('res_id'):
if not result.get('res_ids') and self.env.context.get('active_id'):
result['res_id'] = self.env.context.get('active_id')
return result
# documents
composition_mode = fields.Selection([
('numbers', 'Send to numbers'),
('comment', 'Post on a document'),
('mass', 'Send SMS in batch')], string='Composition Mode',
compute='_compute_composition_mode', precompute=True, readonly=False, required=True, store=True)
res_model = fields.Char('Document Model Name')
res_model_description = fields.Char('Document Model Description', compute='_compute_res_model_description')
res_id = fields.Integer('Document ID')
res_ids = fields.Char('Document IDs')
res_ids_count = fields.Integer(
'Visible records count', compute='_compute_res_ids_count', compute_sudo=False,
help='Number of recipients that will receive the SMS if sent in mass mode, without applying the Active Domain value')
comment_single_recipient = fields.Boolean(
'Single Mode', compute='_compute_comment_single_recipient', compute_sudo=False,
help='Indicates if the SMS composer targets a single specific recipient')
# options for comment and mass mode
mass_keep_log = fields.Boolean('Keep a note on document', default=True)
mass_force_send = fields.Boolean('Send directly', default=False)
mass_use_blacklist = fields.Boolean('Use blacklist', default=True)
# recipients
recipient_valid_count = fields.Integer('# Valid recipients', compute='_compute_recipients', compute_sudo=False)
recipient_invalid_count = fields.Integer('# Invalid recipients', compute='_compute_recipients', compute_sudo=False)
recipient_single_description = fields.Text('Recipients (Partners)', compute='_compute_recipient_single_non_stored', compute_sudo=False)
recipient_single_number = fields.Char('Stored Recipient Number', compute='_compute_recipient_single_non_stored', compute_sudo=False)
recipient_single_number_itf = fields.Char(
'Recipient Number', compute='_compute_recipient_single_stored',
readonly=False, compute_sudo=False, store=True,
help='Phone number of the recipient. If changed, it will be recorded on recipient\'s profile.')
recipient_single_valid = fields.Boolean("Is valid", compute='_compute_recipient_single_valid', compute_sudo=False)
number_field_name = fields.Char('Number Field')
numbers = fields.Char('Recipients (Numbers)')
sanitized_numbers = fields.Char('Sanitized Number', compute='_compute_sanitized_numbers', compute_sudo=False)
# content
template_id = fields.Many2one('sms.template', string='Use Template', domain="[('model', '=', res_model)]")
body = fields.Text(
'Message', compute='_compute_body',
precompute=True, readonly=False, store=True, required=True)
@api.depends('res_ids_count')
@api.depends_context('sms_composition_mode')
def _compute_composition_mode(self):
for composer in self:
if self.env.context.get('sms_composition_mode') == 'guess' or not composer.composition_mode:
if composer.res_ids_count > 1:
composer.composition_mode = 'mass'
else:
composer.composition_mode = 'comment'
@api.depends('res_model')
def _compute_res_model_description(self):
self.res_model_description = False
for composer in self.filtered('res_model'):
composer.res_model_description = self.env['ir.model']._get(composer.res_model).display_name
@api.depends('res_model', 'res_id', 'res_ids')
def _compute_res_ids_count(self):
for composer in self:
composer.res_ids_count = len(literal_eval(composer.res_ids)) if composer.res_ids else 0
@api.depends('res_id', 'composition_mode')
def _compute_comment_single_recipient(self):
for composer in self:
composer.comment_single_recipient = bool(composer.res_id and composer.composition_mode == 'comment')
@api.depends('res_model', 'res_id', 'res_ids', 'composition_mode', 'number_field_name', 'sanitized_numbers')
def _compute_recipients(self):
for composer in self:
composer.recipient_valid_count = 0
composer.recipient_invalid_count = 0
if composer.composition_mode not in ('comment', 'mass') or not composer.res_model:
continue
records = composer._get_records()
if records and isinstance(records, self.pool['mail.thread']):
res = records._sms_get_recipients_info(force_field=composer.number_field_name, partner_fallback=not composer.comment_single_recipient)
composer.recipient_valid_count = len([rid for rid, rvalues in res.items() if rvalues['sanitized']])
composer.recipient_invalid_count = len([rid for rid, rvalues in res.items() if not rvalues['sanitized']])
else:
composer.recipient_invalid_count = 0 if (
composer.sanitized_numbers or composer.composition_mode == 'mass'
) else 1
@api.depends('res_model', 'number_field_name')
def _compute_recipient_single_stored(self):
for composer in self:
records = composer._get_records()
if not records or not isinstance(records, self.pool['mail.thread']) or not composer.comment_single_recipient:
composer.recipient_single_number_itf = ''
continue
records.ensure_one()
res = records._sms_get_recipients_info(force_field=composer.number_field_name, partner_fallback=False)
if not composer.recipient_single_number_itf:
composer.recipient_single_number_itf = res[records.id]['number'] or ''
if not composer.number_field_name:
composer.number_field_name = res[records.id]['field_store']
@api.depends('res_model', 'number_field_name')
def _compute_recipient_single_non_stored(self):
for composer in self:
records = composer._get_records()
if not records or not isinstance(records, self.pool['mail.thread']) or not composer.comment_single_recipient:
composer.recipient_single_description = False
composer.recipient_single_number = ''
continue
records.ensure_one()
res = records._sms_get_recipients_info(force_field=composer.number_field_name, partner_fallback=True)
composer.recipient_single_description = res[records.id]['partner'].name or records._mail_get_partners()[records[0].id].display_name
composer.recipient_single_number = res[records.id]['number'] or ''
@api.depends('recipient_single_number', 'recipient_single_number_itf')
def _compute_recipient_single_valid(self):
for composer in self:
value = composer.recipient_single_number_itf or composer.recipient_single_number
if value:
records = composer._get_records()
composer.recipient_single_valid = bool(records._phone_format(number=value)) if len(records) == 1 else False
else:
composer.recipient_single_valid = False
@api.depends('numbers', 'res_model', 'res_id')
def _compute_sanitized_numbers(self):
for composer in self:
if composer.numbers:
record = composer._get_records() if composer.res_model and composer.res_id else self.env.user
numbers = [number.strip() for number in composer.numbers.split(',')]
sanitized_numbers = [record._phone_format(number=number) for number in numbers]
invalid_numbers = [number for sanitized, number in zip(sanitized_numbers, numbers) if not sanitized]
if invalid_numbers:
raise UserError(_('Following numbers are not correctly encoded: %s', repr(invalid_numbers)))
composer.sanitized_numbers = ','.join(sanitized_numbers)
else:
composer.sanitized_numbers = False
@api.depends('composition_mode', 'res_model', 'res_id', 'template_id')
def _compute_body(self):
for record in self:
if record.template_id and record.composition_mode == 'comment' and record.res_id:
record.body = record.template_id._render_field('body', [record.res_id], compute_lang=True)[record.res_id]
elif record.template_id:
record.body = record.template_id.body
# ------------------------------------------------------------
# Actions
# ------------------------------------------------------------
def action_send_sms(self):
if self.composition_mode in ('numbers', 'comment'):
if self.comment_single_recipient and not self.recipient_single_valid:
raise UserError(_('Invalid recipient number. Please update it.'))
elif not self.comment_single_recipient and self.recipient_invalid_count:
raise UserError(_('%s invalid recipients', self.recipient_invalid_count))
self._action_send_sms()
return False
def action_send_sms_mass_now(self):
if not self.mass_force_send:
self.write({'mass_force_send': True})
return self.action_send_sms()
def _action_send_sms(self):
records = self._get_records()
if self.composition_mode == 'numbers':
return self._action_send_sms_numbers()
elif self.composition_mode == 'comment':
if records is None or not isinstance(records, self.pool['mail.thread']):
return self._action_send_sms_numbers()
if self.comment_single_recipient:
return self._action_send_sms_comment_single(records)
else:
return self._action_send_sms_comment(records)
else:
return self._action_send_sms_mass(records)
def _action_send_sms_numbers(self):
sms_values = [{'body': self.body, 'number': number} for number in self.sanitized_numbers.split(',')]
self.env['sms.sms'].sudo().create(sms_values).send()
return True
def _action_send_sms_comment_single(self, records=None):
# If we have a recipient_single_original number, it's possible this number has been corrected in the popup
# if invalid. As a consequence, the test cannot be based on recipient_invalid_count, which count is based
# on the numbers in the database.
records = records if records is not None else self._get_records()
records.ensure_one()
if not self.number_field_name or self.number_field_name not in records:
self.numbers = self.recipient_single_number_itf or self.recipient_single_number
elif self.recipient_single_number_itf and self.recipient_single_number_itf != self.recipient_single_number:
records.write({self.number_field_name: self.recipient_single_number_itf})
return self._action_send_sms_comment(records=records)
def _action_send_sms_comment(self, records=None):
records = records if records is not None else self._get_records()
subtype_id = self.env['ir.model.data']._xmlid_to_res_id('mail.mt_note')
messages = self.env['mail.message']
all_bodies = self._prepare_body_values(records)
for record in records:
messages += record._message_sms(
all_bodies[record.id],
subtype_id=subtype_id,
number_field=self.number_field_name,
sms_numbers=self.sanitized_numbers.split(',') if self.sanitized_numbers else None)
return messages
def _action_send_sms_mass(self, records=None):
records = records if records is not None else self._get_records()
sms_record_values = self._prepare_mass_sms_values(records)
sms_all = self._prepare_mass_sms(records, sms_record_values)
if sms_all and self.mass_keep_log and records and isinstance(records, self.pool['mail.thread']):
log_values = self._prepare_mass_log_values(records, sms_record_values)
records._message_log_batch(**log_values)
if sms_all and self.mass_force_send:
sms_all.filtered(lambda sms: sms.state == 'outgoing').send(auto_commit=False, raise_exception=False)
return self.env['sms.sms'].sudo().search([('id', 'in', sms_all.ids)])
return sms_all
# ------------------------------------------------------------
# Mass mode specific
# ------------------------------------------------------------
def _get_blacklist_record_ids(self, records, recipients_info):
""" Get a list of blacklisted records. Those will be directly canceled
with the right error code. """
if self.mass_use_blacklist:
bl_numbers = self.env['phone.blacklist'].sudo().search([]).mapped('number')
return [r.id for r in records if recipients_info[r.id]['sanitized'] in bl_numbers]
return []
def _get_optout_record_ids(self, records, recipients_info):
""" Compute opt-outed contacts, not necessarily blacklisted. Void by default
as no opt-out mechanism exist in SMS, see SMS Marketing. """
return []
def _get_done_record_ids(self, records, recipients_info):
""" Get a list of already-done records. Order of record set is used to
spot duplicates so pay attention to it if necessary. """
done_ids, done = [], []
for record in records:
sanitized = recipients_info[record.id]['sanitized']
if sanitized in done:
done_ids.append(record.id)
else:
done.append(sanitized)
return done_ids
def _prepare_recipient_values(self, records):
recipients_info = records._sms_get_recipients_info(force_field=self.number_field_name)
return recipients_info
def _prepare_body_values(self, records):
if self.template_id and self.body == self.template_id.body:
all_bodies = self.template_id._render_field('body', records.ids, compute_lang=True)
else:
all_bodies = self.env['mail.render.mixin']._render_template(self.body, records._name, records.ids)
return all_bodies
def _prepare_mass_sms_values(self, records):
all_bodies = self._prepare_body_values(records)
all_recipients = self._prepare_recipient_values(records)
blacklist_ids = self._get_blacklist_record_ids(records, all_recipients)
optout_ids = self._get_optout_record_ids(records, all_recipients)
done_ids = self._get_done_record_ids(records, all_recipients)
result = {}
for record in records:
recipients = all_recipients[record.id]
sanitized = recipients['sanitized']
if sanitized and record.id in blacklist_ids:
state = 'canceled'
failure_type = 'sms_blacklist'
elif sanitized and record.id in optout_ids:
state = 'canceled'
failure_type = 'sms_optout'
elif sanitized and record.id in done_ids:
state = 'canceled'
failure_type = 'sms_duplicate'
elif not sanitized:
state = 'canceled'
failure_type = 'sms_number_format' if recipients['number'] else 'sms_number_missing'
else:
state = 'outgoing'
failure_type = ''
result[record.id] = {
'body': all_bodies[record.id],
'failure_type': failure_type,
'number': sanitized if sanitized else recipients['number'],
'partner_id': recipients['partner'].id,
'state': state,
'uuid': uuid4().hex,
}
return result
def _prepare_mass_sms(self, records, sms_record_values):
sms_create_vals = [sms_record_values[record.id] for record in records]
return self.env['sms.sms'].sudo().create(sms_create_vals)
def _prepare_log_body_values(self, sms_records_values):
result = {}
for record_id, sms_values in sms_records_values.items():
result[record_id] = sms_content_to_rendered_html(sms_values['body'])
return result
def _prepare_mass_log_values(self, records, sms_records_values):
return {
'bodies': self._prepare_log_body_values(sms_records_values),
'message_type': 'sms',
}
# ------------------------------------------------------------
# Tools
# ------------------------------------------------------------
def _get_composer_values(self, composition_mode, res_model, res_id, body, template_id):
result = {}
if composition_mode == 'comment':
if not body and template_id and res_id:
template = self.env['sms.template'].browse(template_id)
result['body'] = template._render_template(template.body, res_model, [res_id])[res_id]
elif template_id:
template = self.env['sms.template'].browse(template_id)
result['body'] = template.body
else:
if not body and template_id:
template = self.env['sms.template'].browse(template_id)
result['body'] = template.body
return result
def _get_records(self):
if not self.res_model:
return None
if self.res_ids:
records = self.env[self.res_model].browse(literal_eval(self.res_ids))
elif self.res_id:
records = self.env[self.res_model].browse(self.res_id)
else:
records = self.env[self.res_model]
records = records.with_context(mail_notify_author=True)
return records
|