[MERGE][REF] mail, sms, various: refactor SMS implementation and add templates
PURPOSE
Refactor SMS to better integrate it in interface and Discuss. Allow users to
send texts in different business objects: crm.lead, event.event, etc. Introduce
SMS templates to ease mass SMS sending and scheduling.
DETAILS
This merge is mainly a big refactoring of SMS module. Future commits will come
with improvements in various addons to better include SMS, notably: crm,
events, scheduled actions, mass sms, marketing automation, stock, ...
This merge has been done in several main steps
* preparing mail to send hold other notification methods than Inbox and
email;
* refactor / improve posting with SMS notifications;
* improve SMS composer;
* add SMS templates and integrate them in composer;
* integrate into Discuss UX with wizards to resend and cancel failed
notifications;
* add tests;
See sub commits for more details.
Related to task 1922163
Co-Authored-By: Thibault Delavallee <tde@odoo.com>
Co-Authored-By: Pierre Rousseau <pro@odoo.com>
closes odoo/odoo#33510
Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
co-authored by
Thibault Delavallee
Pierre Rousseau
commit
4287481bf0
@@ -9,6 +9,7 @@
|
|||||||
'version': '1.0',
|
'version': '1.0',
|
||||||
'depends': ['calendar', 'sms'],
|
'depends': ['calendar', 'sms'],
|
||||||
'data': [
|
'data': [
|
||||||
|
'data/sms_data.xml',
|
||||||
'views/calendar_views.xml',
|
'views/calendar_views.xml',
|
||||||
],
|
],
|
||||||
'application': False,
|
'application': False,
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" ?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<record id="sms_template_data_calendar_reminder" model="sms.template">
|
||||||
|
<field name="name">Calendar Event: Reminder</field>
|
||||||
|
<field name="model_id" ref="calendar.model_calendar_event"/>
|
||||||
|
<field name="body">Event reminder: ${object.name} on ${object.start_datetime or object.start_date}</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
@@ -11,7 +11,7 @@ _logger = logging.getLogger(__name__)
|
|||||||
class CalendarEvent(models.Model):
|
class CalendarEvent(models.Model):
|
||||||
_inherit = 'calendar.event'
|
_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).
|
""" 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).
|
SMS text messages will be sent to attendees that haven't declined the event(s).
|
||||||
"""
|
"""
|
||||||
@@ -20,9 +20,12 @@ class CalendarEvent(models.Model):
|
|||||||
def _do_sms_reminder(self):
|
def _do_sms_reminder(self):
|
||||||
""" Send an SMS text reminder to attendees that haven't declined the event """
|
""" Send an SMS text reminder to attendees that haven't declined the event """
|
||||||
for event in self:
|
for event in self:
|
||||||
sms_msg = _("Event reminder: %s on %s.") % (event.name, event.start_datetime or event.start_date)
|
event._message_sms_with_template(
|
||||||
note_msg = _('SMS text message reminder sent !')
|
template_xmlid='calendar_sms.sms_template_data_calendar_reminder',
|
||||||
event.message_post_send_sms(sms_msg, note_msg=note_msg)
|
template_fallback=_("Event reminder: %s on %s.") % (event.name, event.start_datetime or event.start_date),
|
||||||
|
partner_ids=self._sms_get_default_partners().ids,
|
||||||
|
put_in_queue=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CalendarAlarm(models.Model):
|
class CalendarAlarm(models.Model):
|
||||||
|
|||||||
@@ -396,3 +396,9 @@ class HrEmployeePrivate(models.Model):
|
|||||||
@api.returns('mail.message', lambda value: value.id)
|
@api.returns('mail.message', lambda value: value.id)
|
||||||
def message_post(self, *args, **kwargs):
|
def message_post(self, *args, **kwargs):
|
||||||
return super(HrEmployeePrivate, self._post_author()).message_post(*args, **kwargs)
|
return super(HrEmployeePrivate, self._post_author()).message_post(*args, **kwargs)
|
||||||
|
|
||||||
|
def _sms_get_default_partners(self):
|
||||||
|
return self.mapped('user_partner_id')
|
||||||
|
|
||||||
|
def _sms_get_number_fields(self):
|
||||||
|
return ['mobile_phone']
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ Allows to contact directly the employee in case of unjustified absence.
|
|||||||
'views/res_config_settings_views.xml',
|
'views/res_config_settings_views.xml',
|
||||||
'data/ir_actions_server.xml',
|
'data/ir_actions_server.xml',
|
||||||
'views/hr_employee_views.xml',
|
'views/hr_employee_views.xml',
|
||||||
|
'data/sms_data.xml',
|
||||||
'data/mail_data.xml',
|
'data/mail_data.xml',
|
||||||
],
|
],
|
||||||
'demo': [],
|
'demo': [],
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" ?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<record id="sms_template_data_hr_presence" model="sms.template">
|
||||||
|
<field name="name">Employee: Presence Reminder</field>
|
||||||
|
<field name="model_id" ref="hr.model_hr_employee"/>
|
||||||
|
<field name="body">Exception made if there was a mistake of ours, it seems that you are not at your office and there is not request of leaves from you.
|
||||||
|
Please, take appropriate measures in order to carry out this work absence.
|
||||||
|
Do not hesitate to contact your manager or the human resource department.</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
@@ -122,24 +122,33 @@ class ResCompany(models.Model):
|
|||||||
"context": {'default_employee_id': self.id},
|
"context": {'default_employee_id': self.id},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --------------------------------------------------
|
||||||
|
# Messaging
|
||||||
|
# --------------------------------------------------
|
||||||
|
|
||||||
def action_send_sms(self):
|
def action_send_sms(self):
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
if not self.env.user.has_group('hr.group_hr_manager'):
|
if not self.env.user.has_group('hr.group_hr_manager'):
|
||||||
raise UserError(_("You don't have the right to do this. Please contact an Administrator."))
|
raise UserError(_("You don't have the right to do this. Please contact an Administrator."))
|
||||||
if not self.mobile_phone:
|
if not self.mobile_phone:
|
||||||
raise UserError(_("There is no professional phone for this employee."))
|
raise UserError(_("There is no professional phone for this employee."))
|
||||||
body = _("""Exception made if there was a mistake of ours, it seems that you are not at your office and there is not request of leaves from you.
|
|
||||||
|
context = dict(self.env.context)
|
||||||
|
context.update(default_res_model='hr.employee', default_res_id=self.id, default_composition_mode='comment', default_number_field_name='mobile_phone')
|
||||||
|
|
||||||
|
template = self.env.ref('hr_presence.sms_template_presence', False)
|
||||||
|
if not template:
|
||||||
|
context['default_body'] = _("""Exception made if there was a mistake of ours, it seems that you are not at your office and there is not request of leaves from you.
|
||||||
Please, take appropriate measures in order to carry out this work absence.
|
Please, take appropriate measures in order to carry out this work absence.
|
||||||
Do not hesitate to contact your manager or the human resource department.""")
|
Do not hesitate to contact your manager or the human resource department.""")
|
||||||
|
else:
|
||||||
|
context['default_template_id'] = template.id
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"type": "ir.actions.act_window",
|
"type": "ir.actions.act_window",
|
||||||
"res_model": "sms.composer",
|
"res_model": "sms.composer",
|
||||||
"view_mode": 'form',
|
"view_mode": 'form',
|
||||||
"context": {
|
"context": context,
|
||||||
'active_id': self.id,
|
|
||||||
'default_message': body,
|
|
||||||
'default_recipients': self.mobile_phone,
|
|
||||||
},
|
|
||||||
"name": "Send SMS",
|
"name": "Send SMS",
|
||||||
"target": "new",
|
"target": "new",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -503,15 +503,10 @@ class Channel(models.Model):
|
|||||||
notifications.append([(self._cr.dbname, 'res.partner', partner.id), channel_info])
|
notifications.append([(self._cr.dbname, 'res.partner', partner.id), channel_info])
|
||||||
return notifications
|
return notifications
|
||||||
|
|
||||||
def _notify_thread(self, message, msg_vals=False, model_description=False, mail_auto_delete=True):
|
def _notify_thread(self, message, msg_vals=False, **kwargs):
|
||||||
# When posting a message on a mail channel, manage moderation and postpone notify users
|
# When posting a message on a mail channel, manage moderation and postpone notify users
|
||||||
if not msg_vals or msg_vals.get('moderation_status') != 'pending_moderation':
|
if not msg_vals or msg_vals.get('moderation_status') != 'pending_moderation':
|
||||||
super(Channel, self)._notify_thread(
|
super(Channel, self)._notify_thread(message, msg_vals=msg_vals, **kwargs)
|
||||||
message,
|
|
||||||
msg_vals=msg_vals,
|
|
||||||
model_description=model_description,
|
|
||||||
mail_auto_delete=mail_auto_delete,
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
message._notify_pending_by_chat()
|
message._notify_pending_by_chat()
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ class Followers(models.Model):
|
|||||||
# Private tools methods to fetch followers data
|
# Private tools methods to fetch followers data
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
|
|
||||||
def _get_recipient_data(self, records, subtype_id, pids=None, cids=None):
|
def _get_recipient_data(self, records, message_type, subtype_id, pids=None, cids=None):
|
||||||
""" Private method allowing to fetch recipients data based on a subtype.
|
""" Private method allowing to fetch recipients data based on a subtype.
|
||||||
Purpose of this method is to fetch all data necessary to notify recipients
|
Purpose of this method is to fetch all data necessary to notify recipients
|
||||||
in a single query. It fetches data from
|
in a single query. It fetches data from
|
||||||
@@ -92,6 +92,7 @@ class Followers(models.Model):
|
|||||||
* channels if cids is given;
|
* channels if cids is given;
|
||||||
|
|
||||||
:param records: fetch data from followers of records that follow subtype_id;
|
:param records: fetch data from followers of records that follow subtype_id;
|
||||||
|
:param message_type: mail.message.message_type in order to allow custom behavior depending on it (SMS for example);
|
||||||
:param subtype_id: mail.message.subtype to check against followers;
|
:param subtype_id: mail.message.subtype to check against followers;
|
||||||
:param pids: additional set of partner IDs from which to fetch recipient data;
|
:param pids: additional set of partner IDs from which to fetch recipient data;
|
||||||
:param cids: additional set of channel IDs from which to fetch recipient data;
|
:param cids: additional set of channel IDs from which to fetch recipient data;
|
||||||
|
|||||||
@@ -154,24 +154,24 @@ class MailMail(models.Model):
|
|||||||
notif_mails_ids = [mail.id for mail in self if mail.notification]
|
notif_mails_ids = [mail.id for mail in self if mail.notification]
|
||||||
if notif_mails_ids:
|
if notif_mails_ids:
|
||||||
notifications = self.env['mail.notification'].search([
|
notifications = self.env['mail.notification'].search([
|
||||||
('is_email', '=', True),
|
('notification_type', '=', 'email'),
|
||||||
('mail_id', 'in', notif_mails_ids),
|
('mail_id', 'in', notif_mails_ids),
|
||||||
('email_status', 'not in', ('sent', 'canceled'))
|
('notification_status', 'not in', ('sent', 'canceled'))
|
||||||
])
|
])
|
||||||
if notifications:
|
if notifications:
|
||||||
#find all notification linked to a failure
|
# find all notification linked to a failure
|
||||||
failed = self.env['mail.notification']
|
failed = self.env['mail.notification']
|
||||||
if failure_type:
|
if failure_type:
|
||||||
failed = notifications.filtered(lambda notif: notif.res_partner_id not in success_pids)
|
failed = notifications.filtered(lambda notif: notif.res_partner_id not in success_pids)
|
||||||
failed.sudo().write({
|
failed.sudo().write({
|
||||||
'email_status': 'exception',
|
'notification_status': 'exception',
|
||||||
'failure_type': failure_type,
|
'failure_type': failure_type,
|
||||||
'failure_reason': failure_reason,
|
'failure_reason': failure_reason,
|
||||||
})
|
})
|
||||||
messages = notifications.mapped('mail_message_id').filtered(lambda m: m.is_thread_message())
|
messages = notifications.mapped('mail_message_id').filtered(lambda m: m.is_thread_message())
|
||||||
messages._notify_failure_update() # notify user that we have a failure
|
messages._notify_mail_failure_update() # notify user that we have a failure
|
||||||
(notifications - failed).sudo().write({
|
(notifications - failed).sudo().write({
|
||||||
'email_status': 'sent',
|
'notification_status': 'sent',
|
||||||
'failure_type': '',
|
'failure_type': '',
|
||||||
'failure_reason': '',
|
'failure_reason': '',
|
||||||
})
|
})
|
||||||
@@ -335,14 +335,14 @@ class MailMail(models.Model):
|
|||||||
# update in case an email bounces while sending all emails related to current
|
# update in case an email bounces while sending all emails related to current
|
||||||
# mail record.
|
# mail record.
|
||||||
notifs = self.env['mail.notification'].search([
|
notifs = self.env['mail.notification'].search([
|
||||||
('is_email', '=', True),
|
('notification_type', '=', 'email'),
|
||||||
('mail_id', 'in', mail.ids),
|
('mail_id', 'in', mail.ids),
|
||||||
('email_status', 'not in', ('sent', 'canceled'))
|
('notification_status', 'not in', ('sent', 'canceled'))
|
||||||
])
|
])
|
||||||
if notifs:
|
if notifs:
|
||||||
notif_msg = _('Error without exception. Probably due do concurrent access update of notification records. Please see with an administrator.')
|
notif_msg = _('Error without exception. Probably due do concurrent access update of notification records. Please see with an administrator.')
|
||||||
notifs.sudo().write({
|
notifs.sudo().write({
|
||||||
'email_status': 'exception',
|
'notification_status': 'exception',
|
||||||
'failure_type': 'UNKNOWN',
|
'failure_type': 'UNKNOWN',
|
||||||
'failure_reason': notif_msg,
|
'failure_reason': notif_msg,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import logging
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from binascii import Error as binascii_error
|
from binascii import Error as binascii_error
|
||||||
|
from collections import defaultdict
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
from email.utils import formataddr
|
from email.utils import formataddr
|
||||||
from openerp.http import request
|
from openerp.http import request
|
||||||
@@ -148,15 +149,15 @@ class Message(models.Model):
|
|||||||
def _compute_has_error(self):
|
def _compute_has_error(self):
|
||||||
error_from_notification = self.env['mail.notification'].sudo().search([
|
error_from_notification = self.env['mail.notification'].sudo().search([
|
||||||
('mail_message_id', 'in', self.ids),
|
('mail_message_id', 'in', self.ids),
|
||||||
('email_status', 'in', ('bounce', 'exception'))]).mapped('mail_message_id')
|
('notification_status', 'in', ('bounce', 'exception'))]).mapped('mail_message_id')
|
||||||
for message in self:
|
for message in self:
|
||||||
message.has_error = message in error_from_notification
|
message.has_error = message in error_from_notification
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def _search_has_error(self, operator, operand):
|
def _search_has_error(self, operator, operand):
|
||||||
if operator == '=' and operand:
|
if operator == '=' and operand:
|
||||||
return [('notification_ids.email_status', 'in', ('bounce', 'exception'))]
|
return [('notification_ids.notification_status', 'in', ('bounce', 'exception'))]
|
||||||
return ['!', ('notification_ids.email_status', 'in', ('bounce', 'exception'))] # this wont work and will be equivalent to "not in" beacause of orm restrictions. Dont use "has_error = False"
|
return ['!', ('notification_ids.notification_status', 'in', ('bounce', 'exception'))] # this wont work and will be equivalent to "not in" beacause of orm restrictions. Dont use "has_error = False"
|
||||||
|
|
||||||
@api.depends('starred_partner_ids')
|
@api.depends('starred_partner_ids')
|
||||||
def _get_starred(self):
|
def _get_starred(self):
|
||||||
@@ -373,25 +374,22 @@ class Message(models.Model):
|
|||||||
partner_ids = []
|
partner_ids = []
|
||||||
if message.subtype_id:
|
if message.subtype_id:
|
||||||
partner_ids = [partner_tree[partner.id] for partner in message.partner_ids
|
partner_ids = [partner_tree[partner.id] for partner in message.partner_ids
|
||||||
if partner.id in partner_tree]
|
if partner.id in partner_tree]
|
||||||
else:
|
else:
|
||||||
partner_ids = [partner_tree[partner.id] for partner in message.partner_ids
|
partner_ids = [partner_tree[partner.id] for partner in message.partner_ids
|
||||||
if partner.id in partner_tree]
|
if partner.id in partner_tree]
|
||||||
# we read customer_email_status before filtering inactive user because we don't want to miss a red enveloppe
|
# we read customer_email_status before filtering inactive user because we don't want to miss a red enveloppe
|
||||||
customer_email_status = (
|
customer_email_status = (
|
||||||
(all(n.email_status == 'sent' for n in message.notification_ids) and 'sent') or
|
(all(n.notification_status == 'sent' for n in message.notification_ids if n.notification_type == 'email') and 'sent') or
|
||||||
(any(n.email_status == 'exception' for n in message.notification_ids) and 'exception') or
|
(any(n.notification_status == 'exception' for n in message.notification_ids if n.notification_type == 'email') and 'exception') or
|
||||||
(any(n.email_status == 'bounce' for n in message.notification_ids) and 'bounce') or
|
(any(n.notification_status == 'bounce' for n in message.notification_ids if n.notification_type == 'email') and 'bounce') or
|
||||||
'ready'
|
'ready'
|
||||||
)
|
)
|
||||||
customer_email_data = []
|
customer_email_data = []
|
||||||
def filter_notification(notif):
|
for notification in message.notification_ids.filtered(
|
||||||
return (
|
lambda n: n.notification_type == 'email' and n.res_partner_id.active and
|
||||||
(notif.email_status in ('bounce', 'exception', 'canceled') or notif.res_partner_id.partner_share) and
|
(n.notification_status in ('bounce', 'exception', 'canceled') or n.res_partner_id.partner_share)):
|
||||||
notif.res_partner_id.active
|
customer_email_data.append((partner_tree[notification.res_partner_id.id][0], partner_tree[notification.res_partner_id.id][1], notification.notification_status))
|
||||||
)
|
|
||||||
for notification in message.notification_ids.filtered(filter_notification):
|
|
||||||
customer_email_data.append((partner_tree[notification.res_partner_id.id][0], partner_tree[notification.res_partner_id.id][1], notification.email_status))
|
|
||||||
|
|
||||||
has_access_to_model = message.model and self.env[message.model].check_access_rights('read', raise_exception=False)
|
has_access_to_model = message.model and self.env[message.model].check_access_rights('read', raise_exception=False)
|
||||||
if message.attachment_ids and message.res_id and issubclass(self.pool[message.model], self.pool['mail.thread']) and has_access_to_model:
|
if message.attachment_ids and message.res_id and issubclass(self.pool[message.model], self.pool['mail.thread']) and has_access_to_model:
|
||||||
@@ -512,7 +510,7 @@ class Message(models.Model):
|
|||||||
|
|
||||||
# fetch notification status
|
# fetch notification status
|
||||||
notif_dict = {}
|
notif_dict = {}
|
||||||
notifs = self.env['mail.notification'].sudo().search([('mail_message_id', 'in', list(mid for mid in message_tree)), ('is_read', '=', False)])
|
notifs = self.env['mail.notification'].sudo().search([('mail_message_id', 'in', list(mid for mid in message_tree)), ('res_partner_id', '!=', False), ('is_read', '=', False)])
|
||||||
for notif in notifs:
|
for notif in notifs:
|
||||||
mid = notif.mail_message_id.id
|
mid = notif.mail_message_id.id
|
||||||
if not notif_dict.get(mid):
|
if not notif_dict.get(mid):
|
||||||
@@ -540,13 +538,45 @@ class Message(models.Model):
|
|||||||
'moderation_status',
|
'moderation_status',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def _get_mail_failure_dict(self):
|
||||||
|
return {
|
||||||
|
'message_id': self.id,
|
||||||
|
'record_name': self.record_name,
|
||||||
|
'model_name': self.env['ir.model']._get(self.model).display_name,
|
||||||
|
'uuid': self.message_id,
|
||||||
|
'res_id': self.res_id,
|
||||||
|
'model': self.model,
|
||||||
|
'last_message_date': self.date,
|
||||||
|
'module_icon': '/mail/static/src/img/smiley/mailfailure.jpg',
|
||||||
|
}
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def _format_mail_failures(self):
|
def _format_mail_failures(self):
|
||||||
"""
|
""" A shorter message to notify a failure update """
|
||||||
A shorter message to notify a failure update
|
|
||||||
"""
|
|
||||||
failures_infos = []
|
failures_infos = []
|
||||||
|
|
||||||
|
# prepare notifications computation in batch
|
||||||
|
all_notifications = self.env['mail.notification'].sudo().search([
|
||||||
|
('mail_message_id', 'in', self.ids)
|
||||||
|
])
|
||||||
|
msgid_to_notif = defaultdict(lambda: self.env['mail.notification'].sudo())
|
||||||
|
for notif in all_notifications:
|
||||||
|
msgid_to_notif[notif.mail_message_id.id] += notif
|
||||||
|
|
||||||
# for each channel, build the information header and include the logged partner information
|
# for each channel, build the information header and include the logged partner information
|
||||||
|
for message in self:
|
||||||
|
notifications = msgid_to_notif[message.id]
|
||||||
|
if not any(notification.notification_type == 'email' for notification in notifications):
|
||||||
|
continue
|
||||||
|
info = dict(message._get_mail_failure_dict(),
|
||||||
|
failure_type='mail',
|
||||||
|
notifications=dict((notif.res_partner_id.id, (notif.notification_status, notif.res_partner_id.name)) for notif in notifications))
|
||||||
|
failures_infos.append(info)
|
||||||
|
return failures_infos
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def _notify_mail_failure_update(self):
|
||||||
|
messages = self.env['mail.message']
|
||||||
for message in self:
|
for message in self:
|
||||||
# Check if user has access to the record before displaying a notification about it.
|
# Check if user has access to the record before displaying a notification about it.
|
||||||
# In case the user switches from one company to another, it might happen that he doesn't
|
# In case the user switches from one company to another, it might happen that he doesn't
|
||||||
@@ -558,24 +588,10 @@ class Message(models.Model):
|
|||||||
record.check_access_rule('read')
|
record.check_access_rule('read')
|
||||||
except AccessError:
|
except AccessError:
|
||||||
continue
|
continue
|
||||||
info = {
|
else:
|
||||||
'message_id': message.id,
|
messages |= message
|
||||||
'record_name': message.record_name,
|
|
||||||
'model_name': self.env['ir.model']._get(message.model).display_name,
|
|
||||||
'uuid': message.message_id,
|
|
||||||
'res_id': message.res_id,
|
|
||||||
'model': message.model,
|
|
||||||
'last_message_date': message.date,
|
|
||||||
'module_icon': '/mail/static/src/img/smiley/mailfailure.jpg',
|
|
||||||
'notifications': dict((notif.res_partner_id.id, (notif.email_status, notif.res_partner_id.name)) for notif in message.notification_ids.sudo())
|
|
||||||
}
|
|
||||||
failures_infos.append(info)
|
|
||||||
return failures_infos
|
|
||||||
|
|
||||||
@api.multi
|
for author, author_messages in groupby(messages, itemgetter('author_id')):
|
||||||
def _notify_failure_update(self):
|
|
||||||
authors = {}
|
|
||||||
for author, author_messages in groupby(self, itemgetter('author_id')):
|
|
||||||
self.env['bus.bus'].sendone(
|
self.env['bus.bus'].sendone(
|
||||||
(self._cr.dbname, 'res.partner', author.id),
|
(self._cr.dbname, 'res.partner', author.id),
|
||||||
{'type': 'mail_failure', 'elements': self.env['mail.message'].concat(*author_messages)._format_mail_failures()}
|
{'type': 'mail_failure', 'elements': self.env['mail.message'].concat(*author_messages)._format_mail_failures()}
|
||||||
|
|||||||
@@ -14,10 +14,12 @@ class Notification(models.Model):
|
|||||||
mail_message_id = fields.Many2one(
|
mail_message_id = fields.Many2one(
|
||||||
'mail.message', 'Message', index=True, ondelete='cascade', required=True)
|
'mail.message', 'Message', index=True, ondelete='cascade', required=True)
|
||||||
res_partner_id = fields.Many2one(
|
res_partner_id = fields.Many2one(
|
||||||
'res.partner', 'Needaction Recipient', index=True, ondelete='cascade', required=True)
|
'res.partner', 'Needaction Recipient', index=True, ondelete='cascade', required=False)
|
||||||
is_read = fields.Boolean('Is Read', index=True)
|
is_read = fields.Boolean('Is Read', index=True)
|
||||||
is_email = fields.Boolean('Sent by Email', index=True)
|
notification_type = fields.Selection([
|
||||||
email_status = fields.Selection([
|
('inbox', 'Inbox'), ('email', 'Email')], string='Notification Type',
|
||||||
|
default='inbox', index=True, required=True)
|
||||||
|
notification_status = fields.Selection([
|
||||||
('ready', 'Ready to Send'),
|
('ready', 'Ready to Send'),
|
||||||
('sent', 'Sent'),
|
('sent', 'Sent'),
|
||||||
('bounce', 'Bounced'),
|
('bounce', 'Bounced'),
|
||||||
@@ -37,10 +39,17 @@ class Notification(models.Model):
|
|||||||
], string='Failure type')
|
], string='Failure type')
|
||||||
failure_reason = fields.Text('Failure reason', copy=False)
|
failure_reason = fields.Text('Failure reason', copy=False)
|
||||||
|
|
||||||
|
_sql_constraints = [
|
||||||
|
# email notification;: partner is required
|
||||||
|
('notification_partner_required',
|
||||||
|
"CHECK(notification_type NOT IN ('email', 'inbox') OR res_partner_id IS NOT NULL)",
|
||||||
|
'Customer is required for inbox / email notification'),
|
||||||
|
]
|
||||||
|
|
||||||
def init(self):
|
def init(self):
|
||||||
self._cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('mail_notification_res_partner_id_is_read_email_status_mail_message_id',))
|
self._cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('mail_notification_res_partner_id_is_read_notification_status_mail_message_id',))
|
||||||
if not self._cr.fetchone():
|
if not self._cr.fetchone():
|
||||||
self._cr.execute('CREATE INDEX mail_notification_res_partner_id_is_read_email_status_mail_message_id ON mail_message_res_partner_needaction_rel (res_partner_id, is_read, email_status, mail_message_id)')
|
self._cr.execute('CREATE INDEX mail_notification_res_partner_id_is_read_notification_status_mail_message_id ON mail_message_res_partner_needaction_rel (res_partner_id, is_read, notification_status, mail_message_id)')
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def format_failure_reason(self):
|
def format_failure_reason(self):
|
||||||
@@ -49,5 +58,3 @@ class Notification(models.Model):
|
|||||||
return dict(type(self).failure_type.selection).get(self.failure_type, _('No Error'))
|
return dict(type(self).failure_type.selection).get(self.failure_type, _('No Error'))
|
||||||
else:
|
else:
|
||||||
return _("Unknown error") + ": %s" % (self.failure_reason or '')
|
return _("Unknown error") + ": %s" % (self.failure_reason or '')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ class MailThread(models.AbstractModel):
|
|||||||
if self.ids:
|
if self.ids:
|
||||||
self._cr.execute(""" SELECT msg.res_id, COUNT(msg.res_id) FROM mail_message msg
|
self._cr.execute(""" SELECT msg.res_id, COUNT(msg.res_id) FROM mail_message msg
|
||||||
RIGHT JOIN mail_message_res_partner_needaction_rel rel
|
RIGHT JOIN mail_message_res_partner_needaction_rel rel
|
||||||
ON rel.mail_message_id = msg.id AND rel.email_status in ('exception','bounce')
|
ON rel.mail_message_id = msg.id AND rel.notification_status in ('exception','bounce')
|
||||||
WHERE msg.author_id = %s AND msg.model = %s AND msg.res_id in %s AND msg.message_type != 'user_notification'
|
WHERE msg.author_id = %s AND msg.model = %s AND msg.res_id in %s AND msg.message_type != 'user_notification'
|
||||||
GROUP BY msg.res_id""",
|
GROUP BY msg.res_id""",
|
||||||
(self.env.user.partner_id.id, self._name, tuple(self.ids),))
|
(self.env.user.partner_id.id, self._name, tuple(self.ids),))
|
||||||
@@ -915,7 +915,7 @@ class MailThread(models.AbstractModel):
|
|||||||
('mail_message_id', '=', mail_message.id),
|
('mail_message_id', '=', mail_message.id),
|
||||||
('res_partner_id', 'in', partners.ids)])
|
('res_partner_id', 'in', partners.ids)])
|
||||||
notifications.write({
|
notifications.write({
|
||||||
'email_status': 'bounce'
|
'notification_status': 'bounce'
|
||||||
})
|
})
|
||||||
|
|
||||||
if bounced_model in self.env and hasattr(self.env[bounced_model], '_message_receive_bounce') and bounced_thread_id:
|
if bounced_model in self.env and hasattr(self.env[bounced_model], '_message_receive_bounce') and bounced_thread_id:
|
||||||
@@ -1679,7 +1679,7 @@ class MailThread(models.AbstractModel):
|
|||||||
email_from=False, author_id=None, parent_id=False,
|
email_from=False, author_id=None, parent_id=False,
|
||||||
subtype_id=False, subtype=None, partner_ids=None, channel_ids=None,
|
subtype_id=False, subtype=None, partner_ids=None, channel_ids=None,
|
||||||
attachments=None, attachment_ids=None,
|
attachments=None, attachment_ids=None,
|
||||||
add_sign=True, model_description=False, mail_auto_delete=True, record_name=False,
|
add_sign=True, record_name=False,
|
||||||
**kwargs):
|
**kwargs):
|
||||||
""" Post a new message in an existing thread, returning the new
|
""" Post a new message in an existing thread, returning the new
|
||||||
mail.message ID.
|
mail.message ID.
|
||||||
@@ -1707,11 +1707,14 @@ class MailThread(models.AbstractModel):
|
|||||||
:return int: ID of newly created mail.message
|
:return int: ID of newly created mail.message
|
||||||
"""
|
"""
|
||||||
self.ensure_one() # should always be posted on a record, use message_notify if no record
|
self.ensure_one() # should always be posted on a record, use message_notify if no record
|
||||||
|
# split message additional values from notify additional values
|
||||||
|
msg_kwargs = dict((key, val) for key, val in kwargs.items() if key in self.env['mail.message']._fields)
|
||||||
|
notif_kwargs = dict((key, val) for key, val in kwargs.items() if key not in msg_kwargs)
|
||||||
|
|
||||||
if self._name == 'mail.thread' or not self.id or message_type == 'user_notification':
|
if self._name == 'mail.thread' or not self.id or message_type == 'user_notification':
|
||||||
raise ValueError('message_post should only be call to post message on record. Use message_notify instead')
|
raise ValueError('message_post should only be call to post message on record. Use message_notify instead')
|
||||||
|
|
||||||
if 'model' in kwargs or 'res_id' in kwargs:
|
if 'model' in msg_kwargs or 'res_id' in msg_kwargs:
|
||||||
raise ValueError("message_post doesn't support model and res_id parameters anymore. Please call message_post on record")
|
raise ValueError("message_post doesn't support model and res_id parameters anymore. Please call message_post on record")
|
||||||
|
|
||||||
self = self.with_lang() # add lang to context imediatly since it will be usefull in various flows latter.
|
self = self.with_lang() # add lang to context imediatly since it will be usefull in various flows latter.
|
||||||
@@ -1746,7 +1749,7 @@ class MailThread(models.AbstractModel):
|
|||||||
# parent_message searched in sudo for performance, only used for id.
|
# parent_message searched in sudo for performance, only used for id.
|
||||||
# Note that with sudo we will match message with internal subtypes.
|
# Note that with sudo we will match message with internal subtypes.
|
||||||
parent_id = parent_message.id if parent_message else False
|
parent_id = parent_message.id if parent_message else False
|
||||||
elif parent_id:
|
elif parent_id:
|
||||||
old_parent_id = parent_id
|
old_parent_id = parent_id
|
||||||
parent_message = MailMessage_sudo.search([('id', '=', parent_id), ('parent_id', '!=', False)], limit=1)
|
parent_message = MailMessage_sudo.search([('id', '=', parent_id), ('parent_id', '!=', False)], limit=1)
|
||||||
# avoid loops when finding ancestors
|
# avoid loops when finding ancestors
|
||||||
@@ -1757,7 +1760,8 @@ class MailThread(models.AbstractModel):
|
|||||||
processed_list.append(new_parent_id)
|
processed_list.append(new_parent_id)
|
||||||
parent_message = parent_message.parent_id
|
parent_message = parent_message.parent_id
|
||||||
parent_id = parent_message.id
|
parent_id = parent_message.id
|
||||||
values = dict(kwargs)
|
|
||||||
|
values = dict(msg_kwargs)
|
||||||
values.update({
|
values.update({
|
||||||
'author_id': author_id,
|
'author_id': author_id,
|
||||||
'model': self._name,
|
'model': self._name,
|
||||||
@@ -1776,19 +1780,19 @@ class MailThread(models.AbstractModel):
|
|||||||
attachments = attachments or []
|
attachments = attachments or []
|
||||||
attachment_ids = attachment_ids or []
|
attachment_ids = attachment_ids or []
|
||||||
attachement_values = self._message_post_process_attachments(attachments, attachment_ids, values)
|
attachement_values = self._message_post_process_attachments(attachments, attachment_ids, values)
|
||||||
values.update(attachement_values) # attachement_ids, [body]
|
values.update(attachement_values) # attachement_ids, [body]
|
||||||
|
|
||||||
new_message= self._message_create(values)
|
new_message = self._message_create(values)
|
||||||
|
|
||||||
# Set main attachment field if necessary
|
# Set main attachment field if necessary
|
||||||
self._message_set_main_attachment_id(values['attachment_ids'])
|
self._message_set_main_attachment_id(values['attachment_ids'])
|
||||||
|
|
||||||
if values['author_id'] and values['message_type'] != 'notification' and not self._context.get('mail_create_nosubscribe'):
|
if values['author_id'] and values['message_type'] != 'notification' and not self._context.get('mail_create_nosubscribe'):
|
||||||
#if self.env['res.partner'].browse(values['author_id']).active: # we dont want to add odoobot/inactive as a follower
|
# if self.env['res.partner'].browse(values['author_id']).active: # we dont want to add odoobot/inactive as a follower
|
||||||
self._message_subscribe([values['author_id']])
|
self._message_subscribe([values['author_id']])
|
||||||
|
|
||||||
self._message_post_after_hook(new_message, values)
|
self._message_post_after_hook(new_message, values)
|
||||||
self._notify_thread(new_message, values, model_description=model_description, mail_auto_delete=mail_auto_delete)
|
self._notify_thread(new_message, values, **notif_kwargs)
|
||||||
return new_message
|
return new_message
|
||||||
|
|
||||||
def _message_set_main_attachment_id(self, attachment_ids): # todo move this out of mail.thread
|
def _message_set_main_attachment_id(self, attachment_ids): # todo move this out of mail.thread
|
||||||
@@ -1863,14 +1867,15 @@ class MailThread(models.AbstractModel):
|
|||||||
return composer.send_mail()
|
return composer.send_mail()
|
||||||
|
|
||||||
def message_notify(self, partner_ids=False, parent_id=False, model=False, res_id=False,
|
def message_notify(self, partner_ids=False, parent_id=False, model=False, res_id=False,
|
||||||
author_id=False, body='', subject=False, model_description=False,
|
author_id=False, body='', subject=False, **kwargs):
|
||||||
mail_auto_delete=True, **kwargs):
|
|
||||||
""" Shortcut allowing to notify partners of messages that shouldn't be
|
""" Shortcut allowing to notify partners of messages that shouldn't be
|
||||||
displayed on a document. It pushes notifications on inbox or by email depending
|
displayed on a document. It pushes notifications on inbox or by email depending
|
||||||
on the user configuration, like other notifications. """
|
on the user configuration, like other notifications. """
|
||||||
|
|
||||||
if self:
|
if self:
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
|
# split message additional values from notify additional values
|
||||||
|
msg_kwargs = dict((key, val) for key, val in kwargs.items() if key in self.env['mail.message']._fields)
|
||||||
|
notif_kwargs = dict((key, val) for key, val in kwargs.items() if key not in msg_kwargs)
|
||||||
|
|
||||||
if author_id:
|
if author_id:
|
||||||
author = self.env['res.partner'].sudo().browse(author_id)
|
author = self.env['res.partner'].sudo().browse(author_id)
|
||||||
@@ -1914,9 +1919,9 @@ class MailThread(models.AbstractModel):
|
|||||||
'reply_to': MailThread._notify_get_reply_to(default=email_from, records=None)[False],
|
'reply_to': MailThread._notify_get_reply_to(default=email_from, records=None)[False],
|
||||||
'message_id': tools.generate_tracking_message_id('message-notify'),
|
'message_id': tools.generate_tracking_message_id('message-notify'),
|
||||||
}
|
}
|
||||||
values.update(kwargs)
|
values.update(msg_kwargs)
|
||||||
new_message = MailThread._message_create(values)
|
new_message = MailThread._message_create(values)
|
||||||
MailThread._notify_thread(new_message, values, model_description=model_description, mail_auto_delete=mail_auto_delete)
|
MailThread._notify_thread(new_message, values, **notif_kwargs)
|
||||||
return new_message
|
return new_message
|
||||||
|
|
||||||
def _message_log(self, body='', author_id=None, subject=False, message_type='notification', **kwargs):
|
def _message_log(self, body='', author_id=None, subject=False, message_type='notification', **kwargs):
|
||||||
@@ -1972,24 +1977,27 @@ class MailThread(models.AbstractModel):
|
|||||||
# ------------------------------------------------------
|
# ------------------------------------------------------
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def _notify_thread(self, message, msg_vals=False, model_description=False, mail_auto_delete=True):
|
def _notify_thread(self, message, msg_vals=False, **kwargs):
|
||||||
""" Main notification method. This method basically does two things
|
""" Main notification method. This method basically does two things
|
||||||
|
|
||||||
* call ``_notify_compute_recipients`` that computes recipients to
|
* call ``_notify_compute_recipients`` that computes recipients to
|
||||||
notify based on message record or message creation values if given
|
notify based on message record or message creation values if given
|
||||||
(to optimize performance if we already have data computed);
|
(to optimize performance if we already have data computed);
|
||||||
* performs the notification process;
|
* performs the notification process by calling the various notification
|
||||||
Can be overridden to intercept and postpone notification mecanism (mail.channel moderation)
|
methods implemented;
|
||||||
:param message: posted message;
|
|
||||||
:param msg_vals: dictionary of values used to create the message. If given
|
|
||||||
it is used instead of accessing ``self`` to lesen query count in some
|
|
||||||
simple cases where no notification is actually required;
|
|
||||||
:param force_send: tells whether to send notification emails within the
|
|
||||||
current transaction or to use the email queue;
|
|
||||||
:param model_description: optional data used in notification process (see
|
|
||||||
notification templates);
|
|
||||||
:param mail_auto_delete: delete notification emails once sent;
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
This method cnn be overridden to intercept and postpone notification
|
||||||
|
mechanism like mail.channel moderation.
|
||||||
|
|
||||||
|
:param message: mail.message record to notify;
|
||||||
|
:param msg_vals: dictionary of values used to create the message. If given
|
||||||
|
it is used instead of accessing ``self`` to lessen query count in some
|
||||||
|
simple cases where no notification is actually required;
|
||||||
|
|
||||||
|
Kwargs allow to pass various parameters that are given to sub notification
|
||||||
|
methods. See those methods for more details about the additional parameters.
|
||||||
|
Parameters used for email-style notifications
|
||||||
|
"""
|
||||||
msg_vals = msg_vals if msg_vals else {}
|
msg_vals = msg_vals if msg_vals else {}
|
||||||
rdata = self._notify_compute_recipients(message, msg_vals)
|
rdata = self._notify_compute_recipients(message, msg_vals)
|
||||||
if not rdata:
|
if not rdata:
|
||||||
@@ -1998,59 +2006,78 @@ class MailThread(models.AbstractModel):
|
|||||||
message_values = {}
|
message_values = {}
|
||||||
if rdata['channels']:
|
if rdata['channels']:
|
||||||
message_values['channel_ids'] = [(6, 0, [r['id'] for r in rdata['channels']])]
|
message_values['channel_ids'] = [(6, 0, [r['id'] for r in rdata['channels']])]
|
||||||
if rdata['partners']:
|
|
||||||
message_values['needaction_partner_ids'] = [(6, 0, [r['id'] for r in rdata['partners'] if r['type'] != 'channel_email'])]
|
|
||||||
# change of behavior to check: since email_cids partner are added in _notify_compute_recipients,
|
|
||||||
# they will be added to needaction_partner_ids to.
|
|
||||||
# we may want to filter them (example with channel_email, a cleaner solution may be great)
|
|
||||||
# -> instead of using _notify_customize_recipients, we could add a flag on rdata
|
|
||||||
# (would work for needactions, not if we want to erase partner_ids, ids)
|
|
||||||
# (could also be interesting for, we could add partners with r['notif'] = 'ocn_client' and r['needaction']=False)
|
|
||||||
# then override a notify_recipients (as it was before) to effectively send ocn notifications.
|
|
||||||
# envelope will contain more needaction, those for the member of a email channel.
|
|
||||||
if message_values and self:
|
|
||||||
message_values.update(self._notify_customize_recipients(message, msg_vals))
|
|
||||||
if message_values:
|
|
||||||
message.write(message_values)
|
|
||||||
|
|
||||||
inbox_pids = [r['id'] for r in rdata['partners'] if r['notif'] == 'inbox']
|
self._notify_record_by_inbox(message, rdata, msg_vals=msg_vals, **kwargs)
|
||||||
partner_email_rdata = [r for r in rdata['partners'] if r['notif'] == 'email']
|
self._notify_record_by_email(message, rdata, msg_vals=msg_vals, **kwargs)
|
||||||
channel_ids = [r['id'] for r in rdata['channels']]
|
|
||||||
|
|
||||||
notifications = []
|
return rdata
|
||||||
|
|
||||||
|
def _notify_record_by_inbox(self, message, recipients_data, msg_vals=False, **kwargs):
|
||||||
|
""" Notification method: inbox. Do two main things
|
||||||
|
|
||||||
|
* create an inbox notification for users;
|
||||||
|
* create channel / message link (channel_ids field of mail.message);
|
||||||
|
* send bus notifications;
|
||||||
|
|
||||||
|
TDE/XDO TODO: flag rdata directly, with for example r['notif'] = 'ocn_client' and r['needaction']=False
|
||||||
|
and correctly override notify_recipients
|
||||||
|
"""
|
||||||
|
channel_ids = [r['id'] for r in recipients_data['channels']]
|
||||||
|
if channel_ids:
|
||||||
|
message.write({'channel_ids': [(6, 0, channel_ids)]})
|
||||||
|
|
||||||
|
inbox_pids = [r['id'] for r in recipients_data['partners'] if r['notif'] == 'inbox']
|
||||||
|
if inbox_pids:
|
||||||
|
notif_create_values = [{
|
||||||
|
'mail_message_id': message.id,
|
||||||
|
'res_partner_id': pid,
|
||||||
|
'notification_type': 'inbox',
|
||||||
|
} for pid in inbox_pids]
|
||||||
|
self.env['mail.notification'].sudo().create(notif_create_values)
|
||||||
|
|
||||||
|
bus_notifications = []
|
||||||
if inbox_pids or channel_ids:
|
if inbox_pids or channel_ids:
|
||||||
message_values = False
|
message_format_values = False
|
||||||
if inbox_pids:
|
if inbox_pids:
|
||||||
message_values = message.message_format()[0]
|
message_format_values = message.message_format()[0]
|
||||||
for partner in self.env['res.partner'].browse(inbox_pids):
|
for partner in self.env['res.partner'].browse(inbox_pids):
|
||||||
notifications.append([(self._cr.dbname, 'ir.needaction', partner), dict(message_values)])
|
bus_notifications.append([(self._cr.dbname, 'ir.needaction', partner), dict(message_format_values)])
|
||||||
if channel_ids:
|
if channel_ids:
|
||||||
notifications += self.env['mail.channel'].sudo().browse(channel_ids)._channel_message_notifications(message, message_values)
|
bus_notifications += self.env['mail.channel'].sudo().browse(channel_ids)._channel_message_notifications(message, message_format_values)
|
||||||
if partner_email_rdata:
|
if bus_notifications:
|
||||||
self._notify_record_by_email(message, partner_email_rdata, msg_vals=msg_vals, model_description=model_description, mail_auto_delete=mail_auto_delete)
|
self.env['bus.bus'].sudo().sendmany(bus_notifications)
|
||||||
if notifications:
|
|
||||||
self.env['bus.bus'].sudo().sendmany(notifications)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _notify_record_by_email(self, message, partners_data, msg_vals=False, model_description=False, mail_auto_delete=True, send_after_commit=True):
|
def _notify_record_by_email(self, message, recipients_data, msg_vals=False,
|
||||||
|
model_description=False, mail_auto_delete=True, check_existing=False,
|
||||||
|
force_send=True, send_after_commit=True,
|
||||||
|
**kwargs):
|
||||||
""" Method to send email linked to notified messages.
|
""" Method to send email linked to notified messages.
|
||||||
|
|
||||||
:param message: mail.message record to notify;
|
:param message: mail.message record to notify;
|
||||||
:param partners_data: partner to notify by email coming from _notify_compute_recipients
|
:param recipients_data: see ``_notify_thread``;
|
||||||
:param msg_vals: message creation values if available
|
:param msg_vals: see ``_notify_thread``;
|
||||||
|
|
||||||
|
:param model_description: model description used in email notification process
|
||||||
|
(computed if not given);
|
||||||
|
:param mail_auto_delete: delete notification emails once sent;
|
||||||
|
:param check_existing: check for existing notifications to update based on
|
||||||
|
mailed recipient, otherwise create new notifications;
|
||||||
|
|
||||||
|
:param force_send: send emails directly instead of using queue;
|
||||||
:param send_after_commit: if force_send, tells whether to send emails after
|
:param send_after_commit: if force_send, tells whether to send emails after
|
||||||
the transaction has been committed using a post-commit hook;
|
the transaction has been committed using a post-commit hook;
|
||||||
:param model_description: optional data used in notification process (see
|
|
||||||
notification templates);
|
|
||||||
:param mail_auto_delete: delete notification emails once sent;
|
|
||||||
"""
|
"""
|
||||||
|
partners_data = [r for r in recipients_data['partners'] if r['notif'] == 'email']
|
||||||
|
if not partners_data:
|
||||||
|
return True
|
||||||
|
|
||||||
model = msg_vals.get('model') if msg_vals else message.model
|
model = msg_vals.get('model') if msg_vals else message.model
|
||||||
model_name = model_description or (self.with_lang().env['ir.model']._get(model).display_name if model else False) # one query for display name
|
model_name = model_description or (self.with_lang().env['ir.model']._get(model).display_name if model else False) # one query for display name
|
||||||
recipients_groups_data = self._notify_classify_recipients(partners_data, model_name)
|
recipients_groups_data = self._notify_classify_recipients(partners_data, model_name)
|
||||||
|
|
||||||
if not recipients_groups_data:
|
if not recipients_groups_data:
|
||||||
return True
|
return True
|
||||||
|
force_send = self.env.context.get('mail_notify_force_send', force_send)
|
||||||
force_send = self.env.context.get('mail_notify_force_send', True)
|
|
||||||
|
|
||||||
template_values = self._notify_prepare_template_context(message, msg_vals, model_description=model_description) # 10 queries
|
template_values = self._notify_prepare_template_context(message, msg_vals, model_description=model_description) # 10 queries
|
||||||
|
|
||||||
@@ -2062,7 +2089,6 @@ class MailThread(models.AbstractModel):
|
|||||||
_logger.warning('QWeb template %s not found when sending notification emails. Sending without layouting.' % (template_xmlid))
|
_logger.warning('QWeb template %s not found when sending notification emails. Sending without layouting.' % (template_xmlid))
|
||||||
base_template = False
|
base_template = False
|
||||||
|
|
||||||
|
|
||||||
mail_subject = message.subject or (message.record_name and 'Re: %s' % message.record_name) # in cache, no queries
|
mail_subject = message.subject or (message.record_name and 'Re: %s' % message.record_name) # in cache, no queries
|
||||||
# prepare notification mail values
|
# prepare notification mail values
|
||||||
base_mail_values = {
|
base_mail_values = {
|
||||||
@@ -2080,6 +2106,7 @@ class MailThread(models.AbstractModel):
|
|||||||
emails = self.env['mail.mail'].sudo()
|
emails = self.env['mail.mail'].sudo()
|
||||||
|
|
||||||
# loop on groups (customer, portal, user, ... + model specific like group_sale_salesman)
|
# loop on groups (customer, portal, user, ... + model specific like group_sale_salesman)
|
||||||
|
notif_create_values = []
|
||||||
recipients_max = 50
|
recipients_max = 50
|
||||||
for recipients_group_data in recipients_groups_data:
|
for recipients_group_data in recipients_groups_data:
|
||||||
# generate notification email content
|
# generate notification email content
|
||||||
@@ -2093,7 +2120,8 @@ class MailThread(models.AbstractModel):
|
|||||||
else:
|
else:
|
||||||
mail_body = message.body
|
mail_body = message.body
|
||||||
mail_body = self._replace_local_links(mail_body)
|
mail_body = self._replace_local_links(mail_body)
|
||||||
# send email
|
|
||||||
|
# create email
|
||||||
for recipients_ids_chunk in split_every(recipients_max, recipients_ids):
|
for recipients_ids_chunk in split_every(recipients_max, recipients_ids):
|
||||||
recipient_values = self._notify_email_recipient_values(recipients_ids_chunk)
|
recipient_values = self._notify_email_recipient_values(recipients_ids_chunk)
|
||||||
email_to = recipient_values['email_to']
|
email_to = recipient_values['email_to']
|
||||||
@@ -2110,22 +2138,32 @@ class MailThread(models.AbstractModel):
|
|||||||
email = Mail.create(create_values)
|
email = Mail.create(create_values)
|
||||||
|
|
||||||
if email and recipient_ids:
|
if email and recipient_ids:
|
||||||
notifications = self.env['mail.notification'].sudo().search([
|
tocreate_recipient_ids = list(recipient_ids)
|
||||||
('mail_message_id', '=', email.mail_message_id.id),
|
if check_existing:
|
||||||
('res_partner_id', 'in', list(recipient_ids)) # not sure to check.
|
existing_notifications = self.env['mail.notification'].sudo().search([
|
||||||
# TODO XDO what if recipient_ids are empty because of _notify_email_recipient_values
|
('mail_message_id', '=', message.id),
|
||||||
# should we use recipients_ids_chunk?
|
('notification_type', '=', 'email'),
|
||||||
# should we unlink recipients_ids_chunk - recipient_ids ?
|
('res_partner_id', 'in', tocreate_recipient_ids)
|
||||||
# should we avoid to create needation? by calling _notify_email_recipient_values at the same place _notify_customize_recipients does? (but no chubnk at this step)
|
])
|
||||||
])
|
if existing_notifications:
|
||||||
notifications.write({
|
tocreate_recipient_ids = [rid for rid in recipient_ids if rid not in existing_notifications.mapped('res_partner_id.id')]
|
||||||
'is_email': True,
|
existing_notifications.write({
|
||||||
|
'notification_status': 'ready',
|
||||||
|
'mail_id': email.id,
|
||||||
|
})
|
||||||
|
notif_create_values += [{
|
||||||
|
'mail_message_id': message.id,
|
||||||
|
'res_partner_id': recipient_id,
|
||||||
|
'notification_type': 'email',
|
||||||
'mail_id': email.id,
|
'mail_id': email.id,
|
||||||
'is_read': True, # handle by email discards Inbox notification
|
'is_read': True, # discard Inbox notification
|
||||||
'email_status': 'ready',
|
'notification_status': 'ready',
|
||||||
})
|
} for recipient_id in tocreate_recipient_ids]
|
||||||
emails |= email
|
emails |= email
|
||||||
|
|
||||||
|
if notif_create_values:
|
||||||
|
self.env['mail.notification'].sudo().create(notif_create_values)
|
||||||
|
|
||||||
# NOTE:
|
# NOTE:
|
||||||
# 1. for more than 50 followers, use the queue system
|
# 1. for more than 50 followers, use the queue system
|
||||||
# 2. do not send emails immediately if the registry is not loaded,
|
# 2. do not send emails immediately if the registry is not loaded,
|
||||||
@@ -2158,7 +2196,7 @@ class MailThread(models.AbstractModel):
|
|||||||
author = message.env['res.partner'].browse(msg_vals.get('author_id')) if msg_vals else message.author_id
|
author = message.env['res.partner'].browse(msg_vals.get('author_id')) if msg_vals else message.author_id
|
||||||
model = msg_vals.get('model') if msg_vals else message.model
|
model = msg_vals.get('model') if msg_vals else message.model
|
||||||
add_sign = msg_vals.get('add_sign') if msg_vals else message.add_sign
|
add_sign = msg_vals.get('add_sign') if msg_vals else message.add_sign
|
||||||
subtype_id = msg_vals.get('subtype_id') if msg_vals else message.subtype_id.id
|
subtype_id = msg_vals.get('subtype_id') if msg_vals else message.subtype_id.id
|
||||||
message_id = message.id
|
message_id = message.id
|
||||||
record_name = msg_vals.get('record_name') if msg_vals else message.record_name
|
record_name = msg_vals.get('record_name') if msg_vals else message.record_name
|
||||||
author_user = user if user.partner_id == author else author.user_ids[0] if author and author.user_ids else False
|
author_user = user if user.partner_id == author else author.user_ids[0] if author and author.user_ids else False
|
||||||
@@ -2222,13 +2260,14 @@ class MailThread(models.AbstractModel):
|
|||||||
# get values from msg_vals or from message if msg_vals doen't exists
|
# get values from msg_vals or from message if msg_vals doen't exists
|
||||||
pids = msg_vals.get('partner_ids', []) if msg_vals else msg_sudo.partner_ids.ids
|
pids = msg_vals.get('partner_ids', []) if msg_vals else msg_sudo.partner_ids.ids
|
||||||
cids = msg_vals.get('channel_ids', []) if msg_vals else msg_sudo.channel_ids.ids
|
cids = msg_vals.get('channel_ids', []) if msg_vals else msg_sudo.channel_ids.ids
|
||||||
|
message_type = msg_vals.get('message_type') if msg_vals else msg_sudo.message_type
|
||||||
subtype_id = msg_vals.get('subtype_id') if msg_vals else msg_sudo.subtype_id.id
|
subtype_id = msg_vals.get('subtype_id') if msg_vals else msg_sudo.subtype_id.id
|
||||||
# is it possible to have record but no subtype_id ?
|
# is it possible to have record but no subtype_id ?
|
||||||
recipient_data = {
|
recipient_data = {
|
||||||
'partners': [],
|
'partners': [],
|
||||||
'channels': [],
|
'channels': [],
|
||||||
}
|
}
|
||||||
res = self.env['mail.followers']._get_recipient_data(self, subtype_id, pids, cids)
|
res = self.env['mail.followers']._get_recipient_data(self, message_type, subtype_id, pids, cids)
|
||||||
if not res:
|
if not res:
|
||||||
return recipient_data
|
return recipient_data
|
||||||
|
|
||||||
@@ -2243,11 +2282,11 @@ class MailThread(models.AbstractModel):
|
|||||||
if notif == 'inbox':
|
if notif == 'inbox':
|
||||||
recipient_data['partners'].append(dict(pdata, notif=notif, type='user'))
|
recipient_data['partners'].append(dict(pdata, notif=notif, type='user'))
|
||||||
elif not pshare and notif: # has an user and is not shared, is therefore user
|
elif not pshare and notif: # has an user and is not shared, is therefore user
|
||||||
recipient_data['partners'].append(dict(pdata, notif='email', type='user'))
|
recipient_data['partners'].append(dict(pdata, notif=notif, type='user'))
|
||||||
elif pshare and notif: # has an user but is shared, is therefore portal
|
elif pshare and notif: # has an user but is shared, is therefore portal
|
||||||
recipient_data['partners'].append(dict(pdata, notif='email', type='portal'))
|
recipient_data['partners'].append(dict(pdata, notif=notif, type='portal'))
|
||||||
else: # has no user, is therefore customer
|
else: # has no user, is therefore customer
|
||||||
recipient_data['partners'].append(dict(pdata, notif='email', type='customer'))
|
recipient_data['partners'].append(dict(pdata, notif=notif if notif else 'email', type='customer'))
|
||||||
elif cid:
|
elif cid:
|
||||||
recipient_data['channels'].append({'id': cid, 'notif': notif, 'type': ctype})
|
recipient_data['channels'].append({'id': cid, 'notif': notif, 'type': ctype})
|
||||||
|
|
||||||
@@ -2514,9 +2553,6 @@ class MailThread(models.AbstractModel):
|
|||||||
'email_to': False,
|
'email_to': False,
|
||||||
'recipient_ids': recipient_ids,
|
'recipient_ids': recipient_ids,
|
||||||
}
|
}
|
||||||
@api.multi
|
|
||||||
def _notify_customize_recipients(self, message, msg_vals):
|
|
||||||
return {}
|
|
||||||
|
|
||||||
# ------------------------------------------------------
|
# ------------------------------------------------------
|
||||||
# Followers API
|
# Followers API
|
||||||
|
|||||||
@@ -75,8 +75,8 @@
|
|||||||
<tree>
|
<tree>
|
||||||
<field name="res_partner_id"/>
|
<field name="res_partner_id"/>
|
||||||
<field name="is_read"/>
|
<field name="is_read"/>
|
||||||
<field name="is_email"/>
|
<field name="notification_type"/>
|
||||||
<field name="email_status"/>
|
<field name="notification_status"/>
|
||||||
</tree>
|
</tree>
|
||||||
</field>
|
</field>
|
||||||
</group>
|
</group>
|
||||||
|
|||||||
@@ -72,13 +72,13 @@ class Invite(models.TransientModel):
|
|||||||
'no_auto_thread': True,
|
'no_auto_thread': True,
|
||||||
'add_sign': True,
|
'add_sign': True,
|
||||||
})
|
})
|
||||||
partners_data = [{
|
recipients_data = {'partners': [{
|
||||||
'id': pid,
|
'id': pid,
|
||||||
'share': True,
|
'share': True,
|
||||||
'notif': 'email',
|
'notif': 'email',
|
||||||
'type': 'customer',
|
'type': 'customer',
|
||||||
'groups': []
|
'groups': []
|
||||||
} for pid in new_partners.ids]
|
} for pid in new_partners.ids]}
|
||||||
document._notify_record_by_email(message, partners_data, send_after_commit=False)
|
document._notify_record_by_email(message, recipients_data, send_after_commit=False)
|
||||||
message.unlink()
|
message.unlink()
|
||||||
return {'type': 'ir.actions.act_window_close'}
|
return {'type': 'ir.actions.act_window_close'}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
from odoo import _, api, fields, models
|
from odoo import _, api, fields, models
|
||||||
|
|
||||||
|
|
||||||
class MailCancelResend(models.TransientModel):
|
class MailResendCancel(models.TransientModel):
|
||||||
_name = 'mail.resend.cancel'
|
_name = 'mail.resend.cancel'
|
||||||
_description = 'Dismiss notification for resend by model'
|
_description = 'Dismiss notification for resend by model'
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ class MailCancelResend(models.TransientModel):
|
|||||||
FROM mail_message_res_partner_needaction_rel notif
|
FROM mail_message_res_partner_needaction_rel notif
|
||||||
JOIN mail_message mes
|
JOIN mail_message mes
|
||||||
ON notif.mail_message_id = mes.id
|
ON notif.mail_message_id = mes.id
|
||||||
WHERE notif.email_status IN ('bounce', 'exception')
|
WHERE notif.notification_status IN ('bounce', 'exception')
|
||||||
AND mes.model = %s
|
AND mes.model = %s
|
||||||
AND mes.author_id = %s
|
AND mes.author_id = %s
|
||||||
""", (wizard.model, author_id))
|
""", (wizard.model, author_id))
|
||||||
@@ -34,6 +34,6 @@ class MailCancelResend(models.TransientModel):
|
|||||||
notif_ids = [row[0] for row in res]
|
notif_ids = [row[0] for row in res]
|
||||||
messages_ids = list(set([row[1] for row in res]))
|
messages_ids = list(set([row[1] for row in res]))
|
||||||
if notif_ids:
|
if notif_ids:
|
||||||
self.env["mail.notification"].browse(notif_ids).sudo().write({'email_status': 'canceled'})
|
self.env["mail.notification"].browse(notif_ids).sudo().write({'notification_status': 'canceled'})
|
||||||
self.env["mail.message"].browse(messages_ids)._notify_failure_update()
|
self.env["mail.message"].browse(messages_ids)._notify_mail_failure_update()
|
||||||
return {'type': 'ir.actions.act_window_close'}
|
return {'type': 'ir.actions.act_window_close'}
|
||||||
|
|||||||
@@ -28,16 +28,14 @@ class MailResendMessage(models.TransientModel):
|
|||||||
message_id = self._context.get('mail_message_to_resend')
|
message_id = self._context.get('mail_message_to_resend')
|
||||||
if message_id:
|
if message_id:
|
||||||
mail_message_id = self.env['mail.message'].browse(message_id)
|
mail_message_id = self.env['mail.message'].browse(message_id)
|
||||||
notification_ids = mail_message_id.notification_ids.filtered(lambda notif: notif.email_status in ('exception', 'bounce'))
|
notification_ids = mail_message_id.notification_ids.filtered(lambda notif: notif.notification_type == 'email' and notif.notification_status in ('exception', 'bounce'))
|
||||||
partner_ids = [(0, 0,
|
partner_ids = [(0, 0, {
|
||||||
{
|
"partner_id": notif.res_partner_id.id,
|
||||||
"partner_id": notif.res_partner_id.id,
|
"name": notif.res_partner_id.name,
|
||||||
"name": notif.res_partner_id.name,
|
"email": notif.res_partner_id.email,
|
||||||
"email": notif.res_partner_id.email,
|
"resend": True,
|
||||||
"resend": True,
|
"message": notif.format_failure_reason(),
|
||||||
"message": notif.format_failure_reason(),
|
}) for notif in notification_ids]
|
||||||
}
|
|
||||||
) for notif in notification_ids]
|
|
||||||
has_user = any([notif.res_partner_id.user_ids for notif in notification_ids])
|
has_user = any([notif.res_partner_id.user_ids for notif in notification_ids])
|
||||||
if has_user:
|
if has_user:
|
||||||
partner_readonly = not self.env['res.users'].check_access_rights('write', raise_exception=False)
|
partner_readonly = not self.env['res.users'].check_access_rights('write', raise_exception=False)
|
||||||
@@ -59,14 +57,14 @@ class MailResendMessage(models.TransientModel):
|
|||||||
"If a partner disappeared from partner list, we cancel the notification"
|
"If a partner disappeared from partner list, we cancel the notification"
|
||||||
to_cancel = wizard.partner_ids.filtered(lambda p: not p.resend).mapped("partner_id")
|
to_cancel = wizard.partner_ids.filtered(lambda p: not p.resend).mapped("partner_id")
|
||||||
to_send = wizard.partner_ids.filtered(lambda p: p.resend).mapped("partner_id")
|
to_send = wizard.partner_ids.filtered(lambda p: p.resend).mapped("partner_id")
|
||||||
notif_to_cancel = wizard.notification_ids.filtered(lambda notif: notif.res_partner_id in to_cancel and notif.email_status in ('exception', 'bounce'))
|
notif_to_cancel = wizard.notification_ids.filtered(lambda notif: notif.notification_type == 'email' and notif.res_partner_id in to_cancel and notif.notification_status in ('exception', 'bounce'))
|
||||||
notif_to_cancel.sudo().write({'email_status': 'canceled'})
|
notif_to_cancel.sudo().write({'notification_status': 'canceled'})
|
||||||
if to_send:
|
if to_send:
|
||||||
message = wizard.mail_message_id
|
message = wizard.mail_message_id
|
||||||
record = self.env[message.model].browse(message.res_id) if message.is_thread_message() else self.env['mail.thread']
|
record = self.env[message.model].browse(message.res_id) if message.is_thread_message() else self.env['mail.thread']
|
||||||
|
|
||||||
email_partners_data = []
|
email_partners_data = []
|
||||||
for pid, cid, active, pshare, ctype, notif, groups in self.env['mail.followers']._get_recipient_data(None, False, pids=to_send.ids):
|
for pid, cid, active, pshare, ctype, notif, groups in self.env['mail.followers']._get_recipient_data(None, 'comment', False, pids=to_send.ids):
|
||||||
if pid and notif == 'email' or not notif:
|
if pid and notif == 'email' or not notif:
|
||||||
pdata = {'id': pid, 'share': pshare, 'active': active, 'notif': 'email', 'groups': groups or []}
|
pdata = {'id': pid, 'share': pshare, 'active': active, 'notif': 'email', 'groups': groups or []}
|
||||||
if not pshare and notif: # has an user and is not shared, is therefore user
|
if not pshare and notif: # has an user and is not shared, is therefore user
|
||||||
@@ -76,17 +74,17 @@ class MailResendMessage(models.TransientModel):
|
|||||||
else: # has no user, is therefore customer
|
else: # has no user, is therefore customer
|
||||||
email_partners_data.append(dict(pdata, type='customer'))
|
email_partners_data.append(dict(pdata, type='customer'))
|
||||||
|
|
||||||
record._notify_record_by_email(message, email_partners_data, send_after_commit=False)
|
record._notify_record_by_email(message, {'partners': email_partners_data}, check_existing=True, send_after_commit=False)
|
||||||
|
|
||||||
self.mail_message_id._notify_failure_update()
|
self.mail_message_id._notify_mail_failure_update()
|
||||||
return {'type': 'ir.actions.act_window_close'}
|
return {'type': 'ir.actions.act_window_close'}
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def cancel_mail_action(self):
|
def cancel_mail_action(self):
|
||||||
for wizard in self:
|
for wizard in self:
|
||||||
for notif in wizard.notification_ids:
|
for notif in wizard.notification_ids:
|
||||||
notif.filtered(lambda notif: notif.email_status in ('exception', 'bounce')).sudo().write({'email_status': 'canceled'})
|
notif.filtered(lambda notif: notif.notification_type == 'email' and notif.notification_status in ('exception', 'bounce')).sudo().write({'notification_status': 'canceled'})
|
||||||
wizard.mail_message_id._notify_failure_update()
|
wizard.mail_message_id._notify_mail_failure_update()
|
||||||
return {'type': 'ir.actions.act_window_close'}
|
return {'type': 'ir.actions.act_window_close'}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -77,51 +77,43 @@ except ImportError:
|
|||||||
|
|
||||||
|
|
||||||
def phone_sanitize_numbers(numbers, country_code, country_phone_code, force_format='E164'):
|
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:
|
for number in numbers:
|
||||||
if not number:
|
if not number:
|
||||||
void_count += 1
|
result[number] = {'sanitized': False, 'code': 'empty', 'msg': False}
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
sanitized = phone_format(
|
sanitized = phone_format(
|
||||||
number, country_code, country_phone_code,
|
number, country_code, country_phone_code,
|
||||||
force_format=force_format, raise_exception=True)
|
force_format=force_format, raise_exception=True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
invalid.append(number)
|
result[number] = {'sanitized': False, 'code': 'invalid', 'msg': e}
|
||||||
else:
|
else:
|
||||||
valid.append(sanitized)
|
result[number] = {'sanitized': sanitized, 'code': False, 'msg': False}
|
||||||
return valid, invalid, void_count
|
return result
|
||||||
|
|
||||||
|
|
||||||
def phone_sanitize_numbers_w_record(numbers, country_code, country_phone_code, record, record_country_fname='country_id', force_format='E164'):
|
def phone_sanitize_numbers_w_record(numbers, record, country=False, record_country_fname='country_id', force_format='E164'):
|
||||||
if not country_code or not country_phone_code:
|
if not country:
|
||||||
country = False
|
if record and hasattr(record, record_country_fname) and record[record_country_fname]:
|
||||||
if record and record_country_fname in record and record[record_country_fname]:
|
|
||||||
country = record[record_country_fname]
|
country = record[record_country_fname]
|
||||||
elif record:
|
elif record:
|
||||||
country = record.env.company.country_id
|
country = record.env.company.country_id
|
||||||
if country:
|
country_code = country.code if country else None
|
||||||
country_code = country_code if country_code else country.code
|
country_phone_code = country.phone_code if country else None
|
||||||
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)
|
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(',')]
|
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'):
|
def phone_get_sanitized_records_number(records, number_fname='mobile', country_fname='country_id', force_format='E164'):
|
||||||
res = dict.fromkeys(records.ids, False)
|
res = dict.fromkeys(records.ids, False)
|
||||||
for record in records:
|
for record in records:
|
||||||
number = record[number_fname]
|
number = record[number_fname]
|
||||||
valid, invalid, void_count = phone_sanitize_numbers_w_record([number], None, None, records, country_fname,force_format=force_format)
|
res[record.id] = phone_sanitize_numbers_w_record([number], records, record_country_fname=country_fname,force_format=force_format)[number]['sanitized']
|
||||||
if valid:
|
|
||||||
res[record.id] = valid[0]
|
|
||||||
elif void_count:
|
|
||||||
res[record.id] = False
|
|
||||||
else:
|
|
||||||
res[record.id] = False
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,13 +12,23 @@ The service is provided by the In App Purchase Odoo platform.
|
|||||||
""",
|
""",
|
||||||
'depends': ['base', 'iap', 'mail', 'phone_validation'],
|
'depends': ['base', 'iap', 'mail', 'phone_validation'],
|
||||||
'data': [
|
'data': [
|
||||||
|
'data/ir_cron_data.xml',
|
||||||
|
'wizard/sms_cancel_views.xml',
|
||||||
'wizard/sms_composer_views.xml',
|
'wizard/sms_composer_views.xml',
|
||||||
|
'wizard/sms_resend_views.xml',
|
||||||
'views/res_config_settings_views.xml',
|
'views/res_config_settings_views.xml',
|
||||||
'views/res_partner_views.xml',
|
'views/res_partner_views.xml',
|
||||||
'views/assets.xml',
|
'views/assets.xml',
|
||||||
|
'views/sms_sms_views.xml',
|
||||||
|
'views/sms_template_views.xml',
|
||||||
|
'security/ir.model.access.csv',
|
||||||
|
],
|
||||||
|
'demo': [
|
||||||
|
'data/sms_demo.xml',
|
||||||
|
'data/mail_demo.xml',
|
||||||
],
|
],
|
||||||
'qweb': [
|
'qweb': [
|
||||||
'static/src/xml/sms_widget.xml',
|
'static/src/xml/thread.xml',
|
||||||
],
|
],
|
||||||
'installable': True,
|
'installable': True,
|
||||||
'auto_install': True,
|
'auto_install': True,
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo><data noupdate="1">
|
||||||
|
<record forcecreate="True" id="ir_cron_sms_scheduler_action" model="ir.cron">
|
||||||
|
<field name="name">SMS: SMS Queue Manager</field>
|
||||||
|
<field name="model_id" ref="model_sms_sms"/>
|
||||||
|
<field name="state">code</field>
|
||||||
|
<field name="code">model._process_queue()</field>
|
||||||
|
<field name="user_id" ref="base.user_root"/>
|
||||||
|
<field name="interval_number">1</field>
|
||||||
|
<field name="interval_type">hours</field>
|
||||||
|
<field name="numbercall">-1</field>
|
||||||
|
<field eval="False" name="doall"/>
|
||||||
|
</record>
|
||||||
|
</data></odoo>
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo><data noupdate="1">
|
||||||
|
<record id="message_demo_partner_1_0" model="mail.message">
|
||||||
|
<field name="model">res.partner</field>
|
||||||
|
<field name="res_id" ref="base.res_partner_address_28"/>
|
||||||
|
<field name="body" type="html"><p>Hello! This is an example of incoming email.</p></field>
|
||||||
|
<field name="message_type">email</field>
|
||||||
|
<field name="subtype_id" ref="mail.mt_comment"/>
|
||||||
|
<field name="author_id" ref="base.partner_demo"/>
|
||||||
|
<field name="date" eval="(DateTime.today() - timedelta(days=5)).strftime('%Y-%m-%d %H:%M:00')"/>
|
||||||
|
</record>
|
||||||
|
<record id="message_demo_partner_1_1" model="mail.message">
|
||||||
|
<field name="model">res.partner</field>
|
||||||
|
<field name="res_id" ref="base.res_partner_address_28"/>
|
||||||
|
<field name="body" type="html"><p>Hello! This is an example of user comment.</p></field>
|
||||||
|
<field name="message_type">comment</field>
|
||||||
|
<field name="subtype_id" ref="mail.mt_comment"/>
|
||||||
|
<field name="author_id" ref="base.partner_admin"/>
|
||||||
|
<field name="date" eval="(DateTime.today() - timedelta(days=4)).strftime('%Y-%m-%d %H:%M:00')"/>
|
||||||
|
</record>
|
||||||
|
<record id="message_demo_partner_1_2_notif_0" model="mail.notification">
|
||||||
|
<field name="mail_message_id" ref="message_demo_partner_1_1"/>
|
||||||
|
<field name="res_partner_id" ref="base.res_partner_address_28"/>
|
||||||
|
<field name="notification_type">email</field>
|
||||||
|
<field name="notification_status">exception</field>
|
||||||
|
<field name="failure_type">SMTP</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="message_demo_partner_1_2" model="mail.message">
|
||||||
|
<field name="model">res.partner</field>
|
||||||
|
<field name="res_id" ref="base.res_partner_address_28"/>
|
||||||
|
<field name="body" type="html"><p>Hello! This is an example of SMS.</p></field>
|
||||||
|
<field name="message_type">sms</field>
|
||||||
|
<field name="subtype_id" ref="mail.mt_comment"/>
|
||||||
|
<field name="author_id" ref="base.partner_demo"/>
|
||||||
|
<field name="date" eval="(DateTime.today() - timedelta(days=3)).strftime('%Y-%m-%d %H:%M:00')"/>
|
||||||
|
</record>
|
||||||
|
<record id="message_demo_partner_1_3" model="mail.message">
|
||||||
|
<field name="model">res.partner</field>
|
||||||
|
<field name="res_id" ref="base.res_partner_address_28"/>
|
||||||
|
<field name="body" type="html"><p>Hello! This is an example of another SMS with notifications.</p></field>
|
||||||
|
<field name="message_type">sms</field>
|
||||||
|
<field name="subtype_id" ref="mail.mt_comment"/>
|
||||||
|
<field name="author_id" ref="base.partner_admin"/>
|
||||||
|
<field name="date" eval="(DateTime.today() - timedelta(days=2)).strftime('%Y-%m-%d %H:%M:00')"/>
|
||||||
|
</record>
|
||||||
|
<record id="message_demo_partner_1_3_notif_0" model="mail.notification">
|
||||||
|
<field name="mail_message_id" ref="message_demo_partner_1_3"/>
|
||||||
|
<field name="res_partner_id" ref="base.res_partner_address_28"/>
|
||||||
|
<field name="notification_type">sms</field>
|
||||||
|
<field name="notification_status">exception</field>
|
||||||
|
<field name="failure_type">sms_credit</field>
|
||||||
|
</record>
|
||||||
|
<record id="message_demo_partner_1_3_notif_1" model="mail.notification">
|
||||||
|
<field name="mail_message_id" ref="message_demo_partner_1_3"/>
|
||||||
|
<field name="res_partner_id" ref="base.res_partner_address_16"/>
|
||||||
|
<field name="notification_type">sms</field>
|
||||||
|
<field name="notification_status">exception</field>
|
||||||
|
<field name="failure_type">sms_credit</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</data></odoo>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo><data noupdate="1">
|
||||||
|
<record id="sms_template_demo_0" model="sms.template">
|
||||||
|
<field name="name">Customer: automated SMS</field>
|
||||||
|
<field name="model_id" ref="base.model_res_partner"/>
|
||||||
|
<field name="body">Dear ${object.display_name} this is an automated SMS.</field>
|
||||||
|
</record>
|
||||||
|
</data></odoo>
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
# 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 mail_thread
|
||||||
from . import res_partner
|
from . import res_partner
|
||||||
from . import sms_api
|
from . import sms_api
|
||||||
|
from . import sms_sms
|
||||||
|
from . import sms_template
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
|
from operator import itemgetter
|
||||||
|
|
||||||
|
from odoo import api, exceptions, fields, models
|
||||||
|
from odoo.tools import groupby
|
||||||
|
|
||||||
|
|
||||||
|
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')])
|
||||||
|
has_sms_error = fields.Boolean(
|
||||||
|
'Has SMS error', compute='_compute_has_sms_error', search='_search_has_sms_error',
|
||||||
|
help='Has error')
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def _compute_has_sms_error(self):
|
||||||
|
sms_error_from_notification = self.env['mail.notification'].sudo().search([
|
||||||
|
('notification_type', '=', 'sms'),
|
||||||
|
('mail_message_id', 'in', self.ids),
|
||||||
|
('notification_status', '=', 'exception')]).mapped('mail_message_id')
|
||||||
|
for message in self:
|
||||||
|
message.has_error = message in sms_error_from_notification
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def _search_has_sms_error(self, operator, operand):
|
||||||
|
if operator == '=' and operand:
|
||||||
|
return ['&', ('notification_ids.notification_status', '=', 'exception'), ('notification_ids.notification_type', '=', True)]
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def _format_mail_failures(self):
|
||||||
|
""" A shorter message to notify a SMS delivery failure update
|
||||||
|
|
||||||
|
TDE FIXME: should be cleaned
|
||||||
|
"""
|
||||||
|
res = super(MailMessage, self)._format_mail_failures()
|
||||||
|
|
||||||
|
# prepare notifications computation in batch
|
||||||
|
all_notifications = self.env['mail.notification'].sudo().search([
|
||||||
|
('mail_message_id', 'in', self.ids)
|
||||||
|
])
|
||||||
|
msgid_to_notif = defaultdict(lambda: self.env['mail.notification'].sudo())
|
||||||
|
for notif in all_notifications:
|
||||||
|
msgid_to_notif[notif.mail_message_id.id] += notif
|
||||||
|
|
||||||
|
for message in self:
|
||||||
|
notifications = msgid_to_notif[message.id]
|
||||||
|
if not any(notification.notification_type == 'sms' for notification in notifications):
|
||||||
|
continue
|
||||||
|
info = dict(message._get_mail_failure_dict(),
|
||||||
|
failure_type='sms',
|
||||||
|
notifications=dict((notif.res_partner_id.id, (notif.notification_status, notif.res_partner_id.name)) for notif in notifications if notif.notification_type == 'sms'),
|
||||||
|
module_icon='/sms/static/img/sms_failure.png'
|
||||||
|
)
|
||||||
|
res.append(info)
|
||||||
|
return res
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def _notify_sms_update(self):
|
||||||
|
""" Send bus notifications to update status of notifications in chatter.
|
||||||
|
Purpose is to send the updated status per author.
|
||||||
|
|
||||||
|
TDE FIXME: author_id strategy seems curious, check with JS """
|
||||||
|
messages = self.env['mail.message']
|
||||||
|
for message in self:
|
||||||
|
# YTI FIXME: check allowed_company_ids if necessary
|
||||||
|
if message.model and message.res_id:
|
||||||
|
record = self.env[message.model].browse(message.res_id)
|
||||||
|
try:
|
||||||
|
record.check_access_rights('read')
|
||||||
|
record.check_access_rule('read')
|
||||||
|
except exceptions.AccessError:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
messages |= message
|
||||||
|
|
||||||
|
""" Notify channels after update of SMS status """
|
||||||
|
updates = [[
|
||||||
|
(self._cr.dbname, 'res.partner', author.id),
|
||||||
|
{'type': 'sms_update', 'elements': self.env['mail.message'].concat(*author_messages)._format_mail_failures()}
|
||||||
|
] for author, author_messages in groupby(messages, itemgetter('author_id'))]
|
||||||
|
self.env['bus.bus'].sendmany(updates)
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def message_format(self):
|
||||||
|
""" Override in order to retrieves data about SMS (recipient name and
|
||||||
|
SMS status)
|
||||||
|
|
||||||
|
TDE FIXME: clean the overall message_format thingy
|
||||||
|
"""
|
||||||
|
message_values = super(MailMessage, self).message_format()
|
||||||
|
all_sms_notifications = self.env['mail.notification'].sudo().search([
|
||||||
|
('mail_message_id', 'in', [r['id'] for r in message_values]),
|
||||||
|
('notification_type', '=', 'sms')
|
||||||
|
])
|
||||||
|
msgid_to_notif = defaultdict(lambda: self.env['mail.notification'].sudo())
|
||||||
|
for notif in all_sms_notifications:
|
||||||
|
msgid_to_notif[notif.mail_message_id.id] += notif
|
||||||
|
|
||||||
|
for message in message_values:
|
||||||
|
customer_sms_data = [(notif.id, notif.res_partner_id.display_name or notif.sms_number, notif.notification_status) for notif in msgid_to_notif.get(message['id'], [])]
|
||||||
|
message['sms_ids'] = customer_sms_data
|
||||||
|
return message_values
|
||||||
@@ -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')]
|
||||||
|
)
|
||||||
@@ -3,9 +3,9 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from odoo import models, _
|
from odoo import api, models, fields
|
||||||
|
from odoo.addons.phone_validation.tools import phone_validation
|
||||||
from odoo.addons.iap.models.iap import InsufficientCreditError
|
from odoo.tools import html2plaintext
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -13,7 +13,30 @@ _logger = logging.getLogger(__name__)
|
|||||||
class MailThread(models.AbstractModel):
|
class MailThread(models.AbstractModel):
|
||||||
_inherit = 'mail.thread'
|
_inherit = 'mail.thread'
|
||||||
|
|
||||||
def _get_default_sms_recipients(self):
|
message_has_sms_error = fields.Boolean(
|
||||||
|
'SMS Delivery error', compute='_compute_message_has_sms_error', search='_search_message_has_sms_error',
|
||||||
|
help="If checked, some messages have a delivery error.")
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def _compute_message_has_sms_error(self):
|
||||||
|
res = {}
|
||||||
|
if self.ids:
|
||||||
|
self._cr.execute(""" SELECT msg.res_id, COUNT(msg.res_id) FROM mail_message msg
|
||||||
|
RIGHT JOIN mail_message_res_partner_needaction_rel rel
|
||||||
|
ON rel.mail_message_id = msg.id AND rel.notification_type = 'sms' AND rel.notification_status in ('exception')
|
||||||
|
WHERE msg.author_id = %s AND msg.model = %s AND msg.res_id in %s AND msg.message_type != 'user_notification'
|
||||||
|
GROUP BY msg.res_id""",
|
||||||
|
(self.env.user.partner_id.id, self._name, tuple(self.ids),))
|
||||||
|
res.update(self._cr.fetchall())
|
||||||
|
|
||||||
|
for record in self:
|
||||||
|
record.message_has_sms_error = bool(res.get(record._origin.id, 0))
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _search_message_has_sms_error(self, operator, operand):
|
||||||
|
return ['&', ('message_ids.has_sms_error', operator, operand), ('message_ids.author_id', '=', self.env.user.partner_id.id)]
|
||||||
|
|
||||||
|
def _sms_get_default_partners(self):
|
||||||
""" This method will likely need to be overriden by inherited models.
|
""" This method will likely need to be overriden by inherited models.
|
||||||
:returns partners: recordset of res.partner
|
:returns partners: recordset of res.partner
|
||||||
"""
|
"""
|
||||||
@@ -24,36 +47,250 @@ class MailThread(models.AbstractModel):
|
|||||||
partners |= self.mapped('partner_ids')
|
partners |= self.mapped('partner_ids')
|
||||||
return partners
|
return partners
|
||||||
|
|
||||||
def message_post_send_sms(self, sms_message, numbers=None, partners=None, note_msg=None, log_error=False):
|
def _sms_get_number_fields(self):
|
||||||
""" Send an SMS text message and post an internal note in the chatter if successfull
|
""" This method returns the fields to use to find the number to use to
|
||||||
:param sms_message: plaintext message to send by sms
|
send an SMS on a record. """
|
||||||
:param partners: the numbers to send to, if none are given it will take those
|
return ['mobile']
|
||||||
from partners or _get_default_sms_recipients
|
|
||||||
:param partners: the recipients partners, if none are given it will take those
|
def _sms_get_recipients_info(self, force_field=False):
|
||||||
from _get_default_sms_recipients, this argument
|
"""" Get SMS recipient information on current record set. This method
|
||||||
is ignored if numbers is defined
|
checks for numbers and sanitation in order to centralize computation.
|
||||||
:param note_msg: message to log in the chatter, if none is given a default one
|
|
||||||
containing the sms_message is logged
|
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:
|
result = dict.fromkeys(self.ids, False)
|
||||||
if not partners:
|
number_fields = self._sms_get_number_fields()
|
||||||
partners = self._get_default_sms_recipients()
|
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
|
valid_number = False
|
||||||
numbers = list(set([i.mobile for i in partners if i.mobile]))
|
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:
|
if valid_number:
|
||||||
try:
|
result[record.id] = {
|
||||||
self.env['sms.api']._send_sms(numbers, sms_message)
|
'partner': all_partners[0] if all_partners else self.env['res.partner'],
|
||||||
mail_message = note_msg or _('SMS message sent: %s') % sms_message
|
'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 partner_number:
|
||||||
if not log_error:
|
result[record.id] = {'partner': partner, 'sanitized': partner_number, 'number': partner_number}
|
||||||
raise e
|
else:
|
||||||
mail_message = _('Insufficient credit, unable to send SMS message: %s') % sms_message
|
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
|
||||||
|
|
||||||
|
def _message_sms_schedule_mass(self, body='', template=False, active_domain=None):
|
||||||
|
""" Shortcut method to schedule a mass sms sending on a recordset.
|
||||||
|
|
||||||
|
:param template: an optional sms.template record;
|
||||||
|
:param active_domain: bypass self.ids and apply composer on active_domain
|
||||||
|
instead;
|
||||||
|
"""
|
||||||
|
composer_context = {
|
||||||
|
'default_res_model': self._name,
|
||||||
|
'default_composition_mode': 'mass',
|
||||||
|
'default_template_id': template.id if template else False,
|
||||||
|
'default_body': body if body and not template else False,
|
||||||
|
}
|
||||||
|
if active_domain is not None:
|
||||||
|
composer_context['default_use_active_domain'] = True
|
||||||
|
composer_context['default_active_domain'] = repr(active_domain)
|
||||||
else:
|
else:
|
||||||
mail_message = _('No mobile number defined, unable to send SMS message: %s') % sms_message
|
composer_context['default_res_ids'] = self.ids
|
||||||
|
|
||||||
for thread in self:
|
composer = self.env['sms.composer'].with_context(**composer_context).create({})
|
||||||
thread.message_post(body=mail_message)
|
return composer._action_send_sms()
|
||||||
return False
|
|
||||||
|
def _message_sms_with_template(self, template=False, template_xmlid=False, template_fallback='', partner_ids=False, **kwargs):
|
||||||
|
""" Shortcut method to perform a _message_sms with an sms.template.
|
||||||
|
|
||||||
|
:param template: a valid sms.template record;
|
||||||
|
:param template_xmlid: XML ID of an sms.template (if no template given);
|
||||||
|
:param template_fallback: plaintext (jinja-enabled) in case template
|
||||||
|
and template xml id are falsy (for example due to deleted data);
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
if not template and template_xmlid:
|
||||||
|
template = self.env.ref(template_xmlid, raise_if_not_found=False)
|
||||||
|
if template:
|
||||||
|
template_w_lang = template._get_context_lang_per_id(self.ids)[self.id]
|
||||||
|
body = template._render_template(template_w_lang.body, self._name, self.ids)[self.id]
|
||||||
|
else:
|
||||||
|
body = self.env['sms.template']._render_template(template_fallback, self._name, self.ids)[self.id]
|
||||||
|
return self._message_sms(body, partner_ids=partner_ids, **kwargs)
|
||||||
|
|
||||||
|
def _message_sms(self, body, subtype_id=False, partner_ids=False, number_field=False,
|
||||||
|
sms_numbers=None, sms_pid_to_number=None, **kwargs):
|
||||||
|
""" Main method to post a message on a record using SMS-based notification
|
||||||
|
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
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from odoo import models
|
|||||||
class ResPartner(models.Model):
|
class ResPartner(models.Model):
|
||||||
_inherit = 'res.partner'
|
_inherit = 'res.partner'
|
||||||
|
|
||||||
def _get_default_sms_recipients(self):
|
def _sms_get_default_partners(self):
|
||||||
""" Override of mail.thread method.
|
""" Override of mail.thread method.
|
||||||
SMS recipients on partners are the partners themselves.
|
SMS recipients on partners are the partners themselves.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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
|
||||||
|
})
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from odoo import api, fields, models, _
|
||||||
|
|
||||||
|
|
||||||
|
class SMSTemplate(models.Model):
|
||||||
|
"Templates for sending SMS"
|
||||||
|
_name = "sms.template"
|
||||||
|
_description = 'SMS Templates'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def default_get(self, fields):
|
||||||
|
res = super(SMSTemplate, self).default_get(fields)
|
||||||
|
if not fields or 'model_id' in fields and not res.get('model_id') and res.get('model'):
|
||||||
|
res['model_id'] = self.env['ir.model']._get(res['model']).id
|
||||||
|
return res
|
||||||
|
|
||||||
|
name = fields.Char()
|
||||||
|
model_id = fields.Many2one(
|
||||||
|
'ir.model', string='Applies to', required=True,
|
||||||
|
domain=['&', ('is_mail_thread', '=', True), ('transient', '=', False)],
|
||||||
|
help="The type of document this template can be used with")
|
||||||
|
model = fields.Char('Related Document Model', related='model_id.model', index=True, store=True, readonly=True)
|
||||||
|
body = fields.Char('Body', translate=True, required=True)
|
||||||
|
lang = fields.Char('Language', placeholder="${object.partner_id.lang}")
|
||||||
|
# Fake fields used to implement the placeholder assistant
|
||||||
|
model_object_field = fields.Many2one('ir.model.fields', string="Field", store=False,
|
||||||
|
help="Select target field from the related document model.\n"
|
||||||
|
"If it is a relationship field you will be able to select "
|
||||||
|
"a target field at the destination of the relationship.")
|
||||||
|
sub_object = fields.Many2one('ir.model', 'Sub-model', readonly=True, store=False,
|
||||||
|
help="When a relationship field is selected as first field, "
|
||||||
|
"this field shows the document model the relationship goes to.")
|
||||||
|
sub_model_object_field = fields.Many2one('ir.model.fields', 'Sub-field', store=False,
|
||||||
|
help="When a relationship field is selected as first field, "
|
||||||
|
"this field lets you select the target field within the "
|
||||||
|
"destination document model (sub-model).")
|
||||||
|
null_value = fields.Char('Default Value', store=False, help="Optional value to use if the target field is empty")
|
||||||
|
copyvalue = fields.Char('Placeholder Expression', store=False,
|
||||||
|
help="Final placeholder expression, to be copy-pasted in the desired template field.")
|
||||||
|
|
||||||
|
@api.onchange('model_object_field', 'sub_model_object_field', 'null_value')
|
||||||
|
def _onchange_dynamic_placeholder(self):
|
||||||
|
""" Generate the dynamic placeholder """
|
||||||
|
if self.model_object_field:
|
||||||
|
if self.model_object_field.ttype in ['many2one', 'one2many', 'many2many']:
|
||||||
|
model = self.env['ir.model']._get(self.model_object_field.relation)
|
||||||
|
if model:
|
||||||
|
self.sub_object = model.id
|
||||||
|
sub_field_name = self.sub_model_object_field.name
|
||||||
|
self.copyvalue = self._build_expression(self.model_object_field.name,
|
||||||
|
sub_field_name, self.null_value or False)
|
||||||
|
else:
|
||||||
|
self.sub_object = False
|
||||||
|
self.sub_model_object_field = False
|
||||||
|
self.copyvalue = self._build_expression(self.model_object_field.name, False, self.null_value or False)
|
||||||
|
else:
|
||||||
|
self.sub_object = False
|
||||||
|
self.copyvalue = False
|
||||||
|
self.sub_model_object_field = False
|
||||||
|
self.null_value = False
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _build_expression(self, field_name, sub_field_name, null_value):
|
||||||
|
"""Returns a placeholder expression for use in a template field,
|
||||||
|
based on the values provided in the placeholder assistant.
|
||||||
|
|
||||||
|
:param field_name: main field name
|
||||||
|
:param sub_field_name: sub field name (M2O)
|
||||||
|
:param null_value: default value if the target value is empty
|
||||||
|
:return: final placeholder expression """
|
||||||
|
expression = ''
|
||||||
|
if field_name:
|
||||||
|
expression = "${object." + field_name
|
||||||
|
if sub_field_name:
|
||||||
|
expression += "." + sub_field_name
|
||||||
|
if null_value:
|
||||||
|
expression += " or '''%s'''" % null_value
|
||||||
|
expression += "}"
|
||||||
|
return expression
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
@api.returns('self', lambda value: value.id)
|
||||||
|
def copy(self, default=None):
|
||||||
|
default = dict(default or {},
|
||||||
|
name=_("%s (copy)") % self.name)
|
||||||
|
return super(SMSTemplate, self).copy(default=default)
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def _get_context_lang_per_id(self, res_ids):
|
||||||
|
self.ensure_one()
|
||||||
|
if res_ids is None:
|
||||||
|
return {None: self}
|
||||||
|
|
||||||
|
if self.env.context.get('template_preview_lang'):
|
||||||
|
lang = self.env.context.get('template_preview_lang')
|
||||||
|
results = dict((res_id, self.with_context(lang=lang)) for res_id in res_ids)
|
||||||
|
else:
|
||||||
|
rendered_langs = self._render_template(self.lang, self.model, res_ids)
|
||||||
|
results = dict(
|
||||||
|
(res_id, self.with_context(lang=lang) if lang else self)
|
||||||
|
for res_id, lang in rendered_langs.items())
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def _get_ids_per_lang(self, res_ids):
|
||||||
|
self.ensure_one()
|
||||||
|
|
||||||
|
rids_to_tpl = self._get_context_lang_per_id(res_ids)
|
||||||
|
tpl_to_rids = {}
|
||||||
|
for res_id, template in rids_to_tpl.items():
|
||||||
|
tpl_to_rids.setdefault(template._context.get('lang', self.env.user.lang), []).append(res_id)
|
||||||
|
|
||||||
|
return tpl_to_rids
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _render_template(self, template_txt, model, res_ids):
|
||||||
|
""" Render the jinja template """
|
||||||
|
return self.env['mail.template']._render_template(template_txt, model, res_ids)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
access_sms_sms_all,access.sms.sms.all,model_sms_sms,,0,0,0,0
|
||||||
|
access_sms_sms_system,access.sms.sms.system,model_sms_sms,base.group_system,1,1,1,1
|
||||||
|
access_sms_template_all,access.sms.template.all,model_sms_template,,0,0,0,0
|
||||||
|
access_sms_template_user,access.sms.template.user,model_sms_template,base.group_user,1,0,0,0
|
||||||
|
access_sms_template_system,access.sms.template.system,model_sms_template,base.group_system,1,1,1,0
|
||||||
|
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
@@ -0,0 +1,70 @@
|
|||||||
|
odoo.define('sms.fields', function (require) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var basic_fields = require('web.basic_fields');
|
||||||
|
var core = require('web.core');
|
||||||
|
var session = require('web.session');
|
||||||
|
|
||||||
|
var _t = core._t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override of FieldPhone to use add a button calling SMS composer if option activated
|
||||||
|
*/
|
||||||
|
|
||||||
|
var Phone = basic_fields.FieldPhone;
|
||||||
|
Phone.include({
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
// Private
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open SMS composer wizard
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_onClickSMS: function (ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
var context = session.user_context;
|
||||||
|
context = _.extend({}, context, {
|
||||||
|
default_res_model: this.model,
|
||||||
|
default_res_id: parseInt(this.res_id),
|
||||||
|
default_number_field_name: this.name,
|
||||||
|
default_composition_mode: 'comment',
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.do_action({
|
||||||
|
title: _t('Send SMS Text Message'),
|
||||||
|
type: 'ir.actions.act_window',
|
||||||
|
res_model: 'sms.composer',
|
||||||
|
target: 'new',
|
||||||
|
views: [[false, 'form']],
|
||||||
|
context: context,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a button to call the composer wizard
|
||||||
|
*
|
||||||
|
* @override
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_renderReadonly: function () {
|
||||||
|
var def = this._super.apply(this, arguments);
|
||||||
|
if (this.nodeOptions.enable_sms) {
|
||||||
|
var $composerButton = $('<a>', {
|
||||||
|
title: _t('Send SMS Text Message'),
|
||||||
|
href: '',
|
||||||
|
class: 'btn-sm btn-link mb4 fa fa-comment o_field_phone_sms',
|
||||||
|
});
|
||||||
|
$composerButton.on('click', this._onClickSMS.bind(this));
|
||||||
|
this.$el = $('<div/>').append(this.$el).append($composerButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
return def;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return Phone;
|
||||||
|
|
||||||
|
});
|
||||||
+63
-26
@@ -4,21 +4,17 @@ odoo.define('sms.sms_widget', function (require) {
|
|||||||
var basicFields = require('web.basic_fields');
|
var basicFields = require('web.basic_fields');
|
||||||
var core = require('web.core');
|
var core = require('web.core');
|
||||||
var fieldRegistry = require('web.field_registry');
|
var fieldRegistry = require('web.field_registry');
|
||||||
var dom = require('web.dom');
|
|
||||||
var framework = require('web.framework');
|
|
||||||
|
|
||||||
|
|
||||||
var FieldText = basicFields.FieldText;
|
var FieldText = basicFields.FieldText;
|
||||||
var InputField = basicFields.InputField;
|
|
||||||
var QWeb = core.qweb;
|
|
||||||
|
|
||||||
var _t = core._t
|
var _t = core._t;
|
||||||
/**
|
/**
|
||||||
* SmsWidget is a widget to display a textarea (the body) and a text representing
|
* SmsWidget is a widget to display a textarea (the body) and a text representing
|
||||||
* the number of SMS and the number of characters. This text is computed every
|
* the number of SMS and the number of characters. This text is computed every
|
||||||
* time the user changes the body.
|
* time the user changes the body.
|
||||||
*/
|
*/
|
||||||
var SmsWidget = InputField.extend({
|
var SmsWidget = FieldText.extend({
|
||||||
|
className: 'o_field_text',
|
||||||
/**
|
/**
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
@@ -26,12 +22,31 @@ var SmsWidget = InputField.extend({
|
|||||||
this._super.apply(this, arguments);
|
this._super.apply(this, arguments);
|
||||||
this.nbrChar = 0;
|
this.nbrChar = 0;
|
||||||
this.nbrSMS = 0;
|
this.nbrSMS = 0;
|
||||||
this.encoding = "GSM7";
|
this.encoding = 'GSM7';
|
||||||
this.tagName = 'div';
|
|
||||||
},
|
},
|
||||||
|
|
||||||
//--------------------------------------------------------------------------
|
//--------------------------------------------------------------------------
|
||||||
// Private
|
// Private: override widget
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
_renderEdit: function () {
|
||||||
|
var def = this._super.apply(this, arguments);
|
||||||
|
|
||||||
|
this._compute();
|
||||||
|
var $sms_container = $('<div class="o_sms_container"/>');
|
||||||
|
$sms_container.append(this._renderSMSInfo());
|
||||||
|
$sms_container.append(this._renderIAPButton());
|
||||||
|
this.$el = this.$el.add($sms_container);
|
||||||
|
|
||||||
|
return def;
|
||||||
|
},
|
||||||
|
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
// Private: SMS
|
||||||
//--------------------------------------------------------------------------
|
//--------------------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,8 +59,8 @@ var SmsWidget = InputField.extend({
|
|||||||
this.nbrChar = content.length;
|
this.nbrChar = content.length;
|
||||||
this.nbrChar += (content.match(/\n/g) || []).length;
|
this.nbrChar += (content.match(/\n/g) || []).length;
|
||||||
this.nbrSMS = this._countSMS(this.nbrChar, this.encoding);
|
this.nbrSMS = this._countSMS(this.nbrChar, this.encoding);
|
||||||
this._renderSMS();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Count the number of SMS of the content
|
* Count the number of SMS of the content
|
||||||
* @private
|
* @private
|
||||||
@@ -66,16 +81,7 @@ var SmsWidget = InputField.extend({
|
|||||||
}
|
}
|
||||||
return Math.ceil(this.nbrChar / 153);
|
return Math.ceil(this.nbrChar / 153);
|
||||||
},
|
},
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
* @override
|
|
||||||
*/
|
|
||||||
_renderEdit: function () {
|
|
||||||
this.$el.empty();
|
|
||||||
this._prepareInput($('<textarea/>')).appendTo(this.$el);
|
|
||||||
this.$el.append($(QWeb.render("sms.sms_count", {})));
|
|
||||||
this._compute();
|
|
||||||
},
|
|
||||||
/**
|
/**
|
||||||
* Extract the encoding depending on the characters in the content
|
* Extract the encoding depending on the characters in the content
|
||||||
* @private
|
* @private
|
||||||
@@ -88,17 +94,47 @@ var SmsWidget = InputField.extend({
|
|||||||
}
|
}
|
||||||
return 'UNICODE';
|
return 'UNICODE';
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the IAP button to redirect to IAP pricing
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_renderIAPButton: function () {
|
||||||
|
return $('<a>', {
|
||||||
|
'href': 'https://iap-services.odoo.com/iap/sms/pricing',
|
||||||
|
'target': '_blank',
|
||||||
|
'title': _t('SMS Pricing'),
|
||||||
|
'aria-label': _t('SMS Pricing'),
|
||||||
|
'class': 'fa fa-lg fa-info-circle',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the number of characters, sms and the encoding.
|
* Render the number of characters, sms and the encoding.
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_renderSMS: function () {
|
_renderSMSInfo: function () {
|
||||||
this.$('.o_sms_count').text(_.str.sprintf(_t('%s chars, fits in %s SMS (%s) '), this.nbrChar, this.nbrSMS, this.encoding));
|
var string = _.str.sprintf(_t('%s chars, fits in %s SMS (%s) '), this.nbrChar, this.nbrSMS, this.encoding);
|
||||||
|
var $span = $('<span>', {
|
||||||
|
'class': 'text-muted o_sms_count',
|
||||||
|
});
|
||||||
|
$span.text(string);
|
||||||
|
return $span;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update widget SMS information with re-computed info about length, ...
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_updateSMSInfo: function () {
|
||||||
|
this._compute();
|
||||||
|
var string = _.str.sprintf(_t('%s chars, fits in %s SMS (%s) '), this.nbrChar, this.nbrSMS, this.encoding);
|
||||||
|
this.$('.o_sms_count').text(string);
|
||||||
},
|
},
|
||||||
|
|
||||||
//--------------------------------------------------------------------------
|
//--------------------------------------------------------------------------
|
||||||
// Handlers
|
// Handlers
|
||||||
//--------------------------------------------------------------------------
|
//--------------------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @override
|
* @override
|
||||||
@@ -106,15 +142,16 @@ var SmsWidget = InputField.extend({
|
|||||||
*/
|
*/
|
||||||
_onChange: function () {
|
_onChange: function () {
|
||||||
this._super.apply(this, arguments);
|
this._super.apply(this, arguments);
|
||||||
this._compute();
|
this._updateSMSInfo();
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @override
|
* @override
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_onInput: function () {
|
_onInput: function () {
|
||||||
this._super.apply(this, arguments);
|
this._super.apply(this, arguments);
|
||||||
this._compute();
|
this._updateSMSInfo();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
odoo.define('sms.model.MailFailure', function (require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var MailFailure = require('mail.model.MailFailure');
|
||||||
|
var core = require('web.core');
|
||||||
|
var _t = core._t;
|
||||||
|
|
||||||
|
MailFailure.include({
|
||||||
|
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
// Public
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
getPreview: function () {
|
||||||
|
var preview = this._super.apply(this, arguments);
|
||||||
|
if (this._failureType === 'sms') {
|
||||||
|
_.extend(preview, {
|
||||||
|
body: _t('An error occurred when sending SMS'),
|
||||||
|
id: 'sms_failure',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return preview;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
odoo.define('sms.model.Message', function (require) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var Message = require('mail.model.Message');
|
||||||
|
|
||||||
|
Message.include({
|
||||||
|
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
// Public
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the list of SMS
|
||||||
|
*
|
||||||
|
* @returns {Array} Array of Array(smsID, recipientName, smsStatus)
|
||||||
|
*/
|
||||||
|
getSmsIds: function () {
|
||||||
|
return this._smsIds;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Retrieves the SMS status
|
||||||
|
*
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
getSmsStatus: function () {
|
||||||
|
var self = this;
|
||||||
|
this._smsStatus = 'sent';
|
||||||
|
_.each(this._smsIds, function (sms) {
|
||||||
|
if (sms[2] === 'bounce' || sms[2] === 'exception') {
|
||||||
|
self._smsStatus = 'error';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return this._smsStatus;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Does the message contains at least one SMS failure
|
||||||
|
*
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isError: function () {
|
||||||
|
return this.getSmsStatus() === 'error';
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Update the status of a SMS
|
||||||
|
*
|
||||||
|
* @param {integer} smsID ID of the SMS to update
|
||||||
|
* @param {string} smsStatus New status of the SMS
|
||||||
|
*/
|
||||||
|
setSmsStatus: function (smsID, smsStatus) {
|
||||||
|
var self = this;
|
||||||
|
var sms = _.find(self._smsIds, function (sms) {
|
||||||
|
return sms[0] === smsID;
|
||||||
|
});
|
||||||
|
sms[2] = smsStatus;
|
||||||
|
},
|
||||||
|
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
// Private
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_setInitialData: function (data) {
|
||||||
|
this._super.apply(this, arguments);
|
||||||
|
this._smsIds = data.sms_ids;
|
||||||
|
if (this._smsStatus === false) {
|
||||||
|
this._smsStatus = 'sent';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
odoo.define('sms.NotificationManager', function (require) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var MailManager = require('mail.Manager');
|
||||||
|
var MailFailure = require('mail.model.MailFailure');
|
||||||
|
|
||||||
|
MailManager.include({
|
||||||
|
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
// Handlers
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
* @param {Object} data structure depending on the type
|
||||||
|
* @param {integer} data.id
|
||||||
|
*/
|
||||||
|
_handlePartnerNotification: function (data) {
|
||||||
|
if (data.type === 'sms_update') {
|
||||||
|
this._handleSMSUpdateNotification(data);
|
||||||
|
} else {
|
||||||
|
this._super.apply(this, arguments);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Updates message in thread when there's an update in a SMS letter
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {Object} datas
|
||||||
|
* @param {Object[]} datas.elements list of SMS failure data
|
||||||
|
* @param {string} datas.elements[].message_id ID of related message that
|
||||||
|
* has a sms failure.
|
||||||
|
*/
|
||||||
|
_handleSMSUpdateNotification: function (datas) {
|
||||||
|
var self = this;
|
||||||
|
_.each(datas.elements, function (data) {
|
||||||
|
var isNewFailure = data.sms_status === 'error';
|
||||||
|
var matchedFailure = _.find(self._mailFailures, function (failure) {
|
||||||
|
return failure.getMessageID() === data.message_id;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (matchedFailure) {
|
||||||
|
var index = _.findIndex(self._mailFailures, matchedFailure);
|
||||||
|
if (isNewFailure) {
|
||||||
|
self._mailFailures[index] = new MailFailure(self, data);
|
||||||
|
} else {
|
||||||
|
self._mailFailures.splice(index, 1);
|
||||||
|
}
|
||||||
|
} else if (isNewFailure) {
|
||||||
|
self._mailFailures.push(new MailFailure(self, data));
|
||||||
|
}
|
||||||
|
var message = _.find(self._messages, function (msg) {
|
||||||
|
return msg.getID() === data.message_id;
|
||||||
|
});
|
||||||
|
if (message) {
|
||||||
|
message.setSmsStatus(data.sms_id, data.sms_status);
|
||||||
|
self._mailBus.trigger('update_message', message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this._mailBus.trigger('update_needaction', this.needactionCounter);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
odoo.define('sms.systray.MessagingMenu', function (require) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var core = require('web.core');
|
||||||
|
var MessagingMenu = require('mail.systray.MessagingMenu');
|
||||||
|
|
||||||
|
var _t = core._t;
|
||||||
|
|
||||||
|
MessagingMenu.include({
|
||||||
|
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
// Private
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when clicking on a preview related to a snailmail failure
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {$.Element} $target DOM of preview element clicked
|
||||||
|
*/
|
||||||
|
_clickSMSFailurePreview: function ($target) {
|
||||||
|
var documentID = $target.data('document-id');
|
||||||
|
var documentModel = $target.data('document-model');
|
||||||
|
if (documentModel && documentID) {
|
||||||
|
this._openDocument(documentModel, documentID);
|
||||||
|
} else if (documentModel !== 'mail.channel') {
|
||||||
|
// preview of SMS failures grouped to different document of same model
|
||||||
|
this.do_action({
|
||||||
|
name: _t('SMS Failures'),
|
||||||
|
type: 'ir.actions.act_window',
|
||||||
|
view_mode: 'kanban,list,form',
|
||||||
|
views: [[false, 'kanban'], [false, 'list'], [false, 'form']],
|
||||||
|
target: 'current',
|
||||||
|
res_model: documentModel,
|
||||||
|
domain: [['message_has_sms_error', '=', true]],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
// Handlers
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
_onClickPreview: function (ev) {
|
||||||
|
var $target = $(ev.currentTarget);
|
||||||
|
var previewID = $target.data('preview-id');
|
||||||
|
if (previewID === 'sms_failure') {
|
||||||
|
this._clickSMSFailurePreview($target);
|
||||||
|
} else {
|
||||||
|
this._super.apply(this, arguments);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
_onClickPreviewMarkAsRead: function (ev) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
var $preview = $(ev.currentTarget).closest('.o_mail_preview');
|
||||||
|
var previewID = $preview.data('preview-id');
|
||||||
|
if (previewID === 'sms_failure') {
|
||||||
|
var documentModel = $preview.data('document-model');
|
||||||
|
var unreadCounter = $preview.data('unread-counter');
|
||||||
|
this.do_action('sms.sms_cancel_action', {
|
||||||
|
additional_context: {
|
||||||
|
default_model: documentModel,
|
||||||
|
unread_counter: unreadCounter
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this._super.apply(this, arguments);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
odoo.define('sms.widget.Thread', function (require) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var ThreadWidget = require('mail.widget.Thread');
|
||||||
|
var core = require('web.core');
|
||||||
|
|
||||||
|
var QWeb = core.qweb;
|
||||||
|
|
||||||
|
ThreadWidget.include({
|
||||||
|
events: _.extend({}, ThreadWidget.prototype.events, {
|
||||||
|
'click .o_thread_message_sms_error': '_onClickSMSError'
|
||||||
|
}),
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
init: function () {
|
||||||
|
this._super.apply(this, arguments);
|
||||||
|
this._enabledOptions = _.defaults(this._enabledOptions, {
|
||||||
|
displaySmsIcons: true,
|
||||||
|
});
|
||||||
|
this._disabledOptions = _.defaults(this._disabledOptions, {
|
||||||
|
displaySmsIcons: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
render: function (thread, options) {
|
||||||
|
this._super.apply(this, arguments);
|
||||||
|
var messages = _.clone(thread.getMessages({domain: options.domain || []}));
|
||||||
|
this._renderMessageSmsPopover(messages);
|
||||||
|
},
|
||||||
|
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
// Private
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the popover when mouse-hovering on the mail icon of a message
|
||||||
|
* in the thread. There is at most one such popover at any given time.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {mail.model.AbstractMessage[]} messages list of messages in the
|
||||||
|
* rendered thread, for which popover on mouseover interaction is
|
||||||
|
* permitted.
|
||||||
|
*/
|
||||||
|
_renderMessageSmsPopover: function (messages) {
|
||||||
|
if (this._messageSmsPopover) {
|
||||||
|
this._messageSmsPopover.popover('hide');
|
||||||
|
}
|
||||||
|
if (!this.$('.o_thread_sms_tooltip').length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._messageSmsPopover = this.$('.o_thread_sms_tooltip').popover({
|
||||||
|
html: true,
|
||||||
|
boundary: 'viewport',
|
||||||
|
placement: 'auto',
|
||||||
|
trigger: 'hover',
|
||||||
|
offset: '0, 1',
|
||||||
|
content: function () {
|
||||||
|
var messageID = $(this).data('message-id');
|
||||||
|
var message = _.find(messages, function (message) {
|
||||||
|
return message.getID() === messageID;
|
||||||
|
});
|
||||||
|
return QWeb.render('sms.widget.Thread.Message.SmsTooltip', {
|
||||||
|
data: message.getSmsIds()
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
// Handlers
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* @param {MouseEvent} ev
|
||||||
|
*/
|
||||||
|
_onClickSMSError: function (ev) {
|
||||||
|
var messageID = $(ev.currentTarget).data('message-id');
|
||||||
|
this.do_action('sms.sms_resend_action', {
|
||||||
|
additional_context: {
|
||||||
|
default_mail_message_id: messageID
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
i.o_thread_message_sms.o_thread_message_sms_error {
|
||||||
|
color: red;
|
||||||
|
opacity: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<templates>
|
|
||||||
<t t-name="sms.sms_count">
|
|
||||||
<span class="float-right"><span class="text-muted o_sms_count"></span> <a href="https://iap-services.odoo.com/iap/sms/pricing" target="_blank" class="fa fa-lg fa-info-circle" aria-label="SMS Pricing" title="SMS Pricing"/></span>
|
|
||||||
</t>
|
|
||||||
</templates>
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<template id="template" xml:space="preserve">
|
||||||
|
|
||||||
|
<t t-extend="mail.widget.Thread.Message">
|
||||||
|
<t t-jquery=".o_thread_tooltip_container" t-operation="after">
|
||||||
|
<span t-if="message.getType() === 'sms' and options.displaySmsIcons" class="o_thread_sms_tooltip_container">
|
||||||
|
<t t-set="thread_icon_class" t-value="'o_thread_sms_tooltip o_thread_message_sms o_thread_message_sms_' + message.getSmsStatus()" />
|
||||||
|
<i t-attf-class="o_thread_sms_tooltip o_thread_message_sms o_thread_message_sms_#{message.getSmsStatus()} #{message.isError() ? 'o_thread_message_sms_error' : ''} fa fa-comment"
|
||||||
|
t-att-data-message-id="message.getID()"/>
|
||||||
|
</span>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<t t-name="sms.widget.Thread.Message.SmsTooltip">
|
||||||
|
<div t-foreach="data" t-as="customerSMSData">
|
||||||
|
<span class="d-inline-block text-center o_thread_tooltip_icon">
|
||||||
|
<i t-if="customerSMSData[2] === 'sent'" class='fa fa-check' title="Sent" role="img" aria-label="Sent"/>
|
||||||
|
<i t-if="customerSMSData[2] === 'canceled'" class='fa fa-trash-o' title="Canceled" role="img" aria-label="Canceled"/>
|
||||||
|
<i t-if="customerSMSData[2] === 'outgoing'" class='fa fa-clock-o' title="Awaiting Dispatch" role="img" aria-label="Awaiting Dispatch"/>
|
||||||
|
<i t-if="customerSMSData[2] === 'exception'" class='fa fa-exclamation text-danger' title="Error" role="img" aria-label="Error"/>
|
||||||
|
</span>
|
||||||
|
<span t-esc="customerSMSData[1]"/>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</template>
|
||||||
@@ -3,7 +3,8 @@
|
|||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from unittest.mock import patch
|
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.tests import common
|
||||||
from odoo.addons.sms.models.sms_api import SmsApi
|
from odoo.addons.sms.models.sms_api import SmsApi
|
||||||
|
|
||||||
@@ -54,18 +55,106 @@ class MockSMS(common.BaseCase):
|
|||||||
finally:
|
finally:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def _clear_sms_sent(self):
|
||||||
|
self._sms = []
|
||||||
|
|
||||||
def assertSMSSent(self, numbers, content):
|
def assertSMSSent(self, numbers, content):
|
||||||
""" Check sent SMS. Order is not checked. Each number should have received
|
""" 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 numbers: list of numbers;
|
||||||
:param content: content to check for each number;
|
:param content: content to check for each number;
|
||||||
"""
|
"""
|
||||||
self.assertEqual(len(self._sms), len(numbers))
|
|
||||||
for number in numbers:
|
for number in numbers:
|
||||||
sent_sms = next((sms for sms in self._sms if sms['number'] == number), None)
|
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.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)
|
self.assertEqual(sent_sms['body'], content)
|
||||||
|
|
||||||
def _clear_sms_sent(self):
|
def assertSMSCanceled(self, partner, number, error_code, content=None):
|
||||||
self._sms = []
|
""" Check canceled SMS. Search is done for a pair partner / number where
|
||||||
|
partner can be an empty recordset. """
|
||||||
|
if number is None and partner:
|
||||||
|
number = phone_validation.phone_get_sanitized_record_number(partner)
|
||||||
|
sms = self.env['sms.sms'].sudo().search([
|
||||||
|
('partner_id', '=', partner.id), ('number', '=', number),
|
||||||
|
('state', '=', 'canceled')
|
||||||
|
])
|
||||||
|
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. """
|
||||||
|
if number is None and partner:
|
||||||
|
number = phone_validation.phone_get_sanitized_record_number(partner)
|
||||||
|
sms = self.env['sms.sms'].sudo().search([
|
||||||
|
('partner_id', '=', partner.id), ('number', '=', number),
|
||||||
|
('state', '=', 'error')
|
||||||
|
])
|
||||||
|
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. """
|
||||||
|
if number is None and partner:
|
||||||
|
number = phone_validation.phone_get_sanitized_record_number(partner)
|
||||||
|
sms = self.env['sms.sms'].sudo().search([
|
||||||
|
('partner_id', '=', partner.id), ('number', '=', number),
|
||||||
|
('state', '=', 'outgoing')
|
||||||
|
])
|
||||||
|
self.assertTrue(sms, 'SMS: not found failed SMS for %s (number: %s)' % (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'))
|
||||||
|
|||||||
@@ -3,7 +3,14 @@
|
|||||||
<data>
|
<data>
|
||||||
<template id="assets_backend" name="sms_assets" inherit_id="web.assets_backend">
|
<template id="assets_backend" name="sms_assets" inherit_id="web.assets_backend">
|
||||||
<xpath expr="." position="inside">
|
<xpath expr="." position="inside">
|
||||||
<script type="text/javascript" src="/sms/static/src/js/sms_widget.js"></script>
|
<script type="text/javascript" src="/sms/static/src/js/fields_phone_widget.js"></script>
|
||||||
|
<script type="text/javascript" src="/sms/static/src/js/fields_sms_widget.js"></script>
|
||||||
|
<script type="text/javascript" src="/sms/static/src/js/mail_failure.js"></script>
|
||||||
|
<script type="text/javascript" src="/sms/static/src/js/message.js"></script>
|
||||||
|
<script type="text/javascript" src="/sms/static/src/js/sms_notification_manager.js"></script>
|
||||||
|
<script type="text/javascript" src="/sms/static/src/js/systray_messaging_menu.js"></script>
|
||||||
|
<script type="text/javascript" src="/sms/static/src/js/thread_widget.js"></script>
|
||||||
|
<link rel="stylesheet" type="text/scss" href="/sms/static/src/scss/thread.scss"/>
|
||||||
</xpath>
|
</xpath>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -11,34 +11,40 @@
|
|||||||
<xpath expr="//field[@name='phone']" position="replace">
|
<xpath expr="//field[@name='phone']" position="replace">
|
||||||
<label for="phone"/>
|
<label for="phone"/>
|
||||||
<div class="o_row">
|
<div class="o_row">
|
||||||
<field name="phone" widget="phone"/>
|
<field name="phone" widget="phone" options="{'enable_sms': True}"/>
|
||||||
<button
|
|
||||||
type="action"
|
|
||||||
name="%(sms.sms_composer_action_form)d"
|
|
||||||
class="btn-sm btn-link mb4 fa fa-envelope-o"
|
|
||||||
aria-label="Send SMS"
|
|
||||||
title="Send SMS"
|
|
||||||
attrs="{'invisible':[('phone', '=', False)]}"
|
|
||||||
context="{'field_name': 'phone'}"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</xpath>
|
</xpath>
|
||||||
<xpath expr="//field[@name='mobile']" position="replace">
|
<xpath expr="//field[@name='mobile']" position="replace">
|
||||||
<label for="mobile"/>
|
<label for="mobile"/>
|
||||||
<div class="o_row">
|
<div class="o_row">
|
||||||
<field name="mobile" widget="phone"/>
|
<field name="mobile" widget="phone" options="{'enable_sms': True}"/>
|
||||||
<button
|
</div>
|
||||||
type="action"
|
</xpath>
|
||||||
name="%(sms.sms_composer_action_form)d"
|
|
||||||
class="btn-sm btn-link mb4 fa fa-envelope-o"
|
|
||||||
aria-label="Send SMS"
|
|
||||||
title="Send SMS"
|
|
||||||
attrs="{'invisible':[('mobile', '=', False)]}"
|
|
||||||
context="{'field_name': 'mobile'}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</xpath>
|
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
<!-- Add action entry in the Action Menu for Partners -->
|
||||||
|
<act_window id="res_partner_act_window_sms_composer_single"
|
||||||
|
name="Send SMS Text Message"
|
||||||
|
binding_model="res.partner"
|
||||||
|
res_model="sms.composer"
|
||||||
|
binding_views="list"
|
||||||
|
view_mode="form"
|
||||||
|
target="new"
|
||||||
|
context="{
|
||||||
|
'default_composition_mode': 'mass',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<act_window id="res_partner_act_window_sms_composer_multi"
|
||||||
|
name="Send SMS Text Message"
|
||||||
|
binding_model="res.partner"
|
||||||
|
res_model="sms.composer"
|
||||||
|
binding_views="form"
|
||||||
|
view_mode="form"
|
||||||
|
target="new"
|
||||||
|
context="{
|
||||||
|
'default_composition_mode': 'comment',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<odoo><data>
|
||||||
|
<record id="sms_tsms_view_form" model="ir.ui.view">
|
||||||
|
<field name="name">sms.sms.view.form</field>
|
||||||
|
<field name="model">sms.sms</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="SMS">
|
||||||
|
<header>
|
||||||
|
<button name="send" string="Send Now" type="object" states='outgoing' class="oe_highlight"/>
|
||||||
|
<button name="cancel" string="Cancel" type="object" states='outgoing'/>
|
||||||
|
<field name="state" widget="statusbar"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<field name="body"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="partner_id"/>
|
||||||
|
<field name="number"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="error_code"/>
|
||||||
|
<field name="mail_message_id"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="sms_sms_view_tree" model="ir.ui.view">
|
||||||
|
<field name="name">sms.sms.view.tree</field>
|
||||||
|
<field name="model">sms.sms</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<tree string="SMS Templates">
|
||||||
|
<field name="number"/>
|
||||||
|
<field name="partner_id"/>
|
||||||
|
<field name="state"/>
|
||||||
|
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="sms_sms_view_search" model="ir.ui.view">
|
||||||
|
<field name="name">sms.sms.view.search</field>
|
||||||
|
<field name="model">sms.sms</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search string="Search SMS Templates">
|
||||||
|
<field name="number"/>
|
||||||
|
<field name="partner_id"/>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="sms_sms_action" model="ir.actions.act_window">
|
||||||
|
<field name="name">SMS</field>
|
||||||
|
<field name="res_model">sms.sms</field>
|
||||||
|
<field name="view_mode">tree,form</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem id="sms_menu_main" name="SMS" parent="base.menu_custom" sequence="2"/>
|
||||||
|
<menuitem id="sms_sms_menu" parent="sms_menu_main" action="sms_sms_action" sequence="1"/>
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<odoo><data>
|
||||||
|
<record id="sms_template_view_form" model="ir.ui.view">
|
||||||
|
<field name="name">sms.template.view.form</field>
|
||||||
|
<field name="model">sms.template</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="SMS Templates">
|
||||||
|
<sheet>
|
||||||
|
<div class="oe_title">
|
||||||
|
<label for="name" class="oe_edit_only"/>
|
||||||
|
<h1><field name="name" required="1"/></h1>
|
||||||
|
<group>
|
||||||
|
<field name="model_id" required="1" options="{'no_create': True}"/>
|
||||||
|
<field name="model" invisible="1"/>
|
||||||
|
<field name="lang"/>
|
||||||
|
</group>
|
||||||
|
</div>
|
||||||
|
<notebook>
|
||||||
|
<page string="Content">
|
||||||
|
<group>
|
||||||
|
<field name="body" widget="sms_widget" nolabel="1"/>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
<page string="Dynamic Placeholder Generator" groups="base.group_no_one">
|
||||||
|
<group>
|
||||||
|
<field name="model_object_field"
|
||||||
|
domain="[('model_id','=',model_id),('ttype','!=','one2many'),('ttype','!=','many2many')]"/>
|
||||||
|
<field name="sub_object" readonly="1"/>
|
||||||
|
<field name="sub_model_object_field"
|
||||||
|
domain="[('model_id','=',sub_object),('ttype','!=','one2many'),('ttype','!=','many2many')]"
|
||||||
|
attrs="{'readonly':[('sub_object','=',False)],'required':[('sub_object','!=',False)]}"/>
|
||||||
|
<field name="null_value"/>
|
||||||
|
<field name="copyvalue"/>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="sms_template_view_tree" model="ir.ui.view">
|
||||||
|
<field name="name">sms.template.view.tree</field>
|
||||||
|
<field name="model">sms.template</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<tree string="SMS Templates">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="model_id"/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="sms_template_view_search" model="ir.ui.view">
|
||||||
|
<field name="name">sms.template.view.search</field>
|
||||||
|
<field name="model">sms.template</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search string="Search SMS Templates">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="model_id"/>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="sms_template_action" model="ir.actions.act_window">
|
||||||
|
<field name="name">Templates</field>
|
||||||
|
<field name="res_model">sms.template</field>
|
||||||
|
<field name="view_mode">tree,form</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem id="sms_template_menu" parent="sms_menu_main" action="sms_template_action"/>
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from . import sms_cancel
|
||||||
from . import sms_composer
|
from . import sms_composer
|
||||||
|
from . import sms_resend
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from odoo import _, api, fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class SMSCancel(models.TransientModel):
|
||||||
|
_name = 'sms.cancel'
|
||||||
|
_description = 'Dismiss notification for resend by model'
|
||||||
|
|
||||||
|
model = fields.Char(string='Model', required=True)
|
||||||
|
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 SMS delivery failures. You won't be able to re-send these SMS later!") % (wizard._context.get('unread_counter'))
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def action_cancel(self):
|
||||||
|
# TDE CHECK: delete pending SMS
|
||||||
|
author_id = self.env.user.partner_id.id
|
||||||
|
for wizard in self:
|
||||||
|
self._cr.execute("""
|
||||||
|
SELECT notif.id, msg.id
|
||||||
|
FROM mail_message_res_partner_needaction_rel notif
|
||||||
|
JOIN mail_message msg
|
||||||
|
ON notif.mail_message_id = msg.id
|
||||||
|
WHERE notif.notification_type = 'sms' IS TRUE AND notif.notification_status IN ('bounce', 'exception')
|
||||||
|
AND msg.model = %s
|
||||||
|
AND msg.author_id = %s """, (wizard.model, author_id))
|
||||||
|
res = self._cr.fetchall()
|
||||||
|
notif_ids = [row[0] for row in res]
|
||||||
|
message_ids = list(set([row[1] for row in res]))
|
||||||
|
if notif_ids:
|
||||||
|
self.env['mail.notification'].browse(notif_ids).sudo().write({'notification_status': 'canceled'})
|
||||||
|
if message_ids:
|
||||||
|
self.env['mail.message'].browse(message_ids)._notify_sms_update()
|
||||||
|
return {'type': 'ir.actions.act_window_close'}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo><data>
|
||||||
|
<record id="sms_cancel" model="ir.ui.view">
|
||||||
|
<field name="name">sms.cancel.form</field>
|
||||||
|
<field name="model">sms.cancel</field>
|
||||||
|
<field name="groups_id" eval="[(4,ref('base.group_user'))]"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Cancel notification in failure">
|
||||||
|
<field name="model" invisible='1'/>
|
||||||
|
<field name="help_message"/>
|
||||||
|
<p>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 icon next to each message.</p>
|
||||||
|
<footer>
|
||||||
|
<button string="Discard delivery failures" name="cancel_resend_action" type="object" class="btn-primary" />
|
||||||
|
<button string="Cancel" class="btn-secondary" special="cancel" />
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="sms_cancel_action" model="ir.actions.act_window">
|
||||||
|
<field name="name">Discard SMS delivery failures</field>
|
||||||
|
<field name="res_model">sms.cancel</field>
|
||||||
|
<field name="type">ir.actions.act_window</field>
|
||||||
|
<field name="view_mode">form</field>
|
||||||
|
<field name="target">new</field>
|
||||||
|
</record>
|
||||||
|
</data></odoo>
|
||||||
@@ -1,95 +1,261 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
import logging
|
|
||||||
|
|
||||||
|
from ast import literal_eval
|
||||||
|
|
||||||
from odoo import api, fields, models, _
|
from odoo import api, fields, models, _
|
||||||
|
from odoo.addons.phone_validation.tools import phone_validation
|
||||||
from odoo.exceptions import UserError
|
from odoo.exceptions import UserError
|
||||||
from odoo.addons.iap.models import iap
|
from odoo.tools.safe_eval import safe_eval
|
||||||
|
|
||||||
_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):
|
class SendSMS(models.TransientModel):
|
||||||
_name = 'sms.composer'
|
_name = 'sms.composer'
|
||||||
_description = 'Send SMS'
|
_description = 'Send SMS Wizard'
|
||||||
|
_RECIPIENTS_DISPLAY_NBR = 3
|
||||||
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'))
|
|
||||||
elif self.env.context.get('active_ids'):
|
|
||||||
records = model.browse(self.env.context.get('active_ids', []))
|
|
||||||
else:
|
|
||||||
records = model.browse(self.env.context.get('active_id', []))
|
|
||||||
return records
|
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def default_get(self, fields):
|
def default_get(self, fields):
|
||||||
result = super(SendSMS, self).default_get(fields)
|
result = super(SendSMS, self).default_get(fields)
|
||||||
active_model = self.env.context.get('active_model')
|
if fields == 'partner_ids':
|
||||||
|
# shortcut because default_get in cache, avoid issues
|
||||||
|
return result
|
||||||
|
|
||||||
if not self.env.context.get('default_recipients') and active_model and hasattr(self.env[active_model], '_get_default_sms_recipients'):
|
result['res_model'] = result.get('res_model') or self.env.context.get('active_model')
|
||||||
model = self.env[active_model]
|
result['composition_mode'] = result.get('composition_mode') or 'comment'
|
||||||
records = self._get_records(model)
|
|
||||||
partners = records._get_default_sms_recipients()
|
if result['composition_mode'] == 'comment' and not result.get('res_id'):
|
||||||
phone_numbers = []
|
result['res_id'] = self.env.context.get('active_id')
|
||||||
no_phone_partners = []
|
if result['composition_mode'] != 'comment':
|
||||||
for partner in partners:
|
if result.get('use_active_domain') and not result.get('active_domain'):
|
||||||
number = self._sms_sanitization(partner, self.env.context.get('field_name') or 'mobile')
|
result['active_domain'] = self.env.context['active_domain']
|
||||||
if number:
|
elif not result.get('res_ids'):
|
||||||
phone_numbers.append(number)
|
result['res_ids'] = repr(self.env.context.get('active_ids'))
|
||||||
else:
|
|
||||||
no_phone_partners.append(partner.name)
|
if result['res_model']:
|
||||||
if len(partners) > 1:
|
result.update(
|
||||||
if no_phone_partners:
|
self._get_composer_values(
|
||||||
raise UserError(_('Missing mobile number for %s.') % ', '.join(no_phone_partners))
|
result['composition_mode'], result['res_model'], result.get('res_id'),
|
||||||
result['recipients'] = ', '.join(phone_numbers)
|
result.get('body'), result.get('template_id')
|
||||||
|
)
|
||||||
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def action_send_sms(self):
|
# documents
|
||||||
numbers = [number.strip() for number in self.recipients.split(',') if number.strip()]
|
composition_mode = fields.Selection([
|
||||||
|
('numbers', 'Send to numbers'),
|
||||||
|
('comment', 'Post on a document'),
|
||||||
|
('mass', 'Send SMS in batch')],
|
||||||
|
string='Composition Mode', default='comment', required=True)
|
||||||
|
res_model = fields.Char('Document Model Name')
|
||||||
|
res_id = fields.Integer('Document ID')
|
||||||
|
res_ids = fields.Char('Document IDs')
|
||||||
|
use_active_domain = fields.Boolean('Use active domain')
|
||||||
|
active_domain = fields.Text('Active domain', readonly=True)
|
||||||
|
# options for comment and mass mode
|
||||||
|
mass_keep_log = fields.Boolean('Keep a note on document')
|
||||||
|
# recipients
|
||||||
|
recipient_description = fields.Text('Recipients (Partners)', compute='_compute_description')
|
||||||
|
recipient_invalid = fields.Text('Invalid recipients', compute='_compute_description')
|
||||||
|
number_field_name = fields.Char(string='Field holding number')
|
||||||
|
partner_ids = fields.Many2many('res.partner')
|
||||||
|
numbers = fields.Char('Recipients (Numbers)')
|
||||||
|
sanitized_numbers = fields.Char('Sanitized Number', compute='_compute_sanitized_numbers')
|
||||||
|
# content
|
||||||
|
template_id = fields.Many2one('sms.template', string='Use Template', domain="[('model', '=', res_model)]")
|
||||||
|
body = fields.Text('Message', required=True)
|
||||||
|
|
||||||
|
@api.depends('partner_ids', 'res_model', 'res_id', 'res_ids', 'use_active_domain', 'composition_mode', 'number_field_name', 'sanitized_numbers')
|
||||||
|
def _compute_description(self):
|
||||||
|
if self.partner_ids:
|
||||||
|
description = ''
|
||||||
|
description = ','.join('%s - %s' % (partner.display_name, partner.mobile or partner.phone) for partner in self.partner_ids[:self._RECIPIENTS_DISPLAY_NBR])
|
||||||
|
if len(self.partner_ids) > self._RECIPIENTS_DISPLAY_NBR:
|
||||||
|
description += _(' (and %s more)') % (len(self.partner_ids) - self._RECIPIENTS_DISPLAY_NBR)
|
||||||
|
self.recipient_description = description
|
||||||
|
self.recipient_invalid = False
|
||||||
|
|
||||||
|
elif self.composition_mode in ('comment', 'mass') and self.res_model:
|
||||||
|
records = None
|
||||||
|
if self.composition_mode == 'comment' and self.res_id:
|
||||||
|
records = self.env[self.res_model].browse(self.res_id)
|
||||||
|
elif self.composition_mode == 'mass' and self.res_ids:
|
||||||
|
records = self.env[self.res_model].browse(literal_eval(self.res_ids))
|
||||||
|
|
||||||
|
if records and issubclass(type(records), self.pool['mail.thread']):
|
||||||
|
res = records._sms_get_recipients_info(force_field=self.number_field_name)
|
||||||
|
valid_ids = [rid for rid, rvalues in res.items() if rvalues['sanitized']]
|
||||||
|
invalid_ids = [rid for rid, rvalues in res.items() if not rvalues['sanitized']]
|
||||||
|
|
||||||
|
self.recipient_description = ', '.join('%s (%s)' % (
|
||||||
|
res[record.id]['partner'].name or record.display_name,
|
||||||
|
res[record.id]['sanitized'])
|
||||||
|
for record in records if record.id in valid_ids[:self._RECIPIENTS_DISPLAY_NBR]
|
||||||
|
) or False
|
||||||
|
if len(valid_ids) > self._RECIPIENTS_DISPLAY_NBR:
|
||||||
|
self.recipient_description += _(', and %s more') % (len(valid_ids) - self._RECIPIENTS_DISPLAY_NBR)
|
||||||
|
|
||||||
|
self.recipient_invalid = ', '.join('%s (%s)' % (
|
||||||
|
res[record.id]['partner'].display_name or record.display_name,
|
||||||
|
res[record.id]['number'])
|
||||||
|
for record in records if record.id in invalid_ids[:self._RECIPIENTS_DISPLAY_NBR]
|
||||||
|
) or False
|
||||||
|
if len(invalid_ids) > self._RECIPIENTS_DISPLAY_NBR:
|
||||||
|
self.recipient_description += _(', and %s more') % (len(invalid_ids) - self._RECIPIENTS_DISPLAY_NBR)
|
||||||
|
else:
|
||||||
|
self.recipient_description = ''
|
||||||
|
self.recipient_invalid = '' if self.sanitized_numbers or (self.composition_mode == 'mass' and self.use_active_domain) else _('No record found')
|
||||||
|
|
||||||
active_model = self.env.context.get('active_model')
|
|
||||||
if active_model and hasattr(self.env[active_model], 'message_post_send_sms'):
|
|
||||||
model = self.env[active_model]
|
|
||||||
records = self._get_records(model)
|
|
||||||
records.message_post_send_sms(self.message, numbers=numbers)
|
|
||||||
else:
|
else:
|
||||||
self.env['sms.api']._send_sms(numbers, self.message)
|
self.recipient_description = False
|
||||||
|
|
||||||
|
@api.depends('numbers', 'res_model', 'res_id')
|
||||||
|
def _compute_sanitized_numbers(self):
|
||||||
|
if self.numbers:
|
||||||
|
record = self._get_records() if self.res_model and self.res_id else self.env.user
|
||||||
|
sanitize_res = phone_validation.phone_sanitize_numbers_string_w_record(self.numbers, record)
|
||||||
|
sanitized_numbers = [info['sanitized'] for info in sanitize_res.values() if info['sanitized']]
|
||||||
|
invalid_numbers = [number for number, info in sanitize_res.items() if info['code']]
|
||||||
|
if invalid_numbers:
|
||||||
|
raise UserError(_('Following numbers are not correctly encoded: %s') % repr(invalid_numbers))
|
||||||
|
self.sanitized_numbers = ','.join(sanitized_numbers)
|
||||||
|
else:
|
||||||
|
self.sanitized_numbers = False
|
||||||
|
|
||||||
|
@api.onchange('composition_mode', 'res_model', 'res_id', 'template_id')
|
||||||
|
def _onchange_template_id(self):
|
||||||
|
if self.template_id and self.composition_mode == 'comment':
|
||||||
|
self.body = self.template_id._render_template(self.template_id.body, self.res_model, [self.res_id])[self.res_id]
|
||||||
|
elif self.template_id:
|
||||||
|
self.body = self.template_id.body
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Actions
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
|
def action_send_sms(self):
|
||||||
|
if self.recipient_invalid:
|
||||||
|
raise UserError(_('Invalid recipients: %s') % self.recipient_invalid)
|
||||||
|
self._action_send_sms()
|
||||||
|
if self.composition_mode == 'comment':
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.client',
|
||||||
|
'tag': 'reload',
|
||||||
|
}
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _action_send_sms(self, force_send=False):
|
||||||
|
records = self._get_records()
|
||||||
|
if self.composition_mode == 'numbers':
|
||||||
|
return self._action_send_sms_numbers()
|
||||||
|
elif self.composition_mode == 'comment':
|
||||||
|
if records is not None and issubclass(type(records), self.pool['mail.thread']):
|
||||||
|
return self._action_send_sms_comment(records)
|
||||||
|
return self._action_send_sms_numbers()
|
||||||
|
elif self.mass_keep_log:
|
||||||
|
return self._action_send_sms_mass_w_log(records)
|
||||||
|
else:
|
||||||
|
return self._action_send_sms_mass(records)
|
||||||
|
|
||||||
|
def _action_send_sms_numbers(self):
|
||||||
|
self.env['sms.api']._send_sms_batch([{
|
||||||
|
'res_id': 0,
|
||||||
|
'number': number,
|
||||||
|
'content': self.body,
|
||||||
|
} for number in self.sanitized_numbers.split(',')])
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def _action_send_sms_comment(self, records=None):
|
||||||
|
records = records if records is not None else self._get_records()
|
||||||
|
subtype_id = self.env['ir.model.data'].xmlid_to_res_id('mail.mt_comment')
|
||||||
|
|
||||||
|
messages = self.env['mail.message']
|
||||||
|
for record in records:
|
||||||
|
messages |= records._message_sms(
|
||||||
|
self.body, subtype_id=subtype_id,
|
||||||
|
partner_ids=self.partner_ids.ids or False,
|
||||||
|
number_field=self.number_field_name,
|
||||||
|
sms_numbers=self.sanitized_numbers.split(',') if self.sanitized_numbers else None)
|
||||||
|
return messages
|
||||||
|
|
||||||
|
def _action_send_sms_mass(self, records=None):
|
||||||
|
records = records if records is not None else self._get_records()
|
||||||
|
|
||||||
|
record_values = self._prepare_mass_sms_values(records)
|
||||||
|
sms_create_vals = [record_values[record.id] for record in records]
|
||||||
|
sms = self.env['sms.sms'].sudo().create(sms_create_vals)
|
||||||
|
return sms
|
||||||
|
|
||||||
|
def _action_send_sms_mass_w_log(self):
|
||||||
|
records = self._get_records()
|
||||||
|
if records and hasattr(records, '_message_sms'):
|
||||||
|
subtype_id = self.env['ir.model.data'].xmlid_to_res_id('mail.mt_note')
|
||||||
|
for record in records:
|
||||||
|
record._message_sms(self.body, subtype_id=subtype_id, partner_ids=False, sms_numbers=None)
|
||||||
|
else:
|
||||||
|
self.env['sms.api']._send_sms(self.numbers, self.body)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Mass mode specific
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
|
def _prepare_recipient_values(self, records=None):
|
||||||
|
records = records if records is not None else self._get_records()
|
||||||
|
recipients_info = records._sms_get_recipients_info(force_field=self.number_field_name)
|
||||||
|
return recipients_info
|
||||||
|
|
||||||
|
def _prepare_body_values(self, records=None):
|
||||||
|
records = records if records is not None else self._get_records()
|
||||||
|
if self.template_id and self.body == self.template_id.body:
|
||||||
|
lang_to_rids = self.template_id._get_ids_per_lang(records.ids)
|
||||||
|
all_bodies = {}
|
||||||
|
for lang, rids in lang_to_rids.items():
|
||||||
|
template = self.template_id.with_context(lang=lang)
|
||||||
|
all_bodies.update(template._render_template(template.body, records._name, rids))
|
||||||
|
else:
|
||||||
|
all_bodies = self.env['mail.template']._render_template(self.body, records._name, records.ids)
|
||||||
|
return all_bodies
|
||||||
|
|
||||||
|
def _prepare_mass_sms_values(self, records=None):
|
||||||
|
records = records if records is not None else self._get_records()
|
||||||
|
all_bodies = self._prepare_body_values(records=records)
|
||||||
|
all_recipients = self._prepare_recipient_values(records=records)
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
for record in records:
|
||||||
|
recipients = all_recipients[record.id]
|
||||||
|
result[record.id] = {
|
||||||
|
'body': all_bodies[record.id],
|
||||||
|
'partner_id': recipients['partner'].id,
|
||||||
|
'number': recipients['sanitized'] or recipients['number'],
|
||||||
|
'state': 'outgoing' if recipients['sanitized'] else 'error',
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Tools
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
|
def _get_composer_values(self, composition_mode, res_model, res_id, body, template_id):
|
||||||
|
result = {}
|
||||||
|
if composition_mode == 'comment':
|
||||||
|
if not body and template_id:
|
||||||
|
template = self.env['sms.template'].browse(template_id)
|
||||||
|
result['body'] = template._render_template(template.body, res_model, [res_id])[res_id]
|
||||||
|
else:
|
||||||
|
if not body and template_id:
|
||||||
|
template = self.env['sms.template'].browse(template_id)
|
||||||
|
result['body'] = template.body
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _get_records(self):
|
||||||
|
if not self.res_model:
|
||||||
|
return None
|
||||||
|
if self.use_active_domain: # TDE FIXME: clear active_domain (False,[], ..)
|
||||||
|
records = self.env[self.res_model].search(safe_eval(self.active_domain))
|
||||||
|
elif self.res_id:
|
||||||
|
records = self.env[self.res_model].browse(self.res_id)
|
||||||
|
else:
|
||||||
|
records = self.env[self.res_model].browse(literal_eval(self.res_ids))
|
||||||
|
return records
|
||||||
|
|||||||
@@ -7,35 +7,76 @@
|
|||||||
<form string="Send an SMS">
|
<form string="Send an SMS">
|
||||||
<sheet>
|
<sheet>
|
||||||
<group>
|
<group>
|
||||||
<field name="recipients"/>
|
<field name="composition_mode" invisible="1"/>
|
||||||
<field name="message" widget="sms_widget"/>
|
<field name="res_id" invisible="1"/>
|
||||||
|
<field name="res_ids" invisible="1"/>
|
||||||
|
<field name="active_domain" invisible="1"/>
|
||||||
|
<field name="res_model" invisible="1"/>
|
||||||
|
<field name="number_field_name" invisible="1"/>
|
||||||
|
<field name="partner_ids" invisible="1"/>
|
||||||
|
<field name="numbers" invisible="1"/>
|
||||||
|
<field name="sanitized_numbers" invisible="1"/>
|
||||||
|
|
||||||
|
<!-- Recipient warning -->
|
||||||
|
<div colspan="2" class="alert alert-danger" role="alert"
|
||||||
|
attrs="{'invisible': [('recipient_invalid', '=', False)]}">
|
||||||
|
<strong>Some recipients are invalid</strong><br />
|
||||||
|
<field name="recipient_invalid"/>
|
||||||
|
</div>
|
||||||
|
<!-- Mass mode information (res_ids versus active domain) -->
|
||||||
|
<div colspan="2" class="oe_form_box_info bg-info oe_text_center mb-3"
|
||||||
|
attrs="{'invisible': [('composition_mode', '=', 'comment')]}">
|
||||||
|
<p attrs="{'invisible': [('use_active_domain', '=', False)]}">
|
||||||
|
<strong>All records matching your current search filter will receive a SMS,
|
||||||
|
not only the ids selected in the list view.</strong><br />
|
||||||
|
The SMS will be sent for all the records selected in the list.<br />
|
||||||
|
Confirming this wizard will probably take a few minutes blocking your browser.
|
||||||
|
</p>
|
||||||
|
<p attrs="{'invisible': [('use_active_domain', '=', True)]}">
|
||||||
|
<strong>Only records checked in list view will be used.</strong><br />
|
||||||
|
The SMS will be sent for all the records selected in the list.
|
||||||
|
</p>
|
||||||
|
<p class="mt8">
|
||||||
|
<span attrs="{'invisible': [('use_active_domain', '=', True)]}">
|
||||||
|
If you want to send it for all the records matching your search criterion, check this box :
|
||||||
|
</span>
|
||||||
|
<span attrs="{'invisible': [('use_active_domain', '=', False)]}">
|
||||||
|
If you want to use only selected records please uncheck this selection box :
|
||||||
|
</span>
|
||||||
|
<field class="oe_inline" name="use_active_domain"/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label for="partner_ids" string="Recipients"/>
|
||||||
|
<div>
|
||||||
|
<span attrs="{'invisible': ['|', ('composition_mode', '!=', 'mass'), ('use_active_domain', '=', False)]}">
|
||||||
|
<strong>Mass SMS</strong> on the current search filter
|
||||||
|
</span>
|
||||||
|
<field name="recipient_description" readonly="1" attrs="{
|
||||||
|
'invisible': [('use_active_domain', '=', True)]}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<field name="body" widget="sms_widget"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="template_id" options="{'no_create': True}" attrs="{'invisible': [('composition_mode', '!=', 'mass')]}"/>
|
||||||
|
</group>
|
||||||
</group>
|
</group>
|
||||||
</sheet>
|
</sheet>
|
||||||
<footer>
|
<footer>
|
||||||
<group>
|
<button string="Send SMS" type="object" class="oe_highlight" name="action_send_sms"/>
|
||||||
<span>
|
<button string="Close" class="btn btn-secondary" special="cancel"/>
|
||||||
<button string="Send" type="object" class="oe_highlight" name="action_send_sms"/>
|
|
||||||
<button string="Cancel" class="oe_link" special="cancel" />
|
|
||||||
</span>
|
|
||||||
</group>
|
|
||||||
</footer>
|
</footer>
|
||||||
</form>
|
</form>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="sms_composer_action_form" model="ir.actions.act_window">
|
<record id="sms_composer_action_form" model="ir.actions.act_window">
|
||||||
<field name="name">Send SMS</field>
|
<field name="name">Send SMS Text Message</field>
|
||||||
<field name="res_model">sms.composer</field>
|
<field name="res_model">sms.composer</field>
|
||||||
<field name="view_mode">form</field>
|
<field name="view_mode">form</field>
|
||||||
<field name="target">new</field>
|
<field name="target">new</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<!-- Add action entry in the Action Menu for Partners -->
|
|
||||||
<act_window id="sms_composer_act_window"
|
|
||||||
name="Send SMS"
|
|
||||||
binding_model="res.partner"
|
|
||||||
res_model="sms.composer"
|
|
||||||
binding_views="list"
|
|
||||||
view_mode="form"
|
|
||||||
target="new"/>
|
|
||||||
|
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from odoo import _, api, exceptions, fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class SMSRecipient(models.TransientModel):
|
||||||
|
_name = 'sms.resend.recipient'
|
||||||
|
_description = 'Resend Notification'
|
||||||
|
_rec_name = 'sms_resend_id'
|
||||||
|
|
||||||
|
sms_resend_id = fields.Many2one('sms.resend', required=True)
|
||||||
|
notification_id = fields.Many2one('mail.notification', required=True, ondelete='cascade')
|
||||||
|
resend = fields.Boolean(string="Resend", default=True)
|
||||||
|
failure_type = fields.Selection([
|
||||||
|
('sms_number_missing', 'Missing Number'),
|
||||||
|
('sms_number_format', 'Wrong Number Format'),
|
||||||
|
('sms_credit', 'Insufficient Credit'),
|
||||||
|
('sms_server', 'Server Error')], related='notification_id.failure_type', related_sudo=True, readonly=True)
|
||||||
|
partner_id = fields.Many2one('res.partner', 'Partner', related='notification_id.res_partner_id', readonly=True)
|
||||||
|
partner_name = fields.Char('Recipient', readonly='True')
|
||||||
|
sms_number = fields.Char('Number')
|
||||||
|
|
||||||
|
|
||||||
|
class SMSResend(models.TransientModel):
|
||||||
|
_name = 'sms.resend'
|
||||||
|
_description = 'SMS Resend'
|
||||||
|
_rec_name = 'mail_message_id'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def default_get(self, fields):
|
||||||
|
result = super(SMSResend, self).default_get(fields)
|
||||||
|
if result.get('mail_message_id'):
|
||||||
|
mail_message_id = self.env['mail.message'].browse(result['mail_message_id'])
|
||||||
|
result['recipient_ids'] = [(0, 0, {
|
||||||
|
'notification_id': notif.id,
|
||||||
|
'resend': True,
|
||||||
|
'failure_type': notif.failure_type,
|
||||||
|
'partner_name': notif.res_partner_id.display_name or mail_message_id.record_name,
|
||||||
|
'sms_number': notif.sms_number,
|
||||||
|
}) for notif in mail_message_id.notification_ids if notif.notification_type == 'sms' and notif.notification_status in ('exception', 'bounce')]
|
||||||
|
return result
|
||||||
|
|
||||||
|
mail_message_id = fields.Many2one('mail.message', 'Message', readonly=True, required=True)
|
||||||
|
recipient_ids = fields.One2many('sms.resend.recipient', 'sms_resend_id', string='Recipients')
|
||||||
|
has_cancel = fields.Boolean(compute='_compute_has_cancel')
|
||||||
|
has_insufficient_credit = fields.Boolean(compute='_compute_has_insufficient_credit')
|
||||||
|
|
||||||
|
@api.depends("recipient_ids.failure_type")
|
||||||
|
def _compute_has_insufficient_credit(self):
|
||||||
|
self.has_insufficient_credit = self.recipient_ids.filtered(lambda p: p.failure_type == 'sms_credit')
|
||||||
|
|
||||||
|
@api.depends("recipient_ids.resend")
|
||||||
|
def _compute_has_cancel(self):
|
||||||
|
self.has_cancel = self.recipient_ids.filtered(lambda p: not p.resend)
|
||||||
|
|
||||||
|
def _check_access(self):
|
||||||
|
if not self.mail_message_id or not self.mail_message_id.model or not self.mail_message_id.res_id:
|
||||||
|
raise exceptions.UserError(_('You do not have access to the message and/or related document.'))
|
||||||
|
record = self.env[self.mail_message_id.model].browse(self.mail_message_id.res_id)
|
||||||
|
record.check_access_rights('read')
|
||||||
|
record.check_access_rule('read')
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def action_resend(self):
|
||||||
|
self._check_access()
|
||||||
|
|
||||||
|
all_notifications = self.env['mail.notification'].sudo().search([
|
||||||
|
('mail_message_id', '=', self.mail_message_id.id),
|
||||||
|
('notification_type', '=', 'sms'),
|
||||||
|
('notification_status', 'in', ('exception', 'bounce'))
|
||||||
|
])
|
||||||
|
sudo_self = self.sudo()
|
||||||
|
to_cancel_ids = [r.notification_id.id for r in sudo_self.recipient_ids if not r.resend]
|
||||||
|
to_resend_ids = [r.notification_id.id for r in sudo_self.recipient_ids if r.resend]
|
||||||
|
|
||||||
|
if to_cancel_ids:
|
||||||
|
all_notifications.filtered(lambda n: n.id in to_cancel_ids).write({'notification_status': 'canceled'})
|
||||||
|
|
||||||
|
if to_resend_ids:
|
||||||
|
record = self.env[self.mail_message_id.model].browse(self.mail_message_id.res_id)
|
||||||
|
|
||||||
|
sms_pid_to_number = dict((r.partner_id.id, r.sms_number) for r in self.recipient_ids if r.resend and r.partner_id)
|
||||||
|
pids = list(sms_pid_to_number.keys())
|
||||||
|
numbers = [r.sms_number for r in self.recipient_ids if r.resend and not r.partner_id]
|
||||||
|
|
||||||
|
rdata = []
|
||||||
|
for pid, cid, active, pshare, ctype, notif, groups in self.env['mail.followers']._get_recipient_data(record, 'sms', False, pids=pids):
|
||||||
|
if pid and notif == 'sms':
|
||||||
|
rdata.append({'id': pid, 'share': pshare, 'active': active, 'notif': notif, 'groups': groups or [], 'type': 'customer' if pshare else 'user'})
|
||||||
|
if rdata or numbers:
|
||||||
|
record._notify_record_by_sms(
|
||||||
|
self.mail_message_id, {'partners': rdata}, check_existing=True,
|
||||||
|
sms_numbers=numbers, sms_pid_to_number=sms_pid_to_number,
|
||||||
|
put_in_queue=False
|
||||||
|
)
|
||||||
|
|
||||||
|
self.mail_message_id._notify_sms_update()
|
||||||
|
return {'type': 'ir.actions.act_window_close'}
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def action_cancel(self):
|
||||||
|
self._check_access()
|
||||||
|
|
||||||
|
sudo_self = self.sudo()
|
||||||
|
sudo_self.mapped('recipient_ids.notification_id').write({'notification_status': 'canceled'})
|
||||||
|
self.mail_message_id._notify_sms_update()
|
||||||
|
return {'type': 'ir.actions.act_window_close'}
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def action_buy_credits(self):
|
||||||
|
url = self.env['iap.account'].get_credits_url(service_name='sms')
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.act_url',
|
||||||
|
'url': url,
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo><data>
|
||||||
|
<record id="mail_resend_message_view_form" model="ir.ui.view">
|
||||||
|
<field name="name">sms.resend.form</field>
|
||||||
|
<field name="model">sms.resend</field>
|
||||||
|
<field name="groups_id" eval="[(4,ref('base.group_user'))]"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Edit Partners">
|
||||||
|
<field name="mail_message_id" invisible="1"/>
|
||||||
|
<field name="has_cancel" invisible="1"/>
|
||||||
|
<field name="has_insufficient_credit" invisible="1"/>
|
||||||
|
<field name="recipient_ids">
|
||||||
|
<tree string="Recipient" editable="top" create="0" delete="0">
|
||||||
|
<field name="partner_name"/>
|
||||||
|
<field name="sms_number"/>
|
||||||
|
<field name="failure_type" string="Reason"/>
|
||||||
|
<field name="resend" widget="boolean_toggle"/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
<div class="alert alert-warning" role="alert" attrs="{'invisible': [('has_cancel', '=', False)]}">
|
||||||
|
<span class="fa fa-info-circle"/> Caution: It won't be possible to send this SMS again to the recipients you did not select.
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
<button string="Buy credits" name="action_buy_credits" type="object" class="btn-primary o_mail_send"
|
||||||
|
attrs="{'invisible': [('has_insufficient_credit', '=', False)]}"/>
|
||||||
|
<button string="Resend" name="action_resend" type="object" class="btn-primary o_mail_send"/>
|
||||||
|
<button string="Ignore all" name="action_cancel" type="object" class="btn-secondary" />
|
||||||
|
<button string="Cancel" class="btn-secondary" special="cancel" />
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="sms_resend_action" model="ir.actions.act_window">
|
||||||
|
<field name="name">Sending Failures</field>
|
||||||
|
<field name="res_model">sms.resend</field>
|
||||||
|
<field name="type">ir.actions.act_window</field>
|
||||||
|
<field name="view_mode">form</field>
|
||||||
|
<field name="target">new</field>
|
||||||
|
</record>
|
||||||
|
</data></odoo>
|
||||||
@@ -86,13 +86,13 @@ class BaseFunctionalTest(common.SavepointCase):
|
|||||||
self.assertEqual(expected, real, 'Invalid number of notification for %s: %s instead of %s' %
|
self.assertEqual(expected, real, 'Invalid number of notification for %s: %s instead of %s' %
|
||||||
(partner.name, real, expected))
|
(partner.name, real, expected))
|
||||||
if partner_notif:
|
if partner_notif:
|
||||||
self.assertTrue(all(n.is_email == (notif_type == 'email') for n in partner_notif))
|
self.assertTrue(all(n.notification_type == notif_type for n in partner_notif))
|
||||||
self.assertTrue(all(n.is_read == (notif_read == 'read') for n in partner_notif),
|
self.assertTrue(all(n.is_read == (notif_read == 'read') for n in partner_notif),
|
||||||
'Invalid read status for %s' % partner.name)
|
'Invalid read status for %s' % partner.name)
|
||||||
|
|
||||||
# for simplification, limitate to single message asserts
|
# for simplification, limitate to single message asserts
|
||||||
if hasattr(self, 'assertEmails') and len(new_messages) == 1:
|
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'))
|
self.assertEmails(new_messages.author_id, new_notifications.filtered(lambda n: n.notification_type == 'email').mapped('res_partner_id'))
|
||||||
|
|
||||||
def assertBusNotification(self, channels, message_items=None, init=True):
|
def assertBusNotification(self, channels, message_items=None, init=True):
|
||||||
""" Check for bus notifications. Basic check is about used channels.
|
""" Check for bus notifications. Basic check is about used channels.
|
||||||
@@ -174,13 +174,13 @@ class TestRecipients(common.SavepointCase):
|
|||||||
'name': 'Valid Lelitre',
|
'name': 'Valid Lelitre',
|
||||||
'email': 'valid.lelitre@agrolait.com',
|
'email': 'valid.lelitre@agrolait.com',
|
||||||
'country_id': cls.env.ref('base.be').id,
|
'country_id': cls.env.ref('base.be').id,
|
||||||
'mobile': '0475001122',
|
'mobile': '0456001122',
|
||||||
})
|
})
|
||||||
cls.partner_2 = Partner.create({
|
cls.partner_2 = Partner.create({
|
||||||
'name': 'Valid Poilvache',
|
'name': 'Valid Poilvache',
|
||||||
'email': 'valid.other@gmail.com',
|
'email': 'valid.other@gmail.com',
|
||||||
'country_id': cls.env.ref('base.be').id,
|
'country_id': cls.env.ref('base.be').id,
|
||||||
'mobile': '+32 475 22 11 00',
|
'mobile': '+32 456 22 11 00',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
from contextlib import closing
|
|
||||||
import psycopg2
|
import psycopg2
|
||||||
|
|
||||||
from odoo import api
|
from odoo import api
|
||||||
from odoo.addons.test_mail.tests import common as mail_common
|
from odoo.addons.test_mail.tests import common as mail_common
|
||||||
from odoo.tests import common
|
from odoo.tests import common
|
||||||
from odoo.tools import mute_logger
|
from odoo.tools import mute_logger
|
||||||
|
|
||||||
|
|
||||||
class TestMailRace(common.TransactionCase, mail_common.MockEmails):
|
class TestMailRace(common.TransactionCase, mail_common.MockEmails):
|
||||||
|
|
||||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||||
@@ -26,15 +27,15 @@ class TestMailRace(common.TransactionCase, mail_common.MockEmails):
|
|||||||
'subject': 'S',
|
'subject': 'S',
|
||||||
'body': 'B',
|
'body': 'B',
|
||||||
'subtype_id': self.ref('mail.mt_comment'),
|
'subtype_id': self.ref('mail.mt_comment'),
|
||||||
'needaction_partner_ids': [(6, 0, [self.partner.id])],
|
'notification_ids': [(0, 0, {
|
||||||
|
'res_partner_id': self.partner.id,
|
||||||
|
'mail_id': mail.id,
|
||||||
|
'notification_type': 'email',
|
||||||
|
'is_read': True,
|
||||||
|
'notification_status': 'ready',
|
||||||
|
})],
|
||||||
})
|
})
|
||||||
notif = self.env['mail.notification'].search([('res_partner_id', '=', self.partner.id)])
|
notif = self.env['mail.notification'].search([('res_partner_id', '=', self.partner.id)])
|
||||||
notif.write({
|
|
||||||
'mail_id': mail.id,
|
|
||||||
'is_email': True,
|
|
||||||
'is_read': True,
|
|
||||||
'email_status': 'ready',
|
|
||||||
})
|
|
||||||
# we need to commit transaction or cr will keep the lock on notif
|
# we need to commit transaction or cr will keep the lock on notif
|
||||||
self.cr.commit()
|
self.cr.commit()
|
||||||
|
|
||||||
@@ -46,7 +47,7 @@ class TestMailRace(common.TransactionCase, mail_common.MockEmails):
|
|||||||
with this.registry.cursor() as cr, mute_logger('odoo.sql_db'):
|
with this.registry.cursor() as cr, mute_logger('odoo.sql_db'):
|
||||||
try:
|
try:
|
||||||
# try ro aquire lock (no wait) on notification (should fail)
|
# try ro aquire lock (no wait) on notification (should fail)
|
||||||
cr.execute("SELECT email_status FROM mail_message_res_partner_needaction_rel WHERE id = %s FOR UPDATE NOWAIT", [notif.id])
|
cr.execute("SELECT notification_status FROM mail_message_res_partner_needaction_rel WHERE id = %s FOR UPDATE NOWAIT", [notif.id])
|
||||||
except psycopg2.OperationalError:
|
except psycopg2.OperationalError:
|
||||||
# record already locked by send, all good
|
# record already locked by send, all good
|
||||||
bounce_deferred.append(True)
|
bounce_deferred.append(True)
|
||||||
@@ -55,14 +56,14 @@ class TestMailRace(common.TransactionCase, mail_common.MockEmails):
|
|||||||
# Only here to simulate the initial use case
|
# Only here to simulate the initial use case
|
||||||
# If the record is lock, this line would create a deadlock since we are in the same thread
|
# If the record is lock, this line would create a deadlock since we are in the same thread
|
||||||
# In practice, the update will wait the end of the send() transaction and set the notif as bounce, as expeced
|
# In practice, the update will wait the end of the send() transaction and set the notif as bounce, as expeced
|
||||||
cr.execute("UPDATE mail_message_res_partner_needaction_rel SET email_status='bounce' WHERE id = %s", [notif.id])
|
cr.execute("UPDATE mail_message_res_partner_needaction_rel SET notification_status='bounce' WHERE id = %s", [notif.id])
|
||||||
return message['Message-Id']
|
return message['Message-Id']
|
||||||
self.env['ir.mail_server']._patch_method('send_email', send_email)
|
self.env['ir.mail_server']._patch_method('send_email', send_email)
|
||||||
|
|
||||||
mail.send()
|
mail.send()
|
||||||
|
|
||||||
self.assertTrue(bounce_deferred, "The bounce should have been deferred")
|
self.assertTrue(bounce_deferred, "The bounce should have been deferred")
|
||||||
self.assertEqual(notif.email_status, 'sent')
|
self.assertEqual(notif.notification_status, 'sent')
|
||||||
|
|
||||||
# some cleaning since we commited the cr
|
# some cleaning since we commited the cr
|
||||||
notif.unlink()
|
notif.unlink()
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class TestMailResend(common.BaseFunctionalTest, common.MockEmails):
|
|||||||
|
|
||||||
def assertNotifStates(self, states, message):
|
def assertNotifStates(self, states, message):
|
||||||
notif = self.env['mail.notification'].search([('mail_message_id', '=', message.id)], order="res_partner_id asc")
|
notif = self.env['mail.notification'].search([('mail_message_id', '=', message.id)], order="res_partner_id asc")
|
||||||
self.assertEquals(tuple(notif.mapped('email_status')), states)
|
self.assertEquals(tuple(notif.mapped('notification_status')), states)
|
||||||
return notif
|
return notif
|
||||||
|
|
||||||
def assertBusMessage(self, partners):
|
def assertBusMessage(self, partners):
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ class TestAdvMailPerformance(BaseMailPerformance):
|
|||||||
def test_message_assignation_email(self):
|
def test_message_assignation_email(self):
|
||||||
self.user_test.write({'notification_type': 'email'})
|
self.user_test.write({'notification_type': 'email'})
|
||||||
record = self.env['mail.test.track'].create({'name': 'Test'})
|
record = self.env['mail.test.track'].create({'name': 'Test'})
|
||||||
with self.assertQueryCount(__system__=55, emp=58): # com runbot: 55 - 58 // test_mail only: 55 - 58
|
with self.assertQueryCount(__system__=52, emp=54): # com runbot: 52 - 54 // test_mail only: 52 - 54
|
||||||
record.write({
|
record.write({
|
||||||
'user_id': self.user_test.id,
|
'user_id': self.user_test.id,
|
||||||
})
|
})
|
||||||
@@ -209,7 +209,7 @@ class TestAdvMailPerformance(BaseMailPerformance):
|
|||||||
@warmup
|
@warmup
|
||||||
def test_message_assignation_inbox(self):
|
def test_message_assignation_inbox(self):
|
||||||
record = self.env['mail.test.track'].create({'name': 'Test'})
|
record = self.env['mail.test.track'].create({'name': 'Test'})
|
||||||
with self.assertQueryCount(__system__=33, emp=39): # test_mail only: 33 - 39
|
with self.assertQueryCount(__system__=32, emp=37): # test_mail only: 32 - 37
|
||||||
record.write({
|
record.write({
|
||||||
'user_id': self.user_test.id,
|
'user_id': self.user_test.id,
|
||||||
})
|
})
|
||||||
@@ -253,7 +253,7 @@ class TestAdvMailPerformance(BaseMailPerformance):
|
|||||||
def test_message_post_one_email_notification(self):
|
def test_message_post_one_email_notification(self):
|
||||||
record = self.env['mail.test.simple'].create({'name': 'Test'})
|
record = self.env['mail.test.simple'].create({'name': 'Test'})
|
||||||
|
|
||||||
with self.assertQueryCount(__system__=47, emp=51): # com runbot: 47 - 51 // test_mail only: 47 - 51
|
with self.assertQueryCount(__system__=44, emp=47): # com runbot: 44 - 47 // test_mail only: 44 - 47
|
||||||
record.message_post(
|
record.message_post(
|
||||||
body='<p>Test Post Performances with an email ping</p>',
|
body='<p>Test Post Performances with an email ping</p>',
|
||||||
partner_ids=self.customer.ids,
|
partner_ids=self.customer.ids,
|
||||||
@@ -265,7 +265,7 @@ class TestAdvMailPerformance(BaseMailPerformance):
|
|||||||
def test_message_post_one_inbox_notification(self):
|
def test_message_post_one_inbox_notification(self):
|
||||||
record = self.env['mail.test.simple'].create({'name': 'Test'})
|
record = self.env['mail.test.simple'].create({'name': 'Test'})
|
||||||
|
|
||||||
with self.assertQueryCount(__system__=30, emp=36): # com runbot 30 - 36 // test_mail only: 30 - 36
|
with self.assertQueryCount(__system__=29, emp=34): # com runbot 29 - 34 // test_mail only: 29 - 34
|
||||||
record.message_post(
|
record.message_post(
|
||||||
body='<p>Test Post Performances with an inbox ping</p>',
|
body='<p>Test Post Performances with an inbox ping</p>',
|
||||||
partner_ids=self.user_test.partner_id.ids,
|
partner_ids=self.user_test.partner_id.ids,
|
||||||
@@ -373,7 +373,7 @@ class TestHeavyMailPerformance(BaseMailPerformance):
|
|||||||
self.umbrella.message_subscribe(self.user_portal.partner_id.ids)
|
self.umbrella.message_subscribe(self.user_portal.partner_id.ids)
|
||||||
record = self.umbrella.with_user(self.env.user)
|
record = self.umbrella.with_user(self.env.user)
|
||||||
|
|
||||||
with self.assertQueryCount(__system__=78, emp=82): # com runbot: 78 - 82 // test_mail only: 78 - 82
|
with self.assertQueryCount(__system__=82, emp=85): # com runbot: 82 - 85 // test_mail only: 82 - 85
|
||||||
record.message_post(
|
record.message_post(
|
||||||
body='<p>Test Post Performances</p>',
|
body='<p>Test Post Performances</p>',
|
||||||
message_type='comment',
|
message_type='comment',
|
||||||
@@ -390,7 +390,7 @@ class TestHeavyMailPerformance(BaseMailPerformance):
|
|||||||
record = self.umbrella.with_user(self.env.user)
|
record = self.umbrella.with_user(self.env.user)
|
||||||
template_id = self.env.ref('test_mail.mail_test_tpl').id
|
template_id = self.env.ref('test_mail.mail_test_tpl').id
|
||||||
|
|
||||||
with self.assertQueryCount(__system__=93, emp=99): # com runbot: 93 - 99 // test_mail only: 93 - 99
|
with self.assertQueryCount(__system__=98, emp=103): # com runbot: 98 - 103 // test_mail only: 98 - 103
|
||||||
record.message_post_with_template(template_id, message_type='comment', composition_mode='comment')
|
record.message_post_with_template(template_id, message_type='comment', composition_mode='comment')
|
||||||
|
|
||||||
self.assertEqual(record.message_ids[0].body, '<p>Adding stuff on %s</p>' % record.name)
|
self.assertEqual(record.message_ids[0].body, '<p>Adding stuff on %s</p>' % record.name)
|
||||||
@@ -459,7 +459,7 @@ class TestHeavyMailPerformance(BaseMailPerformance):
|
|||||||
'user_id': self.env.uid,
|
'user_id': self.env.uid,
|
||||||
})
|
})
|
||||||
self.assertEqual(rec.message_partner_ids, self.partners | self.env.user.partner_id)
|
self.assertEqual(rec.message_partner_ids, self.partners | self.env.user.partner_id)
|
||||||
with self.assertQueryCount(__system__=54, emp=57): # com runbot: 54 - 57 // test_mail only: 54 - 57
|
with self.assertQueryCount(__system__=51, emp=53): # com runbot: 51 - 53 // test_mail only: 51 - 53
|
||||||
rec.write({'user_id': self.user_portal.id})
|
rec.write({'user_id': self.user_portal.id})
|
||||||
self.assertEqual(rec.message_partner_ids, self.partners | self.env.user.partner_id | self.user_portal.partner_id)
|
self.assertEqual(rec.message_partner_ids, self.partners | self.env.user.partner_id | self.user_portal.partner_id)
|
||||||
# write tracking message
|
# write tracking message
|
||||||
@@ -479,7 +479,7 @@ class TestHeavyMailPerformance(BaseMailPerformance):
|
|||||||
customer_id = self.customer.id
|
customer_id = self.customer.id
|
||||||
user_id = self.user_portal.id
|
user_id = self.user_portal.id
|
||||||
|
|
||||||
with self.assertQueryCount(__system__=145, emp=149): # com runbot: 145 - 149 // test_mail only: 145 - 149
|
with self.assertQueryCount(__system__=146, emp=149): # com runbot: 146 - 149 // test_mail only: 146 - 149
|
||||||
rec = self.env['mail.test.full'].create({
|
rec = self.env['mail.test.full'].create({
|
||||||
'name': 'Test',
|
'name': 'Test',
|
||||||
'umbrella_id': umbrella_id,
|
'umbrella_id': umbrella_id,
|
||||||
@@ -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(rec.message_partner_ids, self.user_portal.partner_id | self.env.user.partner_id)
|
||||||
self.assertEqual(len(rec.message_ids), 1)
|
self.assertEqual(len(rec.message_ids), 1)
|
||||||
with self.assertQueryCount(__system__=95, emp=100): # com runbot: 95 - 100 // test_mail only: 95 - 100
|
with self.assertQueryCount(__system__=100, emp=106): # com runbot: 100 -106 // test_mail only: 100 - 106
|
||||||
rec.write({
|
rec.write({
|
||||||
'name': 'Test2',
|
'name': 'Test2',
|
||||||
'umbrella_id': self.umbrella.id,
|
'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)
|
self.assertEqual(rec.message_partner_ids, self.user_portal.partner_id | self.env.user.partner_id)
|
||||||
|
|
||||||
with self.assertQueryCount(__system__=100, emp=105): # test_mail only: 100 - 105
|
with self.assertQueryCount(__system__=105, emp=111): # test_mail only: 105 - 111
|
||||||
rec.write({
|
rec.write({
|
||||||
'name': 'Test2',
|
'name': 'Test2',
|
||||||
'umbrella_id': umbrella_id,
|
'umbrella_id': umbrella_id,
|
||||||
@@ -704,14 +704,14 @@ class TestMailPerformancePost(BaseMailPerformance):
|
|||||||
partner_ids = [self.user_inbox.partner_id.id, self.user_email.partner_id.id, self.partner.id]
|
partner_ids = [self.user_inbox.partner_id.id, self.user_email.partner_id.id, self.partner.id]
|
||||||
channel_ids = [self.channel_inbox.id, self.channel_email.id]
|
channel_ids = [self.channel_inbox.id, self.channel_email.id]
|
||||||
record = self.record.with_user(self.env.user)
|
record = self.record.with_user(self.env.user)
|
||||||
attachements = [ # not linear on number of attachements
|
attachements = [ # not linear on number of attachements
|
||||||
('attach tuple 1', "attachement tupple content 1"),
|
('attach tuple 1', "attachement tupple content 1"),
|
||||||
('attach tuple 2', "attachement tupple content 2", {'cid': 'cid1'}),
|
('attach tuple 2', "attachement tupple content 2", {'cid': 'cid1'}),
|
||||||
('attach tuple 3', "attachement tupple content 3", {'cid': 'cid2'}),
|
('attach tuple 3', "attachement tupple content 3", {'cid': 'cid2'}),
|
||||||
]
|
]
|
||||||
self.attachements = self.env['ir.attachment'].with_user(self.env.user).create(self.vals) #-> 163-> 165 query
|
self.attachements = self.env['ir.attachment'].with_user(self.env.user).create(self.vals)
|
||||||
attachement_ids = self.attachements.ids
|
attachement_ids = self.attachements.ids
|
||||||
with self.assertQueryCount(emp=175): # com runbot 154 // test_mail only: 133
|
with self.assertQueryCount(emp=175): # com runbot 154 // test_mail only: 132
|
||||||
self.cr.sql_log = self.warm and self.cr.sql_log_count
|
self.cr.sql_log = self.warm and self.cr.sql_log_count
|
||||||
record.with_context({}).message_post(
|
record.with_context({}).message_post(
|
||||||
body='<p>Test body <img src="cid:cid1"> <img src="cid:cid2"></p>',
|
body='<p>Test body <img src="cid:cid1"> <img src="cid:cid2"></p>',
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ real applications. """,
|
|||||||
'mail',
|
'mail',
|
||||||
'mail_bot',
|
'mail_bot',
|
||||||
# 'snailmail',
|
# 'snailmail',
|
||||||
'mass_mailing',
|
# 'mass_mailing',
|
||||||
'phone_validation',
|
'phone_validation',
|
||||||
'sms',
|
'sms',
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ class MailTestSMS(models.Model):
|
|||||||
_description = 'Chatter Model for SMS Gateway'
|
_description = 'Chatter Model for SMS Gateway'
|
||||||
_name = 'mail.test.sms'
|
_name = 'mail.test.sms'
|
||||||
_inherit = ['mail.thread']
|
_inherit = ['mail.thread']
|
||||||
|
_order = 'name asc, id asc'
|
||||||
|
|
||||||
name = fields.Char()
|
name = fields.Char()
|
||||||
subject = fields.Char()
|
subject = fields.Char()
|
||||||
@@ -18,6 +19,19 @@ class MailTestSMS(models.Model):
|
|||||||
mobile_nbr = fields.Char()
|
mobile_nbr = fields.Char()
|
||||||
customer_id = fields.Many2one('res.partner', 'Customer')
|
customer_id = fields.Many2one('res.partner', 'Customer')
|
||||||
|
|
||||||
@api.multi
|
def _sms_get_default_partners(self):
|
||||||
def _get_default_sms_recipients(self):
|
|
||||||
return self.mapped('customer_id')
|
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')
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
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_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_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
|
||||||
|
|||||||
|
@@ -2,4 +2,8 @@
|
|||||||
|
|
||||||
from . import common
|
from . import common
|
||||||
from . import test_sms_composer
|
from . import test_sms_composer
|
||||||
|
from . import test_sms_management
|
||||||
|
from . import test_sms_performance
|
||||||
from . import test_sms_post
|
from . import test_sms_post
|
||||||
|
from . import test_sms_sms
|
||||||
|
from . import test_sms_template
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from odoo.addons.phone_validation.tools import phone_validation
|
||||||
from odoo.addons.test_mail.tests import common as test_mail_common
|
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
|
# 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})
|
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]
|
||||||
|
|||||||
@@ -1,74 +1,309 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
# 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.sms.tests import common as sms_common
|
||||||
from odoo.addons.test_mail.tests import common as test_mail_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.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):
|
class TestSMSComposerComment(test_mail_full_common.BaseFunctionalTest, sms_common.MockSMS, test_mail_common.MockEmails, test_mail_common.TestRecipients):
|
||||||
|
""" TODO LIST
|
||||||
|
|
||||||
|
* add test for default_res_model / default_res_id and stuff like that;
|
||||||
|
* add test for comment put in queue;
|
||||||
|
* add test for language support (set template lang context);
|
||||||
|
* add test for sanitized / wrong numbers;
|
||||||
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
super(TestSMSComposer, cls).setUpClass()
|
super(TestSMSComposerComment, cls).setUpClass()
|
||||||
cls._test_body = 'VOID CONTENT'
|
cls._test_body = 'VOID CONTENT'
|
||||||
|
|
||||||
cls.partner_numbers = [
|
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)
|
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):
|
cls.test_record = cls.env['mail.test.sms'].with_context(**cls._test_context).create({
|
||||||
composer = self.env['sms.composer'].with_context().create({
|
'name': 'Test',
|
||||||
'message': self._test_body,
|
'customer_id': cls.partner_1.id,
|
||||||
'recipients': '+32475998877, 0475997788'
|
'mobile_nbr': cls.test_numbers[0],
|
||||||
|
'phone_nbr': cls.test_numbers[1],
|
||||||
|
})
|
||||||
|
cls.test_record = cls._reset_mail_context(cls.test_record)
|
||||||
|
|
||||||
|
cls.sms_template = cls.env['sms.template'].create({
|
||||||
|
'name': 'Test Template',
|
||||||
|
'model_id': cls.env['ir.model']._get('mail.test.sms').id,
|
||||||
|
'body': 'Dear ${object.display_name} this is an SMS.',
|
||||||
})
|
})
|
||||||
|
|
||||||
with self.mockSMSGateway():
|
def test_composer_comment_not_mail_thread(self):
|
||||||
composer.action_send_sms()
|
with self.sudo('employee'):
|
||||||
# self.assertSMSSent(self.random_numbers, self._test_body) # TDE FIXME: actually sanitizer is not called on numbers in current master (saas 12.3)
|
record = self.env['test_performance.base'].create({'name': 'TestBase'})
|
||||||
self.assertSMSSent('+32475998877, 0475997788'.split(', '), self._test_body)
|
composer = self.env['sms.composer'].with_context(
|
||||||
|
active_model='test_performance.base', active_id=record.id
|
||||||
def test_composer_no_mail_thread(self):
|
).create({
|
||||||
composer = self.env['sms.composer'].with_context(active_model='test_performance.base').create({
|
'body': self._test_body,
|
||||||
'message': self._test_body,
|
'numbers': ','.join(self.random_numbers),
|
||||||
'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(
|
with self.mockSMSGateway():
|
||||||
active_model='res.partner',
|
composer._action_send_sms()
|
||||||
active_domain=[('id', 'in', partners.ids)]
|
|
||||||
).create({
|
self.assertSMSSent(self.random_numbers_san, self._test_body)
|
||||||
'message': self._test_body,
|
|
||||||
|
def test_composer_comment_default(self):
|
||||||
|
with self.sudo('employee'):
|
||||||
|
composer = self.env['sms.composer'].with_context(
|
||||||
|
active_model='mail.test.sms', active_id=self.test_record.id
|
||||||
|
).create({
|
||||||
|
'body': self._test_body,
|
||||||
|
})
|
||||||
|
|
||||||
|
with self.mockSMSGateway():
|
||||||
|
messages = composer._action_send_sms()
|
||||||
|
|
||||||
|
self.assertSMSNotification([{'partner': self.test_record.customer_id, 'number': self.test_numbers_san[1]}], self._test_body, messages)
|
||||||
|
|
||||||
|
def test_composer_comment_field_1(self):
|
||||||
|
with self.sudo('employee'):
|
||||||
|
composer = self.env['sms.composer'].with_context(
|
||||||
|
active_model='mail.test.sms', active_id=self.test_record.id,
|
||||||
|
).create({
|
||||||
|
'body': self._test_body,
|
||||||
|
'number_field_name': 'mobile_nbr',
|
||||||
|
})
|
||||||
|
|
||||||
|
with self.mockSMSGateway():
|
||||||
|
messages = composer._action_send_sms()
|
||||||
|
|
||||||
|
self.assertSMSNotification([{'partner': self.test_record.customer_id, 'number': self.test_numbers_san[0]}], self._test_body, messages)
|
||||||
|
|
||||||
|
def test_composer_comment_field_2(self):
|
||||||
|
with self.sudo('employee'):
|
||||||
|
composer = self.env['sms.composer'].with_context(
|
||||||
|
active_model='mail.test.sms', active_id=self.test_record.id,
|
||||||
|
).create({
|
||||||
|
'body': self._test_body,
|
||||||
|
'number_field_name': 'phone_nbr',
|
||||||
|
})
|
||||||
|
|
||||||
|
with self.mockSMSGateway():
|
||||||
|
messages = composer._action_send_sms()
|
||||||
|
|
||||||
|
self.assertSMSNotification([{'partner': self.test_record.customer_id, 'number': self.test_numbers_san[1]}], self._test_body, messages)
|
||||||
|
|
||||||
|
def test_composer_comment_field_w_numbers(self):
|
||||||
|
with self.sudo('employee'):
|
||||||
|
composer = self.env['sms.composer'].with_context(
|
||||||
|
active_model='mail.test.sms', active_id=self.test_record.id,
|
||||||
|
default_number_field_name='mobile_nbr',
|
||||||
|
).create({
|
||||||
|
'body': self._test_body,
|
||||||
|
'numbers': ','.join(self.random_numbers),
|
||||||
|
})
|
||||||
|
|
||||||
|
with self.mockSMSGateway():
|
||||||
|
messages = composer._action_send_sms()
|
||||||
|
|
||||||
|
self.assertSMSNotification([
|
||||||
|
{'partner': self.test_record.customer_id, 'number': self.test_record.mobile_nbr},
|
||||||
|
{'number': self.random_numbers_san[0]}, {'number': self.random_numbers_san[1]}], self._test_body, messages)
|
||||||
|
|
||||||
|
def test_composer_comment_field_w_template(self):
|
||||||
|
with self.sudo('employee'):
|
||||||
|
composer = self.env['sms.composer'].with_context(
|
||||||
|
active_model='mail.test.sms', active_id=self.test_record.id,
|
||||||
|
default_template_id=self.sms_template.id,
|
||||||
|
default_number_field_name='mobile_nbr',
|
||||||
|
).create({})
|
||||||
|
|
||||||
|
with self.mockSMSGateway():
|
||||||
|
messages = composer._action_send_sms()
|
||||||
|
|
||||||
|
self.assertSMSNotification([{'partner': self.test_record.customer_id, 'number': self.test_record.mobile_nbr}], 'Dear %s this is an SMS.' % self.test_record.display_name, messages)
|
||||||
|
|
||||||
|
def test_composer_numbers_no_model(self):
|
||||||
|
with self.sudo('employee'):
|
||||||
|
composer = self.env['sms.composer'].with_context(
|
||||||
|
default_composition_mode='numbers'
|
||||||
|
).create({
|
||||||
|
'body': self._test_body,
|
||||||
|
'numbers': ','.join(self.random_numbers),
|
||||||
|
})
|
||||||
|
|
||||||
|
with self.mockSMSGateway():
|
||||||
|
composer._action_send_sms()
|
||||||
|
self.assertSMSSent(self.random_numbers_san, 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
|
||||||
|
# with self.sudo('employee'):
|
||||||
|
# composer = self.env['sms.composer'].with_context(
|
||||||
|
# active_model='res.partner',
|
||||||
|
# active_domain=[('id', 'in', partners.ids)]
|
||||||
|
# ).create({
|
||||||
|
# 'body': self._test_body,
|
||||||
|
# })
|
||||||
|
|
||||||
|
# with self.mockSMSGateway():
|
||||||
|
# composer.action_send_sms()
|
||||||
|
# self.assertSMSSent((self.partner_1 | self.partner_2).mapped('mobile'), test_body)
|
||||||
|
|
||||||
|
|
||||||
|
class TestSMSComposerMass(test_mail_full_common.BaseFunctionalTest, sms_common.MockSMS, test_mail_common.MockEmails, test_mail_common.TestRecipients):
|
||||||
|
""" TODO LIST
|
||||||
|
|
||||||
|
* add test for mass with log note
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super(TestSMSComposerMass, cls).setUpClass()
|
||||||
|
cls._test_body = 'Zizisse an SMS.'
|
||||||
|
|
||||||
|
records = cls.env['mail.test.sms']
|
||||||
|
partners = cls.env['res.partner']
|
||||||
|
country_id = cls.env.ref('base.be').id,
|
||||||
|
for x in range(3):
|
||||||
|
partners += cls.env['res.partner'].with_context(**cls._test_context).create({
|
||||||
|
'name': 'Partner_%s' % (x),
|
||||||
|
'email': '_test_partner_%s@example.com' % (x),
|
||||||
|
'country_id': country_id,
|
||||||
|
'mobile': '047500%s%s99' % (x, x)
|
||||||
|
})
|
||||||
|
records += cls.env['mail.test.sms'].with_context(**cls._test_context).create({
|
||||||
|
'name': 'Test_%s' % (x),
|
||||||
|
'customer_id': partners[x].id,
|
||||||
|
})
|
||||||
|
cls.records = cls._reset_mail_context(records)
|
||||||
|
cls.partners = partners
|
||||||
|
|
||||||
|
cls.sms_template = cls.env['sms.template'].create({
|
||||||
|
'name': 'Test Template',
|
||||||
|
'model_id': cls.env['ir.model']._get('mail.test.sms').id,
|
||||||
|
'body': 'Dear ${object.display_name} this is an SMS.',
|
||||||
})
|
})
|
||||||
|
|
||||||
with self.mockSMSGateway():
|
def test_composer_mass_active_domain(self):
|
||||||
composer.action_send_sms()
|
with self.sudo('employee'):
|
||||||
# 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))
|
composer = self.env['sms.composer'].with_context(
|
||||||
|
default_composition_mode='mass',
|
||||||
|
default_res_model='mail.test.sms',
|
||||||
|
default_use_active_domain=True,
|
||||||
|
active_domain=repr([('id', 'in', self.records.ids)]),
|
||||||
|
).create({
|
||||||
|
'body': self._test_body,
|
||||||
|
})
|
||||||
|
|
||||||
|
with self.mockSMSGateway():
|
||||||
|
composer.action_send_sms()
|
||||||
|
|
||||||
|
for record in self.records:
|
||||||
|
self.assertSMSOutgoing(record.customer_id, None, self._test_body)
|
||||||
|
|
||||||
|
def test_composer_mass_active_domain_w_template(self):
|
||||||
|
with self.sudo('employee'):
|
||||||
|
composer = self.env['sms.composer'].with_context(
|
||||||
|
default_composition_mode='mass',
|
||||||
|
default_res_model='mail.test.sms',
|
||||||
|
default_use_active_domain=True,
|
||||||
|
active_domain=repr([('id', 'in', self.records.ids)]),
|
||||||
|
default_template_id=self.sms_template.id,
|
||||||
|
).create({
|
||||||
|
})
|
||||||
|
|
||||||
|
with self.mockSMSGateway():
|
||||||
|
composer.action_send_sms()
|
||||||
|
|
||||||
|
for record in self.records:
|
||||||
|
self.assertSMSOutgoing(record.customer_id, None, 'Dear %s this is an SMS.' % record.display_name)
|
||||||
|
|
||||||
|
def test_composer_mass_active_ids(self):
|
||||||
|
with self.sudo('employee'):
|
||||||
|
composer = self.env['sms.composer'].with_context(
|
||||||
|
default_composition_mode='mass',
|
||||||
|
default_res_model='mail.test.sms',
|
||||||
|
active_ids=self.records.ids,
|
||||||
|
).create({
|
||||||
|
'body': self._test_body,
|
||||||
|
})
|
||||||
|
|
||||||
|
with self.mockSMSGateway():
|
||||||
|
composer.action_send_sms()
|
||||||
|
|
||||||
|
for partner in self.partners:
|
||||||
|
self.assertSMSOutgoing(partner, None, self._test_body)
|
||||||
|
|
||||||
|
def test_composer_mass_active_ids_w_template(self):
|
||||||
|
with self.sudo('employee'):
|
||||||
|
composer = self.env['sms.composer'].with_context(
|
||||||
|
default_composition_mode='mass',
|
||||||
|
default_res_model='mail.test.sms',
|
||||||
|
active_ids=self.records.ids,
|
||||||
|
default_template_id=self.sms_template.id,
|
||||||
|
).create({})
|
||||||
|
|
||||||
|
with self.mockSMSGateway():
|
||||||
|
composer.action_send_sms()
|
||||||
|
|
||||||
|
for record in self.records:
|
||||||
|
self.assertSMSOutgoing(record.customer_id, None, 'Dear %s this is an SMS.' % record.display_name)
|
||||||
|
|
||||||
|
def test_composer_mass_active_ids_w_template_and_lang(self):
|
||||||
|
self.env.ref('base.lang_fr').write({'active': True})
|
||||||
|
self.env['ir.translation'].create({
|
||||||
|
'type': 'model',
|
||||||
|
'name': 'sms.template,body',
|
||||||
|
'lang': 'fr_FR',
|
||||||
|
'res_id': self.sms_template.id,
|
||||||
|
'src': self.sms_template.body,
|
||||||
|
'value': 'Cher·e· ${object.display_name} ceci est un SMS.',
|
||||||
|
})
|
||||||
|
# set template to try to use customer lang
|
||||||
|
self.sms_template.write({
|
||||||
|
'lang': '${object.customer_id.lang}',
|
||||||
|
})
|
||||||
|
# set one customer as french speaking
|
||||||
|
self.partners[2].write({'lang': 'fr_FR'})
|
||||||
|
|
||||||
|
with self.sudo('employee'):
|
||||||
|
composer = self.env['sms.composer'].with_context(
|
||||||
|
default_composition_mode='mass',
|
||||||
|
default_res_model='mail.test.sms',
|
||||||
|
active_ids=self.records.ids,
|
||||||
|
default_template_id=self.sms_template.id,
|
||||||
|
).create({})
|
||||||
|
|
||||||
|
with self.mockSMSGateway():
|
||||||
|
composer.action_send_sms()
|
||||||
|
|
||||||
|
for record in self.records:
|
||||||
|
if record.customer_id == self.partners[2]:
|
||||||
|
self.assertSMSOutgoing(record.customer_id, None, 'Cher·e· %s ceci est un SMS.' % record.display_name)
|
||||||
|
else:
|
||||||
|
self.assertSMSOutgoing(record.customer_id, None, 'Dear %s this is an SMS.' % record.display_name)
|
||||||
|
|
||||||
|
def test_message_schedule_sms(self):
|
||||||
|
with self.sudo('employee'):
|
||||||
|
with self.mockSMSGateway():
|
||||||
|
self.env['mail.test.sms'].browse(self.records.ids)._message_sms_schedule_mass(body=self._test_body)
|
||||||
|
|
||||||
|
for record in self.records:
|
||||||
|
self.assertSMSOutgoing(record.customer_id, None, self._test_body)
|
||||||
|
|
||||||
|
def test_message_schedule_sms_w_template(self):
|
||||||
|
with self.sudo('employee'):
|
||||||
|
with self.mockSMSGateway():
|
||||||
|
self.env['mail.test.sms'].browse(self.records.ids)._message_sms_schedule_mass(template=self.sms_template)
|
||||||
|
|
||||||
|
for record in self.records:
|
||||||
|
self.assertSMSOutgoing(record.customer_id, None, 'Dear %s this is an SMS.' % record.display_name)
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
# -*- 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 TestSMSWizards(test_mail_full_common.BaseFunctionalTest, sms_common.MockSMS, test_mail_common.MockEmails, test_mail_common.TestRecipients):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super(TestSMSWizards, 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)
|
||||||
|
cls.msg = cls.test_record.message_post(body='TEST BODY', author_id=cls.partner_employee.id)
|
||||||
|
cls.notif_p1 = cls.env['mail.notification'].create({
|
||||||
|
'mail_message_id': cls.msg.id,
|
||||||
|
'res_partner_id': cls.partner_1.id,
|
||||||
|
'sms_number': cls.partner_1.mobile,
|
||||||
|
'notification_type': 'sms',
|
||||||
|
'notification_status': 'exception',
|
||||||
|
'failure_type': 'sms_number_format',
|
||||||
|
})
|
||||||
|
cls.notif_p2 = cls.env['mail.notification'].create({
|
||||||
|
'mail_message_id': cls.msg.id,
|
||||||
|
'res_partner_id': cls.partner_2.id,
|
||||||
|
'sms_number': cls.partner_2.mobile,
|
||||||
|
'notification_type': 'sms',
|
||||||
|
'notification_status': 'exception',
|
||||||
|
'failure_type': 'sms_credit',
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_sms_resend(self):
|
||||||
|
self._clear_bus()
|
||||||
|
|
||||||
|
with self.sudo('employee'):
|
||||||
|
wizard = self.env['sms.resend'].with_context(default_mail_message_id=self.msg.id).create({})
|
||||||
|
wizard.write({'recipient_ids': [(1, r.id, {'resend': True}) for r in wizard.recipient_ids]})
|
||||||
|
with self.mockSMSGateway():
|
||||||
|
wizard.action_resend()
|
||||||
|
|
||||||
|
self.assertSMSNotification([
|
||||||
|
{'partner': self.partner_1, 'state': 'sent'},
|
||||||
|
{'partner': self.partner_2, 'state': 'sent'}
|
||||||
|
], 'TEST BODY', self.msg, check_sms=True)
|
||||||
|
self.assertBusNotification(
|
||||||
|
[(self.cr.dbname, 'res.partner', self.partner_employee.id)],
|
||||||
|
[{'type': 'sms_update', 'elements': [{
|
||||||
|
'message_id': self.msg.id,
|
||||||
|
'failure_type': 'sms',
|
||||||
|
'notifications': {'%s' % self.partner_1.id: ['sent', self.partner_1.name], '%s' % self.partner_2.id: ['sent', self.partner_2.name]}
|
||||||
|
}]}]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_sms_resend_update_number(self):
|
||||||
|
self._clear_bus()
|
||||||
|
|
||||||
|
with self.sudo('employee'):
|
||||||
|
wizard = self.env['sms.resend'].with_context(default_mail_message_id=self.msg.id).create({})
|
||||||
|
wizard.write({'recipient_ids': [(1, r.id, {'resend': True, 'sms_number': self.random_numbers[idx]}) for idx, r in enumerate(wizard.recipient_ids)]})
|
||||||
|
with self.mockSMSGateway():
|
||||||
|
wizard.action_resend()
|
||||||
|
|
||||||
|
self.assertSMSNotification([
|
||||||
|
{'partner': self.partner_1, 'state': 'sent', 'number': self.random_numbers_san[0]},
|
||||||
|
{'partner': self.partner_2, 'state': 'sent', 'number': self.random_numbers_san[1]}
|
||||||
|
], 'TEST BODY', self.msg, check_sms=True)
|
||||||
|
self.assertBusNotification(
|
||||||
|
[(self.cr.dbname, 'res.partner', self.partner_employee.id)],
|
||||||
|
[{'type': 'sms_update', 'elements': [{
|
||||||
|
'message_id': self.msg.id,
|
||||||
|
'failure_type': 'sms',
|
||||||
|
'notifications': {'%s' % self.partner_1.id: ['sent', self.partner_1.name], '%s' % self.partner_2.id: ['sent', self.partner_2.name]}
|
||||||
|
}]}]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_sms_resend_cancel(self):
|
||||||
|
self._clear_bus()
|
||||||
|
|
||||||
|
with self.sudo('employee'):
|
||||||
|
wizard = self.env['sms.resend'].with_context(default_mail_message_id=self.msg.id).create({})
|
||||||
|
with self.mockSMSGateway():
|
||||||
|
wizard.action_cancel()
|
||||||
|
|
||||||
|
self.assertSMSNotification([
|
||||||
|
{'partner': self.partner_1, 'state': 'canceled', 'number': self.notif_p1.sms_number, 'failure_type': 'sms_number_format'},
|
||||||
|
{'partner': self.partner_2, 'state': 'canceled', 'number': self.notif_p2.sms_number, 'failure_type': 'sms_credit'}
|
||||||
|
], 'TEST BODY', self.msg, check_sms=False)
|
||||||
|
self.assertBusNotification(
|
||||||
|
[(self.cr.dbname, 'res.partner', self.partner_employee.id)],
|
||||||
|
[{'type': 'sms_update', 'elements': [{
|
||||||
|
'message_id': self.msg.id,
|
||||||
|
'failure_type': 'sms',
|
||||||
|
'notifications': {'%s' % self.partner_1.id: ['canceled', self.partner_1.name], '%s' % self.partner_2.id: ['canceled', self.partner_2.name]}
|
||||||
|
}]}]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_sms_resend_internals(self):
|
||||||
|
self._clear_bus()
|
||||||
|
self.assertSMSNotification([
|
||||||
|
{'partner': self.partner_1, 'state': 'exception', 'number': self.notif_p1.sms_number, 'failure_type': 'sms_number_format'},
|
||||||
|
{'partner': self.partner_2, 'state': 'exception', 'number': self.notif_p2.sms_number, 'failure_type': 'sms_credit'}
|
||||||
|
], 'TEST BODY', self.msg, check_sms=False)
|
||||||
|
|
||||||
|
with self.sudo('employee'):
|
||||||
|
wizard = self.env['sms.resend'].with_context(default_mail_message_id=self.msg.id).create({})
|
||||||
|
self.assertTrue(wizard.has_insufficient_credit)
|
||||||
|
self.assertEqual(set(wizard.mapped('recipient_ids.partner_name')), set((self.partner_1 | self.partner_2).mapped('display_name')))
|
||||||
|
wizard.write({'recipient_ids': [(1, r.id, {'resend': True}) for r in wizard.recipient_ids]})
|
||||||
|
with self.mockSMSGateway():
|
||||||
|
wizard.action_resend()
|
||||||
|
|
||||||
|
def test_sms_resend_w_cancel(self):
|
||||||
|
self._clear_bus()
|
||||||
|
|
||||||
|
with self.sudo('employee'):
|
||||||
|
wizard = self.env['sms.resend'].with_context(default_mail_message_id=self.msg.id).create({})
|
||||||
|
wizard.write({'recipient_ids': [(1, r.id, {'resend': True if r.partner_id == self.partner_1 else False}) for r in wizard.recipient_ids]})
|
||||||
|
with self.mockSMSGateway():
|
||||||
|
wizard.action_resend()
|
||||||
|
|
||||||
|
self.assertSMSNotification([{'partner': self.partner_1, 'state': 'sent'}], 'TEST BODY', self.msg, check_sms=True)
|
||||||
|
self.assertSMSNotification([{'partner': self.partner_2, 'state': 'canceled', 'number': self.notif_p2.sms_number, 'failure_type': 'sms_credit'}], 'TEST BODY', self.msg, check_sms=False)
|
||||||
|
self.assertBusNotification(
|
||||||
|
[(self.cr.dbname, 'res.partner', self.partner_employee.id)],
|
||||||
|
[{'type': 'sms_update', 'elements': [{
|
||||||
|
'message_id': self.msg.id,
|
||||||
|
'failure_type': 'sms',
|
||||||
|
'notifications': {'%s' % self.partner_1.id: ['sent', self.partner_1.name], '%s' % self.partner_2.id: ['canceled', self.partner_2.name]}
|
||||||
|
}]}]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_sms_cancel(self):
|
||||||
|
self._clear_bus()
|
||||||
|
|
||||||
|
with self.mockSMSGateway(), self.sudo('employee'):
|
||||||
|
wizard = self.env['sms.cancel'].with_context(default_model=self.msg.model).create({})
|
||||||
|
wizard.action_cancel()
|
||||||
|
|
||||||
|
self.assertEqual((self.notif_p1 | self.notif_p2).mapped('notification_status'), ['canceled', 'canceled'])
|
||||||
|
|
||||||
|
self.assertBusNotification(
|
||||||
|
[(self.cr.dbname, 'res.partner', self.partner_employee.id)],
|
||||||
|
[{'type': 'sms_update', 'elements': [{
|
||||||
|
'message_id': self.msg.id,
|
||||||
|
'failure_type': 'sms',
|
||||||
|
'notifications': {'%s' % self.partner_1.id: ['canceled', self.partner_1.name], '%s' % self.partner_2.id: ['canceled', self.partner_2.name]}
|
||||||
|
}]}]
|
||||||
|
)
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
# -*- 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',
|
||||||
|
'country_id': self.env.ref('base.be').id,
|
||||||
|
})
|
||||||
|
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, '<p>Performance Test</p>')
|
||||||
|
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, '<p>Performance Test</p>')
|
||||||
|
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, '<p>Performance Test</p>')
|
||||||
|
self.assertSMSNotification([{'partner': self.customer}], 'Performance Test', messages)
|
||||||
|
|
||||||
|
|
||||||
|
@tagged('mail_performance')
|
||||||
|
class TestSMSMassPerformance(BaseMailPerformance, sms_common.MockSMS):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestSMSMassPerformance, self).setUp()
|
||||||
|
be_country_id = self.env.ref('base.be').id,
|
||||||
|
self.user_employee.write({
|
||||||
|
'login': 'employee',
|
||||||
|
'country_id': be_country_id,
|
||||||
|
})
|
||||||
|
self.admin = self.env.user
|
||||||
|
self.admin.write({
|
||||||
|
'country_id': be_country_id,
|
||||||
|
})
|
||||||
|
|
||||||
|
self._test_body = 'MASS SMS'
|
||||||
|
|
||||||
|
records = self.env['mail.test.sms']
|
||||||
|
partners = self.env['res.partner']
|
||||||
|
for x in range(50):
|
||||||
|
partners += self.env['res.partner'].with_context(**self._quick_create_ctx).create({
|
||||||
|
'name': 'Partner_%s' % (x),
|
||||||
|
'email': '_test_partner_%s@example.com' % (x),
|
||||||
|
'country_id': be_country_id,
|
||||||
|
'mobile': '047500%s%s99' % (x, x)
|
||||||
|
})
|
||||||
|
records += self.env['mail.test.sms'].with_context(**self._quick_create_ctx).create({
|
||||||
|
'name': 'Test_%s' % (x),
|
||||||
|
'customer_id': partners[x].id,
|
||||||
|
})
|
||||||
|
self.partners = partners
|
||||||
|
self.records = records
|
||||||
|
|
||||||
|
self.sms_template = self.env['sms.template'].create({
|
||||||
|
'name': 'Test Template',
|
||||||
|
'model_id': self.env['ir.model']._get('mail.test.sms').id,
|
||||||
|
'body': 'Dear ${object.display_name} this is an SMS.',
|
||||||
|
})
|
||||||
|
|
||||||
|
@mute_logger('odoo.addons.sms.models.sms_sms')
|
||||||
|
@users('employee')
|
||||||
|
@warmup
|
||||||
|
def test_composer_mass_active_domain(self):
|
||||||
|
composer = self.env['sms.composer'].with_context(
|
||||||
|
default_composition_mode='mass',
|
||||||
|
default_res_model='mail.test.sms',
|
||||||
|
default_use_active_domain=True,
|
||||||
|
active_domain=repr([('id', 'in', self.records.ids)]),
|
||||||
|
).create({
|
||||||
|
'body': self._test_body,
|
||||||
|
})
|
||||||
|
|
||||||
|
with self.mockSMSGateway(), self.assertQueryCount(employee=108):
|
||||||
|
composer.action_send_sms()
|
||||||
|
|
||||||
|
# TDE FIXME
|
||||||
|
# for record in self.records:
|
||||||
|
# self.assertSMSOutgoing(record.customer_id, None, self._test_body)
|
||||||
@@ -1,54 +1,337 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
# 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.sms.tests import common as sms_common
|
||||||
from odoo.addons.test_mail.tests import common as test_mail_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.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):
|
class TestSMSPost(test_mail_full_common.BaseFunctionalTest, sms_common.MockSMS, test_mail_common.MockEmails, test_mail_common.TestRecipients):
|
||||||
|
""" TODO
|
||||||
|
|
||||||
|
* add tests for new mail.message and mail.thread fields;
|
||||||
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
super(TestSMSPost, cls).setUpClass()
|
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('<p>Mega SMS<br/>Top moumoutte</p>', partner_ids=self.partner_1.ids)
|
||||||
|
|
||||||
|
self.assertEqual(messages.body, '<p>Mega SMS<br>Top moumoutte</p>')
|
||||||
|
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)
|
||||||
|
|
||||||
|
def test_message_sms_with_template(self):
|
||||||
|
sms_template = self.env['sms.template'].create({
|
||||||
|
'name': 'Test Template',
|
||||||
|
'model_id': self.env['ir.model']._get('mail.test.sms').id,
|
||||||
|
'body': 'Dear ${object.display_name} this is an SMS.',
|
||||||
|
})
|
||||||
|
|
||||||
|
with self.sudo('employee'):
|
||||||
|
with self.mockSMSGateway():
|
||||||
|
test_record = self.env['mail.test.sms'].browse(self.test_record.id)
|
||||||
|
messages = test_record._message_sms_with_template(template=sms_template)
|
||||||
|
|
||||||
|
self.assertSMSNotification([{'partner': self.partner_1, 'number': self.test_numbers_san[1]}], 'Dear %s this is an SMS.' % self.test_record.display_name, messages)
|
||||||
|
|
||||||
|
def test_message_sms_with_template_fallback(self):
|
||||||
|
with self.sudo('employee'):
|
||||||
|
with self.mockSMSGateway():
|
||||||
|
test_record = self.env['mail.test.sms'].browse(self.test_record.id)
|
||||||
|
messages = test_record._message_sms_with_template(template_xmlid='test_mail_full.this_should_not_exists', template_fallback='Fallback for ${object.id}')
|
||||||
|
|
||||||
|
self.assertSMSNotification([{'partner': self.partner_1, 'number': self.test_numbers_san[1]}], 'Fallback for %s' % self.test_record.id, messages)
|
||||||
|
|
||||||
|
def test_message_sms_with_template_xmlid(self):
|
||||||
|
sms_template = self.env['sms.template'].create({
|
||||||
|
'name': 'Test Template',
|
||||||
|
'model_id': self.env['ir.model']._get('mail.test.sms').id,
|
||||||
|
'body': 'Dear ${object.display_name} this is an SMS.',
|
||||||
|
})
|
||||||
|
self.env['ir.model.data'].create({
|
||||||
|
'name': 'this_should_exists',
|
||||||
|
'module': 'test_mail_full',
|
||||||
|
'model': sms_template._name,
|
||||||
|
'res_id': sms_template.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
with self.sudo('employee'):
|
||||||
|
with self.mockSMSGateway():
|
||||||
|
test_record = self.env['mail.test.sms'].browse(self.test_record.id)
|
||||||
|
messages = test_record._message_sms_with_template(template_xmlid='test_mail_full.this_should_exists')
|
||||||
|
|
||||||
|
self.assertSMSNotification([{'partner': self.partner_1, 'number': self.test_numbers_san[1]}], 'Dear %s this is an SMS.' % self.test_record.display_name, 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({
|
cls.test_record = cls.env['mail.test.sms'].with_context(**cls._test_context).create({
|
||||||
'name': 'Test',
|
'name': 'Test',
|
||||||
'customer_id': cls.partner_1.id,
|
'customer_id': cls.partner_1.id,
|
||||||
})
|
})
|
||||||
cls.test_record = cls._reset_mail_context(cls.test_record)
|
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):
|
def test_message_sms_w_numbers_invalid(self):
|
||||||
test_body = 'Void body'
|
random_numbers = self.random_numbers + ['6988754']
|
||||||
with self.mockSMSGateway():
|
with self.sudo('employee'), self.mockSMSGateway():
|
||||||
(self.partner_1 | self.partner_2).message_post_send_sms(test_body)
|
test_record = self.env['mail.test.sms'].browse(self.test_record.id)
|
||||||
self.assertSMSSent((self.partner_1 | self.partner_2).mapped('mobile'), test_body)
|
messages = test_record._message_sms(self._test_body, sms_numbers=random_numbers)
|
||||||
|
|
||||||
def test_message_post_with_sms_w_default(self):
|
# invalid numbers are still given to IAP currently as they are
|
||||||
test_body = 'Void body'
|
self.assertSMSNotification([{'number': self.random_numbers_san[0]}, {'number': self.random_numbers_san[1]}, {'number': random_numbers[2]}], self._test_body, messages)
|
||||||
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):
|
def test_message_sms_w_partners_nocountry(self):
|
||||||
test_body = 'Void body'
|
self.test_record.customer_id.write({
|
||||||
test_numbers = ['0475114477', '0475225588']
|
'mobile': self.random_numbers[0],
|
||||||
with self.mockSMSGateway():
|
'phone': self.random_numbers[1],
|
||||||
self.test_record.message_post_send_sms(test_body, numbers=test_numbers)
|
'country_id': False,
|
||||||
self.assertSMSSent(test_numbers, test_body)
|
})
|
||||||
self.assertIn(test_body, self.test_record.message_ids.body)
|
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):
|
self.assertSMSNotification([{'partner': self.test_record.customer_id}], self._test_body, messages)
|
||||||
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):
|
def test_message_sms_w_partners_falsy(self):
|
||||||
test_body = 'Void body'
|
# TDE FIXME: currently sent to IAP
|
||||||
with self.mockSMSGateway():
|
self.test_record.customer_id.write({
|
||||||
self.test_record.message_post_send_sms(test_body, partners=self.partner_1 | self.partner_2)
|
'mobile': 'youpie',
|
||||||
self.assertSMSSent((self.partner_1 | self.partner_2).mapped('mobile'), test_body)
|
'phone': 'youpla',
|
||||||
self.assertIn(test_body, self.test_record.message_ids.body)
|
})
|
||||||
|
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)
|
||||||
|
|||||||
@@ -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']))
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# -*- 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 TestSmsTemplate(test_mail_full_common.BaseFunctionalTest, sms_common.MockSMS, test_mail_common.MockEmails, test_mail_common.TestRecipients):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super(TestSmsTemplate, 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)
|
||||||
|
|
||||||
|
cls.body_en = 'Dear ${object.display_name} this is an SMS.'
|
||||||
|
cls.body_fr = u"Hello ${object.display_name} ceci est en français."
|
||||||
|
cls.sms_template = cls.env['sms.template'].create({
|
||||||
|
'name': 'Test Template',
|
||||||
|
'model_id': cls.env['ir.model']._get('mail.test.sms').id,
|
||||||
|
'body': cls.body_en,
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_sms_template_render(self):
|
||||||
|
rendered_body = self.sms_template._render_template(self.sms_template.body, self.sms_template.model, self.test_record.ids)
|
||||||
|
self.assertEqual(rendered_body[self.test_record.id], 'Dear %s this is an SMS.' % self.test_record.display_name)
|
||||||
|
|
||||||
|
def test_sms_template_lang(self):
|
||||||
|
self.env.ref('base.lang_fr').write({'active': True})
|
||||||
|
self.user_admin.write({'lang': 'en_US'})
|
||||||
|
self.env['ir.translation'].create({
|
||||||
|
'type': 'model',
|
||||||
|
'name': 'sms.template,body',
|
||||||
|
'lang': 'fr_FR',
|
||||||
|
'res_id': self.sms_template.id,
|
||||||
|
'src': self.sms_template.body,
|
||||||
|
'value': self.body_fr,
|
||||||
|
})
|
||||||
|
# set template to try to use customer lang
|
||||||
|
self.sms_template.write({
|
||||||
|
'lang': '${object.customer_id.lang}',
|
||||||
|
})
|
||||||
|
# create a second record linked to a customer in another language
|
||||||
|
self.partner_2.write({
|
||||||
|
'lang': 'fr_FR',
|
||||||
|
})
|
||||||
|
test_record_2 = self.env['mail.test.sms'].create({
|
||||||
|
'name': 'Test',
|
||||||
|
'customer_id': self.partner_2.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
self.assertEqual(self.sms_template.body, self.body_en)
|
||||||
|
self.assertEqual(self.sms_template.with_context(lang='fr_FR').body, self.body_fr)
|
||||||
|
|
||||||
|
rid_to_tpl = self.sms_template._get_context_lang_per_id((self.test_record | test_record_2).ids)
|
||||||
|
self.assertEqual(set(rid_to_tpl.keys()), set((self.test_record | test_record_2).ids))
|
||||||
|
for rid, tpl in rid_to_tpl.items():
|
||||||
|
if rid == self.test_record.id:
|
||||||
|
self.assertEqual(tpl._context.get('lang'), 'en_US')
|
||||||
|
elif rid == test_record_2.id:
|
||||||
|
self.assertEqual(tpl._context.get('lang'), 'fr_FR')
|
||||||
|
else:
|
||||||
|
self.assertTrue(False)
|
||||||
|
|
||||||
|
lang_to_rids = self.sms_template._get_ids_per_lang((self.test_record | test_record_2).ids)
|
||||||
|
for lang, rids in lang_to_rids.items():
|
||||||
|
if lang == 'en_US':
|
||||||
|
self.assertEqual(rids, self.test_record.ids)
|
||||||
|
elif lang == 'fr_FR':
|
||||||
|
self.assertEqual(rids, test_record_2.ids)
|
||||||
|
else:
|
||||||
|
self.assertTrue(False, 'Should not return lang %s' % lang)
|
||||||
@@ -259,14 +259,13 @@ class BlogPost(models.Model):
|
|||||||
return groups
|
return groups
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def _notify_customize_recipients(self, message, msg_vals):
|
def _notify_record_by_inbox(self, message, recipients_data, msg_vals=False, **kwargs):
|
||||||
""" Override to avoid keeping all notified recipients of a comment.
|
""" Override to avoid keeping all notified recipients of a comment.
|
||||||
We avoid tracking needaction on post comments. Only emails should be
|
We avoid tracking needaction on post comments. Only emails should be
|
||||||
sufficient. """
|
sufficient. """
|
||||||
msg_type = msg_vals.get('message_type') or message.message_type
|
if msg_vals.get('message_type', message.message_type) == 'comment':
|
||||||
if msg_type == 'comment':
|
return
|
||||||
return {'needaction_partner_ids': []}
|
return super(BlogPost, self)._notify_record_by_inbox(message, recipients_data, msg_vals=msg_vals, **kwargs)
|
||||||
return super(BlogPost, self)._notify_customize_recipients(message, msg_vals)
|
|
||||||
|
|
||||||
def _default_website_meta(self):
|
def _default_website_meta(self):
|
||||||
res = super(BlogPost, self)._default_website_meta()
|
res = super(BlogPost, self)._default_website_meta()
|
||||||
|
|||||||
@@ -830,14 +830,13 @@ class Post(models.Model):
|
|||||||
return super(Post, self).message_post(message_type=message_type, **kwargs)
|
return super(Post, self).message_post(message_type=message_type, **kwargs)
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def _notify_customize_recipients(self, message, msg_vals):
|
def _notify_record_by_inbox(self, message, recipients_data, msg_vals=False, **kwargs):
|
||||||
""" Override to avoid keeping all notified recipients of a comment.
|
""" Override to avoid keeping all notified recipients of a comment.
|
||||||
We avoid tracking needaction on post comments. Only emails should be
|
We avoid tracking needaction on post comments. Only emails should be
|
||||||
sufficient. """
|
sufficient. """
|
||||||
msg_type = msg_vals.get('message_type') or message.message_type
|
if msg_vals.get('message_type', message.message_type) == 'comment':
|
||||||
if msg_type == 'comment':
|
return
|
||||||
return {'needaction_partner_ids': [], 'partner_ids': []}
|
return super(Post, self)._notify_record_by_inbox(message, recipients_data, msg_vals=msg_vals, **kwargs)
|
||||||
return super(Post, self)._notify_customize_recipients(message, msg_vals)
|
|
||||||
|
|
||||||
|
|
||||||
class PostReason(models.Model):
|
class PostReason(models.Model):
|
||||||
|
|||||||
Reference in New Issue
Block a user