diff options
| -rw-r--r-- | addons/sms/__manifest__.py | 1 | ||||
| -rw-r--r-- | addons/sms/models/__init__.py | 1 | ||||
| -rw-r--r-- | addons/sms/models/mail_thread.py | 40 | ||||
| -rw-r--r-- | addons/sms/models/sms_template.py | 121 | ||||
| -rw-r--r-- | addons/sms/security/ir.model.access.csv | 3 | ||||
| -rw-r--r-- | addons/sms/tests/common.py | 8 | ||||
| -rw-r--r-- | addons/sms/views/res_partner_views.xml | 44 | ||||
| -rw-r--r-- | addons/sms/views/sms_template_views.xml | 71 | ||||
| -rw-r--r-- | addons/sms/wizard/sms_composer.py | 279 | ||||
| -rw-r--r-- | addons/sms/wizard/sms_composer_views.xml | 77 | ||||
| -rw-r--r-- | addons/test_mail_full/tests/__init__.py | 1 | ||||
| -rw-r--r-- | addons/test_mail_full/tests/test_sms_composer.py | 329 | ||||
| -rw-r--r-- | addons/test_mail_full/tests/test_sms_performance.py | 61 | ||||
| -rw-r--r-- | addons/test_mail_full/tests/test_sms_post.py | 42 | ||||
| -rw-r--r-- | addons/test_mail_full/tests/test_sms_template.py | 76 |
15 files changed, 1040 insertions, 114 deletions
diff --git a/addons/sms/__manifest__.py b/addons/sms/__manifest__.py index dad1ac221da..0de865810e7 100644 --- a/addons/sms/__manifest__.py +++ b/addons/sms/__manifest__.py @@ -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': [ diff --git a/addons/sms/models/__init__.py b/addons/sms/models/__init__.py index b855b4b7955..4adcd67ab64 100644 --- a/addons/sms/models/__init__.py +++ b/addons/sms/models/__init__.py @@ -8,3 +8,4 @@ from . import mail_thread from . import res_partner from . import sms_api from . import sms_sms +from . import sms_template diff --git a/addons/sms/models/mail_thread.py b/addons/sms/models/mail_thread.py index fd47be27a5d..b088309cbfe 100644 --- a/addons/sms/models/mail_thread.py +++ b/addons/sms/models/mail_thread.py @@ -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 diff --git a/addons/sms/models/sms_template.py b/addons/sms/models/sms_template.py new file mode 100644 index 00000000000..6835bac7eb6 --- /dev/null +++ b/addons/sms/models/sms_template.py @@ -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) diff --git a/addons/sms/security/ir.model.access.csv b/addons/sms/security/ir.model.access.csv index 42c4d624aaa..c906bc097b7 100644 --- a/addons/sms/security/ir.model.access.csv +++ b/addons/sms/security/ir.model.access.csv @@ -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 diff --git a/addons/sms/tests/common.py b/addons/sms/tests/common.py index 7c0c3b5d22a..f0f96c0e8d6 100644 --- a/addons/sms/tests/common.py +++ b/addons/sms/tests/common.py @@ -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) diff --git a/addons/sms/views/res_partner_views.xml b/addons/sms/views/res_partner_views.xml index 1a483568178..e841fad4005 100644 --- a/addons/sms/views/res_partner_views.xml +++ b/addons/sms/views/res_partner_views.xml @@ -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> diff --git a/addons/sms/views/sms_template_views.xml b/addons/sms/views/sms_template_views.xml new file mode 100644 index 00000000000..07d752f31b6 --- /dev/null +++ b/addons/sms/views/sms_template_views.xml @@ -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> diff --git a/addons/sms/wizard/sms_composer.py b/addons/sms/wizard/sms_composer.py index 93b3202a83a..c4ab81f0ede 100644 --- a/addons/sms/wizard/sms_composer.py +++ b/addons/sms/wizard/sms_composer.py @@ -1,58 +1,261 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from ast import literal_eval + from odoo import api, fields, models, _ from odoo.addons.phone_validation.tools import phone_validation from odoo.exceptions import UserError +from odoo.tools.safe_eval import safe_eval class SendSMS(models.TransientModel): _name = 'sms.composer' - _description = 'Send SMS' - - recipients = fields.Char('Recipients', required=True) - message = fields.Text('Message', required=True) - - def _get_records(self, model): - if self.env.context.get('active_domain'): - records = model.search(self.env.context.get('active_domain')) - elif self.env.context.get('active_ids'): - records = model.browse(self.env.context.get('active_ids', [])) - else: - records = model.browse(self.env.context.get('active_id', [])) - return records + _description = 'Send SMS Wizard' + _RECIPIENTS_DISPLAY_NBR = 3 @api.model def default_get(self, fields): result = super(SendSMS, self).default_get(fields) - active_model = self.env.context.get('active_model') - - if not self.env.context.get('default_recipients') and active_model and hasattr(self.env[active_model], '_sms_get_default_partners'): - model = self.env[active_model] - records = self._get_records(model) - partners = records._sms_get_default_partners() - phone_numbers = [] - no_phone_partners = [] - for partner in partners: - number = phone_validation.phone_get_sanitized_record_number(partner, self.env.context.get('field_name') or 'mobile', 'country_id') - if number: - phone_numbers.append(number) - else: - no_phone_partners.append(partner.name) - if len(partners) > 1: - if no_phone_partners: - raise UserError(_('Missing mobile number for %s.') % ', '.join(no_phone_partners)) - result['recipients'] = ', '.join(phone_numbers) + if fields == 'partner_ids': + # shortcut because default_get in cache, avoid issues + return result + + result['res_model'] = result.get('res_model') or self.env.context.get('active_model') + result['composition_mode'] = result.get('composition_mode') or 'comment' + + if result['composition_mode'] == 'comment' and not result.get('res_id'): + result['res_id'] = self.env.context.get('active_id') + if result['composition_mode'] != 'comment': + if result.get('use_active_domain') and not result.get('active_domain'): + result['active_domain'] = self.env.context['active_domain'] + elif not result.get('res_ids'): + result['res_ids'] = repr(self.env.context.get('active_ids')) + + if result['res_model']: + result.update( + self._get_composer_values( + result['composition_mode'], result['res_model'], result.get('res_id'), + result.get('body'), result.get('template_id') + ) + ) return result + # documents + composition_mode = fields.Selection([ + ('numbers', 'Send to numbers'), + ('comment', 'Post on a document'), + ('mass', 'Send SMS in batch')], + string='Composition Mode', default='comment', required=True) + res_model = fields.Char('Document Model Name') + res_id = fields.Integer('Document ID') + res_ids = fields.Char('Document IDs') + use_active_domain = fields.Boolean('Use active domain') + active_domain = fields.Text('Active domain', readonly=True) + # options for comment and mass mode + mass_keep_log = fields.Boolean('Keep a note on document') + # recipients + recipient_description = fields.Text('Recipients (Partners)', compute='_compute_description') + recipient_invalid = fields.Text('Invalid recipients', compute='_compute_description') + number_field_name = fields.Char(string='Field holding number') + partner_ids = fields.Many2many('res.partner') + numbers = fields.Char('Recipients (Numbers)') + sanitized_numbers = fields.Char('Sanitized Number', compute='_compute_sanitized_numbers') + # content + template_id = fields.Many2one('sms.template', string='Use Template', domain="[('model', '=', res_model)]") + body = fields.Text('Message', required=True) + + @api.depends('partner_ids', 'res_model', 'res_id', 'res_ids', 'use_active_domain', 'composition_mode', 'number_field_name', 'sanitized_numbers') + def _compute_description(self): + if self.partner_ids: + description = '' + description = ','.join('%s - %s' % (partner.display_name, partner.mobile or partner.phone) for partner in self.partner_ids[:self._RECIPIENTS_DISPLAY_NBR]) + if len(self.partner_ids) > self._RECIPIENTS_DISPLAY_NBR: + description += _(' (and %s more)') % (len(self.partner_ids) - self._RECIPIENTS_DISPLAY_NBR) + self.recipient_description = description + self.recipient_invalid = False + + elif self.composition_mode in ('comment', 'mass') and self.res_model: + records = None + if self.composition_mode == 'comment' and self.res_id: + records = self.env[self.res_model].browse(self.res_id) + elif self.composition_mode == 'mass' and self.res_ids: + records = self.env[self.res_model].browse(literal_eval(self.res_ids)) + + if records and issubclass(type(records), self.pool['mail.thread']): + res = records._sms_get_recipients_info(force_field=self.number_field_name) + valid_ids = [rid for rid, rvalues in res.items() if rvalues['sanitized']] + invalid_ids = [rid for rid, rvalues in res.items() if not rvalues['sanitized']] + + self.recipient_description = ', '.join('%s (%s)' % ( + res[record.id]['partner'].name or record.display_name, + res[record.id]['sanitized']) + for record in records if record.id in valid_ids[:self._RECIPIENTS_DISPLAY_NBR] + ) or False + if len(valid_ids) > self._RECIPIENTS_DISPLAY_NBR: + self.recipient_description += _(', and %s more') % (len(valid_ids) - self._RECIPIENTS_DISPLAY_NBR) + + self.recipient_invalid = ', '.join('%s (%s)' % ( + res[record.id]['partner'].display_name or record.display_name, + res[record.id]['number']) + for record in records if record.id in invalid_ids[:self._RECIPIENTS_DISPLAY_NBR] + ) or False + if len(invalid_ids) > self._RECIPIENTS_DISPLAY_NBR: + self.recipient_description += _(', and %s more') % (len(invalid_ids) - self._RECIPIENTS_DISPLAY_NBR) + else: + self.recipient_description = '' + self.recipient_invalid = '' if self.sanitized_numbers or (self.composition_mode == 'mass' and self.use_active_domain) else _('No record found') + + else: + self.recipient_description = False + + @api.depends('numbers', 'res_model', 'res_id') + def _compute_sanitized_numbers(self): + if self.numbers: + record = self._get_records() if self.res_model and self.res_id else self.env.user + sanitize_res = phone_validation.phone_sanitize_numbers_string_w_record(self.numbers, record) + sanitized_numbers = [info['sanitized'] for info in sanitize_res.values() if info['sanitized']] + invalid_numbers = [number for number, info in sanitize_res.items() if info['code']] + if invalid_numbers: + raise UserError(_('Following numbers are not correctly encoded: %s') % repr(invalid_numbers)) + self.sanitized_numbers = ','.join(sanitized_numbers) + else: + self.sanitized_numbers = False + + @api.onchange('composition_mode', 'res_model', 'res_id', 'template_id') + def _onchange_template_id(self): + if self.template_id and self.composition_mode == 'comment': + self.body = self.template_id._render_template(self.template_id.body, self.res_model, [self.res_id])[self.res_id] + elif self.template_id: + self.body = self.template_id.body + + # ------------------------------------------------------------ + # Actions + # ------------------------------------------------------------ + def action_send_sms(self): - numbers = [number.strip() for number in self.recipients.split(',') if number.strip()] + if self.recipient_invalid: + raise UserError(_('Invalid recipients: %s') % self.recipient_invalid) + self._action_send_sms() + if self.composition_mode == 'comment': + return { + 'type': 'ir.actions.client', + 'tag': 'reload', + } + return False + + def _action_send_sms(self, force_send=False): + records = self._get_records() + if self.composition_mode == 'numbers': + return self._action_send_sms_numbers() + elif self.composition_mode == 'comment': + if records is not None and issubclass(type(records), self.pool['mail.thread']): + return self._action_send_sms_comment(records) + return self._action_send_sms_numbers() + elif self.mass_keep_log: + return self._action_send_sms_mass_w_log(records) + else: + return self._action_send_sms_mass(records) + + def _action_send_sms_numbers(self): + self.env['sms.api']._send_sms_batch([{ + 'res_id': 0, + 'number': number, + 'content': self.body, + } for number in self.sanitized_numbers.split(',')]) + return True + + def _action_send_sms_comment(self, records=None): + records = records if records is not None else self._get_records() + subtype_id = self.env['ir.model.data'].xmlid_to_res_id('mail.mt_comment') + + messages = self.env['mail.message'] + for record in records: + messages |= records._message_sms( + self.body, subtype_id=subtype_id, + partner_ids=self.partner_ids.ids or False, + number_field=self.number_field_name, + sms_numbers=self.sanitized_numbers.split(',') if self.sanitized_numbers else None) + return messages + + def _action_send_sms_mass(self, records=None): + records = records if records is not None else self._get_records() - active_model = self.env.context.get('active_model') - if active_model and hasattr(self.env[active_model], '_message_sms'): - model = self.env[active_model] - records = self._get_records(model) - records[0]._message_sms(self.message, sms_numbers=numbers) + record_values = self._prepare_mass_sms_values(records) + sms_create_vals = [record_values[record.id] for record in records] + sms = self.env['sms.sms'].sudo().create(sms_create_vals) + return sms + + def _action_send_sms_mass_w_log(self): + records = self._get_records() + if records and hasattr(records, '_message_sms'): + subtype_id = self.env['ir.model.data'].xmlid_to_res_id('mail.mt_note') + for record in records: + record._message_sms(self.body, subtype_id=subtype_id, partner_ids=False, sms_numbers=None) else: - self.env['sms.api']._send_sms(numbers, self.message) + self.env['sms.api']._send_sms(self.numbers, self.body) return True + + # ------------------------------------------------------------ + # Mass mode specific + # ------------------------------------------------------------ + + def _prepare_recipient_values(self, records=None): + records = records if records is not None else self._get_records() + recipients_info = records._sms_get_recipients_info(force_field=self.number_field_name) + return recipients_info + + def _prepare_body_values(self, records=None): + records = records if records is not None else self._get_records() + if self.template_id and self.body == self.template_id.body: + lang_to_rids = self.template_id._get_ids_per_lang(records.ids) + all_bodies = {} + for lang, rids in lang_to_rids.items(): + template = self.template_id.with_context(lang=lang) + all_bodies.update(template._render_template(template.body, records._name, rids)) + else: + all_bodies = self.env['mail.template']._render_template(self.body, records._name, records.ids) + return all_bodies + + def _prepare_mass_sms_values(self, records=None): + records = records if records is not None else self._get_records() + all_bodies = self._prepare_body_values(records=records) + all_recipients = self._prepare_recipient_values(records=records) + + result = {} + for record in records: + recipients = all_recipients[record.id] + result[record.id] = { + 'body': all_bodies[record.id], + 'partner_id': recipients['partner'].id, + 'number': recipients['sanitized'] or recipients['number'], + 'state': 'outgoing' if recipients['sanitized'] else 'error', + } + return result + + # ------------------------------------------------------------ + # Tools + # ------------------------------------------------------------ + + def _get_composer_values(self, composition_mode, res_model, res_id, body, template_id): + result = {} + if composition_mode == 'comment': + if not body and template_id: + template = self.env['sms.template'].browse(template_id) + result['body'] = template._render_template(template.body, res_model, [res_id])[res_id] + else: + if not body and template_id: + template = self.env['sms.template'].browse(template_id) + result['body'] = template.body + return result + + def _get_records(self): + if not self.res_model: + return None + if self.use_active_domain: # TDE FIXME: clear active_domain (False,[], ..) + records = self.env[self.res_model].search(safe_eval(self.active_domain)) + elif self.res_id: + records = self.env[self.res_model].browse(self.res_id) + else: + records = self.env[self.res_model].browse(literal_eval(self.res_ids)) + return records diff --git a/addons/sms/wizard/sms_composer_views.xml b/addons/sms/wizard/sms_composer_views.xml index 4b6651a00cb..0ed78dbccda 100644 --- a/addons/sms/wizard/sms_composer_views.xml +++ b/addons/sms/wizard/sms_composer_views.xml @@ -7,35 +7,76 @@ <form string="Send an SMS"> <sheet> <group> - <field name="recipients"/> - <field name="message" widget="sms_widget"/> + <field name="composition_mode" invisible="1"/> + <field name="res_id" invisible="1"/> + <field name="res_ids" invisible="1"/> + <field name="active_domain" invisible="1"/> + <field name="res_model" invisible="1"/> + <field name="number_field_name" invisible="1"/> + <field name="partner_ids" invisible="1"/> + <field name="numbers" invisible="1"/> + <field name="sanitized_numbers" invisible="1"/> + + <!-- Recipient warning --> + <div colspan="2" class="alert alert-danger" role="alert" + attrs="{'invisible': [('recipient_invalid', '=', False)]}"> + <strong>Some recipients are invalid</strong><br /> + <field name="recipient_invalid"/> + </div> + <!-- Mass mode information (res_ids versus active domain) --> + <div colspan="2" class="oe_form_box_info bg-info oe_text_center mb-3" + attrs="{'invisible': [('composition_mode', '=', 'comment')]}"> + <p attrs="{'invisible': [('use_active_domain', '=', False)]}"> + <strong>All records matching your current search filter will receive a SMS, + not only the ids selected in the list view.</strong><br /> + The SMS will be sent for all the records selected in the list.<br /> + Confirming this wizard will probably take a few minutes blocking your browser. + </p> + <p attrs="{'invisible': [('use_active_domain', '=', True)]}"> + <strong>Only records checked in list view will be used.</strong><br /> + The SMS will be sent for all the records selected in the list. + </p> + <p class="mt8"> + <span attrs="{'invisible': [('use_active_domain', '=', True)]}"> + If you want to send it for all the records matching your search criterion, check this box : + </span> + <span attrs="{'invisible': [('use_active_domain', '=', False)]}"> + If you want to use only selected records please uncheck this selection box : + </span> + <field class="oe_inline" name="use_active_domain"/> + </p> + </div> + + <label for="partner_ids" string="Recipients"/> + <div> + <span attrs="{'invisible': ['|', ('composition_mode', '!=', 'mass'), ('use_active_domain', '=', False)]}"> + <strong>Mass SMS</strong> on the current search filter + </span> + <field name="recipient_description" readonly="1" attrs="{ + 'invisible': [('use_active_domain', '=', True)]}" + /> + </div> + <field name="body" widget="sms_widget"/> </group> - </sheet> - <footer> <group> - <span> - <button string="Send" type="object" class="oe_highlight" name="action_send_sms"/> - <button string="Cancel" class="oe_link" special="cancel" /> - </span> + <group> + <field name="template_id" options="{'no_create': True}" attrs="{'invisible': [('composition_mode', '!=', 'mass')]}"/> + </group> </group> + </sheet> + <footer> + <button string="Send SMS" type="object" class="oe_highlight" name="action_send_sms"/> + <button string="Close" class="btn btn-secondary" special="cancel"/> </footer> </form> </field> </record> + <record id="sms_composer_action_form" model="ir.actions.act_window"> - <field name="name">Send SMS</field> + <field name="name">Send SMS Text Message</field> <field name="res_model">sms.composer</field> <field name="view_mode">form</field> <field name="target">new</field> </record> - <!-- Add action entry in the Action Menu for Partners --> - <act_window id="sms_composer_act_window" - name="Send SMS" - binding_model="res.partner" - res_model="sms.composer" - binding_views="list" - view_mode="form" - target="new"/> - </odoo> diff --git a/addons/test_mail_full/tests/__init__.py b/addons/test_mail_full/tests/__init__.py index b5c867844ba..733d20548ce 100644 --- a/addons/test_mail_full/tests/__init__.py +++ b/addons/test_mail_full/tests/__init__.py @@ -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 diff --git a/addons/test_mail_full/tests/test_sms_composer.py b/addons/test_mail_full/tests/test_sms_composer.py index dd2aaf64ae7..8ea2cc2c197 100644 --- a/addons/test_mail_full/tests/test_sms_composer.py +++ b/addons/test_mail_full/tests/test_sms_composer.py @@ -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) - 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' + 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_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, + 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), + }) + + 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.', }) - with self.mockSMSGateway(): - composer.action_send_sms() - self.assertSMSSent(self.partner_numbers, self._test_body) + 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_partners_sanitize(self): - partner_incorrect = self.env['res.partner'].create({ - 'name': 'Jean-Claude Incorrect', - 'email': 'jean.claude@example.com', - 'mobile': 'coincoin', + 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, }) - 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))
\ No newline at end of file + + 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) diff --git a/addons/test_mail_full/tests/test_sms_performance.py b/addons/test_mail_full/tests/test_sms_performance.py index 65c9e2cb4b5..a33464069f4 100644 --- a/addons/test_mail_full/tests/test_sms_performance.py +++ b/addons/test_mail_full/tests/test_sms_performance.py @@ -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) diff --git a/addons/test_mail_full/tests/test_sms_post.py b/addons/test_mail_full/tests/test_sms_post.py index f1e7d91b1d0..686ed941415 100644 --- a/addons/test_mail_full/tests/test_sms_post.py +++ b/addons/test_mail_full/tests/test_sms_post.py @@ -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): diff --git a/addons/test_mail_full/tests/test_sms_template.py b/addons/test_mail_full/tests/test_sms_template.py new file mode 100644 index 00000000000..dab46cd04c7 --- /dev/null +++ b/addons/test_mail_full/tests/test_sms_template.py @@ -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) |
