Files
odoo_source/addons/sms/wizard/sms_composer.py
T
Dharmraj Jhala 0e57bfd453 [FIX] sms, test_mail_full: make contextual action work for sms templates
While creating contextual action for an sms template, the composition mode
may be decided on the fly, based on number of records and on context the key
`default_composition_mode`, which is set to 'guess'.

It used to work before a recent refactoring[1], because the composition
mode was changed in the default_get, and was being set to appropriate
value instead of 'guess'.

After this refactoring, the composition mode is not being set in the
default_get but is being computed, so the key `default_composition_mode`
tries to set the composition_mode selection field to 'guess', which is not
available in the selection values, resulting into traceback.

This commit fixes the issue by renaming the context key(removing 'default'
prefix) to avoid setting the mode directly and instead let the compute method
decide the composition mode based on context key and number of records.

[1] - https://github.com/odoo/odoo/commit/e02137c48562d824e3e01054e31dae383e5843e2

TaskID - 2286959

closes odoo/odoo#54938

X-original-commit: 2b9ea3d93445e05cb0440aab33f2bf9e066b0fa5
Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
2020-07-27 07:27:18 +00:00

381 lines
19 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from ast import literal_eval
from odoo import api, fields, models, _
from odoo.addons.phone_validation.tools import phone_validation
from odoo.exceptions import UserError
from odoo.tools import html2plaintext
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('active_domain'):
result['active_domain'] = repr(self.env.context.get('active_domain', []))
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', readonly=False, required=True, store=True)
res_model = fields.Char('Document Model Name')
res_id = fields.Integer('Document ID')
res_ids = fields.Char('Document IDs')
res_ids_count = fields.Integer(
'Visible records count', compute='_compute_recipients_count', compute_sudo=False,
help='UX field computing the number of recipients in mass mode without active domain')
use_active_domain = fields.Boolean('Use active domain')
active_domain = fields.Text('Active domain', readonly=True)
active_domain_count = fields.Integer(
'Active records count', compute='_compute_recipients_count', compute_sudo=False,
help='UX field computing the number of recipients in mass mode based on given active domain')
comment_single_recipient = fields.Boolean(
'Single Mode', compute='_compute_comment_single_recipient', compute_sudo=False,
help='UX field allowing to know we are sending an SMS to 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', compute_sudo=False)
recipient_single_number = fields.Char('Stored Recipient Number', compute='_compute_recipient_single', compute_sudo=False)
recipient_single_number_itf = fields.Char(
'Recipient Number', compute='_compute_recipient_single',
readonly=False, compute_sudo=False, store=True,
help='UX field allowing to edit the recipient number. If changed it will be stored onto the recipient.')
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',
readonly=False, store=True, required=True)
@api.depends('res_ids_count', 'active_domain_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 or (composer.use_active_domain and composer.active_domain_count > 1):
composer.composition_mode = 'mass'
else:
composer.composition_mode = 'comment'
@api.depends('res_model', 'res_id', 'res_ids', 'active_domain')
def _compute_recipients_count(self):
for composer in self:
composer.res_ids_count = len(literal_eval(composer.res_ids)) if composer.res_ids else 0
if composer.res_model:
composer.active_domain_count = self.env[composer.res_model].search_count(literal_eval(composer.active_domain or '[]'))
else:
composer.active_domain_count = 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', 'use_active_domain', '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 issubclass(type(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' and composer.use_active_domain)
) else 1
@api.depends('res_model', 'number_field_name')
def _compute_recipient_single(self):
for composer in self:
records = composer._get_records()
if not records or not issubclass(type(records), self.pool['mail.thread']) or not composer.comment_single_recipient:
composer.recipient_single_description = False
composer.recipient_single_number = ''
composer.recipient_single_number_itf = ''
continue
records.ensure_one()
res = records._sms_get_recipients_info(force_field=composer.number_field_name, partner_fallback=False)
composer.recipient_single_description = res[records.id]['partner'].name or records.display_name
composer.recipient_single_number = res[records.id]['number'] or ''
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('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()
sanitized = phone_validation.phone_sanitize_numbers_w_record([value], records)[value]['sanitized']
composer.recipient_single_valid = bool(sanitized)
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(',')]
sanitize_res = phone_validation.phone_sanitize_numbers_w_record(numbers, record)
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 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
# ------------------------------------------------------------
# CRUD
# ------------------------------------------------------------
@api.model
def create(self, values):
# TDE FIXME: currently have to compute manually to avoid required issue, waiting VFE branch
if not values.get('body') or not values.get('composition_mode'):
values_wdef = self._add_missing_default_values(values)
cache_composer = self.new(values_wdef)
cache_composer._compute_body()
cache_composer._compute_composition_mode()
values['body'] = values.get('body') or cache_composer.body
values['composition_mode'] = values.get('composition_mode') or cache_composer.composition_mode
return super(SendSMS, self).create(values)
# ------------------------------------------------------------
# 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 issubclass(type(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):
self.env['sms.api']._send_sms_batch([{
'res_id': 0,
'number': number,
'content': self.body,
} for number in self.sanitized_numbers.split(',')])
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 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']
for record in records:
messages |= record._message_sms(
self.body, 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 issubclass(type(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_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)
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'
error_code = 'sms_blacklist'
elif sanitized and record.id in done_ids:
state = 'canceled'
error_code = 'sms_duplicate'
elif not sanitized:
state = 'error'
error_code = 'sms_number_format' if recipients['number'] else 'sms_number_missing'
else:
state = 'outgoing'
error_code = ''
result[record.id] = {
'body': all_bodies[record.id],
'partner_id': recipients['partner'].id,
'number': sanitized if sanitized else recipients['number'],
'state': state,
'error_code': error_code,
}
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] = html2plaintext(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.use_active_domain:
active_domain = literal_eval(self.active_domain or '[]')
records = self.env[self.res_model].search(active_domain)
elif 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