[IMP] phone_validation, sms: improve phone validation and SMS sending API

Purpose of this commit is to support more formatting options in phone tools,
to provide new API methods for phone numbers validatio. It also introduces
a method on sms.api to send SMS in batch. This one uses a new route given
in IAP services that send a batch of SMS instead of sending them one by one.

Concerning phone validation, tools are updated to correctly handle all
format supported by phonenumbers library. Indeed in addition to national
and international formatting phonenumbers library also supports E164
(international without spaces) and RFC3966 (beautification of phone numbers).
Let us support them and ease the use of our small tool methods in various
addons, notably sms.

Cleaning / check tools methods are added in phone validation module. Various
methods allow to sanitize phone numbers according to a context record and/or
some parameters. Those tools will be used in upcoming sms refactoring.

Another purpose of this commit is to add phone_validation in dependencies
of SMS application. Phone_validation module adds tools to parse and format
phone numbers and allow to replace the manual check done currently. Adding
it in dependencies of SMS does not change anything functionally as

  * library is still optional. If phonenumbers library is not installed the
    features are skipped without crashing;
  * no model is modified as it introduces only tools and a mixin. Models are
    updated in crm_phone_validation that updates lead and partner models;

Related to task 1922163
Linked to PR #34516

Co-Authored-By: Thibault Delavallee <tde@odoo.com>
Co-Authored-By: Pierre Rousseau <pro@odoo.com>
This commit is contained in:
Thibault Delavallée
2019-07-02 10:40:46 +00:00
co-authored by Pierre Rousseau
parent 566244b47b
commit db17763654
4 changed files with 115 additions and 20 deletions
@@ -28,6 +28,6 @@ class PhoneValidationMixin(models.AbstractModel):
number,
country.code if country else None,
country.phone_code if country else None,
always_international=always_international,
force_format='INTERNATIONAL' if always_international else 'NATIONAL',
raise_exception=False
)
@@ -26,14 +26,21 @@ try:
return phone_nbr
def phone_format(number, country_code, country_phone_code, always_international=True, raise_exception=True):
def phone_format(number, country_code, country_phone_code, force_format='INTERNATIONAL', raise_exception=True):
""" Format the given phone number according to the localisation and international options.
:param number: number to convert
:param country_code: the ISO country code in two chars
:type country_code: str
:param country_phone_code: country dial in codes, defined by the ITU-T (Ex: 32 for Belgium)
:type country_phone_code: int
:rtype: str
:param number: number to convert
:param country_code: the ISO country code in two chars
:type country_code: str
:param country_phone_code: country dial in codes, defined by the ITU-T (Ex: 32 for Belgium)
:type country_phone_code: int
:param force_format: stringified version of format globals (see
https://github.com/daviddrysdale/python-phonenumbers/blob/dev/python/phonenumbers/phonenumberutil.py)
'E164' = 0
'INTERNATIONAL' = 1
'NATIONAL' = 2
'RFC3966' = 3
:type force_format: str
:rtype: str
"""
try:
phone_nbr = phone_parse(number, country_code)
@@ -43,7 +50,11 @@ try:
else:
_logger.warning(_('Unable to format %s:\n%s'), number, e)
return number
if always_international or phone_nbr.country_code != country_phone_code:
if force_format == 'E164':
phone_fmt = phonenumbers.PhoneNumberFormat.E164
elif force_format == 'RFC3966':
phone_fmt = phonenumbers.PhoneNumberFormat.RFC3966
elif force_format == 'INTERNATIONAL' or phone_nbr.country_code != country_phone_code:
phone_fmt = phonenumbers.PhoneNumberFormat.INTERNATIONAL
else:
phone_fmt = phonenumbers.PhoneNumberFormat.NATIONAL
@@ -54,7 +65,7 @@ except ImportError:
def phone_parse(number, country_code):
return False
def phone_format(number, country_code, country_phone_code, always_international=True, raise_exception=True):
def phone_format(number, country_code, country_phone_code, force_format='INTERNATIONAL', raise_exception=True):
global _phonenumbers_lib_warning
if not _phonenumbers_lib_warning:
_logger.warning(
@@ -63,3 +74,56 @@ except ImportError:
)
_phonenumbers_lib_warning = True
return number
def phone_sanitize_numbers(numbers, country_code, country_phone_code, force_format='E164'):
valid, invalid, void_count = [], [], 0
for number in numbers:
if not number:
void_count += 1
continue
try:
sanitized = phone_format(
number, country_code, country_phone_code,
force_format=force_format, raise_exception=True)
except Exception as e:
invalid.append(number)
else:
valid.append(sanitized)
return valid, invalid, void_count
def phone_sanitize_numbers_w_record(numbers, country_code, country_phone_code, record, record_country_fname='country_id', force_format='E164'):
if not country_code or not country_phone_code:
country = False
if record and record_country_fname in record and record[record_country_fname]:
country = record[record_country_fname]
elif record:
country = record.env.company.country_id
if country:
country_code = country_code if country_code else country.code
country_phone_code = country_phone_code if country_phone_code else country.phone_code
return phone_sanitize_numbers(numbers, country_code, country_phone_code, force_format=force_format)
def phone_sanitize_numbers_string_w_record(numbers_str, country_code, country_phone_code, record, record_country_fname='country_id', force_format='E164'):
found_numbers = [number.strip() for number in numbers_str.split(',')]
return phone_sanitize_numbers_w_record(found_numbers, country_code, country_phone_code, record, record_country_fname, force_format=force_format)
def phone_get_sanitized_records_number(records, number_fname='mobile', country_fname='country_id', force_format='E164'):
res = dict.fromkeys(records.ids, False)
for record in records:
number = record[number_fname]
valid, invalid, void_count = phone_sanitize_numbers_w_record([number], None, None, records, country_fname,force_format=force_format)
if valid:
res[record.id] = valid[0]
elif void_count:
res[record.id] = False
else:
res[record.id] = False
return res
def phone_get_sanitized_record_number(record, number_fname='mobile', country_fname='country_id', force_format='E164'):
return phone_get_sanitized_records_number(record, number_fname, country_fname, force_format=force_format)[record.id]
+1 -1
View File
@@ -10,7 +10,7 @@ This module gives a framework for SMS text messaging
The service is provided by the In App Purchase Odoo platform.
""",
'depends': ['base', 'iap', 'mail'],
'depends': ['base', 'iap', 'mail', 'phone_validation'],
'data': [
'wizard/sms_composer_views.xml',
'views/res_config_settings_views.xml',
+40 -9
View File
@@ -1,8 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
from odoo.exceptions import UserError
from odoo import api, models
from odoo.addons.iap.models import iap
DEFAULT_ENDPOINT = 'https://iap-sms.odoo.com'
@@ -13,15 +12,47 @@ class SmsApi(models.AbstractModel):
_description = 'SMS API'
@api.model
def _send_sms(self, numbers, message):
""" Send sms
"""
def _contact_iap(self, local_endpoint, params):
account = self.env['iap.account'].get('sms')
params['account_token'] = account.account_token
endpoint = self.env['ir.config_parameter'].sudo().get_param('sms.endpoint', DEFAULT_ENDPOINT)
# TODO PRO, the default timeout is 15, do we have to increase it ?
return iap.jsonrpc(endpoint + local_endpoint, params=params)
@api.model
def _send_sms(self, numbers, message):
""" Send a single message to several numbers
:param numbers: list of E164 formatted phone numbers
:param message: content to send
:raises ? TDE FIXME
"""
params = {
'account_token': account.account_token,
'numbers': numbers,
'message': message,
}
endpoint = self.env['ir.config_parameter'].sudo().get_param('sms.endpoint', DEFAULT_ENDPOINT)
r = iap.jsonrpc(endpoint + '/iap/message_send', params=params)
return True
return self._contact_iap('/iap/message_send', params)
@api.model
def _send_sms_batch(self, messages):
""" Send SMS using IAP in batch mode
:param messages: list of SMS to send, structured as dict [{
'res_id': integer: ID of sms.sms,
'number': string: E164 formatted phone number,
'content': string: content to send
}]
:return: return of /iap/sms/1/send controller which is a list of dict [{
'res_id': integer: ID of sms.sms,
'state': string: 'insufficient_credit' or 'wrong_format_number' or 'success',
'credit': integer: number of credits spent to send this SMS,
}]
:raises: normally none
"""
params = {
'messages': messages
}
return self._contact_iap('/iap/sms/1/send', params)