summaryrefslogtreecommitdiff
path: root/addons/sms/wizard
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/sms/wizard
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/sms/wizard')
-rw-r--r--addons/sms/wizard/__init__.py6
-rw-r--r--addons/sms/wizard/sms_cancel.py38
-rw-r--r--addons/sms/wizard/sms_cancel_views.xml27
-rw-r--r--addons/sms/wizard/sms_composer.py380
-rw-r--r--addons/sms/wizard/sms_composer_views.xml78
-rw-r--r--addons/sms/wizard/sms_resend.py115
-rw-r--r--addons/sms/wizard/sms_resend_views.xml45
-rw-r--r--addons/sms/wizard/sms_template_preview.py50
-rw-r--r--addons/sms/wizard/sms_template_preview_views.xml42
9 files changed, 781 insertions, 0 deletions
diff --git a/addons/sms/wizard/__init__.py b/addons/sms/wizard/__init__.py
new file mode 100644
index 00000000..bd0a5c11
--- /dev/null
+++ b/addons/sms/wizard/__init__.py
@@ -0,0 +1,6 @@
+# -*- coding: utf-8 -*-
+
+from . import sms_cancel
+from . import sms_composer
+from . import sms_resend
+from . import sms_template_preview
diff --git a/addons/sms/wizard/sms_cancel.py b/addons/sms/wizard/sms_cancel.py
new file mode 100644
index 00000000..0587a9f0
--- /dev/null
+++ b/addons/sms/wizard/sms_cancel.py
@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import _, api, fields, models
+
+
+class SMSCancel(models.TransientModel):
+ _name = 'sms.cancel'
+ _description = 'Dismiss notification for resend by model'
+
+ model = fields.Char(string='Model', required=True)
+ help_message = fields.Char(string='Help message', compute='_compute_help_message')
+
+ @api.depends('model')
+ def _compute_help_message(self):
+ for wizard in self:
+ wizard.help_message = _("Are you sure you want to discard %s SMS delivery failures? You won't be able to re-send these SMS later!") % (wizard._context.get('unread_counter'))
+
+ def action_cancel(self):
+ # TDE CHECK: delete pending SMS
+ author_id = self.env.user.partner_id.id
+ for wizard in self:
+ self._cr.execute("""
+SELECT notif.id, msg.id
+FROM mail_message_res_partner_needaction_rel notif
+JOIN mail_message msg
+ ON notif.mail_message_id = msg.id
+WHERE notif.notification_type = 'sms' IS TRUE AND notif.notification_status IN ('bounce', 'exception')
+ AND msg.model = %s
+ AND msg.author_id = %s """, (wizard.model, author_id))
+ res = self._cr.fetchall()
+ notif_ids = [row[0] for row in res]
+ message_ids = list(set([row[1] for row in res]))
+ if notif_ids:
+ self.env['mail.notification'].browse(notif_ids).sudo().write({'notification_status': 'canceled'})
+ if message_ids:
+ self.env['mail.message'].browse(message_ids)._notify_message_notification_update()
+ return {'type': 'ir.actions.act_window_close'}
diff --git a/addons/sms/wizard/sms_cancel_views.xml b/addons/sms/wizard/sms_cancel_views.xml
new file mode 100644
index 00000000..f672a80d
--- /dev/null
+++ b/addons/sms/wizard/sms_cancel_views.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo><data>
+ <record id="sms_cancel" model="ir.ui.view">
+ <field name="name">sms.cancel.form</field>
+ <field name="model">sms.cancel</field>
+ <field name="groups_id" eval="[(4,ref('base.group_user'))]"/>
+ <field name="arch" type="xml">
+ <form string="Cancel notification in failure">
+ <field name="model" invisible='1'/>
+ <field name="help_message"/>
+ <p>If you want to re-send them, click Cancel now, then click on the notification and review them one by one by clicking on the red icon next to each message.</p>
+ <footer>
+ <button string="Discard delivery failures" name="action_cancel" type="object" class="btn-primary" />
+ <button string="Cancel" class="btn-secondary" special="cancel" />
+ </footer>
+ </form>
+ </field>
+ </record>
+
+ <record id="sms_cancel_action" model="ir.actions.act_window">
+ <field name="name">Discard SMS delivery failures</field>
+ <field name="res_model">sms.cancel</field>
+ <field name="type">ir.actions.act_window</field>
+ <field name="view_mode">form</field>
+ <field name="target">new</field>
+ </record>
+</data></odoo>
diff --git a/addons/sms/wizard/sms_composer.py b/addons/sms/wizard/sms_composer.py
new file mode 100644
index 00000000..57b6d27e
--- /dev/null
+++ b/addons/sms/wizard/sms_composer.py
@@ -0,0 +1,380 @@
+# -*- 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='Number of recipients that will receive the SMS if sent in mass mode, without applying the Active Domain value')
+ 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='Number of records found when searching with the value in Active Domain')
+ 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', 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
diff --git a/addons/sms/wizard/sms_composer_views.xml b/addons/sms/wizard/sms_composer_views.xml
new file mode 100644
index 00000000..3ef5820b
--- /dev/null
+++ b/addons/sms/wizard/sms_composer_views.xml
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <record id="sms_composer_view_form" model="ir.ui.view">
+ <field name="name">sms.composer.view.form</field>
+ <field name="model">sms.composer</field>
+ <field name="arch" type="xml">
+ <form string="Send an SMS">
+ <sheet>
+ <group>
+ <field name="composition_mode" invisible="1"/>
+ <field name="comment_single_recipient" 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="mass_force_send" invisible="1"/>
+ <field name="recipient_single_valid" invisible="1"/>
+ <field name="recipient_single_number" invisible="1"/>
+ <field name="number_field_name" invisible="1"/>
+ <field name="numbers" invisible="1"/>
+ <field name="sanitized_numbers" invisible="1"/>
+
+ <!-- Single mode information (invalid number) -->
+ <div colspan="2" class="alert alert-danger text-center mb-3" role="alert"
+ attrs="{'invisible': ['|', ('comment_single_recipient', '=', False), ('recipient_single_valid', '=', True)]}">
+ <p class="my-0">Invalid phone number</p>
+ </div>
+
+ <!-- Mass mode information (res_ids versus active domain) -->
+ <div colspan="2" class="alert alert-info text-center mb-3" role="alert"
+ attrs="{'invisible': [('comment_single_recipient', '=', True)]}">
+ <p class="my-0">
+ <span attrs="{'invisible': [('use_active_domain', '=', 'True')]}">
+ <field class="oe_inline font-weight-bold" name="res_ids_count"/> records selected.
+ </span>
+ Check <field class="oe_inline ml-2" name="use_active_domain"/> to send to all
+ <field class="oe_inline font-weight-bold ml-2" name="active_domain_count"/> records instead. <br/>
+ <field class="oe_inline font-weight-bold" name="recipient_valid_count"/> recipients are valid
+ and <field class="oe_inline font-weight-bold" name="recipient_invalid_count"/> are invalid.
+ </p>
+ </div>
+
+ <label for="recipient_single_description" string="Recipient"
+ class="font-weight-bold"
+ attrs="{'invisible': [('comment_single_recipient', '=', False)]}"/>
+ <div attrs="{'invisible': [('comment_single_recipient', '=', False)]}">
+ <field name="recipient_single_description" class="oe_inline"/>
+ <field name="recipient_single_number_itf" class="oe_inline" nolabel="1" options="{'onchange_on_keydown': True}" placeholder="e.g. +1 415 555 0100"/>
+ </div>
+
+ <field name="body" widget="sms_widget"/>
+ <field name="mass_keep_log" invisible="1"/>
+ </group>
+ </sheet>
+ <footer>
+ <!-- attrs doesn't work for 'disabled'-->
+ <button string="Send SMS" type="object" class="oe_highlight" name="action_send_sms"
+ attrs="{'invisible': ['|',('composition_mode', 'not in', ('comment', 'numbers')),('recipient_single_valid', '=', False)]}"/>
+ <button string="Send SMS" type="object" class="oe_highlight" name="action_send_sms"
+ attrs="{'invisible': ['|',('composition_mode', 'not in', ('comment', 'numbers')),('recipient_single_valid', '=', True)]}" disabled='1'/>
+ <button string="Put in queue" type="object" class="oe_highlight" name="action_send_sms"
+ attrs="{'invisible': [('composition_mode', '!=', 'mass')]}"/>
+ <button string="Send Now" type="object" name="action_send_sms_mass_now"
+ attrs="{'invisible': [('composition_mode', '!=', 'mass')]}"/>
+ <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 Text Message</field>
+ <field name="res_model">sms.composer</field>
+ <field name="view_mode">form</field>
+ <field name="target">new</field>
+ </record>
+
+</odoo>
diff --git a/addons/sms/wizard/sms_resend.py b/addons/sms/wizard/sms_resend.py
new file mode 100644
index 00000000..24d59c1a
--- /dev/null
+++ b/addons/sms/wizard/sms_resend.py
@@ -0,0 +1,115 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import _, api, exceptions, fields, models
+
+
+class SMSRecipient(models.TransientModel):
+ _name = 'sms.resend.recipient'
+ _description = 'Resend Notification'
+ _rec_name = 'sms_resend_id'
+
+ sms_resend_id = fields.Many2one('sms.resend', required=True)
+ notification_id = fields.Many2one('mail.notification', required=True, ondelete='cascade')
+ resend = fields.Boolean(string="Resend", default=True)
+ failure_type = fields.Selection(
+ related='notification_id.failure_type', related_sudo=True, readonly=True)
+ partner_id = fields.Many2one('res.partner', 'Partner', related='notification_id.res_partner_id', readonly=True)
+ partner_name = fields.Char('Recipient', readonly='True')
+ sms_number = fields.Char('Number')
+
+
+class SMSResend(models.TransientModel):
+ _name = 'sms.resend'
+ _description = 'SMS Resend'
+ _rec_name = 'mail_message_id'
+
+ @api.model
+ def default_get(self, fields):
+ result = super(SMSResend, self).default_get(fields)
+ if 'recipient_ids' in fields and result.get('mail_message_id'):
+ mail_message_id = self.env['mail.message'].browse(result['mail_message_id'])
+ result['recipient_ids'] = [(0, 0, {
+ 'notification_id': notif.id,
+ 'resend': True,
+ 'failure_type': notif.failure_type,
+ 'partner_name': notif.res_partner_id.display_name or mail_message_id.record_name,
+ 'sms_number': notif.sms_number,
+ }) for notif in mail_message_id.notification_ids if notif.notification_type == 'sms' and notif.notification_status in ('exception', 'bounce')]
+ return result
+
+ mail_message_id = fields.Many2one('mail.message', 'Message', readonly=True, required=True)
+ recipient_ids = fields.One2many('sms.resend.recipient', 'sms_resend_id', string='Recipients')
+ has_cancel = fields.Boolean(compute='_compute_has_cancel')
+ has_insufficient_credit = fields.Boolean(compute='_compute_has_insufficient_credit')
+ has_unregistered_account = fields.Boolean(compute='_compute_has_unregistered_account')
+
+ @api.depends("recipient_ids.failure_type")
+ def _compute_has_unregistered_account(self):
+ self.has_unregistered_account = self.recipient_ids.filtered(lambda p: p.failure_type == 'sms_acc')
+
+ @api.depends("recipient_ids.failure_type")
+ def _compute_has_insufficient_credit(self):
+ self.has_insufficient_credit = self.recipient_ids.filtered(lambda p: p.failure_type == 'sms_credit')
+
+ @api.depends("recipient_ids.resend")
+ def _compute_has_cancel(self):
+ self.has_cancel = self.recipient_ids.filtered(lambda p: not p.resend)
+
+ def _check_access(self):
+ if not self.mail_message_id or not self.mail_message_id.model or not self.mail_message_id.res_id:
+ raise exceptions.UserError(_('You do not have access to the message and/or related document.'))
+ record = self.env[self.mail_message_id.model].browse(self.mail_message_id.res_id)
+ record.check_access_rights('read')
+ record.check_access_rule('read')
+
+ def action_resend(self):
+ self._check_access()
+
+ all_notifications = self.env['mail.notification'].sudo().search([
+ ('mail_message_id', '=', self.mail_message_id.id),
+ ('notification_type', '=', 'sms'),
+ ('notification_status', 'in', ('exception', 'bounce'))
+ ])
+ sudo_self = self.sudo()
+ to_cancel_ids = [r.notification_id.id for r in sudo_self.recipient_ids if not r.resend]
+ to_resend_ids = [r.notification_id.id for r in sudo_self.recipient_ids if r.resend]
+
+ if to_cancel_ids:
+ all_notifications.filtered(lambda n: n.id in to_cancel_ids).write({'notification_status': 'canceled'})
+
+ if to_resend_ids:
+ record = self.env[self.mail_message_id.model].browse(self.mail_message_id.res_id)
+
+ sms_pid_to_number = dict((r.partner_id.id, r.sms_number) for r in self.recipient_ids if r.resend and r.partner_id)
+ pids = list(sms_pid_to_number.keys())
+ numbers = [r.sms_number for r in self.recipient_ids if r.resend and not r.partner_id]
+
+ rdata = []
+ for pid, cid, active, pshare, ctype, notif, groups in self.env['mail.followers']._get_recipient_data(record, 'sms', False, pids=pids):
+ if pid and notif == 'sms':
+ rdata.append({'id': pid, 'share': pshare, 'active': active, 'notif': notif, 'groups': groups or [], 'type': 'customer' if pshare else 'user'})
+ if rdata or numbers:
+ record._notify_record_by_sms(
+ self.mail_message_id, {'partners': rdata}, check_existing=True,
+ sms_numbers=numbers, sms_pid_to_number=sms_pid_to_number,
+ put_in_queue=False
+ )
+
+ self.mail_message_id._notify_message_notification_update()
+ return {'type': 'ir.actions.act_window_close'}
+
+ def action_cancel(self):
+ self._check_access()
+
+ sudo_self = self.sudo()
+ sudo_self.mapped('recipient_ids.notification_id').write({'notification_status': 'canceled'})
+ self.mail_message_id._notify_message_notification_update()
+ return {'type': 'ir.actions.act_window_close'}
+
+ def action_buy_credits(self):
+ url = self.env['iap.account'].get_credits_url(service_name='sms')
+ return {
+ 'type': 'ir.actions.act_url',
+ 'url': url,
+ }
diff --git a/addons/sms/wizard/sms_resend_views.xml b/addons/sms/wizard/sms_resend_views.xml
new file mode 100644
index 00000000..2f13881f
--- /dev/null
+++ b/addons/sms/wizard/sms_resend_views.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo><data>
+ <record id="mail_resend_message_view_form" model="ir.ui.view">
+ <field name="name">sms.resend.form</field>
+ <field name="model">sms.resend</field>
+ <field name="groups_id" eval="[(4,ref('base.group_user'))]"/>
+ <field name="arch" type="xml">
+ <form string="Edit Partners">
+ <field name="mail_message_id" invisible="1"/>
+ <field name="has_cancel" invisible="1"/>
+ <field name="has_insufficient_credit" invisible="1"/>
+ <field name="has_unregistered_account" invisible="1"/>
+ <field name="recipient_ids">
+ <tree string="Recipient" editable="top" create="0" delete="0">
+ <field name="partner_name"/>
+ <field name="sms_number"/>
+ <field name="failure_type" string="Reason"/>
+ <field name="resend" widget="boolean_toggle"/>
+ <field name="notification_id" invisible="1"/>
+ </tree>
+ </field>
+ <div class="alert alert-warning" role="alert" attrs="{'invisible': [('has_cancel', '=', False)]}">
+ <span class="fa fa-info-circle"/> Caution: It won't be possible to send this SMS again to the recipients you did not select.
+ </div>
+ <footer>
+ <button string="Buy credits" name="action_buy_credits" type="object" class="btn-primary o_mail_send"
+ attrs="{'invisible': [('has_insufficient_credit', '=', False)]}"/>
+ <button string="Set up an account" name="action_buy_credits" type="object" class="btn-primary o_mail_send"
+ attrs="{'invisible': [('has_unregistered_account', '=', False)]}"/>
+ <button string="Resend" name="action_resend" type="object" class="btn-primary o_mail_send"/>
+ <button string="Ignore all" name="action_cancel" type="object" class="btn-secondary" />
+ <button string="Cancel" class="btn-secondary" special="cancel" />
+ </footer>
+ </form>
+ </field>
+ </record>
+
+ <record id="sms_resend_action" model="ir.actions.act_window">
+ <field name="name">Sending Failures</field>
+ <field name="res_model">sms.resend</field>
+ <field name="type">ir.actions.act_window</field>
+ <field name="view_mode">form</field>
+ <field name="target">new</field>
+ </record>
+</data></odoo>
diff --git a/addons/sms/wizard/sms_template_preview.py b/addons/sms/wizard/sms_template_preview.py
new file mode 100644
index 00000000..ffe22a42
--- /dev/null
+++ b/addons/sms/wizard/sms_template_preview.py
@@ -0,0 +1,50 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models
+
+
+class SMSTemplatePreview(models.TransientModel):
+ _name = "sms.template.preview"
+ _description = "SMS Template Preview"
+
+ @api.model
+ def _selection_target_model(self):
+ models = self.env['ir.model'].search([])
+ return [(model.model, model.name) for model in models]
+
+ @api.model
+ def _selection_languages(self):
+ return self.env['res.lang'].get_installed()
+
+ @api.model
+ def default_get(self, fields):
+ result = super(SMSTemplatePreview, self).default_get(fields)
+ sms_template_id = self.env.context.get('default_sms_template_id')
+ if not sms_template_id or 'resource_ref' not in fields:
+ return result
+ sms_template = self.env['sms.template'].browse(sms_template_id)
+ res = self.env[sms_template.model_id.model].search([], limit=1)
+ if res:
+ result['resource_ref'] = '%s,%s' % (sms_template.model_id.model, res.id)
+ return result
+
+ sms_template_id = fields.Many2one('sms.template', required=True, ondelete='cascade')
+ lang = fields.Selection(_selection_languages, string='Template Preview Language')
+ model_id = fields.Many2one('ir.model', related="sms_template_id.model_id")
+ body = fields.Char('Body', compute='_compute_sms_template_fields')
+ resource_ref = fields.Reference(string='Record reference', selection='_selection_target_model')
+ no_record = fields.Boolean('No Record', compute='_compute_no_record')
+
+ @api.depends('model_id')
+ def _compute_no_record(self):
+ for preview in self:
+ preview.no_record = (self.env[preview.model_id.model].search_count([]) == 0) if preview.model_id else True
+
+ @api.depends('lang', 'resource_ref')
+ def _compute_sms_template_fields(self):
+ for wizard in self:
+ if wizard.sms_template_id and wizard.resource_ref:
+ wizard.body = wizard.sms_template_id._render_field('body', [wizard.resource_ref.id], set_lang=wizard.lang)[wizard.resource_ref.id]
+ else:
+ wizard.body = wizard.sms_template_id.body
diff --git a/addons/sms/wizard/sms_template_preview_views.xml b/addons/sms/wizard/sms_template_preview_views.xml
new file mode 100644
index 00000000..c2a3f396
--- /dev/null
+++ b/addons/sms/wizard/sms_template_preview_views.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <data>
+ <!-- SMS Template Preview -->
+ <record model="ir.ui.view" id="sms_template_preview_form">
+ <field name="name">sms.template.preview.form</field>
+ <field name="model">sms.template.preview</field>
+ <field name="arch" type="xml">
+ <form string="SMS Preview">
+ <h3>Preview of <field name="sms_template_id" readonly="1" nolabel="1" class="oe_inline"/></h3>
+ <field name="no_record" invisible="1"/>
+ <div class="o_row">
+ <span>Choose an example <field name="model_id" readonly="1"/> record:</span>
+ <div>
+ <field name="resource_ref" class="oe_inline" options="{'hide_model': True, 'no_create': True, 'no_edit': True, 'no_open': True}" attrs="{'invisible': [('no_record', '=', True)]}"/>
+ <span class="text-warning" attrs="{'invisible': [('no_record', '=', False)]}">No records</span>
+ </div>
+ </div>
+ <p>Choose a language: <field name="lang" class="oe_inline ml8"/></p>
+ <label for="body" string="SMS content"/>
+ <hr/>
+ <field name="body" readonly="1" nolabel="1" options='{"safe": True}'/>
+ <hr/>
+ <footer>
+ <button string="Discard" class="btn-secondary" special="cancel"/>
+ </footer>
+ </form>
+ </field>
+ </record>
+
+ <record id="sms_template_preview_action" model="ir.actions.act_window">
+ <field name="name">Template Preview</field>
+ <field name="res_model">sms.template.preview</field>
+ <field name="type">ir.actions.act_window</field>
+ <field name="view_mode">form</field>
+ <field name="view_id" ref="sms_template_preview_form"/>
+ <field name="target">new</field>
+ <field name="context">{'default_sms_template_id':active_id}</field>
+ </record>
+
+ </data>
+</odoo>