Skip to content
Permalink
Browse files

[IMP] sms: template and failure handling

The aim of this commit is to:

1) Add templates for SMS:
- Users can now create template for SMS Text messages similar to mail templates.
- Templates can be linked with models, especially to create contextual action to quickly send SMS.

2) Improve the SMS composer:
- Integrate the template system.
- Allow to use it for mass SMS.

3) Handle SMS failures:
- SMS is log in the chatter with a fa-comment icon next to the date.
- The icon can be grey if all the SMS have correctly been sent.
- The icon can be red if at least one SMS have not correctly been sent.
- As mail, SMS failures are created if a SMS fails. The failure appears in the inbox.
- When clicking on the red icon, a new modal is displayed, which is used to re-send SMS, discard SMS failures or buy more SMS credits.

Task ID: 1922163
  • Loading branch information...
pro-odoo committed Apr 11, 2019
1 parent 04d5c36 commit ad2b8b9695b8c954b6f0a5acff36c106ceafe264
Showing with 1,283 additions and 218 deletions.
  1. +10 −2 addons/calendar_sms/models/calendar.py
  2. +1 −1 addons/calendar_sms/views/calendar_views.xml
  3. +6 −3 addons/hr_presence/models/hr_employee.py
  4. +6 −1 addons/sms/__manifest__.py
  5. +3 −0 addons/sms/models/__init__.py
  6. +38 −0 addons/sms/models/mail_message.py
  7. +24 −45 addons/sms/models/mail_thread.py
  8. +4 −2 addons/sms/models/sms_api.py
  9. +132 −0 addons/sms/models/sms_sms.py
  10. +112 −0 addons/sms/models/sms_template.py
  11. +3 −0 addons/sms/security/ir.model.access.csv
  12. BIN addons/sms/static/img/sms_failure.png
  13. +29 −0 addons/sms/static/src/js/mail_failure.js
  14. +73 −0 addons/sms/static/src/js/message.js
  15. +64 −0 addons/sms/static/src/js/sms_notification_manager.js
  16. +9 −10 addons/sms/static/src/js/sms_widget.js
  17. +78 −0 addons/sms/static/src/js/systray_messaging_menu.js
  18. +116 −0 addons/sms/static/src/js/thread_widget.js
  19. +7 −0 addons/sms/static/src/scss/thread.scss
  20. +29 −0 addons/sms/static/src/xml/thread.xml
  21. 0 addons/sms/static/{src → }/tests/sms_widget_test.js
  22. +23 −12 addons/sms/views/res_partner_views.xml
  23. +85 −0 addons/sms/views/sms_template_views.xml
  24. +6 −0 addons/sms/views/templates.xml
  25. +3 −1 addons/sms/wizard/__init__.py
  26. +0 −96 addons/sms/wizard/send_sms.py
  27. +0 −44 addons/sms/wizard/send_sms_views.xml
  28. +26 −0 addons/sms/wizard/sms_cancel.py
  29. +28 −0 addons/sms/wizard/sms_cancel_views.xml
  30. +143 −0 addons/sms/wizard/sms_compose_message.py
  31. +86 −0 addons/sms/wizard/sms_compose_message_views.xml
  32. +92 −0 addons/sms/wizard/sms_resend.py
  33. +46 −0 addons/sms/wizard/sms_resend_views.xml
  34. +1 −1 addons/test_mail/tests/test_performance.py
@@ -21,8 +21,16 @@ def _do_sms_reminder(self):
""" Send an SMS text reminder to attendees that haven't declined the event """
for event in self:
sms_msg = _("Event reminder: %s on %s.") % (event.name, event.start_datetime or event.start_date)
note_msg = _('SMS text message reminder sent !')
event.message_post_send_sms(sms_msg, note_msg=note_msg)
note_msg = _("SMS text message reminder sent !")
values = [{
'name': partner.display_name,
'number': partner.mobile if partner.mobile else partner.phone,
'content': sms_msg,
'country_id': partner.country_id.id if partner.country_id else False
} for partner in self._get_default_sms_recipients()]
sms_ids = self.env['sms.sms'].create(values)
sms_ids._send()
event.message_post_send_sms(note_msg, sms_ids)


