diff options
| author | Pierre Rousseau <pro@odoo.com> | 2019-05-24 10:12:44 +0000 |
|---|---|---|
| committer | Thibault Delavallée <tde@odoo.com> | 2019-07-12 14:54:11 +0000 |
| commit | 9ad7a88a08ceccc3665acd8972ac47614205bf0e (patch) | |
| tree | 348f777d91be2bb14fdb48f2a6b2a2a1b29664e7 /addons/sms/wizard | |
| parent | bdebcab0cea467e070c1936fed16173247abafdd (diff) | |
[IMP] sms: add SMS template and its support in SMS composer
Purpose of this commit is to provide users templates to use when sending
SMS. It is inspired from what already exists for mail templates. This commit
* adds templates for SMS
* users can now create template for SMS Text messages similar to mail
templates;
* jinja syntax is supported for body like mail templates, which is why
templates are linked to a given model;
* refactor the SMS composer
* templates are supported in composer like the message composer. This is
supported only in mass mode to avoid bloating the interface in standard
composition mode;
* composer code is rewritten to support various use cases (comment, mass
sms, numbers, ...) and better fits all use cases;
* composer is extended to support notably mass SMS without posting messages;
Templates will also be used in a near future in mass sms sending (mass
mailing application improvement) and in marketing automation (for enterprise).
Tests are updated and added to ensure feature works.
Related to task 1922163
Linked to PR #33510
Co-Authored-By: Thibault Delavallee <tde@odoo.com>
Co-Authored-By: Pierre Rousseau <pro@odoo.com>
Diffstat (limited to 'addons/sms/wizard')
| -rw-r--r-- | addons/sms/wizard/sms_composer.py | 279 | ||||
| -rw-r--r-- | addons/sms/wizard/sms_composer_views.xml | 77 |
2 files changed, 300 insertions, 56 deletions
diff --git a/addons/sms/wizard/sms_composer.py b/addons/sms/wizard/sms_composer.py index 93b3202a83a..c4ab81f0ede 100644 --- a/addons/sms/wizard/sms_composer.py +++ b/addons/sms/wizard/sms_composer.py @@ -1,58 +1,261 @@ # -*- 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.safe_eval import safe_eval class SendSMS(models.TransientModel): _name = 'sms.composer' - _description = 'Send SMS' - - recipients = fields.Char('Recipients', required=True) - message = fields.Text('Message', required=True) - - def _get_records(self, model): - if self.env.context.get('active_domain'): - records = model.search(self.env.context.get('active_domain')) - elif self.env.context.get('active_ids'): - records = model.browse(self.env.context.get('active_ids', [])) - else: - records = model.browse(self.env.context.get('active_id', [])) - return records + _description = 'Send SMS Wizard' + _RECIPIENTS_DISPLAY_NBR = 3 @api.model def default_get(self, fields): result = super(SendSMS, self).default_get(fields) - active_model = self.env.context.get('active_model') - - if not self.env.context.get('default_recipients') and active_model and hasattr(self.env[active_model], '_sms_get_default_partners'): - model = self.env[active_model] - records = self._get_records(model) - partners = records._sms_get_default_partners() - phone_numbers = [] - no_phone_partners = [] - for partner in partners: - number = phone_validation.phone_get_sanitized_record_number(partner, self.env.context.get('field_name') or 'mobile', 'country_id') - if number: - phone_numbers.append(number) - else: - no_phone_partners.append(partner.name) - if len(partners) > 1: - if no_phone_partners: - raise UserError(_('Missing mobile number for %s.') % ', '.join(no_phone_partners)) - result['recipients'] = ', '.join(phone_numbers) + if fields == 'partner_ids': + # shortcut because default_get in cache, avoid issues + return result + + result['res_model'] = result.get('res_model') or self.env.context.get('active_model') + result['composition_mode'] = result.get('composition_mode') or 'comment' + + if result['composition_mode'] == 'comment' and not result.get('res_id'): + result['res_id'] = self.env.context.get('active_id') + if result['composition_mode'] != 'comment': + if result.get('use_active_domain') and not result.get('active_domain'): + result['active_domain'] = self.env.context['active_domain'] + elif not result.get('res_ids'): + result['res_ids'] = repr(self.env.context.get('active_ids')) + + if result['res_model']: + result.update( + self._get_composer_values( + result['composition_mode'], result['res_model'], result.get('res_id'), + result.get('body'), result.get('template_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', default='comment', required=True) + res_model = fields.Char('Document Model Name') + res_id = fields.Integer('Document ID') + res_ids = fields.Char('Document IDs') + use_active_domain = fields.Boolean('Use active domain') + active_domain = fields.Text('Active domain', readonly=True) + # options for comment and mass mode + mass_keep_log = fields.Boolean('Keep a note on document') + # recipients + recipient_description = fields.Text('Recipients (Partners)', compute='_compute_description') + recipient_invalid = fields.Text('Invalid recipients', compute='_compute_description') + number_field_name = fields.Char(string='Field holding number') + partner_ids = fields.Many2many('res.partner') + numbers = fields.Char('Recipients (Numbers)') + sanitized_numbers = fields.Char('Sanitized Number', compute='_compute_sanitized_numbers') + # content + template_id = fields.Many2one('sms.template', string='Use Template', domain="[('model', '=', res_model)]") + body = fields.Text('Message', required=True) + + @api.depends('partner_ids', 'res_model', 'res_id', 'res_ids', 'use_active_domain', 'composition_mode', 'number_field_name', 'sanitized_numbers') + def _compute_description(self): + if self.partner_ids: + description = '' + description = ','.join('%s - %s' % (partner.display_name, partner.mobile or partner.phone) for partner in self.partner_ids[:self._RECIPIENTS_DISPLAY_NBR]) + if len(self.partner_ids) > self._RECIPIENTS_DISPLAY_NBR: + description += _(' (and %s more)') % (len(self.partner_ids) - self._RECIPIENTS_DISPLAY_NBR) + self.recipient_description = description + self.recipient_invalid = False + + elif self.composition_mode in ('comment', 'mass') and self.res_model: + records = None + if self.composition_mode == 'comment' and self.res_id: + records = self.env[self.res_model].browse(self.res_id) + elif self.composition_mode == 'mass' and self.res_ids: + records = self.env[self.res_model].browse(literal_eval(self.res_ids)) + + if records and issubclass(type(records), self.pool['mail.thread']): + res = records._sms_get_recipients_info(force_field=self.number_field_name) + valid_ids = [rid for rid, rvalues in res.items() if rvalues['sanitized']] + invalid_ids = [rid for rid, rvalues in res.items() if not rvalues['sanitized']] + + self.recipient_description = ', '.join('%s (%s)' % ( + res[record.id]['partner'].name or record.display_name, + res[record.id]['sanitized']) + for record in records if record.id in valid_ids[:self._RECIPIENTS_DISPLAY_NBR] + ) or False + if len(valid_ids) > self._RECIPIENTS_DISPLAY_NBR: + self.recipient_description += _(', and %s more') % (len(valid_ids) - self._RECIPIENTS_DISPLAY_NBR) + + self.recipient_invalid = ', '.join('%s (%s)' % ( + res[record.id]['partner'].display_name or record.display_name, + res[record.id]['number']) + for record in records if record.id in invalid_ids[:self._RECIPIENTS_DISPLAY_NBR] + ) or False + if len(invalid_ids) > self._RECIPIENTS_DISPLAY_NBR: + self.recipient_description += _(', and %s more') % (len(invalid_ids) - self._RECIPIENTS_DISPLAY_NBR) + else: + self.recipient_description = '' + self.recipient_invalid = '' if self.sanitized_numbers or (self.composition_mode == 'mass' and self.use_active_domain) else _('No record found') + + else: + self.recipient_description = False + + @api.depends('numbers', 'res_model', 'res_id') + def _compute_sanitized_numbers(self): + if self.numbers: + record = self._get_records() if self.res_model and self.res_id else self.env.user + sanitize_res = phone_validation.phone_sanitize_numbers_string_w_record(self.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)) + self.sanitized_numbers = ','.join(sanitized_numbers) + else: + self.sanitized_numbers = False + + @api.onchange('composition_mode', 'res_model', 'res_id', 'template_id') + def _onchange_template_id(self): + if self.template_id and self.composition_mode == 'comment': + self.body = self.template_id._render_template(self.template_id.body, self.res_model, [self.res_id])[self.res_id] + elif self.template_id: + self.body = self.template_id.body + + # ------------------------------------------------------------ + # Actions + # ------------------------------------------------------------ + def action_send_sms(self): - numbers = [number.strip() for number in self.recipients.split(',') if number.strip()] + if self.recipient_invalid: + raise UserError(_('Invalid recipients: %s') % self.recipient_invalid) + self._action_send_sms() + if self.composition_mode == 'comment': + return { + 'type': 'ir.actions.client', + 'tag': 'reload', + } + return False + + def _action_send_sms(self, force_send=False): + records = self._get_records() + if self.composition_mode == 'numbers': + return self._action_send_sms_numbers() + elif self.composition_mode == 'comment': + if records is not None and issubclass(type(records), self.pool['mail.thread']): + return self._action_send_sms_comment(records) + return self._action_send_sms_numbers() + elif self.mass_keep_log: + return self._action_send_sms_mass_w_log(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(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_comment') + + messages = self.env['mail.message'] + for record in records: + messages |= records._message_sms( + self.body, subtype_id=subtype_id, + partner_ids=self.partner_ids.ids or False, + 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() - active_model = self.env.context.get('active_model') - if active_model and hasattr(self.env[active_model], '_message_sms'): - model = self.env[active_model] - records = self._get_records(model) - records[0]._message_sms(self.message, sms_numbers=numbers) + record_values = self._prepare_mass_sms_values(records) + sms_create_vals = [record_values[record.id] for record in records] + sms = self.env['sms.sms'].sudo().create(sms_create_vals) + return sms + + def _action_send_sms_mass_w_log(self): + records = self._get_records() + if records and hasattr(records, '_message_sms'): + subtype_id = self.env['ir.model.data'].xmlid_to_res_id('mail.mt_note') + for record in records: + record._message_sms(self.body, subtype_id=subtype_id, partner_ids=False, sms_numbers=None) else: - self.env['sms.api']._send_sms(numbers, self.message) + self.env['sms.api']._send_sms(self.numbers, self.body) return True + + # ------------------------------------------------------------ + # Mass mode specific + # ------------------------------------------------------------ + + def _prepare_recipient_values(self, records=None): + records = records if records is not None else self._get_records() + recipients_info = records._sms_get_recipients_info(force_field=self.number_field_name) + return recipients_info + + def _prepare_body_values(self, records=None): + records = records if records is not None else self._get_records() + if self.template_id and self.body == self.template_id.body: + lang_to_rids = self.template_id._get_ids_per_lang(records.ids) + all_bodies = {} + for lang, rids in lang_to_rids.items(): + template = self.template_id.with_context(lang=lang) + all_bodies.update(template._render_template(template.body, records._name, rids)) + else: + all_bodies = self.env['mail.template']._render_template(self.body, records._name, records.ids) + return all_bodies + + def _prepare_mass_sms_values(self, records=None): + records = records if records is not None else self._get_records() + all_bodies = self._prepare_body_values(records=records) + all_recipients = self._prepare_recipient_values(records=records) + + result = {} + for record in records: + recipients = all_recipients[record.id] + result[record.id] = { + 'body': all_bodies[record.id], + 'partner_id': recipients['partner'].id, + 'number': recipients['sanitized'] or recipients['number'], + 'state': 'outgoing' if recipients['sanitized'] else 'error', + } + return result + + # ------------------------------------------------------------ + # 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: + template = self.env['sms.template'].browse(template_id) + result['body'] = template._render_template(template.body, res_model, [res_id])[res_id] + 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: # TDE FIXME: clear active_domain (False,[], ..) + records = self.env[self.res_model].search(safe_eval(self.active_domain)) + elif self.res_id: + records = self.env[self.res_model].browse(self.res_id) + else: + records = self.env[self.res_model].browse(literal_eval(self.res_ids)) + return records diff --git a/addons/sms/wizard/sms_composer_views.xml b/addons/sms/wizard/sms_composer_views.xml index 4b6651a00cb..0ed78dbccda 100644 --- a/addons/sms/wizard/sms_composer_views.xml +++ b/addons/sms/wizard/sms_composer_views.xml @@ -7,35 +7,76 @@ <form string="Send an SMS"> <sheet> <group> - <field name="recipients"/> - <field name="message" widget="sms_widget"/> + <field name="composition_mode" invisible="1"/> + <field name="res_id" invisible="1"/> + <field name="res_ids" invisible="1"/> + <field name="active_domain" invisible="1"/> + <field name="res_model" invisible="1"/> + <field name="number_field_name" invisible="1"/> + <field name="partner_ids" invisible="1"/> + <field name="numbers" invisible="1"/> + <field name="sanitized_numbers" invisible="1"/> + + <!-- Recipient warning --> + <div colspan="2" class="alert alert-danger" role="alert" + attrs="{'invisible': [('recipient_invalid', '=', False)]}"> + <strong>Some recipients are invalid</strong><br /> + <field name="recipient_invalid"/> + </div> + <!-- Mass mode information (res_ids versus active domain) --> + <div colspan="2" class="oe_form_box_info bg-info oe_text_center mb-3" + attrs="{'invisible': [('composition_mode', '=', 'comment')]}"> + <p attrs="{'invisible': [('use_active_domain', '=', False)]}"> + <strong>All records matching your current search filter will receive a SMS, + not only the ids selected in the list view.</strong><br /> + The SMS will be sent for all the records selected in the list.<br /> + Confirming this wizard will probably take a few minutes blocking your browser. + </p> + <p attrs="{'invisible': [('use_active_domain', '=', True)]}"> + <strong>Only records checked in list view will be used.</strong><br /> + The SMS will be sent for all the records selected in the list. + </p> + <p class="mt8"> + <span attrs="{'invisible': [('use_active_domain', '=', True)]}"> + If you want to send it for all the records matching your search criterion, check this box : + </span> + <span attrs="{'invisible': [('use_active_domain', '=', False)]}"> + If you want to use only selected records please uncheck this selection box : + </span> + <field class="oe_inline" name="use_active_domain"/> + </p> + </div> + + <label for="partner_ids" string="Recipients"/> + <div> + <span attrs="{'invisible': ['|', ('composition_mode', '!=', 'mass'), ('use_active_domain', '=', False)]}"> + <strong>Mass SMS</strong> on the current search filter + </span> + <field name="recipient_description" readonly="1" attrs="{ + 'invisible': [('use_active_domain', '=', True)]}" + /> + </div> + <field name="body" widget="sms_widget"/> </group> - </sheet> - <footer> <group> - <span> - <button string="Send" type="object" class="oe_highlight" name="action_send_sms"/> - <button string="Cancel" class="oe_link" special="cancel" /> - </span> + <group> + <field name="template_id" options="{'no_create': True}" attrs="{'invisible': [('composition_mode', '!=', 'mass')]}"/> + </group> </group> + </sheet> + <footer> + <button string="Send SMS" type="object" class="oe_highlight" name="action_send_sms"/> + <button string="Close" class="btn btn-secondary" special="cancel"/> </footer> </form> </field> </record> + <record id="sms_composer_action_form" model="ir.actions.act_window"> - <field name="name">Send SMS</field> + <field name="name">Send SMS Text Message</field> <field name="res_model">sms.composer</field> <field name="view_mode">form</field> <field name="target">new</field> </record> - <!-- Add action entry in the Action Menu for Partners --> - <act_window id="sms_composer_act_window" - name="Send SMS" - binding_model="res.partner" - res_model="sms.composer" - binding_views="list" - view_mode="form" - target="new"/> - </odoo> |
