[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>
This commit is contained in:
committed by
Thibault Delavallée
co-authored by
Thibault Delavallee
parent
bdebcab0ce
commit
9ad7a88a08
@@ -18,6 +18,7 @@ The service is provided by the In App Purchase Odoo platform.
|
||||
'views/res_partner_views.xml',
|
||||
'views/assets.xml',
|
||||
'views/sms_sms_views.xml',
|
||||
'views/sms_template_views.xml',
|
||||
'security/ir.model.access.csv',
|
||||
],
|
||||
'qweb': [
|
||||
|
||||
@@ -8,3 +8,4 @@ from . import mail_thread
|
||||
from . import res_partner
|
||||
from . import sms_api
|
||||
from . import sms_sms
|
||||
from . import sms_template
|
||||
|
||||
@@ -84,6 +84,46 @@ class MailThread(models.AbstractModel):
|
||||
result[record.id] = {'partner': self.env['res.partner'], 'sanitized': False, 'number': False}
|
||||
return result
|
||||
|
||||
def _message_sms_schedule_mass(self, body='', template=False, active_domain=None):
|
||||
""" Shortcut method to schedule a mass sms sending on a recordset.
|
||||
|
||||
:param template: an optional sms.template record;
|
||||
:param active_domain: bypass self.ids and apply composer on active_domain
|
||||
instead;
|
||||
"""
|
||||
composer_context = {
|
||||
'default_res_model': self._name,
|
||||
'default_composition_mode': 'mass',
|
||||
'default_template_id': template.id if template else False,
|
||||
'default_body': body if body and not template else False,
|
||||
}
|
||||
if active_domain is not None:
|
||||
composer_context['default_use_active_domain'] = True
|
||||
composer_context['default_active_domain'] = repr(active_domain)
|
||||
else:
|
||||
composer_context['default_res_ids'] = self.ids
|
||||
|
||||
composer = self.env['sms.composer'].with_context(**composer_context).create({})
|
||||
return composer._action_send_sms()
|
||||
|
||||
def _message_sms_with_template(self, template=False, template_xmlid=False, template_fallback='', partner_ids=False, **kwargs):
|
||||
""" Shortcut method to perform a _message_sms with an sms.template.
|
||||
|
||||
:param template: a valid sms.template record;
|
||||
:param template_xmlid: XML ID of an sms.template (if no template given);
|
||||
:param template_fallback: plaintext (jinja-enabled) in case template
|
||||
and template xml id are falsy (for example due to deleted data);
|
||||
"""
|
||||
self.ensure_one()
|
||||
if not template and template_xmlid:
|
||||
template = self.env.ref(template_xmlid, raise_if_not_found=False)
|
||||
if template:
|
||||
template_w_lang = template._get_context_lang_per_id(self.ids)[self.id]
|
||||
body = template._render_template(template_w_lang.body, self._name, self.ids)[self.id]
|
||||
else:
|
||||
body = self.env['sms.template']._render_template(template_fallback, self._name, self.ids)[self.id]
|
||||
return self._message_sms(body, partner_ids=partner_ids, **kwargs)
|
||||
|
||||
def _message_sms(self, body, subtype_id=False, partner_ids=False, number_field=False,
|
||||
sms_numbers=None, sms_pid_to_number=None, **kwargs):
|
||||
""" Main method to post a message on a record using SMS-based notification
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
|
||||
|
||||
class SMSTemplate(models.Model):
|
||||
"Templates for sending SMS"
|
||||
_name = "sms.template"
|
||||
_description = 'SMS Templates'
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
res = super(SMSTemplate, self).default_get(fields)
|
||||
if not fields or 'model_id' in fields and not res.get('model_id') and res.get('model'):
|
||||
res['model_id'] = self.env['ir.model']._get(res['model']).id
|
||||
return res
|
||||
|
||||
name = fields.Char()
|
||||
model_id = fields.Many2one(
|
||||
'ir.model', string='Applies to', required=True,
|
||||
domain=['&', ('is_mail_thread', '=', True), ('transient', '=', False)],
|
||||
help="The type of document this template can be used with")
|
||||
model = fields.Char('Related Document Model', related='model_id.model', index=True, store=True, readonly=True)
|
||||
body = fields.Char('Body', translate=True, required=True)
|
||||
lang = fields.Char('Language', placeholder="${object.partner_id.lang}")
|
||||
# Fake fields used to implement the placeholder assistant
|
||||
model_object_field = fields.Many2one('ir.model.fields', string="Field", store=False,
|
||||
help="Select target field from the related document model.\n"
|
||||
"If it is a relationship field you will be able to select "
|
||||
"a target field at the destination of the relationship.")
|
||||
sub_object = fields.Many2one('ir.model', 'Sub-model', readonly=True, store=False,
|
||||
help="When a relationship field is selected as first field, "
|
||||
"this field shows the document model the relationship goes to.")
|
||||
sub_model_object_field = fields.Many2one('ir.model.fields', 'Sub-field', store=False,
|
||||
help="When a relationship field is selected as first field, "
|
||||
"this field lets you select the target field within the "
|
||||
"destination document model (sub-model).")
|
||||
null_value = fields.Char('Default Value', store=False, help="Optional value to use if the target field is empty")
|
||||
copyvalue = fields.Char('Placeholder Expression', store=False,
|
||||
help="Final placeholder expression, to be copy-pasted in the desired template field.")
|
||||
|
||||
@api.onchange('model_object_field', 'sub_model_object_field', 'null_value')
|
||||
def _onchange_dynamic_placeholder(self):
|
||||
""" Generate the dynamic placeholder """
|
||||
if self.model_object_field:
|
||||
if self.model_object_field.ttype in ['many2one', 'one2many', 'many2many']:
|
||||
model = self.env['ir.model']._get(self.model_object_field.relation)
|
||||
if model:
|
||||
self.sub_object = model.id
|
||||
sub_field_name = self.sub_model_object_field.name
|
||||
self.copyvalue = self._build_expression(self.model_object_field.name,
|
||||
sub_field_name, self.null_value or False)
|
||||
else:
|
||||
self.sub_object = False
|
||||
self.sub_model_object_field = False
|
||||
self.copyvalue = self._build_expression(self.model_object_field.name, False, self.null_value or False)
|
||||
else:
|
||||
self.sub_object = False
|
||||
self.copyvalue = False
|
||||
self.sub_model_object_field = False
|
||||
self.null_value = False
|
||||
|
||||
@api.model
|
||||
def _build_expression(self, field_name, sub_field_name, null_value):
|
||||
"""Returns a placeholder expression for use in a template field,
|
||||
based on the values provided in the placeholder assistant.
|
||||
|
||||
:param field_name: main field name
|
||||
:param sub_field_name: sub field name (M2O)
|
||||
:param null_value: default value if the target value is empty
|
||||
:return: final placeholder expression """
|
||||
expression = ''
|
||||
if field_name:
|
||||
expression = "${object." + field_name
|
||||
if sub_field_name:
|
||||
expression += "." + sub_field_name
|
||||
if null_value:
|
||||
expression += " or '''%s'''" % null_value
|
||||
expression += "}"
|
||||
return expression
|
||||
|
||||
@api.multi
|
||||
@api.returns('self', lambda value: value.id)
|
||||
def copy(self, default=None):
|
||||
default = dict(default or {},
|
||||
name=_("%s (copy)") % self.name)
|
||||
return super(SMSTemplate, self).copy(default=default)
|
||||
|
||||
@api.multi
|
||||
def _get_context_lang_per_id(self, res_ids):
|
||||
self.ensure_one()
|
||||
if res_ids is None:
|
||||
return {None: self}
|
||||
|
||||
if self.env.context.get('template_preview_lang'):
|
||||
lang = self.env.context.get('template_preview_lang')
|
||||
results = dict((res_id, self.with_context(lang=lang)) for res_id in res_ids)
|
||||
else:
|
||||
rendered_langs = self._render_template(self.lang, self.model, res_ids)
|
||||
results = dict(
|
||||
(res_id, self.with_context(lang=lang) if lang else self)
|
||||
for res_id, lang in rendered_langs.items())
|
||||
|
||||
return results
|
||||
|
||||
@api.multi
|
||||
def _get_ids_per_lang(self, res_ids):
|
||||
self.ensure_one()
|
||||
|
||||
rids_to_tpl = self._get_context_lang_per_id(res_ids)
|
||||
tpl_to_rids = {}
|
||||
for res_id, template in rids_to_tpl.items():
|
||||
tpl_to_rids.setdefault(template._context.get('lang', self.env.user.lang), []).append(res_id)
|
||||
|
||||
return tpl_to_rids
|
||||
|
||||
@api.model
|
||||
def _render_template(self, template_txt, model, res_ids):
|
||||
""" Render the jinja template """
|
||||
return self.env['mail.template']._render_template(template_txt, model, res_ids)
|
||||
@@ -1,3 +1,6 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_sms_sms_all,access.sms.sms.all,model_sms_sms,,0,0,0,0
|
||||
access_sms_sms_system,access.sms.sms.system,model_sms_sms,base.group_system,1,1,1,1
|
||||
access_sms_template_all,access.sms.template.all,model_sms_template,,0,0,0,0
|
||||
access_sms_template_user,access.sms.template.user,model_sms_template,base.group_user,1,0,0,0
|
||||
access_sms_template_system,access.sms.template.system,model_sms_template,base.group_system,1,1,1,0
|
||||
|
||||
|
@@ -73,6 +73,8 @@ class MockSMS(common.BaseCase):
|
||||
def assertSMSCanceled(self, partner, number, error_code, content=None):
|
||||
""" Check canceled SMS. Search is done for a pair partner / number where
|
||||
partner can be an empty recordset. """
|
||||
if number is None and partner:
|
||||
number = phone_validation.phone_get_sanitized_record_number(partner)
|
||||
sms = self.env['sms.sms'].sudo().search([
|
||||
('partner_id', '=', partner.id), ('number', '=', number),
|
||||
('state', '=', 'canceled')
|
||||
@@ -85,6 +87,8 @@ class MockSMS(common.BaseCase):
|
||||
def assertSMSFailed(self, partner, number, error_code, content=None):
|
||||
""" Check failed SMS. Search is done for a pair partner / number where
|
||||
partner can be an empty recordset. """
|
||||
if number is None and partner:
|
||||
number = phone_validation.phone_get_sanitized_record_number(partner)
|
||||
sms = self.env['sms.sms'].sudo().search([
|
||||
('partner_id', '=', partner.id), ('number', '=', number),
|
||||
('state', '=', 'error')
|
||||
@@ -97,11 +101,13 @@ class MockSMS(common.BaseCase):
|
||||
def assertSMSOutgoing(self, partner, number, content=None):
|
||||
""" Check outgoing SMS. Search is done for a pair partner / number where
|
||||
partner can be an empty recordset. """
|
||||
if number is None and partner:
|
||||
number = phone_validation.phone_get_sanitized_record_number(partner)
|
||||
sms = self.env['sms.sms'].sudo().search([
|
||||
('partner_id', '=', partner.id), ('number', '=', number),
|
||||
('state', '=', 'outgoing')
|
||||
])
|
||||
self.assertTrue(sms, 'SMS: not found failed SMS for %s (number: %s, state)' % (partner, number))
|
||||
self.assertTrue(sms, 'SMS: not found failed SMS for %s (number: %s)' % (partner, number))
|
||||
if content is not None:
|
||||
self.assertEqual(sms.body, content)
|
||||
|
||||
|
||||
@@ -15,12 +15,11 @@
|
||||
<button
|
||||
type="action"
|
||||
name="%(sms.sms_composer_action_form)d"
|
||||
class="btn-sm btn-link mb4 fa fa-envelope-o"
|
||||
aria-label="Send SMS"
|
||||
title="Send SMS"
|
||||
class="btn-sm btn-link mb4 fa fa-comment"
|
||||
aria-label="Send SMS Text Message"
|
||||
title="Send SMS Text Message"
|
||||
attrs="{'invisible':[('phone', '=', False)]}"
|
||||
context="{'field_name': 'phone'}"
|
||||
/>
|
||||
context="{'default_partner_field_name': 'phone'}" />
|
||||
</div>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='mobile']" position="replace">
|
||||
@@ -31,14 +30,37 @@
|
||||
type="action"
|
||||
name="%(sms.sms_composer_action_form)d"
|
||||
class="btn-sm btn-link mb4 fa fa-envelope-o"
|
||||
aria-label="Send SMS"
|
||||
title="Send SMS"
|
||||
aria-label="Send SMS Text Message"
|
||||
title="Send SMS Text Message"
|
||||
attrs="{'invisible':[('mobile', '=', False)]}"
|
||||
context="{'field_name': 'mobile'}"
|
||||
/>
|
||||
</div>
|
||||
</xpath>
|
||||
context="{'default_partner_field_name': 'mobile'}" />
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Add action entry in the Action Menu for Partners -->
|
||||
<act_window id="res_partner_act_window_sms_composer_single"
|
||||
name="Send SMS Text Message"
|
||||
binding_model="res.partner"
|
||||
res_model="sms.composer"
|
||||
binding_views="list"
|
||||
view_mode="form"
|
||||
target="new"
|
||||
context="{
|
||||
'default_composition_mode': 'mass',
|
||||
}"
|
||||
/>
|
||||
<act_window id="res_partner_act_window_sms_composer_multi"
|
||||
name="Send SMS Text Message"
|
||||
binding_model="res.partner"
|
||||
res_model="sms.composer"
|
||||
binding_views="form"
|
||||
view_mode="form"
|
||||
target="new"
|
||||
context="{
|
||||
'default_composition_mode': 'comment',
|
||||
}"
|
||||
/>
|
||||
|
||||
</odoo>
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo><data>
|
||||
<record id="sms_template_view_form" model="ir.ui.view">
|
||||
<field name="name">sms.template.view.form</field>
|
||||
<field name="model">sms.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="SMS Templates">
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<label for="name" class="oe_edit_only"/>
|
||||
<h1><field name="name" required="1"/></h1>
|
||||
<group>
|
||||
<field name="model_id" required="1" options="{'no_create': True}"/>
|
||||
<field name="model" invisible="1"/>
|
||||
<field name="lang"/>
|
||||
</group>
|
||||
</div>
|
||||
<notebook>
|
||||
<page string="Content">
|
||||
<field name="body" widget="sms_widget"/>
|
||||
</page>
|
||||
<page string="Dynamic Placeholder Generator" groups="base.group_no_one">
|
||||
<group>
|
||||
<field name="model_object_field"
|
||||
domain="[('model_id','=',model_id),('ttype','!=','one2many'),('ttype','!=','many2many')]"/>
|
||||
<field name="sub_object" readonly="1"/>
|
||||
<field name="sub_model_object_field"
|
||||
domain="[('model_id','=',sub_object),('ttype','!=','one2many'),('ttype','!=','many2many')]"
|
||||
attrs="{'readonly':[('sub_object','=',False)],'required':[('sub_object','!=',False)]}"/>
|
||||
<field name="null_value"/>
|
||||
<field name="copyvalue"/>
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="sms_template_view_tree" model="ir.ui.view">
|
||||
<field name="name">sms.template.view.tree</field>
|
||||
<field name="model">sms.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="SMS Templates">
|
||||
<field name="name"/>
|
||||
<field name="model_id"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="sms_template_view_search" model="ir.ui.view">
|
||||
<field name="name">sms.template.view.search</field>
|
||||
<field name="model">sms.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search SMS Templates">
|
||||
<field name="name"/>
|
||||
<field name="model_id"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="sms_template_action" model="ir.actions.act_window">
|
||||
<field name="name">Templates</field>
|
||||
<field name="res_model">sms.template</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="sms_template_menu" parent="sms_menu_main" action="sms_template_action"/>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -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 fields == 'partner_ids':
|
||||
# shortcut because default_get in cache, avoid issues
|
||||
return result
|
||||
|
||||
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)
|
||||
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
|
||||
|
||||
def action_send_sms(self):
|
||||
numbers = [number.strip() for number in self.recipients.split(',') if number.strip()]
|
||||
# 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')
|
||||
|
||||
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)
|
||||
else:
|
||||
self.env['sms.api']._send_sms(numbers, self.message)
|
||||
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):
|
||||
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()
|
||||
|
||||
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(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
|
||||
|
||||
@@ -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>
|
||||
<group>
|
||||
<group>
|
||||
<field name="template_id" options="{'no_create': True}" attrs="{'invisible': [('composition_mode', '!=', 'mass')]}"/>
|
||||
</group>
|
||||
</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>
|
||||
<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>
|
||||
|
||||
@@ -5,3 +5,4 @@ from . import test_sms_composer
|
||||
from . import test_sms_performance
|
||||
from . import test_sms_post
|
||||
from . import test_sms_sms
|
||||
from . import test_sms_template
|
||||
|
||||
@@ -7,66 +7,303 @@ from odoo.addons.test_mail.tests import common as test_mail_common
|
||||
from odoo.addons.test_mail_full.tests import common as test_mail_full_common
|
||||
|
||||
|
||||
class TestSMSComposer(test_mail_full_common.BaseFunctionalTest, sms_common.MockSMS, test_mail_common.MockEmails, test_mail_common.TestRecipients):
|
||||
class TestSMSComposerComment(test_mail_full_common.BaseFunctionalTest, sms_common.MockSMS, test_mail_common.MockEmails, test_mail_common.TestRecipients):
|
||||
""" TODO LIST
|
||||
|
||||
* add test for default_res_model / default_res_id and stuff like that;
|
||||
* add test for comment put in queue;
|
||||
* add test for language support (set template lang context);
|
||||
* add test for sanitized / wrong numbers;
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestSMSComposer, cls).setUpClass()
|
||||
super(TestSMSComposerComment, cls).setUpClass()
|
||||
cls._test_body = 'VOID CONTENT'
|
||||
|
||||
cls.partner_numbers = [
|
||||
phone_validation.phone_format(partner.mobile, partner.country_id.code, partner.country_id.phone_code, force_format='E164')
|
||||
for partner in (cls.partner_1 | cls.partner_2)
|
||||
]
|
||||
|
||||
def test_composer_no_model(self):
|
||||
composer = self.env['sms.composer'].with_context().create({
|
||||
'message': self._test_body,
|
||||
'recipients': '+32475998877, 0475997788'
|
||||
cls.test_record = cls.env['mail.test.sms'].with_context(**cls._test_context).create({
|
||||
'name': 'Test',
|
||||
'customer_id': cls.partner_1.id,
|
||||
'mobile_nbr': cls.test_numbers[0],
|
||||
'phone_nbr': cls.test_numbers[1],
|
||||
})
|
||||
cls.test_record = cls._reset_mail_context(cls.test_record)
|
||||
|
||||
cls.sms_template = cls.env['sms.template'].create({
|
||||
'name': 'Test Template',
|
||||
'model_id': cls.env['ir.model']._get('mail.test.sms').id,
|
||||
'body': 'Dear ${object.display_name} this is an SMS.',
|
||||
})
|
||||
|
||||
with self.mockSMSGateway():
|
||||
composer.action_send_sms()
|
||||
# self.assertSMSSent(self.random_numbers, self._test_body) # TDE FIXME: actually sanitizer is not called on numbers in current master (saas 12.3)
|
||||
self.assertSMSSent('+32475998877, 0475997788'.split(', '), self._test_body)
|
||||
|
||||
def test_composer_no_mail_thread(self):
|
||||
composer = self.env['sms.composer'].with_context(active_model='test_performance.base').create({
|
||||
'message': self._test_body,
|
||||
'recipients': '+32475998877, 0475997788'
|
||||
})
|
||||
|
||||
with self.mockSMSGateway():
|
||||
composer.action_send_sms()
|
||||
# self.assertSMSSent(self.random_numbers, self._test_body) # TDE FIXME: actually sanitizer is not called on numbers in current master (saas 12.3)
|
||||
self.assertSMSSent('+32475998877, 0475997788'.split(', '), self._test_body)
|
||||
|
||||
def test_composer_partners_active_domain(self):
|
||||
partners = self.partner_1 | self.partner_2
|
||||
composer = self.env['sms.composer'].with_context(
|
||||
active_model='res.partner',
|
||||
active_domain=[('id', 'in', partners.ids)]
|
||||
).create({
|
||||
'message': self._test_body,
|
||||
})
|
||||
|
||||
with self.mockSMSGateway():
|
||||
composer.action_send_sms()
|
||||
self.assertSMSSent(self.partner_numbers, self._test_body)
|
||||
|
||||
def test_composer_partners_sanitize(self):
|
||||
partner_incorrect = self.env['res.partner'].create({
|
||||
'name': 'Jean-Claude Incorrect',
|
||||
'email': 'jean.claude@example.com',
|
||||
'mobile': 'coincoin',
|
||||
def test_composer_comment_not_mail_thread(self):
|
||||
with self.sudo('employee'):
|
||||
record = self.env['test_performance.base'].create({'name': 'TestBase'})
|
||||
composer = self.env['sms.composer'].with_context(
|
||||
active_model='test_performance.base', active_id=record.id
|
||||
).create({
|
||||
'body': self._test_body,
|
||||
'numbers': ','.join(self.random_numbers),
|
||||
})
|
||||
partners = self.partner_1 | self.partner_2 | partner_incorrect
|
||||
# composer = self.env['sms.composer'].with_context(
|
||||
# active_model='res.partner',
|
||||
# active_domain=[('id', 'in', partners.ids)]
|
||||
# ).create({
|
||||
# 'message': self._test_body,
|
||||
# })
|
||||
|
||||
# with self.mockSMSGateway():
|
||||
# composer.action_send_sms()
|
||||
# self.assertSMSSent((self.partner_1 | self.partner_2).mapped('mobile'), test_body) # TDE FIXME: actually sanitizer does not work in current master (saas 12.23))
|
||||
with self.mockSMSGateway():
|
||||
composer._action_send_sms()
|
||||
|
||||
self.assertSMSSent(self.random_numbers_san, self._test_body)
|
||||
|
||||
def test_composer_comment_default(self):
|
||||
with self.sudo('employee'):
|
||||
composer = self.env['sms.composer'].with_context(
|
||||
active_model='mail.test.sms', active_id=self.test_record.id
|
||||
).create({
|
||||
'body': self._test_body,
|
||||
})
|
||||
|
||||
with self.mockSMSGateway():
|
||||
messages = composer._action_send_sms()
|
||||
|
||||
self.assertSMSNotification([{'partner': self.test_record.customer_id, 'number': self.test_numbers_san[1]}], self._test_body, messages)
|
||||
|
||||
def test_composer_comment_field_1(self):
|
||||
with self.sudo('employee'):
|
||||
composer = self.env['sms.composer'].with_context(
|
||||
active_model='mail.test.sms', active_id=self.test_record.id,
|
||||
).create({
|
||||
'body': self._test_body,
|
||||
'number_field_name': 'mobile_nbr',
|
||||
})
|
||||
|
||||
with self.mockSMSGateway():
|
||||
messages = composer._action_send_sms()
|
||||
|
||||
self.assertSMSNotification([{'partner': self.test_record.customer_id, 'number': self.test_numbers_san[0]}], self._test_body, messages)
|
||||
|
||||
def test_composer_comment_field_2(self):
|
||||
with self.sudo('employee'):
|
||||
composer = self.env['sms.composer'].with_context(
|
||||
active_model='mail.test.sms', active_id=self.test_record.id,
|
||||
).create({
|
||||
'body': self._test_body,
|
||||
'number_field_name': 'phone_nbr',
|
||||
})
|
||||
|
||||
with self.mockSMSGateway():
|
||||
messages = composer._action_send_sms()
|
||||
|
||||
self.assertSMSNotification([{'partner': self.test_record.customer_id, 'number': self.test_numbers_san[1]}], self._test_body, messages)
|
||||
|
||||
def test_composer_comment_field_w_numbers(self):
|
||||
with self.sudo('employee'):
|
||||
composer = self.env['sms.composer'].with_context(
|
||||
active_model='mail.test.sms', active_id=self.test_record.id,
|
||||
default_number_field_name='mobile_nbr',
|
||||
).create({
|
||||
'body': self._test_body,
|
||||
'numbers': ','.join(self.random_numbers),
|
||||
})
|
||||
|
||||
with self.mockSMSGateway():
|
||||
messages = composer._action_send_sms()
|
||||
|
||||
self.assertSMSNotification([
|
||||
{'partner': self.test_record.customer_id, 'number': self.test_record.mobile_nbr},
|
||||
{'number': self.random_numbers_san[0]}, {'number': self.random_numbers_san[1]}], self._test_body, messages)
|
||||
|
||||
def test_composer_comment_field_w_template(self):
|
||||
with self.sudo('employee'):
|
||||
composer = self.env['sms.composer'].with_context(
|
||||
active_model='mail.test.sms', active_id=self.test_record.id,
|
||||
default_template_id=self.sms_template.id,
|
||||
default_number_field_name='mobile_nbr',
|
||||
).create({})
|
||||
|
||||
with self.mockSMSGateway():
|
||||
messages = composer._action_send_sms()
|
||||
|
||||
self.assertSMSNotification([{'partner': self.test_record.customer_id, 'number': self.test_record.mobile_nbr}], 'Dear %s this is an SMS.' % self.test_record.display_name, messages)
|
||||
|
||||
def test_composer_numbers_no_model(self):
|
||||
with self.sudo('employee'):
|
||||
composer = self.env['sms.composer'].with_context(
|
||||
default_composition_mode='numbers'
|
||||
).create({
|
||||
'body': self._test_body,
|
||||
'numbers': ','.join(self.random_numbers),
|
||||
})
|
||||
|
||||
with self.mockSMSGateway():
|
||||
composer._action_send_sms()
|
||||
self.assertSMSSent(self.random_numbers_san, self._test_body)
|
||||
|
||||
# def test_composer_partners_sanitize(self):
|
||||
# partner_incorrect = self.env['res.partner'].create({
|
||||
# 'name': 'Jean-Claude Incorrect',
|
||||
# 'email': 'jean.claude@example.com',
|
||||
# 'mobile': 'coincoin',
|
||||
# })
|
||||
# partners = self.partner_1 | self.partner_2 | partner_incorrect
|
||||
# with self.sudo('employee'):
|
||||
# composer = self.env['sms.composer'].with_context(
|
||||
# active_model='res.partner',
|
||||
# active_domain=[('id', 'in', partners.ids)]
|
||||
# ).create({
|
||||
# 'body': self._test_body,
|
||||
# })
|
||||
|
||||
# with self.mockSMSGateway():
|
||||
# composer.action_send_sms()
|
||||
# self.assertSMSSent((self.partner_1 | self.partner_2).mapped('mobile'), test_body)
|
||||
|
||||
|
||||
class TestSMSComposerMass(test_mail_full_common.BaseFunctionalTest, sms_common.MockSMS, test_mail_common.MockEmails, test_mail_common.TestRecipients):
|
||||
""" TODO LIST
|
||||
|
||||
* add test for mass with log note
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestSMSComposerMass, cls).setUpClass()
|
||||
cls._test_body = 'Zizisse an SMS.'
|
||||
|
||||
records = cls.env['mail.test.sms']
|
||||
partners = cls.env['res.partner']
|
||||
country_id = cls.env.ref('base.be').id,
|
||||
for x in range(3):
|
||||
partners += cls.env['res.partner'].with_context(**cls._test_context).create({
|
||||
'name': 'Partner_%s' % (x),
|
||||
'email': '_test_partner_%s@example.com' % (x),
|
||||
'country_id': country_id,
|
||||
'mobile': '047500%s%s99' % (x, x)
|
||||
})
|
||||
records += cls.env['mail.test.sms'].with_context(**cls._test_context).create({
|
||||
'name': 'Test_%s' % (x),
|
||||
'customer_id': partners[x].id,
|
||||
})
|
||||
cls.records = cls._reset_mail_context(records)
|
||||
cls.partners = partners
|
||||
|
||||
cls.sms_template = cls.env['sms.template'].create({
|
||||
'name': 'Test Template',
|
||||
'model_id': cls.env['ir.model']._get('mail.test.sms').id,
|
||||
'body': 'Dear ${object.display_name} this is an SMS.',
|
||||
})
|
||||
|
||||
def test_composer_mass_active_domain(self):
|
||||
with self.sudo('employee'):
|
||||
composer = self.env['sms.composer'].with_context(
|
||||
default_composition_mode='mass',
|
||||
default_res_model='mail.test.sms',
|
||||
default_use_active_domain=True,
|
||||
active_domain=repr([('id', 'in', self.records.ids)]),
|
||||
).create({
|
||||
'body': self._test_body,
|
||||
})
|
||||
|
||||
with self.mockSMSGateway():
|
||||
composer.action_send_sms()
|
||||
|
||||
for record in self.records:
|
||||
self.assertSMSOutgoing(record.customer_id, None, self._test_body)
|
||||
|
||||
def test_composer_mass_active_domain_w_template(self):
|
||||
with self.sudo('employee'):
|
||||
composer = self.env['sms.composer'].with_context(
|
||||
default_composition_mode='mass',
|
||||
default_res_model='mail.test.sms',
|
||||
default_use_active_domain=True,
|
||||
active_domain=repr([('id', 'in', self.records.ids)]),
|
||||
default_template_id=self.sms_template.id,
|
||||
).create({
|
||||
})
|
||||
|
||||
with self.mockSMSGateway():
|
||||
composer.action_send_sms()
|
||||
|
||||
for record in self.records:
|
||||
self.assertSMSOutgoing(record.customer_id, None, 'Dear %s this is an SMS.' % record.display_name)
|
||||
|
||||
def test_composer_mass_active_ids(self):
|
||||
with self.sudo('employee'):
|
||||
composer = self.env['sms.composer'].with_context(
|
||||
default_composition_mode='mass',
|
||||
default_res_model='mail.test.sms',
|
||||
active_ids=self.records.ids,
|
||||
).create({
|
||||
'body': self._test_body,
|
||||
})
|
||||
|
||||
with self.mockSMSGateway():
|
||||
composer.action_send_sms()
|
||||
|
||||
for partner in self.partners:
|
||||
self.assertSMSOutgoing(partner, None, self._test_body)
|
||||
|
||||
def test_composer_mass_active_ids_w_template(self):
|
||||
with self.sudo('employee'):
|
||||
composer = self.env['sms.composer'].with_context(
|
||||
default_composition_mode='mass',
|
||||
default_res_model='mail.test.sms',
|
||||
active_ids=self.records.ids,
|
||||
default_template_id=self.sms_template.id,
|
||||
).create({})
|
||||
|
||||
with self.mockSMSGateway():
|
||||
composer.action_send_sms()
|
||||
|
||||
for record in self.records:
|
||||
self.assertSMSOutgoing(record.customer_id, None, 'Dear %s this is an SMS.' % record.display_name)
|
||||
|
||||
def test_composer_mass_active_ids_w_template_and_lang(self):
|
||||
self.env.ref('base.lang_fr').write({'active': True})
|
||||
self.env['ir.translation'].create({
|
||||
'type': 'model',
|
||||
'name': 'sms.template,body',
|
||||
'lang': 'fr_FR',
|
||||
'res_id': self.sms_template.id,
|
||||
'src': self.sms_template.body,
|
||||
'value': 'Cher·e· ${object.display_name} ceci est un SMS.',
|
||||
})
|
||||
# set template to try to use customer lang
|
||||
self.sms_template.write({
|
||||
'lang': '${object.customer_id.lang}',
|
||||
})
|
||||
# set one customer as french speaking
|
||||
self.partners[2].write({'lang': 'fr_FR'})
|
||||
|
||||
with self.sudo('employee'):
|
||||
composer = self.env['sms.composer'].with_context(
|
||||
default_composition_mode='mass',
|
||||
default_res_model='mail.test.sms',
|
||||
active_ids=self.records.ids,
|
||||
default_template_id=self.sms_template.id,
|
||||
).create({})
|
||||
|
||||
with self.mockSMSGateway():
|
||||
composer.action_send_sms()
|
||||
|
||||
for record in self.records:
|
||||
if record.customer_id == self.partners[2]:
|
||||
self.assertSMSOutgoing(record.customer_id, None, 'Cher·e· %s ceci est un SMS.' % record.display_name)
|
||||
else:
|
||||
self.assertSMSOutgoing(record.customer_id, None, 'Dear %s this is an SMS.' % record.display_name)
|
||||
|
||||
def test_message_schedule_sms(self):
|
||||
with self.sudo('employee'):
|
||||
with self.mockSMSGateway():
|
||||
self.env['mail.test.sms'].browse(self.records.ids)._message_sms_schedule_mass(body=self._test_body)
|
||||
|
||||
for record in self.records:
|
||||
self.assertSMSOutgoing(record.customer_id, None, self._test_body)
|
||||
|
||||
def test_message_schedule_sms_w_template(self):
|
||||
with self.sudo('employee'):
|
||||
with self.mockSMSGateway():
|
||||
self.env['mail.test.sms'].browse(self.records.ids)._message_sms_schedule_mass(template=self.sms_template)
|
||||
|
||||
for record in self.records:
|
||||
self.assertSMSOutgoing(record.customer_id, None, 'Dear %s this is an SMS.' % record.display_name)
|
||||
|
||||
@@ -15,6 +15,7 @@ class TestSMSPerformance(BaseMailPerformance, sms_common.MockSMS):
|
||||
super(TestSMSPerformance, self).setUp()
|
||||
self.user_employee.write({
|
||||
'login': 'employee',
|
||||
'country_id': self.env.ref('base.be').id,
|
||||
})
|
||||
self.admin = self.env.user
|
||||
|
||||
@@ -86,3 +87,63 @@ class TestSMSPerformance(BaseMailPerformance, sms_common.MockSMS):
|
||||
|
||||
self.assertEqual(record.message_ids[0].body, '<p>Performance Test</p>')
|
||||
self.assertSMSNotification([{'partner': self.customer}], 'Performance Test', messages)
|
||||
|
||||
|
||||
@tagged('mail_performance')
|
||||
class TestSMSMassPerformance(BaseMailPerformance, sms_common.MockSMS):
|
||||
|
||||
def setUp(self):
|
||||
super(TestSMSMassPerformance, self).setUp()
|
||||
be_country_id = self.env.ref('base.be').id,
|
||||
self.user_employee.write({
|
||||
'login': 'employee',
|
||||
'country_id': be_country_id,
|
||||
})
|
||||
self.admin = self.env.user
|
||||
self.admin.write({
|
||||
'country_id': be_country_id,
|
||||
})
|
||||
|
||||
self._test_body = 'MASS SMS'
|
||||
|
||||
records = self.env['mail.test.sms']
|
||||
partners = self.env['res.partner']
|
||||
for x in range(50):
|
||||
partners += self.env['res.partner'].with_context(**self._quick_create_ctx).create({
|
||||
'name': 'Partner_%s' % (x),
|
||||
'email': '_test_partner_%s@example.com' % (x),
|
||||
'country_id': be_country_id,
|
||||
'mobile': '047500%s%s99' % (x, x)
|
||||
})
|
||||
records += self.env['mail.test.sms'].with_context(**self._quick_create_ctx).create({
|
||||
'name': 'Test_%s' % (x),
|
||||
'customer_id': partners[x].id,
|
||||
})
|
||||
self.partners = partners
|
||||
self.records = records
|
||||
|
||||
self.sms_template = self.env['sms.template'].create({
|
||||
'name': 'Test Template',
|
||||
'model_id': self.env['ir.model']._get('mail.test.sms').id,
|
||||
'body': 'Dear ${object.display_name} this is an SMS.',
|
||||
})
|
||||
|
||||
@mute_logger('odoo.addons.sms.models.sms_sms')
|
||||
@users('employee')
|
||||
@warmup
|
||||
def test_composer_mass_active_domain(self):
|
||||
composer = self.env['sms.composer'].with_context(
|
||||
default_composition_mode='mass',
|
||||
default_res_model='mail.test.sms',
|
||||
default_use_active_domain=True,
|
||||
active_domain=repr([('id', 'in', self.records.ids)]),
|
||||
).create({
|
||||
'body': self._test_body,
|
||||
})
|
||||
|
||||
with self.mockSMSGateway(), self.assertQueryCount(employee=108):
|
||||
composer.action_send_sms()
|
||||
|
||||
# TDE FIXME
|
||||
# for record in self.records:
|
||||
# self.assertSMSOutgoing(record.customer_id, None, self._test_body)
|
||||
|
||||
@@ -170,6 +170,48 @@ class TestSMSPost(test_mail_full_common.BaseFunctionalTest, sms_common.MockSMS,
|
||||
|
||||
self.assertSMSNotification([{'partner': self.partner_1}, {'number': self.random_numbers_san[0]}], self._test_body, messages)
|
||||
|
||||
def test_message_sms_with_template(self):
|
||||
sms_template = self.env['sms.template'].create({
|
||||
'name': 'Test Template',
|
||||
'model_id': self.env['ir.model']._get('mail.test.sms').id,
|
||||
'body': 'Dear ${object.display_name} this is an SMS.',
|
||||
})
|
||||
|
||||
with self.sudo('employee'):
|
||||
with self.mockSMSGateway():
|
||||
test_record = self.env['mail.test.sms'].browse(self.test_record.id)
|
||||
messages = test_record._message_sms_with_template(template=sms_template)
|
||||
|
||||
self.assertSMSNotification([{'partner': self.partner_1, 'number': self.test_numbers_san[1]}], 'Dear %s this is an SMS.' % self.test_record.display_name, messages)
|
||||
|
||||
def test_message_sms_with_template_fallback(self):
|
||||
with self.sudo('employee'):
|
||||
with self.mockSMSGateway():
|
||||
test_record = self.env['mail.test.sms'].browse(self.test_record.id)
|
||||
messages = test_record._message_sms_with_template(template_xmlid='test_mail_full.this_should_not_exists', template_fallback='Fallback for ${object.id}')
|
||||
|
||||
self.assertSMSNotification([{'partner': self.partner_1, 'number': self.test_numbers_san[1]}], 'Fallback for %s' % self.test_record.id, messages)
|
||||
|
||||
def test_message_sms_with_template_xmlid(self):
|
||||
sms_template = self.env['sms.template'].create({
|
||||
'name': 'Test Template',
|
||||
'model_id': self.env['ir.model']._get('mail.test.sms').id,
|
||||
'body': 'Dear ${object.display_name} this is an SMS.',
|
||||
})
|
||||
self.env['ir.model.data'].create({
|
||||
'name': 'this_should_exists',
|
||||
'module': 'test_mail_full',
|
||||
'model': sms_template._name,
|
||||
'res_id': sms_template.id,
|
||||
})
|
||||
|
||||
with self.sudo('employee'):
|
||||
with self.mockSMSGateway():
|
||||
test_record = self.env['mail.test.sms'].browse(self.test_record.id)
|
||||
messages = test_record._message_sms_with_template(template_xmlid='test_mail_full.this_should_exists')
|
||||
|
||||
self.assertSMSNotification([{'partner': self.partner_1, 'number': self.test_numbers_san[1]}], 'Dear %s this is an SMS.' % self.test_record.display_name, messages)
|
||||
|
||||
|
||||
class TestSMSPostException(test_mail_full_common.BaseFunctionalTest, sms_common.MockSMS, test_mail_common.MockEmails, test_mail_common.TestRecipients):
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.sms.tests import common as sms_common
|
||||
from odoo.addons.test_mail.tests import common as test_mail_common
|
||||
from odoo.addons.test_mail_full.tests import common as test_mail_full_common
|
||||
|
||||
|
||||
class TestSmsTemplate(test_mail_full_common.BaseFunctionalTest, sms_common.MockSMS, test_mail_common.MockEmails, test_mail_common.TestRecipients):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestSmsTemplate, cls).setUpClass()
|
||||
cls.test_record = cls.env['mail.test.sms'].with_context(**cls._test_context).create({
|
||||
'name': 'Test',
|
||||
'customer_id': cls.partner_1.id,
|
||||
})
|
||||
cls.test_record = cls._reset_mail_context(cls.test_record)
|
||||
|
||||
cls.body_en = 'Dear ${object.display_name} this is an SMS.'
|
||||
cls.body_fr = u"Hello ${object.display_name} ceci est en français."
|
||||
cls.sms_template = cls.env['sms.template'].create({
|
||||
'name': 'Test Template',
|
||||
'model_id': cls.env['ir.model']._get('mail.test.sms').id,
|
||||
'body': cls.body_en,
|
||||
})
|
||||
|
||||
def test_sms_template_render(self):
|
||||
rendered_body = self.sms_template._render_template(self.sms_template.body, self.sms_template.model, self.test_record.ids)
|
||||
self.assertEqual(rendered_body[self.test_record.id], 'Dear %s this is an SMS.' % self.test_record.display_name)
|
||||
|
||||
def test_sms_template_lang(self):
|
||||
self.env.ref('base.lang_fr').write({'active': True})
|
||||
self.user_admin.write({'lang': 'en_US'})
|
||||
self.env['ir.translation'].create({
|
||||
'type': 'model',
|
||||
'name': 'sms.template,body',
|
||||
'lang': 'fr_FR',
|
||||
'res_id': self.sms_template.id,
|
||||
'src': self.sms_template.body,
|
||||
'value': self.body_fr,
|
||||
})
|
||||
# set template to try to use customer lang
|
||||
self.sms_template.write({
|
||||
'lang': '${object.customer_id.lang}',
|
||||
})
|
||||
# create a second record linked to a customer in another language
|
||||
self.partner_2.write({
|
||||
'lang': 'fr_FR',
|
||||
})
|
||||
test_record_2 = self.env['mail.test.sms'].create({
|
||||
'name': 'Test',
|
||||
'customer_id': self.partner_2.id,
|
||||
})
|
||||
|
||||
self.assertEqual(self.sms_template.body, self.body_en)
|
||||
self.assertEqual(self.sms_template.with_context(lang='fr_FR').body, self.body_fr)
|
||||
|
||||
rid_to_tpl = self.sms_template._get_context_lang_per_id((self.test_record | test_record_2).ids)
|
||||
self.assertEqual(set(rid_to_tpl.keys()), set((self.test_record | test_record_2).ids))
|
||||
for rid, tpl in rid_to_tpl.items():
|
||||
if rid == self.test_record.id:
|
||||
self.assertEqual(tpl._context.get('lang'), 'en_US')
|
||||
elif rid == test_record_2.id:
|
||||
self.assertEqual(tpl._context.get('lang'), 'fr_FR')
|
||||
else:
|
||||
self.assertTrue(False)
|
||||
|
||||
lang_to_rids = self.sms_template._get_ids_per_lang((self.test_record | test_record_2).ids)
|
||||
for lang, rids in lang_to_rids.items():
|
||||
if lang == 'en_US':
|
||||
self.assertEqual(rids, self.test_record.ids)
|
||||
elif lang == 'fr_FR':
|
||||
self.assertEqual(rids, test_record_2.ids)
|
||||
else:
|
||||
self.assertTrue(False, 'Should not return lang %s' % lang)
|
||||
Reference in New Issue
Block a user