diff --git a/addons/calendar_sms/views/calendar_views.xml b/addons/calendar_sms/views/calendar_views.xml index 84d07165dcf..37d20181e9a 100644 --- a/addons/calendar_sms/views/calendar_views.xml +++ b/addons/calendar_sms/views/calendar_views.xml @@ -5,7 +5,7 @@ diff --git a/addons/hr_presence/models/hr_employee.py b/addons/hr_presence/models/hr_employee.py index 6347555aaa9..a40dfd256eb 100644 --- a/addons/hr_presence/models/hr_employee.py +++ b/addons/hr_presence/models/hr_employee.py @@ -133,7 +133,7 @@ Please, take appropriate measures in order to carry out this work absence. 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.composer", "view_mode": 'form', "context": { 'active_id': self.id, diff --git a/addons/mail/__manifest__.py b/addons/mail/__manifest__.py index 80e5f0fff28..4fece68c14b 100644 --- a/addons/mail/__manifest__.py +++ b/addons/mail/__manifest__.py @@ -11,6 +11,7 @@ 'data': [ 'wizard/invite_view.xml', 'wizard/mail_compose_message_view.xml', + 'wizard/mail_resend_cancel_views.xml', 'wizard/mail_resend_message_views.xml', 'views/mail_message_subtype_views.xml', 'views/mail_tracking_views.xml', diff --git a/addons/mail/wizard/__init__.py b/addons/mail/wizard/__init__.py index e6ba124f4ae..7e0df4a2d19 100644 --- a/addons/mail/wizard/__init__.py +++ b/addons/mail/wizard/__init__.py @@ -3,6 +3,7 @@ from . import invite from . import mail_compose_message +from . import mail_resend_cancel from . import mail_resend_message from . import email_template_preview from . import base_module_uninstall diff --git a/addons/mail/wizard/mail_resend_cancel.py b/addons/mail/wizard/mail_resend_cancel.py new file mode 100644 index 00000000000..277ed8b96db --- /dev/null +++ b/addons/mail/wizard/mail_resend_cancel.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import _, api, fields, models + + +class MailCancelResend(models.TransientModel): + _name = 'mail.resend.cancel' + _description = 'Dismiss notification for resend by model' + + model = fields.Char(string='Model') + help_message = fields.Char(string='Help message', compute='_compute_help_message') + + @api.multi + @api.depends('model') + def _compute_help_message(self): + for wizard in self: + wizard.help_message = _("Are you sure you want to discard %s mail delivery failures. You won't be able to re-send these mails later!") % (wizard._context.get('unread_counter')) + + @api.multi + def cancel_resend_action(self): + author_id = self.env.user.partner_id.id + for wizard in self: + self._cr.execute(""" + SELECT notif.id, mes.id + FROM mail_message_res_partner_needaction_rel notif + JOIN mail_message mes + ON notif.mail_message_id = mes.id + WHERE notif.email_status IN ('bounce', 'exception') + AND mes.model = %s + AND mes.author_id = %s + """, (wizard.model, author_id)) + res = self._cr.fetchall() + notif_ids = [row[0] for row in res] + messages_ids = list(set([row[1] for row in res])) + if notif_ids: + self.env["mail.notification"].browse(notif_ids).sudo().write({'email_status': 'canceled'}) + self.env["mail.message"].browse(messages_ids)._notify_failure_update() + return {'type': 'ir.actions.act_window_close'} diff --git a/addons/mail/wizard/mail_resend_cancel_views.xml b/addons/mail/wizard/mail_resend_cancel_views.xml new file mode 100644 index 00000000000..c8cc6632c75 --- /dev/null +++ b/addons/mail/wizard/mail_resend_cancel_views.xml @@ -0,0 +1,27 @@ + + + + mail.resend.cancel.view.form + mail.resend.cancel + + + + + + If you want to re-send them, click Cancel now, then click on the notification and review them one by one by clicking on the red envelope next to each message. + + + + + + + Discard mail delivery failures + mail.resend.cancel + ir.actions.act_window + form + new + + diff --git a/addons/mail/wizard/mail_resend_message.py b/addons/mail/wizard/mail_resend_message.py index bcff9dc9840..e5e4c069df1 100644 --- a/addons/mail/wizard/mail_resend_message.py +++ b/addons/mail/wizard/mail_resend_message.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import _, api, fields, models -from ast import literal_eval +from odoo import api, fields, models from odoo.exceptions import UserError + class MailResendMessage(models.TransientModel): _name = 'mail.resend.message' _description = 'Email resend wizard' @@ -100,38 +100,3 @@ class PartnerResend(models.TransientModel): resend = fields.Boolean(string="Send Again", default=True) resend_wizard_id = fields.Many2one('mail.resend.message', string="Resend wizard") message = fields.Char(string="Help message") - - -class MailCancelResend(models.TransientModel): - _name = 'mail.resend.cancel' - _description = 'Dismiss notification for resend by model' - - model = fields.Char(string='Model') - help_message = fields.Char(string='Help message', compute='_compute_help_message') - - @api.multi - @api.depends('model') - def _compute_help_message(self): - for wizard in self: - wizard.help_message = _("Are you sure you want to discard %s mail delivery failures. You won't be able to re-send these mails later!") % (wizard._context.get('unread_counter')) - - @api.multi - def cancel_resend_action(self): - author_id = self.env.user.partner_id.id - for wizard in self: - self._cr.execute(""" - SELECT notif.id, mes.id - FROM mail_message_res_partner_needaction_rel notif - JOIN mail_message mes - ON notif.mail_message_id = mes.id - WHERE notif.email_status IN ('bounce', 'exception') - AND mes.model = %s - AND mes.author_id = %s - """, (wizard.model, author_id)) - res = self._cr.fetchall() - notif_ids = [row[0] for row in res] - messages_ids = list(set([row[1] for row in res])) - if notif_ids: - self.env["mail.notification"].browse(notif_ids).sudo().write({'email_status': 'canceled'}) - self.env["mail.message"].browse(messages_ids)._notify_failure_update() - return {'type': 'ir.actions.act_window_close'} diff --git a/addons/mail/wizard/mail_resend_message_views.xml b/addons/mail/wizard/mail_resend_message_views.xml index 1ab5ea047f7..118660e232e 100644 --- a/addons/mail/wizard/mail_resend_message_views.xml +++ b/addons/mail/wizard/mail_resend_message_views.xml @@ -2,7 +2,7 @@ - mail.resend.message.form + mail.resend.message.view.form mail.resend.message @@ -31,23 +31,6 @@ - - mail.resend.cancel.form - mail.resend.cancel - - - - - - If you want to re-send them, click Cancel now, then click on the notification and review them one by one by clicking on the red envelope next to each message. - - - - - Resend mail mail.resend.message @@ -55,13 +38,5 @@ form new - - Discard mail delivery failures - mail.resend.cancel - ir.actions.act_window - form - new - - diff --git a/addons/phone_validation/models/phone_validation_mixin.py b/addons/phone_validation/models/phone_validation_mixin.py index f378387fc41..f062586c7c3 100644 --- a/addons/phone_validation/models/phone_validation_mixin.py +++ b/addons/phone_validation/models/phone_validation_mixin.py @@ -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 ) diff --git a/addons/phone_validation/tools/phone_validation.py b/addons/phone_validation/tools/phone_validation.py index 7ca7bed6ea8..29828ffd739 100644 --- a/addons/phone_validation/tools/phone_validation.py +++ b/addons/phone_validation/tools/phone_validation.py @@ -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] diff --git a/addons/sms/__init__.py b/addons/sms/__init__.py index 2ae6446f9dc..f7a591caed7 100644 --- a/addons/sms/__init__.py +++ b/addons/sms/__init__.py @@ -2,4 +2,5 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import models +from . import tests from . import wizard diff --git a/addons/sms/__manifest__.py b/addons/sms/__manifest__.py index 574a13810d3..4ef5721be8f 100644 --- a/addons/sms/__manifest__.py +++ b/addons/sms/__manifest__.py @@ -10,12 +10,12 @@ 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/send_sms_views.xml', + 'wizard/sms_composer_views.xml', 'views/res_config_settings_views.xml', 'views/res_partner_views.xml', - 'views/templates.xml', + 'views/assets.xml', ], 'qweb': [ 'static/src/xml/sms_widget.xml', diff --git a/addons/sms/models/sms_api.py b/addons/sms/models/sms_api.py index f770676d0ca..1b6be2607fc 100644 --- a/addons/sms/models/sms_api.py +++ b/addons/sms/models/sms_api.py @@ -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) diff --git a/addons/sms/tests/__init__.py b/addons/sms/tests/__init__.py new file mode 100644 index 00000000000..511e649c9e2 --- /dev/null +++ b/addons/sms/tests/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import common diff --git a/addons/sms/tests/common.py b/addons/sms/tests/common.py new file mode 100644 index 00000000000..25ca2dfdf7f --- /dev/null +++ b/addons/sms/tests/common.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- + +from contextlib import contextmanager +from unittest.mock import patch + +from odoo import exceptions +from odoo.tests import common +from odoo.addons.sms.models.sms_api import SmsApi + + +class MockSMS(common.BaseCase): + + def tearDown(self): + super(MockSMS, self).tearDown() + self._clear_sms_sent + + @contextmanager + def mockSMSGateway(self, sim_error=None, nbr_t_error=None): + self._sms = [] + + def _contact_iap(local_endpoint, params): + # mock single sms sending + if local_endpoint == '/iap/message_send': + self._sms += [{ + 'number': number, + 'body': params['message'], + } for number in params['numbers']] + return True # send_message v0 API returns always True + # mock batch sending + if local_endpoint == '/iap/sms/1/send': + result = [] + for to_send in params['messages']: + res = {'res_id': to_send['res_id'], 'state': 'success', 'credit': 1} + error = sim_error or (nbr_t_error and nbr_t_error.get(to_send['number'])) + if error and error == 'credit': + res.update(credit=0, state='insufficient_credit') + elif error and error == 'wrong_format_number': + res.update(state='wrong_format_number') + elif error and error == 'jsonrpc_exception': + raise exceptions.AccessError( + 'The url that this service requested returned an error. Please contact the author of the app. The url it tried to contact was ' + local_endpoint + ) + result.append(res) + if res['state'] == 'success': + self._sms.append({ + 'number': to_send['number'], + 'body': to_send['content'], + }) + return result + + try: + with patch.object(SmsApi, '_contact_iap', side_effect=_contact_iap) as contact_iap_mock: + yield + finally: + pass + + 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. + + :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 = [] diff --git a/addons/sms/views/templates.xml b/addons/sms/views/assets.xml similarity index 100% rename from addons/sms/views/templates.xml rename to addons/sms/views/assets.xml diff --git a/addons/sms/views/res_partner_views.xml b/addons/sms/views/res_partner_views.xml index 1773c31a30c..1a483568178 100644 --- a/addons/sms/views/res_partner_views.xml +++ b/addons/sms/views/res_partner_views.xml @@ -2,8 +2,8 @@ - - res.partner.form.send.sms + + res.partner.view.form.inherit.sms res.partner 10 @@ -14,7 +14,7 @@ - - - sms_send_sms.form - sms.send_sms + + sms.composer.view.form + sms.composer @@ -23,18 +22,18 @@ - + Send SMS - sms.send_sms + sms.composer form new - diff --git a/addons/test_mail/tests/common.py b/addons/test_mail/tests/common.py index fd6ec9ce627..7df3aa0c96a 100644 --- a/addons/test_mail/tests/common.py +++ b/addons/test_mail/tests/common.py @@ -37,6 +37,17 @@ class BaseFunctionalTest(common.SavepointCase): cls.test_record = cls.env['mail.test.simple'].with_context(cls._test_context).create({'name': 'Test', 'email_from': 'ignasse@example.com'}) + @classmethod + def _reset_mail_context(cls, record): + return record.with_context( + mail_create_nolog=False, + mail_create_nosubscribe=False, + mail_notrack=False + ) + + def _clear_bus(self): + self.env['bus.bus'].search([]).unlink() + @contextmanager def assertNotifications(self, **counters): """ Counters: 'partner_attribute': 'inbox' or 'email' """ @@ -83,41 +94,57 @@ class BaseFunctionalTest(common.SavepointCase): if hasattr(self, 'assertEmails') and len(new_messages) == 1: self.assertEmails(new_messages.author_id, new_notifications.filtered(lambda n: n.is_email).mapped('res_partner_id')) - def assertBusNotification(self, channels, message_dicts=None, init=True): + def assertBusNotification(self, channels, message_items=None, init=True): """ Check for bus notifications. Basic check is about used channels. Verifying content is optional. :param channels: list of channel - :param messages: if given, list of message making a valid pair (channel, + :param message_items: if given, list of message making a valid pair (channel, message) to be found in bus.bus """ + def check_content(returned_value, expected_value): + if isinstance(expected_value, list): + done = [] + for expected_item in expected_value: + for returned_item in returned_value: + if check_content(returned_item, expected_item): + done.append(expected_item) + break + else: + return False + return len(done) == len(expected_value) + elif isinstance(expected_value, dict): + return all(k in returned_value for k in expected_value.keys()) and all( + check_content(returned_value[key], val) + for key, val in expected_value.items() + ) + else: + return returned_value == expected_value + if init: self.assertEqual(len(self.env['bus.bus'].search([])), len(channels)) notifications = self.env['bus.bus'].search([('channel', 'in', [json_dump(channel) for channel in channels])]) + notif_messages = [json.loads(n.message) for n in notifications] self.assertEqual(len(notifications), len(channels)) - if message_dicts: - notif_messages = [json.loads(n.message) for n in notifications] - for expected in message_dicts: - found = False - for returned in notif_messages: - for key, val in expected.items(): - if key not in returned: - continue - if isinstance(returned[key], list): - if set(returned[key]) != set(val): - continue - else: - if returned[key] != val: - continue - found = True - break - if found: - break - if not found: - raise AssertionError("Bus notification content %s not found" % (repr(expected))) + + for expected in message_items or []: + for notification in notif_messages: + found_keys, not_found_keys = [], [] + if not all(k in notification for k in expected.keys()): + continue + for expected_key, expected_value in expected.items(): + done = check_content(notification[expected_key], expected_value) + if done: + found_keys.append(expected_key) + else: + not_found_keys.append(expected_key) + if set(found_keys) == set(expected.keys()): + break + else: + raise AssertionError('Keys %s not found (expected: %s - returned: %s)' % (not_found_keys, repr(expected), repr(notif_messages))) @contextmanager - def sudoAs(self, login): + def sudo(self, login): old_uid = self.uid try: user = self.env['res.users'].sudo().search([('login', '=', login)]) @@ -125,13 +152,11 @@ class BaseFunctionalTest(common.SavepointCase): # switch user self.uid = user.id self.env = self.env(user=self.uid) - self.test_record = self.test_record.sudo(self.uid) yield finally: # back self.uid = old_uid self.env = self.env(user=self.uid) - self.test_record = self.test_record_old class TestRecipients(common.SavepointCase): @@ -147,10 +172,16 @@ class TestRecipients(common.SavepointCase): }) cls.partner_1 = Partner.create({ 'name': 'Valid Lelitre', - 'email': 'valid.lelitre@agrolait.com'}) + 'email': 'valid.lelitre@agrolait.com', + 'country_id': cls.env.ref('base.be').id, + 'mobile': '0475001122', + }) cls.partner_2 = Partner.create({ 'name': 'Valid Poilvache', - 'email': 'valid.other@gmail.com'}) + 'email': 'valid.other@gmail.com', + 'country_id': cls.env.ref('base.be').id, + 'mobile': '+32 475 22 11 00', + }) class MockEmails(common.SingleTransactionCase): @@ -308,6 +339,3 @@ class Moderation(MockEmails, BaseFunctionalTest): 'subtype_id': self.env['mail.message.subtype'].search([('name', '=', 'Discussions')]).id }) return message - - def _clear_bus(self): - self.env['bus.bus'].search([]).unlink() diff --git a/addons/test_mail/tests/test_mail_activity.py b/addons/test_mail/tests/test_mail_activity.py index 2d4a0ef3faf..2ac76c21423 100644 --- a/addons/test_mail/tests/test_mail_activity.py +++ b/addons/test_mail/tests/test_mail_activity.py @@ -90,7 +90,7 @@ class TestActivityRights(TestActivityCommon): class TestActivityFlow(TestActivityCommon): def test_activity_flow_employee(self): - with self.sudoAs('ernest'): + with self.sudo('ernest'): test_record = self.env['mail.test.activity'].browse(self.test_record.id) self.assertEqual(test_record.activity_ids, self.env['mail.activity']) @@ -121,7 +121,7 @@ class TestActivityFlow(TestActivityCommon): def test_activity_flow_portal(self): portal_user = mail_new_test_user(self.env, login='chell', groups='base.group_portal', name='Chell Gladys') - with self.sudoAs('chell'): + with self.sudo('chell'): test_record = self.env['mail.test.activity'].browse(self.test_record.id) with self.assertRaises(exceptions.AccessError): self.env['mail.activity'].create({ @@ -164,7 +164,8 @@ class TestActivityMixin(TestActivityCommon): def test_activity_mixin(self): self.user_employee.tz = self.user_admin.tz - with self.sudoAs('ernest'): + with self.sudo('ernest'): + self.test_record = self.env['mail.test.activity'].browse(self.test_record.id) self.assertEqual(self.test_record.env.user, self.user_employee) now_utc = datetime.now(pytz.UTC) diff --git a/addons/test_mail_full/__init__.py b/addons/test_mail_full/__init__.py new file mode 100644 index 00000000000..567548c78f5 --- /dev/null +++ b/addons/test_mail_full/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import models +from . import tests diff --git a/addons/test_mail_full/__manifest__.py b/addons/test_mail_full/__manifest__.py new file mode 100644 index 00000000000..dab4550230a --- /dev/null +++ b/addons/test_mail_full/__manifest__.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- + +{ + 'name': 'Mail Tests (Full)', + 'version': '1.0', + 'category': 'Hidden', + 'sequence': 9876, + 'summary': 'Mail Tests: performances and tests specific to mail with all sub-modules', + 'description': """This module contains tests related to various mail features +and mail-related sub modules. Those tests are contained in a separate module as it +contains models used only to perform tests independently to functional aspects of +real applications. """, + 'depends': [ + 'test_mail', + 'mail', + 'mail_bot', + # 'snailmail', + 'mass_mailing', + 'phone_validation', + 'sms', + ], + 'data': [ + 'security/ir.model.access.csv', + ], + 'demo': [ + ], + 'installable': True, + 'application': False, +} diff --git a/addons/test_mail_full/models/__init__.py b/addons/test_mail_full/models/__init__.py new file mode 100644 index 00000000000..26ed2919633 --- /dev/null +++ b/addons/test_mail_full/models/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import test_mail_models diff --git a/addons/test_mail_full/models/test_mail_models.py b/addons/test_mail_full/models/test_mail_models.py new file mode 100644 index 00000000000..b351ba8ce45 --- /dev/null +++ b/addons/test_mail_full/models/test_mail_models.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class MailTestSMS(models.Model): + """ A model inheriting from mail.thread with some fields used for SMS + gateway, like a partner, a specific mobile phone, ... """ + _description = 'Chatter Model for SMS Gateway' + _name = 'mail.test.sms' + _inherit = ['mail.thread'] + + name = fields.Char() + subject = fields.Char() + email_from = fields.Char() + phone_nbr = fields.Char() + mobile_nbr = fields.Char() + customer_id = fields.Many2one('res.partner', 'Customer') + + @api.multi + def _get_default_sms_recipients(self): + return self.mapped('customer_id') diff --git a/addons/test_mail_full/security/ir.model.access.csv b/addons/test_mail_full/security/ir.model.access.csv new file mode 100644 index 00000000000..2506e6ac804 --- /dev/null +++ b/addons/test_mail_full/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_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 diff --git a/addons/test_mail_full/tests/__init__.py b/addons/test_mail_full/tests/__init__.py new file mode 100644 index 00000000000..3eac1b648f9 --- /dev/null +++ b/addons/test_mail_full/tests/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + +from . import common +from . import test_sms_composer +from . import test_sms_post diff --git a/addons/test_mail_full/tests/common.py b/addons/test_mail_full/tests/common.py new file mode 100644 index 00000000000..b9abbbec7dc --- /dev/null +++ b/addons/test_mail_full/tests/common.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- + +from odoo.addons.test_mail.tests import common as test_mail_common + + +class BaseFunctionalTest(test_mail_common.BaseFunctionalTest): + + @classmethod + def setUpClass(cls): + super(BaseFunctionalTest, cls).setUpClass() + cls.user_employee.write({'login': 'employee'}) + + # 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}) diff --git a/addons/test_mail_full/tests/test_sms_composer.py b/addons/test_mail_full/tests/test_sms_composer.py new file mode 100644 index 00000000000..bcdcbd32f1e --- /dev/null +++ b/addons/test_mail_full/tests/test_sms_composer.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.sms.tests import common as sms_common +from odoo.addons.test_mail.tests import common as test_mail_common +from odoo.addons.test_mail_full.tests import common as test_mail_full_common +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): + + @classmethod + def setUpClass(cls): + 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') + 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({ + 'message': self._test_body, + 'recipients': '+32475998877, 0475997788' + }) + + with self.mockSMSGateway(): + composer.action_send_sms() + # self.assertSMSSent(self.random_numbers, self._test_body) # TDE FIXME: actually sanitizer is not called on numbers in current master (saas 12.3) + self.assertSMSSent('+32475998877, 0475997788'.split(', '), self._test_body) + + def test_composer_no_mail_thread(self): + composer = self.env['sms.composer'].with_context(active_model='test_performance.base').create({ + 'message': self._test_body, + 'recipients': '+32475998877, 0475997788' + }) + + with self.mockSMSGateway(): + composer.action_send_sms() + # self.assertSMSSent(self.random_numbers, self._test_body) # TDE FIXME: actually sanitizer is not called on numbers in current master (saas 12.3) + self.assertSMSSent('+32475998877, 0475997788'.split(', '), self._test_body) + + def test_composer_partners_active_domain(self): + partners = self.partner_1 | self.partner_2 + composer = self.env['sms.composer'].with_context( + active_model='res.partner', + active_domain=[('id', 'in', partners.ids)] + ).create({ + 'message': self._test_body, + }) + + with self.mockSMSGateway(): + composer.action_send_sms() + self.assertSMSSent(self.partner_numbers, self._test_body) + + def test_composer_partners_sanitize(self): + partner_incorrect = self.env['res.partner'].create({ + 'name': 'Jean-Claude Incorrect', + 'email': 'jean.claude@example.com', + 'mobile': 'coincoin', + }) + partners = self.partner_1 | self.partner_2 | partner_incorrect + composer = self.env['sms.composer'].with_context( + active_model='res.partner', + active_domain=[('id', 'in', partners.ids)] + ).create({ + 'message': self._test_body, + }) + + with self.mockSMSGateway(): + composer.action_send_sms() + # self.assertSMSSent((self.partner_1 | self.partner_2).mapped('mobile'), test_body) # TDE FIXME: actually sanitizer does not work in current master (saas 12.23)) \ No newline at end of file diff --git a/addons/test_mail_full/tests/test_sms_post.py b/addons/test_mail_full/tests/test_sms_post.py new file mode 100644 index 00000000000..a12855341a0 --- /dev/null +++ b/addons/test_mail_full/tests/test_sms_post.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.sms.tests import common as sms_common +from odoo.addons.test_mail.tests import common as test_mail_common +from odoo.addons.test_mail_full.tests import common as test_mail_full_common + + +class 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_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) + + 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_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) + + 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_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) + + 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)
If you want to re-send them, click Cancel now, then click on the notification and review them one by one by clicking on the red envelope next to each message.