class CalendarAlarm(models.Model):
@@ -5,7 +5,7 @@
<act_window id="sms_message_send_action_mutli"
name="Send SMS to attendees"
src_model="calendar.event"
res_model="sms.send_sms"
res_model="sms.compose.message"
view_type="form"
view_mode="form"
key2="client_action_multi"
@@ -133,12 +133,15 @@ def action_send_sms(self):
Do not hesitate to contact your manager or the human resource department.""")
return {
"type": "ir.actions.act_window",
"res_model": "sms.send_sms",
"res_model": "sms.compose.message",
"view_mode": 'form',
"context": {
'active_id': self.id,
'default_message': body,
'default_recipients': self.mobile_phone,
'default_content': body,
'default_recipient_ids': [(0, False, {
'partner_id': self.user_partner_id.id if self.user_partner_id else False,
'number': self.mobile_phone
})]
},
"name": "Send SMS",
"target": "new",
@@ -12,13 +12,18 @@
""",
'depends': ['base', 'iap', 'mail'],
'data': [
'wizard/send_sms_views.xml',
'security/ir.model.access.csv',
'wizard/sms_cancel_views.xml',
'wizard/sms_compose_message_views.xml',
'wizard/sms_resend_views.xml',
'views/res_config_settings_views.xml',
'views/res_partner_views.xml',
'views/templates.xml',
'views/sms_template_views.xml'
],
'qweb': [
'static/src/xml/sms_widget.xml',
'static/src/xml/thread.xml',
],
'installable': True,
'auto_install': True,
@@ -1,6 +1,9 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from . import mail_message
from . import mail_thread
from . import res_partner
from . import sms_api
from . import sms_sms
from . import sms_template
@@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from odoo import api, fields, models

class MailMessage(models.Model):
_inherit = 'mail.message'

message_type = fields.Selection(selection_add=[('sms', 'SMS')])
sms_ids = fields.One2many('sms.sms', 'message_id')

@api.multi
def _get_message_format_fields(self):
""" Override in order to fetch the field sms_ids """
res = super(MailMessage, self)._get_message_format_fields()
res.append('sms_ids')
return res

@api.multi
def message_fetch_failed(self):
""" Override in order to fetch the SMS failed """
res = super(MailMessage, self).message_fetch_failed()
return res + self.sms_ids._fetch_failed_sms()

@api.multi
def message_format(self):
""" Override in order to retrieves data about SMS (recipient name and
SMS status)
"""
message_values = super(MailMessage, self).message_format()
for message in message_values:
records = self.env['sms.sms'].browse(message['sms_ids'])
message['sms_ids'] = [[
record.id,
record.name,
record.state
] for record in records]
return message_values
@@ -1,59 +1,38 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

import logging

from odoo import models, _

from odoo.addons.iap.models.iap import InsufficientCreditError

_logger = logging.getLogger(__name__)
from odoo import api, models


class MailThread(models.AbstractModel):
_inherit = 'mail.thread'

def _get_default_sms_recipients(self):
def _get_default_sms_recipients(self, res_model=None, res_ids=False):
""" This method will likely need to be overriden by inherited models.
:returns partners: recordset of res.partner
:returns partners: recordset of res.partner
"""
if res_model and res_ids:
if hasattr(self.env[res_model], '_get_default_sms_recipients'):
return self.env[res_model].browse(res_ids)._get_default_sms_recipients()
records = self.env[res_model].sudo().browse(res_ids)
else:
records = self.sudo()
partners = self.env['res.partner']
if hasattr(self, 'partner_id'):
partners |= self.mapped('partner_id')
if hasattr(self, 'partner_ids'):
partners |= self.mapped('partner_ids')
if 'partner_id' in records._fields:
partners |= records.mapped('partner_id')
if 'partner_ids' in records._fields:
partners |= records.mapped('partner_ids')
return partners

def message_post_send_sms(self, sms_message, numbers=None, partners=None, note_msg=None, log_error=False):
""" Send an SMS text message and post an internal note in the chatter if successfull
:param sms_message: plaintext message to send by sms
:param partners: the numbers to send to, if none are given it will take those
from partners or _get_default_sms_recipients
:param partners: the recipients partners, if none are given it will take those
from _get_default_sms_recipients, this argument
is ignored if numbers is defined
:param note_msg: message to log in the chatter, if none is given a default one
containing the sms_message is logged
@api.multi
def message_post_send_sms(self, body, sms_ids):
""" Post SMS text message as internal note in the chatter, and link sms_ids
to the mail.message
:param body: Note to log in the chatter.
:param sms_ids: IDs of the sms.sms records
:return: ID of the mail.message created
"""
if not numbers:
if not partners:
partners = self._get_default_sms_recipients()

# Collect numbers, we will consider the message to be sent if at least one number can be found
numbers = list(set([i.mobile for i in partners if i.mobile]))

if numbers:
try:
self.env['sms.api']._send_sms(numbers, sms_message)
mail_message = note_msg or _('SMS message sent: %s') % sms_message

except InsufficientCreditError as e:
if not log_error:
raise e
mail_message = _('Insufficient credit, unable to send SMS message: %s') % sms_message
else:
mail_message = _('No mobile number defined, unable to send SMS message: %s') % sms_message

for thread in self:
thread.message_post(body=mail_message)
return False
self.ensure_one()
message_id = self.message_post(body=body, message_type='sms')
message_id.sms_ids = sms_ids
return message_id
@@ -14,7 +14,9 @@ class SmsApi(models.AbstractModel):

@api.model
def _send_sms(self, numbers, message):
""" Send sms
""" Send sms using IAP
:param numbers: List of numbers to which the message must be sent
:param message: Message to send
"""
account = self.env['iap.account'].get('sms')
params = {
@@ -23,5 +25,5 @@ def _send_sms(self, numbers, message):
'message': message,
}
endpoint = self.env['ir.config_parameter'].sudo().get_param('sms.endpoint', DEFAULT_ENDPOINT)
r = iap.jsonrpc(endpoint + '/iap/message_send', params=params)
iap.jsonrpc(endpoint + '/iap/message_send', params=params)
return True
@@ -0,0 +1,132 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

