diff --git a/addons/calendar_sms/models/calendar.py b/addons/calendar_sms/models/calendar.py
index 65cd1a8cad5..0ad5bb3fb31 100644
--- a/addons/calendar_sms/models/calendar.py
+++ b/addons/calendar_sms/models/calendar.py
@@ -11,7 +11,7 @@ _logger = logging.getLogger(__name__)
class CalendarEvent(models.Model):
_inherit = 'calendar.event'
- def _get_default_sms_recipients(self):
+ def _sms_get_default_partners(self):
""" Method overriden from mail.thread (defined in the sms module).
SMS text messages will be sent to attendees that haven't declined the event(s).
"""
@@ -21,8 +21,7 @@ class CalendarEvent(models.Model):
""" 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)
+ event._message_sms(sms_msg)
class CalendarAlarm(models.Model):
diff --git a/addons/mail/models/mail_notification.py b/addons/mail/models/mail_notification.py
index 5cf42a2dc72..571023450d9 100644
--- a/addons/mail/models/mail_notification.py
+++ b/addons/mail/models/mail_notification.py
@@ -42,7 +42,7 @@ class Notification(models.Model):
_sql_constraints = [
# email notification;: partner is required
('notification_partner_required',
- "CHECK(notification_type in ('email', 'inbox') AND res_partner_id IS NOT NULL)",
+ "CHECK(notification_type NOT IN ('email', 'inbox') OR res_partner_id IS NOT NULL)",
'Customer is required for inbox / email notification'),
]
diff --git a/addons/phone_validation/tools/phone_validation.py b/addons/phone_validation/tools/phone_validation.py
index 29828ffd739..e1df861ac8c 100644
--- a/addons/phone_validation/tools/phone_validation.py
+++ b/addons/phone_validation/tools/phone_validation.py
@@ -77,51 +77,43 @@ except ImportError:
def phone_sanitize_numbers(numbers, country_code, country_phone_code, force_format='E164'):
- valid, invalid, void_count = [], [], 0
+ result = dict.fromkeys(numbers, False)
for number in numbers:
if not number:
- void_count += 1
+ result[number] = {'sanitized': False, 'code': 'empty', 'msg': False}
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)
+ result[number] = {'sanitized': False, 'code': 'invalid', 'msg': e}
else:
- valid.append(sanitized)
- return valid, invalid, void_count
+ result[number] = {'sanitized': sanitized, 'code': False, 'msg': False}
+ return result
-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]:
+def phone_sanitize_numbers_w_record(numbers, record, country=False, record_country_fname='country_id', force_format='E164'):
+ if not country:
+ if record and hasattr(record, record_country_fname) 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
+ country_code = country.code if country else None
+ country_phone_code = country.phone_code if country else None
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'):
+def phone_sanitize_numbers_string_w_record(numbers_str, record, country=False, 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)
+ return phone_sanitize_numbers_w_record(found_numbers, record, country=country, record_country_fname=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
+ res[record.id] = phone_sanitize_numbers_w_record([number], records, record_country_fname=country_fname,force_format=force_format)[number]['sanitized']
return res
diff --git a/addons/sms/__manifest__.py b/addons/sms/__manifest__.py
index 4ef5721be8f..dad1ac221da 100644
--- a/addons/sms/__manifest__.py
+++ b/addons/sms/__manifest__.py
@@ -12,10 +12,13 @@ The service is provided by the In App Purchase Odoo platform.
""",
'depends': ['base', 'iap', 'mail', 'phone_validation'],
'data': [
+ 'data/ir_cron_data.xml',
'wizard/sms_composer_views.xml',
'views/res_config_settings_views.xml',
'views/res_partner_views.xml',
'views/assets.xml',
+ 'views/sms_sms_views.xml',
+ 'security/ir.model.access.csv',
],
'qweb': [
'static/src/xml/sms_widget.xml',
diff --git a/addons/sms/data/ir_cron_data.xml b/addons/sms/data/ir_cron_data.xml
new file mode 100644
index 00000000000..e8a5d771dd5
--- /dev/null
+++ b/addons/sms/data/ir_cron_data.xml
@@ -0,0 +1,14 @@
+
+
+
+ SMS: SMS Queue Manager
+
+ code
+ model._process_queue()
+
+ 1
+ hours
+ -1
+
+
+
\ No newline at end of file
diff --git a/addons/sms/models/__init__.py b/addons/sms/models/__init__.py
index 7a75a7a5f6d..b855b4b7955 100644
--- a/addons/sms/models/__init__.py
+++ b/addons/sms/models/__init__.py
@@ -1,6 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from . import mail_followers
+from . import mail_message
+from . import mail_notification
from . import mail_thread
from . import res_partner
from . import sms_api
+from . import sms_sms
diff --git a/addons/sms/models/mail_followers.py b/addons/sms/models/mail_followers.py
new file mode 100644
index 00000000000..76be79b677b
--- /dev/null
+++ b/addons/sms/models/mail_followers.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models
+
+
+class Followers(models.Model):
+ _inherit = ['mail.followers']
+
+ def _get_recipient_data(self, records, message_type, subtype_id, pids=None, cids=None):
+ if message_type == 'sms':
+ if pids is None:
+ sms_pids = records._sms_get_default_partners().ids
+ else:
+ sms_pids = pids
+ res = super(Followers, self)._get_recipient_data(records, message_type, subtype_id, pids=pids, cids=cids)
+ new_res = []
+ for pid, cid, pactive, pshare, ctype, notif, groups in res:
+ if pid and pid in sms_pids:
+ notif = 'sms'
+ new_res.append((pid, cid, pactive, pshare, ctype, notif, groups))
+ return new_res
+ else:
+ return super(Followers, self)._get_recipient_data(records, message_type, subtype_id, pids=pids, cids=cids)
diff --git a/addons/sms/models/mail_message.py b/addons/sms/models/mail_message.py
new file mode 100644
index 00000000000..fbd0465f746
--- /dev/null
+++ b/addons/sms/models/mail_message.py
@@ -0,0 +1,13 @@
+# -*- 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):
+ """ Override MailMessage class in order to add a new type: SMS messages.
+ Those messages comes with their own notification method, using SMS
+ gateway. """
+ _inherit = 'mail.message'
+
+ message_type = fields.Selection(selection_add=[('sms', 'SMS')])
diff --git a/addons/sms/models/mail_notification.py b/addons/sms/models/mail_notification.py
new file mode 100644
index 00000000000..c45981ea0b0
--- /dev/null
+++ b/addons/sms/models/mail_notification.py
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+
+from odoo import api, fields, models
+from odoo.tools.translate import _
+
+
+class Notification(models.Model):
+ _inherit = 'mail.notification'
+
+ notification_type = fields.Selection(selection_add=[('sms', 'SMS')])
+ sms_id = fields.Many2one('sms.sms', string='SMS', index=True, ondelete='set null')
+ sms_number = fields.Char('SMS Number')
+ failure_type = fields.Selection(selection_add=[
+ ('sms_number_missing', 'Missing Number'),
+ ('sms_number_format', 'Wrong Number Format'),
+ ('sms_credit', 'Insufficient Credit'),
+ ('sms_server', 'Server Error')]
+ )
diff --git a/addons/sms/models/mail_thread.py b/addons/sms/models/mail_thread.py
index a439487e4e9..fd47be27a5d 100644
--- a/addons/sms/models/mail_thread.py
+++ b/addons/sms/models/mail_thread.py
@@ -3,9 +3,9 @@
import logging
-from odoo import models, _
-
-from odoo.addons.iap.models.iap import InsufficientCreditError
+from odoo import api, models
+from odoo.addons.phone_validation.tools import phone_validation
+from odoo.tools import html2plaintext
_logger = logging.getLogger(__name__)
@@ -13,7 +13,7 @@ _logger = logging.getLogger(__name__)
class MailThread(models.AbstractModel):
_inherit = 'mail.thread'
- def _get_default_sms_recipients(self):
+ def _sms_get_default_partners(self):
""" This method will likely need to be overriden by inherited models.
:returns partners: recordset of res.partner
"""
@@ -24,36 +24,210 @@ class MailThread(models.AbstractModel):
partners |= self.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
+ def _sms_get_number_fields(self):
+ """ This method returns the fields to use to find the number to use to
+ send an SMS on a record. """
+ return ['mobile']
+
+ def _sms_get_recipients_info(self, force_field=False):
+ """" Get SMS recipient information on current record set. This method
+ checks for numbers and sanitation in order to centralize computation.
+
+ Example of use cases
+
+ * click on a field -> number is actually forced from field, find customer
+ linked to record, force its number to field or fallback on customer fields;
+ * contact -> find numbers from all possible phone fields on record, find
+ customer, force its number to found field number or fallback on customer fields;
+
+ :return dict: record.id: {
+ 'partner': a res.partner recordset that is the customer (void or singleton);
+ 'sanitized': sanitized number to use (coming from record's field or partner's mobile
+ or phone). Set to False is number impossible to parse and format;
+ 'number': original number before sanitation;
+ } for each record in self
"""
- if not numbers:
- if not partners:
- partners = self._get_default_sms_recipients()
+ result = dict.fromkeys(self.ids, False)
+ number_fields = self._sms_get_number_fields()
+ for record in self:
+ tocheck_fields = [force_field] if force_field else number_fields
+ all_numbers = [record[fname] for fname in tocheck_fields if fname in record]
+ all_partners = record._sms_get_default_partners()
- # 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]))
+ valid_number = False
+ for fname in [f for f in tocheck_fields if f in record]:
+ valid_number = phone_validation.phone_get_sanitized_record_number(record, number_fname=fname)
+ if valid_number:
+ break
- if numbers:
- try:
- self.env['sms.api']._send_sms(numbers, sms_message)
- mail_message = note_msg or _('SMS message sent: %s') % sms_message
+ if valid_number:
+ result[record.id] = {
+ 'partner': all_partners[0] if all_partners else self.env['res.partner'],
+ 'sanitized': valid_number, 'number': valid_number,
+ }
+ elif all_partners:
+ partner_number, partner = False, self.env['res.partner']
+ for partner in all_partners:
+ partner_number = partner.mobile or partner.phone
+ if partner_number:
+ partner_number = phone_validation.phone_sanitize_numbers_string_w_record(partner_number, record)[partner_number]['sanitized']
+ if partner_number:
+ break
- 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
+ if partner_number:
+ result[record.id] = {'partner': partner, 'sanitized': partner_number, 'number': partner_number}
+ else:
+ result[record.id] = {'partner': partner, 'sanitized': False, 'number': partner.mobile or partner.phone}
+ elif all_numbers:
+ result[record.id] = {'partner': self.env['res.partner'], 'sanitized': False, 'number': all_numbers[0]}
+ else:
+ result[record.id] = {'partner': self.env['res.partner'], 'sanitized': False, 'number': False}
+ return result
- for thread in self:
- thread.message_post(body=mail_message)
- return False
+ 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
+ method.
+
+ :param body: content of SMS;
+ :param subtype_id: mail.message.subtype used in mail.message associated
+ to the sms notification process;
+ :param partner_ids: if set is a record set of partners to notify;
+ :param number_field: if set is a name of field to use on current record
+ to compute a number to notify;
+ :param sms_numbers: see ``_notify_record_by_sms``;
+ :param sms_pid_to_number: see ``_notify_record_by_sms``;
+ """
+ self.ensure_one()
+ sms_pid_to_number = sms_pid_to_number if sms_pid_to_number is not None else {}
+
+ if number_field or (partner_ids is False and sms_numbers is None):
+ info = self._sms_get_recipients_info(force_field=number_field)[self.id]
+ info_partner_ids = info['partner'].ids if info['partner'] else False
+ info_number = info['sanitized'] if info['sanitized'] else info['number']
+ if info_partner_ids and info_number:
+ sms_pid_to_number[info_partner_ids[0]] = info_number
+ if info_partner_ids:
+ partner_ids = info_partner_ids + (partner_ids or [])
+ if info_number and not info_partner_ids:
+ sms_numbers = [info_number] + (sms_numbers or [])
+
+ if subtype_id is False:
+ subtype_id = self.env['ir.model.data'].xmlid_to_res_id('mail.mt_comment')
+
+ return self.message_post(
+ body=body, partner_ids=partner_ids or [], # TDE FIXME: temp fix otherwise crash mail_thread.py
+ message_type='sms', subtype_id=subtype_id,
+ sms_numbers=sms_numbers, sms_pid_to_number=sms_pid_to_number,
+ **kwargs
+ )
+
+ @api.multi
+ def _notify_thread(self, message, msg_vals=False, **kwargs):
+ recipients_data = super(MailThread, self)._notify_thread(message, msg_vals=msg_vals, **kwargs)
+ self._notify_record_by_sms(message, recipients_data, msg_vals=msg_vals, **kwargs)
+ return recipients_data
+
+ @api.multi
+ def _notify_record_by_sms(self, message, recipients_data, msg_vals=False,
+ sms_numbers=None, sms_pid_to_number=None,
+ check_existing=False, put_in_queue=False, **kwargs):
+ """ Notification method: by SMS.
+
+ :param message: mail.message record to notify;
+ :param recipients_data: see ``_notify_thread``;
+ :param msg_vals: see ``_notify_thread``;
+
+ :param sms_numbers: additional numbers to notify in addition to partners
+ and classic recipients;
+ :param pid_to_number: force a number to notify for a given partner ID
+ instead of taking its mobile / phone number;
+ :param check_existing: check for existing notifications to update based on
+ mailed recipient, otherwise create new notifications;
+ :param put_in_queue: use cron to send queued SMS instead of sending them
+ directly;
+ """
+ sms_pid_to_number = sms_pid_to_number if sms_pid_to_number is not None else {}
+ sms_numbers = sms_numbers if sms_numbers is not None else []
+ sms_create_vals = []
+ sms_all = self.env['sms.sms'].sudo()
+
+ # pre-compute SMS data
+ body = msg_vals['body'] if msg_vals and msg_vals.get('body') else message.body
+ sms_base_vals = {
+ 'body': html2plaintext(body).rstrip('\n'),
+ 'mail_message_id': message.id,
+ 'state': 'outgoing',
+ }
+
+ # notify from computed recipients_data (followers, specific recipients)
+ partners_data = [r for r in recipients_data['partners'] if r['notif'] == 'sms']
+ partner_ids = [r['id'] for r in partners_data]
+ if partner_ids:
+ for partner in self.env['res.partner'].sudo().browse(partner_ids):
+ number = sms_pid_to_number.get(partner.id) or partner.mobile or partner.phone
+ sanitize_res = phone_validation.phone_sanitize_numbers_string_w_record(number, partner)[number]
+ number = sanitize_res['sanitized'] or number
+ sms_create_vals.append(dict(
+ sms_base_vals,
+ partner_id=partner.id,
+ number=number
+ ))
+
+ # notify from additional numbers
+ if sms_numbers:
+ sanitized = phone_validation.phone_sanitize_numbers_w_record(sms_numbers, self)
+ tocreate_numbers = [
+ value['sanitized'] or original
+ for original, value in sanitized.items()
+ if value['code'] != 'empty'
+ ]
+ sms_create_vals += [dict(sms_base_vals, partner_id=False, number=n) for n in tocreate_numbers]
+
+ # create sms and notification
+ existing_pids, existing_numbers = [], []
+ if sms_create_vals:
+ sms_all |= self.env['sms.sms'].sudo().create(sms_create_vals)
+
+ if check_existing:
+ existing = self.env['mail.notification'].sudo().search([
+ '|', ('res_partner_id', 'in', partner_ids),
+ '&', ('res_partner_id', '=', False), ('sms_number', 'in', sms_numbers),
+ ('notification_type', '=', 'sms'),
+ ('mail_message_id', '=', message.id)
+ ])
+ for n in existing:
+ if n.res_partner_id.id in partner_ids and n.mail_message_id == message:
+ existing_pids.append(n.res_partner_id.id)
+ if not n.res_partner_id and n.sms_number in sms_numbers and n.mail_message_id == message:
+ existing_numbers.append(n.sms_number)
+
+ notif_create_values = [{
+ 'mail_message_id': message.id,
+ 'res_partner_id': sms.partner_id.id,
+ 'sms_number': sms.number,
+ 'notification_type': 'sms',
+ 'sms_id': sms.id,
+ 'is_read': True, # discard Inbox notification
+ 'notification_status': 'ready',
+ } for sms in sms_all if (sms.partner_id and sms.partner_id.id not in existing_pids) or (not sms.partner_id and sms.number not in existing_numbers)]
+ if notif_create_values:
+ self.env['mail.notification'].sudo().create(notif_create_values)
+
+ if existing_pids or existing_numbers:
+ for sms in sms_all:
+ notif = next((n for n in existing if
+ (n.res_partner_id.id in existing_pids and n.res_partner_id.id == sms.partner_id.id) or
+ (not n.res_partner_id and n.sms_number in existing_numbers and n.sms_number == sms.number)), False)
+ if notif:
+ notif.write({
+ 'notification_type': 'sms',
+ 'notification_status': 'ready',
+ 'sms_id': sms.id,
+ 'sms_number': sms.number,
+ })
+
+ if sms_all and not put_in_queue:
+ sms_all.send(auto_commit=False, raise_exception=False)
+
+ return True
diff --git a/addons/sms/models/res_partner.py b/addons/sms/models/res_partner.py
index 6d85ef2fe06..6cc6734cc5c 100644
--- a/addons/sms/models/res_partner.py
+++ b/addons/sms/models/res_partner.py
@@ -7,7 +7,7 @@ from odoo import models
class ResPartner(models.Model):
_inherit = 'res.partner'
- def _get_default_sms_recipients(self):
+ def _sms_get_default_partners(self):
""" Override of mail.thread method.
SMS recipients on partners are the partners themselves.
"""
diff --git a/addons/sms/models/sms_sms.py b/addons/sms/models/sms_sms.py
new file mode 100644
index 00000000000..699f4efa5f5
--- /dev/null
+++ b/addons/sms/models/sms_sms.py
@@ -0,0 +1,137 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import logging
+import threading
+
+from odoo import api, fields, models, tools
+
+_logger = logging.getLogger(__name__)
+
+
+class SmsSms(models.Model):
+ _name = 'sms.sms'
+ _description = 'Outgoing SMS'
+ _rec_name = 'number'
+
+ number = fields.Char('Number', required=True)
+ body = fields.Text()
+ partner_id = fields.Many2one('res.partner', 'Customer')
+ mail_message_id = fields.Many2one('mail.message', index=True)
+ state = fields.Selection([
+ ('outgoing', 'In Queue'),
+ ('sent', 'Sent'),
+ ('error', 'Error'),
+ ('canceled', 'Canceled')
+ ], 'SMS Status', readonly=True, copy=False, default='outgoing', required=True)
+ error_code = fields.Selection([
+ ('sms_number_missing', 'Missing Number'),
+ ('sms_number_format', 'Wrong Number Format'),
+ ('sms_credit', 'Insufficient Credit'),
+ ('sms_server', 'Server Error')
+ ])
+
+ @api.multi
+ def send(self, delete_all=False, auto_commit=False, raise_exception=False):
+ """ Main API method to send SMS.
+
+ :param delete_all: delete all SMS (sent or not); otherwise delete only
+ sent SMS;
+ :param auto_commit: commit after each batch of SMS;
+ :param raise_exception: raise if there is an issue contacting IAP;
+ """
+ for batch_ids in self._split_batch():
+ self.browse(batch_ids)._send(delete_all=delete_all, raise_exception=raise_exception)
+ if auto_commit is True:
+ self._cr.commit()
+
+ @api.model
+ def _process_queue(self, ids=None):
+ """ Send immediately queued messages, committing after each message is sent.
+ This is not transactional and should not be called during another transaction!
+
+ :param list ids: optional list of emails ids to send. If passed no search
+ is performed, and these ids are used instead.
+ """
+ domain = [('state', '=', 'outgoing')]
+
+ filtered_ids = self.search(domain, limit=10000).ids # TDE note: arbitrary limit we might have to update
+ if ids:
+ ids = list(set(filtered_ids) & set(ids))
+ else:
+ ids = filtered_ids
+ ids.sort()
+
+ res = None
+ try:
+ # auto-commit except in testing mode
+ auto_commit = not getattr(threading.currentThread(), 'testing', False)
+ res = self.browse(ids).send(delete_all=False, auto_commit=auto_commit, raise_exception=False)
+ except Exception:
+ _logger.exception("Failed processing SMS queue")
+ return res
+
+ def _split_batch(self):
+ batch_size = int(self.env['ir.config_parameter'].sudo().get_param('sms.session.batch.size', 10))
+ for sms_batch in tools.split_every(batch_size, self.ids):
+ yield sms_batch
+
+ @api.multi
+ def _send(self, delete_all=False, raise_exception=False):
+ """ This method tries to send SMS after checking the number (presence and
+ formatting). """
+ iap_data = [{
+ 'res_id': record.id,
+ 'number': record.number,
+ 'content': record.body,
+ } for record in self]
+
+ try:
+ iap_results = self.env['sms.api']._send_sms_batch(iap_data)
+ except Exception as e:
+ _logger.info('Sent batch %s SMS: %s: failed with exception %s', len(self.ids), self.ids, e)
+ if raise_exception:
+ raise
+ self._postprocess_sent_sms([{'res_id': sms.id, 'state': 'server_error'} for sms in self], delete_all=delete_all)
+ else:
+ _logger.info('Send batch %s SMS: %s: gave %s', len(self.ids), self.ids, iap_results)
+ self._postprocess_sent_sms(iap_results, delete_all=delete_all)
+
+ def _postprocess_sent_sms(self, iap_results, failure_reason=None, delete_all=False):
+ sms_to_notif_status = {
+ 'success': False, 'insufficient_credit': 'sms_credit',
+ 'wrong_format_number': 'sms_number_format', 'server_error': 'sms_server'}
+ if delete_all:
+ todelete_sms_ids = [item['res_id'] for item in iap_results]
+ else:
+ todelete_sms_ids = [item['res_id'] for item in iap_results if item['state'] == 'success']
+
+ for state in sms_to_notif_status.keys():
+ sms_ids = [item['res_id'] for item in iap_results if item['state'] == state]
+ if sms_ids:
+ if not delete_all and state != 'success':
+ self.env['sms.sms'].sudo().browse(sms_ids).write({
+ 'state': 'error',
+ 'error_code': sms_to_notif_status[state],
+ })
+ notifications = self.env['mail.notification'].sudo().search([
+ ('notification_type', '=', 'sms'),
+ ('sms_id', 'in', sms_ids),
+ ('notification_status', 'not in', ('sent', 'canceled'))]
+ )
+ if notifications:
+ notifications.write({
+ 'notification_status': 'sent' if state == 'success' else 'exception',
+ 'failure_type': sms_to_notif_status[state],
+ 'failure_reason': failure_reason if failure_reason else False,
+ })
+
+ if todelete_sms_ids:
+ self.browse(todelete_sms_ids).sudo().unlink()
+
+ @api.multi
+ def cancel(self):
+ self.write({
+ 'state': 'canceled',
+ 'error_code': False
+ })
diff --git a/addons/sms/security/ir.model.access.csv b/addons/sms/security/ir.model.access.csv
new file mode 100644
index 00000000000..42c4d624aaa
--- /dev/null
+++ b/addons/sms/security/ir.model.access.csv
@@ -0,0 +1,3 @@
+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
diff --git a/addons/sms/tests/common.py b/addons/sms/tests/common.py
index 25ca2dfdf7f..7c0c3b5d22a 100644
--- a/addons/sms/tests/common.py
+++ b/addons/sms/tests/common.py
@@ -3,7 +3,8 @@
from contextlib import contextmanager
from unittest.mock import patch
-from odoo import exceptions
+from odoo import exceptions, tools
+from odoo.addons.phone_validation.tools import phone_validation
from odoo.tests import common
from odoo.addons.sms.models.sms_api import SmsApi
@@ -54,18 +55,100 @@ class MockSMS(common.BaseCase):
finally:
pass
+ def _clear_sms_sent(self):
+ self._sms = []
+
def assertSMSSent(self, numbers, content):
""" Check sent SMS. Order is not checked. Each number should have received
- the same content. Usefull to check batch sending.
+ the same content. Useful to check batch sending.
:param numbers: list of numbers;
:param content: content to check for each number;
"""
- self.assertEqual(len(self._sms), len(numbers))
for number in numbers:
sent_sms = next((sms for sms in self._sms if sms['number'] == number), None)
self.assertTrue(bool(sent_sms), 'Number %s not found in %s' % (number, repr([s['number'] for s in self._sms])))
self.assertEqual(sent_sms['body'], content)
- def _clear_sms_sent(self):
- self._sms = []
+ 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. """
+ sms = self.env['sms.sms'].sudo().search([
+ ('partner_id', '=', partner.id), ('number', '=', number),
+ ('state', '=', 'canceled')
+ ])
+ self.assertTrue(sms, 'SMS: not found canceled SMS for %s (number: %s)' % (partner, number))
+ self.assertEqual(sms.error_code, error_code)
+ if content is not None:
+ self.assertEqual(sms.body, content)
+
+ 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. """
+ sms = self.env['sms.sms'].sudo().search([
+ ('partner_id', '=', partner.id), ('number', '=', number),
+ ('state', '=', 'error')
+ ])
+ self.assertTrue(sms, 'SMS: not found failed SMS for %s (number: %s)' % (partner, number))
+ self.assertEqual(sms.error_code, error_code)
+ if content is not None:
+ self.assertEqual(sms.body, content)
+
+ 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. """
+ 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))
+ if content is not None:
+ self.assertEqual(sms.body, content)
+
+ def assertSMSNotification(self, recipients_info, content, messages=None, check_sms=True):
+ """ Check content of notifications.
+
+ :param recipients_info: list[{
+ 'partner': res.partner record (may be empty),
+ 'number': number used for notification (may be empty, computed based on partner),
+ 'state': ready / sent / exception / canceled (sent by default),
+ 'failure_type': optional: sms_number_missing / sms_number_format / sms_credit / sms_server
+ }, { ... }]
+ """
+ partners = self.env['res.partner'].concat(*list(p['partner'] for p in recipients_info if p.get('partner')))
+ numbers = [p['number'] for p in recipients_info if p.get('number')]
+ base_domain = [
+ '|', ('res_partner_id', 'in', partners.ids),
+ '&', ('res_partner_id', '=', False), ('sms_number', 'in', numbers),
+ ('notification_type', '=', 'sms')
+ ]
+ if messages is not None:
+ base_domain += [('mail_message_id', 'in', messages.ids)]
+ notifications = self.env['mail.notification'].search(base_domain)
+
+ self.assertEqual(notifications.mapped('res_partner_id'), partners)
+
+ for recipient_info in recipients_info:
+ partner = recipient_info.get('partner', self.env['res.partner'])
+ number = recipient_info.get('number')
+ state = recipient_info.get('state', 'sent')
+ if number is None and partner:
+ number = phone_validation.phone_get_sanitized_record_number(partner)
+
+ notif = notifications.filtered(lambda n: n.res_partner_id == partner and n.sms_number == number and n.notification_status == state)
+ self.assertTrue(notif, 'SMS: not found notification for %s (number: %s, state: %s)' % (partner, number, state))
+
+ if state not in ('sent', 'ready', 'canceled'):
+ self.assertEqual(notif.failure_type, recipient_info['failure_type'])
+ if check_sms:
+ if state == 'sent':
+ self.assertSMSSent([number], content)
+ elif state == 'ready':
+ self.assertSMSOutgoing(partner, number, content)
+ elif state == 'exception':
+ self.assertSMSFailed(partner, number, recipient_info['failure_type'], content)
+ elif state == 'canceled':
+ self.assertSMSCanceled(partner, number, recipient_info.get('failure_type', False), content)
+
+ for message in messages:
+ self.assertEqual(content, tools.html2plaintext(message.body).rstrip('\n'))
diff --git a/addons/sms/views/sms_sms_views.xml b/addons/sms/views/sms_sms_views.xml
new file mode 100644
index 00000000000..a6da804286a
--- /dev/null
+++ b/addons/sms/views/sms_sms_views.xml
@@ -0,0 +1,66 @@
+
+
+
+ sms.sms.view.form
+ sms.sms
+
+
+
+
+
+
+ sms.sms.view.tree
+ sms.sms
+
+
+
+
+
+
+
+
+
+
+
+ sms.sms.view.search
+ sms.sms
+
+
+
+
+
+
+
+
+
+ SMS
+ sms.sms
+ tree,form
+
+
+
+
+
+
+
diff --git a/addons/sms/wizard/sms_composer.py b/addons/sms/wizard/sms_composer.py
index 2eefe1c42b4..93b3202a83a 100644
--- a/addons/sms/wizard/sms_composer.py
+++ b/addons/sms/wizard/sms_composer.py
@@ -1,25 +1,9 @@
# -*- 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.phone_validation.tools import phone_validation
from odoo.exceptions import UserError
-from odoo.addons.iap.models import iap
-
-_logger = logging.getLogger(__name__)
-
-try:
- import phonenumbers
- _sms_phonenumbers_lib_imported = True
-
-except ImportError:
- _sms_phonenumbers_lib_imported = False
- _logger.info(
- "The `phonenumbers` Python module is not available. "
- "Phone number validation will be skipped. "
- "Try `pip3 install phonenumbers` to install it."
- )
class SendSMS(models.TransientModel):
@@ -29,27 +13,6 @@ class SendSMS(models.TransientModel):
recipients = fields.Char('Recipients', required=True)
message = fields.Text('Message', required=True)
- def _phone_get_country(self, partner):
- if 'country_id' in partner:
- return partner.country_id
- return self.env.company.country_id
-
- def _sms_sanitization(self, partner, field_name):
- number = partner[field_name]
- if number and _sms_phonenumbers_lib_imported:
- country = self._phone_get_country(partner)
- country_code = country.code if country else None
- try:
- phone_nbr = phonenumbers.parse(number, region=country_code, keep_raw_input=True)
- except phonenumbers.phonenumberutil.NumberParseException:
- return number
- if not phonenumbers.is_possible_number(phone_nbr) or not phonenumbers.is_valid_number(phone_nbr):
- return number
- phone_fmt = phonenumbers.PhoneNumberFormat.E164
- return phonenumbers.format_number(phone_nbr, phone_fmt)
- else:
- return number
-
def _get_records(self, model):
if self.env.context.get('active_domain'):
records = model.search(self.env.context.get('active_domain'))
@@ -64,14 +27,14 @@ class SendSMS(models.TransientModel):
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], '_get_default_sms_recipients'):
+ 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._get_default_sms_recipients()
+ partners = records._sms_get_default_partners()
phone_numbers = []
no_phone_partners = []
for partner in partners:
- number = self._sms_sanitization(partner, self.env.context.get('field_name') or 'mobile')
+ 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:
@@ -86,10 +49,10 @@ class SendSMS(models.TransientModel):
numbers = [number.strip() for number in self.recipients.split(',') if number.strip()]
active_model = self.env.context.get('active_model')
- if active_model and hasattr(self.env[active_model], 'message_post_send_sms'):
+ if active_model and hasattr(self.env[active_model], '_message_sms'):
model = self.env[active_model]
records = self._get_records(model)
- records.message_post_send_sms(self.message, numbers=numbers)
+ records[0]._message_sms(self.message, sms_numbers=numbers)
else:
self.env['sms.api']._send_sms(numbers, self.message)
return True
diff --git a/addons/test_mail/tests/common.py b/addons/test_mail/tests/common.py
index 9881d631977..752c489cadb 100644
--- a/addons/test_mail/tests/common.py
+++ b/addons/test_mail/tests/common.py
@@ -174,13 +174,13 @@ class TestRecipients(common.SavepointCase):
'name': 'Valid Lelitre',
'email': 'valid.lelitre@agrolait.com',
'country_id': cls.env.ref('base.be').id,
- 'mobile': '0475001122',
+ 'mobile': '0456001122',
})
cls.partner_2 = Partner.create({
'name': 'Valid Poilvache',
'email': 'valid.other@gmail.com',
'country_id': cls.env.ref('base.be').id,
- 'mobile': '+32 475 22 11 00',
+ 'mobile': '+32 456 22 11 00',
})
diff --git a/addons/test_mail/tests/test_performance.py b/addons/test_mail/tests/test_performance.py
index 666728d53dd..2099e0ccc1b 100644
--- a/addons/test_mail/tests/test_performance.py
+++ b/addons/test_mail/tests/test_performance.py
@@ -506,7 +506,7 @@ class TestHeavyMailPerformance(BaseMailPerformance):
})
self.assertEqual(rec.message_partner_ids, self.user_portal.partner_id | self.env.user.partner_id)
self.assertEqual(len(rec.message_ids), 1)
- with self.assertQueryCount(__system__=99, emp=103): # com runbot: 99 -103 // test_mail only: 99 - 103
+ with self.assertQueryCount(__system__=100, emp=106): # com runbot: 100 -106 // test_mail only: 100 - 106
rec.write({
'name': 'Test2',
'umbrella_id': self.umbrella.id,
@@ -542,7 +542,7 @@ class TestHeavyMailPerformance(BaseMailPerformance):
})
self.assertEqual(rec.message_partner_ids, self.user_portal.partner_id | self.env.user.partner_id)
- with self.assertQueryCount(__system__=104, emp=108): # test_mail only: 104 - 108
+ with self.assertQueryCount(__system__=105, emp=111): # test_mail only: 105 - 111
rec.write({
'name': 'Test2',
'umbrella_id': umbrella_id,
diff --git a/addons/test_mail_full/__manifest__.py b/addons/test_mail_full/__manifest__.py
index dab4550230a..3f6c16e5639 100644
--- a/addons/test_mail_full/__manifest__.py
+++ b/addons/test_mail_full/__manifest__.py
@@ -15,7 +15,7 @@ real applications. """,
'mail',
'mail_bot',
# 'snailmail',
- 'mass_mailing',
+ # 'mass_mailing',
'phone_validation',
'sms',
],
diff --git a/addons/test_mail_full/models/test_mail_models.py b/addons/test_mail_full/models/test_mail_models.py
index b351ba8ce45..d0d58e0467a 100644
--- a/addons/test_mail_full/models/test_mail_models.py
+++ b/addons/test_mail_full/models/test_mail_models.py
@@ -10,6 +10,7 @@ class MailTestSMS(models.Model):
_description = 'Chatter Model for SMS Gateway'
_name = 'mail.test.sms'
_inherit = ['mail.thread']
+ _order = 'name asc, id asc'
name = fields.Char()
subject = fields.Char()
@@ -18,6 +19,19 @@ class MailTestSMS(models.Model):
mobile_nbr = fields.Char()
customer_id = fields.Many2one('res.partner', 'Customer')
- @api.multi
- def _get_default_sms_recipients(self):
+ def _sms_get_default_partners(self):
return self.mapped('customer_id')
+
+ def _sms_get_number_fields(self):
+ return ['phone_nbr', 'mobile_nbr']
+
+
+class MailTestSMSSoLike(models.Model):
+ """ A model like sale order having only a customer, not specific phone
+ or mobile fields. """
+ _description = 'Chatter Model for SMS Gateway (Partner only)'
+ _name = 'mail.test.sms.partner'
+ _inherit = ['mail.thread']
+
+ name = fields.Char()
+ partner_id = fields.Many2one('res.partner', 'Customer')
diff --git a/addons/test_mail_full/security/ir.model.access.csv b/addons/test_mail_full/security/ir.model.access.csv
index 2506e6ac804..186986eef83 100644
--- a/addons/test_mail_full/security/ir.model.access.csv
+++ b/addons/test_mail_full/security/ir.model.access.csv
@@ -1,3 +1,5 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_mail_test_sms_all,mail.test.sms.all,model_mail_test_sms,,0,0,0,0
access_mail_test_sms_user,mail.test.sms.user,model_mail_test_sms,base.group_user,1,1,1,1
+access_mail_test_sms_partner_all,mail.test.sms.partner.all,model_mail_test_sms_partner,,0,0,0,0
+access_mail_test_sms_partner_user,mail.test.sms.partner.user,model_mail_test_sms_partner,base.group_user,1,1,1,1
diff --git a/addons/test_mail_full/tests/__init__.py b/addons/test_mail_full/tests/__init__.py
index 3eac1b648f9..b5c867844ba 100644
--- a/addons/test_mail_full/tests/__init__.py
+++ b/addons/test_mail_full/tests/__init__.py
@@ -2,4 +2,6 @@
from . import common
from . import test_sms_composer
+from . import test_sms_performance
from . import test_sms_post
+from . import test_sms_sms
diff --git a/addons/test_mail_full/tests/common.py b/addons/test_mail_full/tests/common.py
index b9abbbec7dc..448ad44ade1 100644
--- a/addons/test_mail_full/tests/common.py
+++ b/addons/test_mail_full/tests/common.py
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
+from odoo.addons.phone_validation.tools import phone_validation
from odoo.addons.test_mail.tests import common as test_mail_common
@@ -12,3 +13,10 @@ class BaseFunctionalTest(test_mail_common.BaseFunctionalTest):
# update country to belgium in order to test sanitization of numbers
cls.user_employee.company_id.write({'country_id': cls.env.ref('base.be').id})
+
+ # some numbers for testing
+ cls.random_numbers_str = '+32456998877, 0456665544'
+ cls.random_numbers = cls.random_numbers_str.split(', ')
+ cls.random_numbers_san = [phone_validation.phone_format(number, 'BE', '32', force_format='E164') for number in cls.random_numbers]
+ cls.test_numbers = ['+32456010203', '0456 04 05 06']
+ cls.test_numbers_san = [phone_validation.phone_format(number, 'BE', '32', force_format='E164') for number in cls.test_numbers]
diff --git a/addons/test_mail_full/tests/test_sms_composer.py b/addons/test_mail_full/tests/test_sms_composer.py
index bcdcbd32f1e..dd2aaf64ae7 100644
--- a/addons/test_mail_full/tests/test_sms_composer.py
+++ b/addons/test_mail_full/tests/test_sms_composer.py
@@ -1,10 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from odoo.addons.phone_validation.tools import phone_validation
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
-from odoo.addons.phone_validation.tools.phone_validation import phone_format
class TestSMSComposer(test_mail_full_common.BaseFunctionalTest, sms_common.MockSMS, test_mail_common.MockEmails, test_mail_common.TestRecipients):
@@ -14,11 +14,9 @@ class TestSMSComposer(test_mail_full_common.BaseFunctionalTest, sms_common.MockS
super(TestSMSComposer, cls).setUpClass()
cls._test_body = 'VOID CONTENT'
cls.partner_numbers = [
- phone_format(partner.mobile, partner.country_id.code, partner.country_id.phone_code, force_format='E164')
+ 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)
]
- cls.random_numbers_str = '+32475998877, 0475997788'
- cls.random_numbers = [phone_format(number, 'BE', '32', force_format='E164') for number in ['+32475998877', '0475997788']]
def test_composer_no_model(self):
composer = self.env['sms.composer'].with_context().create({
@@ -62,13 +60,13 @@ class TestSMSComposer(test_mail_full_common.BaseFunctionalTest, sms_common.MockS
'mobile': 'coincoin',
})
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,
- })
+ # 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()
+ # 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
diff --git a/addons/test_mail_full/tests/test_sms_performance.py b/addons/test_mail_full/tests/test_sms_performance.py
new file mode 100644
index 00000000000..65c9e2cb4b5
--- /dev/null
+++ b/addons/test_mail_full/tests/test_sms_performance.py
@@ -0,0 +1,88 @@
+# -*- 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.test_performance import BaseMailPerformance
+from odoo.tests.common import users, warmup
+from odoo.tests import tagged
+from odoo.tools import mute_logger
+
+
+@tagged('mail_performance')
+class TestSMSPerformance(BaseMailPerformance, sms_common.MockSMS):
+
+ def setUp(self):
+ super(TestSMSPerformance, self).setUp()
+ self.user_employee.write({
+ 'login': 'employee',
+ })
+ self.admin = self.env.user
+
+ self.customer = self.env['res.partner'].with_context(self._quick_create_ctx).create({
+ 'name': 'Test Customer',
+ 'email': 'test@example.com',
+ 'mobile': '0456123456',
+ 'country_id': self.env.ref('base.be').id,
+ })
+ self.test_record = self.env['mail.test.sms'].with_context(self._quick_create_ctx).create({
+ 'name': 'Test',
+ 'customer_id': self.customer.id,
+ 'phone_nbr': '0456999999',
+ })
+
+ # prepare recipients to test for more realistic workload
+ Partners = self.env['res.partner'].with_context(self._quick_create_ctx)
+ self.partners = self.env['res.partner']
+ for x in range(0, 10):
+ self.partners |= Partners.create({
+ 'name': 'Test %s' % x,
+ 'email': 'test%s@example.com' % x,
+ 'mobile': '0456%s%s0000' % (x, x),
+ 'country_id': self.env.ref('base.be').id,
+ })
+
+ # patch registry to simulate a ready environment
+ self.patch(self.env.registry, 'ready', True)
+
+ @mute_logger('odoo.addons.sms.models.sms_sms')
+ @users('employee')
+ @warmup
+ def test_message_sms_record_1_partner(self):
+ record = self.test_record.with_user(self.env.user)
+ pids = self.customer.ids
+ with self.mockSMSGateway(), self.assertQueryCount(employee=22): # test_mail_enterprise: 22
+ messages = record._message_sms(
+ body='Performance Test',
+ partner_ids=pids,
+ )
+
+ self.assertEqual(record.message_ids[0].body, '
Performance Test
')
+ self.assertSMSNotification([{'partner': self.customer}], 'Performance Test', messages)
+
+ @mute_logger('odoo.addons.sms.models.sms_sms')
+ @users('employee')
+ @warmup
+ def test_message_sms_record_10_partners(self):
+ record = self.test_record.with_user(self.env.user)
+ pids = self.partners.ids
+ with self.mockSMSGateway(), self.assertQueryCount(employee=40): # test_mail_enterprise: 40
+ messages = record._message_sms(
+ body='Performance Test',
+ partner_ids=pids,
+ )
+
+ self.assertEqual(record.message_ids[0].body, 'Performance Test
')
+ self.assertSMSNotification([{'partner': partner} for partner in self.partners], 'Performance Test', messages)
+
+ @mute_logger('odoo.addons.sms.models.sms_sms')
+ @users('employee')
+ @warmup
+ def test_message_sms_record_default(self):
+ record = self.test_record.with_user(self.env.user)
+ with self.mockSMSGateway(), self.assertQueryCount(employee=26): # test_mail_enterprise: 26
+ messages = record._message_sms(
+ body='Performance Test',
+ )
+
+ self.assertEqual(record.message_ids[0].body, 'Performance Test
')
+ self.assertSMSNotification([{'partner': self.customer}], 'Performance Test', messages)
diff --git a/addons/test_mail_full/tests/test_sms_post.py b/addons/test_mail_full/tests/test_sms_post.py
index a12855341a0..f1e7d91b1d0 100644
--- a/addons/test_mail_full/tests/test_sms_post.py
+++ b/addons/test_mail_full/tests/test_sms_post.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from odoo.addons.phone_validation.tools import phone_validation
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
@@ -11,44 +12,280 @@ class TestSMSPost(test_mail_full_common.BaseFunctionalTest, sms_common.MockSMS,
@classmethod
def setUpClass(cls):
super(TestSMSPost, 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)
+ ]
+
+ 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)
+
+ def test_message_sms_internals_body(self):
+ with self.sudo('employee'), self.mockSMSGateway():
+ test_record = self.env['mail.test.sms'].browse(self.test_record.id)
+ messages = test_record._message_sms('Mega SMS
Top moumoutte
', partner_ids=self.partner_1.ids)
+
+ self.assertEqual(messages.body, 'Mega SMS
Top moumoutte
')
+ self.assertSMSNotification([{'partner': self.partner_1}], 'Mega SMS\nTop moumoutte', messages)
+
+ def test_message_sms_internals_check_existing(self):
+ with self.sudo('employee'), self.mockSMSGateway(sim_error='wrong_format_number'):
+ test_record = self.env['mail.test.sms'].browse(self.test_record.id)
+ messages = test_record._message_sms(self._test_body, partner_ids=self.partner_1.ids)
+
+ self.assertSMSNotification([{'partner': self.partner_1, 'state': 'exception', 'failure_type': 'sms_number_format'}], self._test_body, messages)
+
+ with self.sudo('employee'), self.mockSMSGateway():
+ test_record = self.env['mail.test.sms'].browse(self.test_record.id)
+ test_record._notify_record_by_sms(messages, {'partners': [{'id': self.partner_1.id, 'notif': 'sms'}]}, check_existing=True)
+ self.assertSMSNotification([{'partner': self.partner_1}], self._test_body, messages)
+
+ def test_message_sms_internals_sms_numbers(self):
+ with self.sudo('employee'), self.mockSMSGateway():
+ test_record = self.env['mail.test.sms'].browse(self.test_record.id)
+ messages = test_record._message_sms(self._test_body, partner_ids=self.partner_1.ids, sms_numbers=self.random_numbers)
+
+ self.assertSMSNotification([{'partner': self.partner_1}, {'number': self.random_numbers_san[0]}, {'number': self.random_numbers_san[1]}], self._test_body, messages)
+
+ def test_message_sms_internals_pid_to_number(self):
+ pid_to_number = {
+ self.partner_1.id: self.random_numbers[0],
+ self.partner_2.id: self.random_numbers[1],
+ }
+ with self.sudo('employee'), self.mockSMSGateway():
+ test_record = self.env['mail.test.sms'].browse(self.test_record.id)
+ messages = test_record._message_sms(self._test_body, partner_ids=(self.partner_1 | self.partner_2).ids, sms_pid_to_number=pid_to_number)
+
+ self.assertSMSNotification([
+ {'partner': self.partner_1, 'number': self.random_numbers_san[0]},
+ {'partner': self.partner_2, 'number': self.random_numbers_san[1]}],
+ self._test_body, messages)
+
+ def test_message_sms_model_partner(self):
+ with self.sudo('employee'), self.mockSMSGateway():
+ messages = self.partner_1._message_sms(self._test_body)
+ messages |= self.partner_2._message_sms(self._test_body)
+ self.assertSMSNotification([{'partner': self.partner_1}, {'partner': self.partner_2}], self._test_body, messages)
+
+ def test_message_sms_model_partner_fallback(self):
+ self.partner_1.write({'mobile': False, 'phone': self.random_numbers[0]})
+
+ with self.mockSMSGateway():
+ messages = self.partner_1._message_sms(self._test_body)
+ messages |= self.partner_2._message_sms(self._test_body)
+
+ self.assertSMSNotification([{'partner': self.partner_1, 'number': self.random_numbers_san[0]}, {'partner': self.partner_2}], self._test_body, messages)
+
+ def test_message_sms_model_w_partner_only(self):
+ with self.sudo('employee'):
+ record = self.env['mail.test.sms.partner'].create({'partner_id': self.partner_1.id})
+
+ with self.mockSMSGateway():
+ messages = record._message_sms(self._test_body)
+
+ self.assertSMSNotification([{'partner': self.partner_1}], self._test_body, messages)
+
+ def test_message_sms_model_w_partner_only_void(self):
+ with self.sudo('employee'):
+ record = self.env['mail.test.sms.partner'].create({'partner_id': False})
+
+ with self.mockSMSGateway():
+ messages = record._message_sms(self._test_body)
+
+ # should not crash but no sms / no recipients
+ notifs = self.env['mail.notification'].search([('mail_message_id', 'in', messages.ids)])
+ self.assertFalse(notifs)
+
+ def test_message_sms_on_field_w_partner(self):
+ with self.sudo('employee'), self.mockSMSGateway():
+ test_record = self.env['mail.test.sms'].browse(self.test_record.id)
+ messages = test_record._message_sms(self._test_body, number_field='mobile_nbr')
+
+ self.assertSMSNotification([{'partner': self.partner_1, 'number': self.test_record.mobile_nbr}], self._test_body, messages)
+
+ def test_message_sms_on_field_wo_partner(self):
+ self.test_record.write({'customer_id': False})
+
+ with self.sudo('employee'), self.mockSMSGateway():
+ test_record = self.env['mail.test.sms'].browse(self.test_record.id)
+ messages = test_record._message_sms(self._test_body, number_field='mobile_nbr')
+
+ self.assertSMSNotification([{'number': self.test_record.mobile_nbr}], self._test_body, messages)
+
+ def test_message_sms_on_field_wo_partner_default_field(self):
+ self.test_record.write({'customer_id': False})
+
+ with self.sudo('employee'), self.mockSMSGateway():
+ test_record = self.env['mail.test.sms'].browse(self.test_record.id)
+ messages = test_record._message_sms(self._test_body)
+
+ self.assertSMSNotification([{'number': self.test_numbers_san[1]}], self._test_body, messages)
+
+ def test_message_sms_on_field_wo_partner_default_field_2(self):
+ self.test_record.write({'customer_id': False, 'phone_nbr': False})
+
+ with self.sudo('employee'), self.mockSMSGateway():
+ test_record = self.env['mail.test.sms'].browse(self.test_record.id)
+ messages = test_record._message_sms(self._test_body)
+
+ self.assertSMSNotification([{'number': self.test_numbers_san[0]}], self._test_body, messages)
+
+ def test_message_sms_on_numbers(self):
+ with self.sudo('employee'), self.mockSMSGateway():
+ test_record = self.env['mail.test.sms'].browse(self.test_record.id)
+ messages = test_record._message_sms(self._test_body, sms_numbers=self.random_numbers_san)
+ self.assertSMSNotification([{'number': self.random_numbers_san[0]}, {'number': self.random_numbers_san[1]}], self._test_body, messages)
+
+ def test_message_sms_on_numbers_sanitization(self):
+ with self.sudo('employee'), self.mockSMSGateway():
+ test_record = self.env['mail.test.sms'].browse(self.test_record.id)
+ messages = test_record._message_sms(self._test_body, sms_numbers=self.random_numbers)
+ self.assertSMSNotification([{'number': self.random_numbers_san[0]}, {'number': self.random_numbers_san[1]}], self._test_body, messages)
+
+ def test_message_sms_on_partner_ids(self):
+ with self.sudo('employee'), self.mockSMSGateway():
+ test_record = self.env['mail.test.sms'].browse(self.test_record.id)
+ messages = test_record._message_sms(self._test_body, partner_ids=(self.partner_1 | self.partner_2).ids)
+
+ self.assertSMSNotification([{'partner': self.partner_1}, {'partner': self.partner_2}], self._test_body, messages)
+
+ def test_message_sms_on_partner_ids_default(self):
+ with self.sudo('employee'), self.mockSMSGateway():
+ test_record = self.env['mail.test.sms'].browse(self.test_record.id)
+ messages = test_record._message_sms(self._test_body)
+
+ self.assertSMSNotification([{'partner': self.test_record.customer_id, 'number': self.test_numbers_san[1]}], self._test_body, messages)
+
+ def test_message_sms_on_partner_ids_w_numbers(self):
+ with self.sudo('employee'), self.mockSMSGateway():
+ test_record = self.env['mail.test.sms'].browse(self.test_record.id)
+ messages = test_record._message_sms(self._test_body, partner_ids=self.partner_1.ids, sms_numbers=self.random_numbers[:1])
+
+ self.assertSMSNotification([{'partner': self.partner_1}, {'number': self.random_numbers_san[0]}], self._test_body, messages)
+
+
+class TestSMSPostException(test_mail_full_common.BaseFunctionalTest, sms_common.MockSMS, test_mail_common.MockEmails, test_mail_common.TestRecipients):
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestSMSPostException, cls).setUpClass()
+ cls._test_body = 'VOID CONTENT'
+
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.partner_3 = cls.env['res.partner'].with_context({
+ 'mail_create_nolog': True,
+ 'mail_create_nosubscribe': True,
+ 'mail_notrack': True,
+ 'no_reset_password': True,
+ }).create({
+ 'name': 'Ernestine Loubine',
+ 'email': 'ernestine.loubine@agrolait.com',
+ 'country_id': cls.env.ref('base.be').id,
+ 'mobile': '0475556644',
+ })
- def test_message_post_with_sms_on_partners(self):
- test_body = 'Void body'
- with self.mockSMSGateway():
- (self.partner_1 | self.partner_2).message_post_send_sms(test_body)
- self.assertSMSSent((self.partner_1 | self.partner_2).mapped('mobile'), test_body)
+ def test_message_sms_w_numbers_invalid(self):
+ random_numbers = self.random_numbers + ['6988754']
+ with self.sudo('employee'), self.mockSMSGateway():
+ test_record = self.env['mail.test.sms'].browse(self.test_record.id)
+ messages = test_record._message_sms(self._test_body, sms_numbers=random_numbers)
- def test_message_post_with_sms_w_default(self):
- test_body = 'Void body'
- with self.mockSMSGateway():
- self.test_record.message_post_send_sms(test_body)
- self.assertSMSSent(self.partner_1.mapped('mobile'), test_body)
- self.assertIn(test_body, self.test_record.message_ids.body)
+ # invalid numbers are still given to IAP currently as they are
+ self.assertSMSNotification([{'number': self.random_numbers_san[0]}, {'number': self.random_numbers_san[1]}, {'number': random_numbers[2]}], self._test_body, messages)
- def test_message_post_with_sms_w_numbers(self):
- test_body = 'Void body'
- test_numbers = ['0475114477', '0475225588']
- with self.mockSMSGateway():
- self.test_record.message_post_send_sms(test_body, numbers=test_numbers)
- self.assertSMSSent(test_numbers, test_body)
- self.assertIn(test_body, self.test_record.message_ids.body)
+ def test_message_sms_w_partners_nocountry(self):
+ self.test_record.customer_id.write({
+ 'mobile': self.random_numbers[0],
+ 'phone': self.random_numbers[1],
+ 'country_id': False,
+ })
+ with self.sudo('employee'), self.mockSMSGateway():
+ test_record = self.env['mail.test.sms'].browse(self.test_record.id)
+ messages = test_record._message_sms(self._test_body, partner_ids=self.test_record.customer_id.ids)
- def test_message_post_with_sms_w_numbers_duplicate(self):
- test_body = 'Void body'
- test_numbers = ['0475114477', '0475225588', '0475114477']
- with self.mockSMSGateway():
- self.test_record.message_post_send_sms(test_body, numbers=test_numbers)
- self.assertSMSSent(test_numbers, test_body)
- self.assertIn(test_body, self.test_record.message_ids.body)
+ self.assertSMSNotification([{'partner': self.test_record.customer_id}], self._test_body, messages)
- def test_message_post_with_sms_w_partners(self):
- test_body = 'Void body'
- with self.mockSMSGateway():
- self.test_record.message_post_send_sms(test_body, partners=self.partner_1 | self.partner_2)
- self.assertSMSSent((self.partner_1 | self.partner_2).mapped('mobile'), test_body)
- self.assertIn(test_body, self.test_record.message_ids.body)
+ def test_message_sms_w_partners_falsy(self):
+ # TDE FIXME: currently sent to IAP
+ self.test_record.customer_id.write({
+ 'mobile': 'youpie',
+ 'phone': 'youpla',
+ })
+ with self.sudo('employee'), self.mockSMSGateway():
+ test_record = self.env['mail.test.sms'].browse(self.test_record.id)
+ messages = test_record._message_sms(self._test_body, partner_ids=self.test_record.customer_id.ids)
+
+ # self.assertSMSNotification({self.test_record.customer_id: {}}, {}, self._test_body, messages)
+
+ def test_message_sms_w_numbers_sanitization_duplicate(self):
+ pass
+ # TDE FIXME: not sure
+ # random_numbers = self.random_numbers + [self.random_numbers[1]]
+ # random_numbers_san = self.random_numbers_san + [self.random_numbers_san[1]]
+ # with self.sudo('employee'), self.mockSMSGateway():
+ # messages = self.test_record._message_sms(self._test_body, sms_numbers=random_numbers)
+ # self.assertSMSNotification({}, {random_numbers_san[0]: {}, random_numbers_san[1]: {}, random_numbers_san[2]: {}}, self._test_body, messages)
+
+ def test_message_sms_crash_credit(self):
+ with self.sudo('employee'), self.mockSMSGateway(sim_error='credit'):
+ test_record = self.env['mail.test.sms'].browse(self.test_record.id)
+ messages = test_record._message_sms(self._test_body, partner_ids=(self.partner_1 | self.partner_2).ids)
+
+ self.assertSMSNotification([
+ {'partner': self.partner_1, 'state': 'exception', 'failure_type': 'sms_credit'},
+ {'partner': self.partner_2, 'state': 'exception', 'failure_type': 'sms_credit'},
+ ], self._test_body, messages)
+
+ def test_message_sms_crash_credit_single(self):
+ with self.sudo('employee'), self.mockSMSGateway(nbr_t_error={phone_validation.phone_get_sanitized_record_number(self.partner_2): 'credit'}):
+ test_record = self.env['mail.test.sms'].browse(self.test_record.id)
+ messages = test_record._message_sms(self._test_body, partner_ids=(self.partner_1 | self.partner_2 | self.partner_3).ids)
+
+ self.assertSMSNotification([
+ {'partner': self.partner_1, 'state': 'sent'},
+ {'partner': self.partner_2, 'state': 'exception', 'failure_type': 'sms_credit'},
+ {'partner': self.partner_3, 'state': 'sent'},
+ ], self._test_body, messages)
+
+ def test_message_sms_crash_server_crash(self):
+ with self.sudo('employee'), self.mockSMSGateway(sim_error='jsonrpc_exception'):
+ test_record = self.env['mail.test.sms'].browse(self.test_record.id)
+ messages = test_record._message_sms(self._test_body, partner_ids=(self.partner_1 | self.partner_2 | self.partner_3).ids)
+
+ self.assertSMSNotification([
+ {'partner': self.partner_1, 'state': 'exception', 'failure_type': 'sms_server'},
+ {'partner': self.partner_2, 'state': 'exception', 'failure_type': 'sms_server'},
+ {'partner': self.partner_3, 'state': 'exception', 'failure_type': 'sms_server'},
+ ], self._test_body, messages)
+
+ def test_message_sms_crash_wrong_number(self):
+ with self.sudo('employee'), self.mockSMSGateway(sim_error='wrong_format_number'):
+ test_record = self.env['mail.test.sms'].browse(self.test_record.id)
+ messages = test_record._message_sms(self._test_body, partner_ids=(self.partner_1 | self.partner_2).ids)
+
+ self.assertSMSNotification([
+ {'partner': self.partner_1, 'state': 'exception', 'failure_type': 'sms_number_format'},
+ {'partner': self.partner_2, 'state': 'exception', 'failure_type': 'sms_number_format'},
+ ], self._test_body, messages)
+
+ def test_message_sms_crash_wrong_number_single(self):
+ with self.sudo('employee'), self.mockSMSGateway(nbr_t_error={phone_validation.phone_get_sanitized_record_number(self.partner_2): 'wrong_format_number'}):
+ test_record = self.env['mail.test.sms'].browse(self.test_record.id)
+ messages = test_record._message_sms(self._test_body, partner_ids=(self.partner_1 | self.partner_2 | self.partner_3).ids)
+
+ self.assertSMSNotification([
+ {'partner': self.partner_1, 'state': 'sent'},
+ {'partner': self.partner_2, 'state': 'exception', 'failure_type': 'sms_number_format'},
+ {'partner': self.partner_3, 'state': 'sent'},
+ ], self._test_body, messages)
diff --git a/addons/test_mail_full/tests/test_sms_sms.py b/addons/test_mail_full/tests/test_sms_sms.py
new file mode 100644
index 00000000000..46149f559a8
--- /dev/null
+++ b/addons/test_mail_full/tests/test_sms_sms.py
@@ -0,0 +1,59 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from unittest.mock import patch
+from unittest.mock import DEFAULT
+
+from odoo import exceptions
+from odoo.addons.sms.models.sms_sms import SmsSms as SmsSms
+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 TestSMSPost(test_mail_full_common.BaseFunctionalTest, sms_common.MockSMS, test_mail_common.MockEmails, test_mail_common.TestRecipients):
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestSMSPost, cls).setUpClass()
+ cls._test_body = 'VOID CONTENT'
+
+ cls.sms_all = cls.env['sms.sms']
+ for x in range(10):
+ cls.sms_all |= cls.env['sms.sms'].create({
+ 'number': '+324560000%s%s' % (x, x),
+ 'body': cls._test_body,
+ })
+
+ def test_sms_send_batch_size(self):
+ self.count = 0
+
+ def _send(sms_self, delete_all=False, raise_exception=False):
+ self.count += 1
+ return DEFAULT
+
+ self.env['ir.config_parameter'].set_param('sms.session.batch.size', '3')
+ with patch.object(SmsSms, '_send', autospec=True, side_effect=_send) as send_mock:
+ self.env['sms.sms'].browse(self.sms_all.ids).send()
+
+ self.assertEqual(self.count, 4)
+
+ def test_sms_send_crash_employee(self):
+ with self.assertRaises(exceptions.AccessError):
+ self.env['sms.sms'].with_user(self.user_employee).browse(self.sms_all.ids).send()
+
+ def test_sms_send_delete_all(self):
+ with self.mockSMSGateway(sim_error='jsonrpc_exception'):
+ self.env['sms.sms'].browse(self.sms_all.ids).send(delete_all=True, raise_exception=False)
+ self.assertFalse(len(self.sms_all.exists()))
+
+ def test_sms_send_raise(self):
+ with self.assertRaises(exceptions.AccessError):
+ with self.mockSMSGateway(sim_error='jsonrpc_exception'):
+ self.env['sms.sms'].browse(self.sms_all.ids).send(raise_exception=True)
+ self.assertEqual(set(self.sms_all.mapped('state')), set(['outgoing']))
+
+ def test_sms_send_raise_catch(self):
+ with self.mockSMSGateway(sim_error='jsonrpc_exception'):
+ self.env['sms.sms'].browse(self.sms_all.ids).send(raise_exception=False)
+ self.assertEqual(set(self.sms_all.mapped('state')), set(['error']))