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 @@