import logging

from odoo import api, fields, models
from odoo.addons.iap.models.iap import InsufficientCreditError

_logger = logging.getLogger(__name__)

try:
import phonenumbers as pn

except ImportError:
pn = None
_logger.info(
"The `phonenumbers` Python module is not available."
"Phone number validation will be skipped."
"Try `pip3 install phonenumbers` to install it."
)

class SmsSms(models.Model):
_name = 'sms.sms'
_description = 'SMS'

name = fields.Char()
number = fields.Char()
content = fields.Text()

user_id = fields.Many2one('res.users', 'Sender', default=lambda self: self.env.user.id)
country_id = fields.Many2one('res.country')
message_id = fields.Many2one('mail.message')

state = fields.Selection([
('pending', 'In Queue'),
('sent', 'Sent'),
('error', 'Error'),
('canceled', 'Canceled')
], 'SMS Status', readonly=True, copy=False, default='pending', required=True)

error_code = fields.Selection([
('missing_number', 'Missing Number'),
('wrong_number_format', 'Wrong Number Format'),
('insufficient_credit', 'Insufficient Credit')
])

@api.multi
def _get_sanitized_number(self):
self.ensure_one()
if not pn:
return self.number
if self.number:
country_id = self.country_id or self.env.user.country_id or self.env.user.company_id.country_id
country_code = country_id.code if country_id else None
try:
parsed_number = pn.parse(self.number, region=country_code, keep_raw_input=True)
if pn.is_valid_number(parsed_number):
return pn.format_number(parsed_number, pn.PhoneNumberFormat.E164)
except pn.phonenumberutil.NumberParseException:
return False
return False

@api.multi
def _send(self):
""" This method try to send SMS after checking the number (presence and
formatting). """
for record in self:
if not record.number:
record.write({
'state': 'error',
'error_code': 'missing_number'
})
continue
number = record._get_sanitized_number()
if not number:
record.write({
'state': 'error',
'error_code': 'wrong_number_format'
})
continue
try:
self.env['sms.api']._send_sms([number], record.content)
record.write({
'state': 'sent',
'error_code': False
})
except InsufficientCreditError:
record.write({
'state': 'error',
'error_code': 'insufficient_credit'
})

@api.multi
def _cancel(self):
""" Cancel SMS """
self.write({
'state': 'canceled',
'error_code': False
})
self._notify_sms_update()

@api.model
def _fetch_failed_sms(self):
""" Retrieves the list of SMS with a delivery failure """
return self.search([('state', '=', 'error'), ('user_id.id', '=', self.env.user.id)])._format_sms_failures()

@api.multi
def _format_sms_failures(self):
""" A shorter message to notify a SMS delivery failure update
"""
return [{
'message_id': record.message_id.id,
'record_name': record.message_id.record_name,
'model_name': self.env['ir.model']._get(record.message_id.model).display_name,
'uuid': record.message_id.message_id,
'res_id': record.message_id.res_id,
'model': record.message_id.model,
'last_message_date': record.message_id.date,
'module_icon': '/sms/static/img/sms_failure.png',
'sms_id': record.id,
'sms_status': record.state,
'failure_type': 'sms'
} for record in self.filtered(lambda record: record.message_id)]

@api.multi
def _notify_sms_update(self):
""" Notify channels after update of SMS status """
updates = [[
(self._cr.dbname, 'res.partner', record.user_id.partner_id.id),
{'type': 'sms_update', 'elements': record._format_sms_failures()}
] for record in self]
self.env['bus.bus'].sendmany(updates)
Oops, something went wrong.

0 comments on commit ad2b8b9

Please sign in to comment.
You can’t perform that action at this time.