[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',
|
||||
'depends': ['calendar', 'sms'],
|
||||
'data': [
|
||||
'data/sms_data.xml',
|
||||
'views/calendar_views.xml',
|
||||
],
|
||||
'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):
|
||||
_inherit = 'calendar.event'
|
||||
|
||||
def _get_default_sms_recipients(self):
|
||||
def _sms_get_default_partners(self):
|
||||
""" Method overriden from mail.thread (defined in the sms module).
|
||||
SMS text messages will be sent to attendees that haven't declined the event(s).
|
||||
"""
|
||||
@@ -20,9 +20,12 @@ class CalendarEvent(models.Model):
|
||||
def _do_sms_reminder(self):
|
||||
""" Send an SMS text reminder to attendees that haven't declined the event """
|
||||
for event in self:
|
||||
sms_msg = _("Event reminder: %s on %s.") % (event.name, event.start_datetime or event.start_date)
|
||||
note_msg = _('SMS text message reminder sent !')
|
||||
event.message_post_send_sms(sms_msg, note_msg=note_msg)
|
||||
event._message_sms_with_template(
|
||||
template_xmlid='calendar_sms.sms_template_data_calendar_reminder',
|
||||
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):
|
||||
|
||||
@@ -396,3 +396,9 @@ class HrEmployeePrivate(models.Model):
|
||||
@api.returns('mail.message', lambda value: value.id)
|
||||
def message_post(self, *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',
|
||||
'data/ir_actions_server.xml',
|
||||
'views/hr_employee_views.xml',
|
||||
'data/sms_data.xml',
|
||||
'data/mail_data.xml',
|
||||
],
|
||||
'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},
|
||||
}
|
||||
|
||||
# --------------------------------------------------
|
||||
# Messaging
|
||||
# --------------------------------------------------
|
||||
|
||||
def action_send_sms(self):
|
||||
self.ensure_one()
|
||||
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."))
|
||||
if not self.mobile_phone:
|
||||
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.
|
||||
Do not hesitate to contact your manager or the human resource department.""")
|
||||
else:
|
||||
context['default_template_id'] = template.id
|
||||
|
||||
return {
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "sms.composer",
|
||||
"view_mode": 'form',
|
||||
"context": {
|
||||
'active_id': self.id,
|
||||
'default_message': body,
|
||||
'default_recipients': self.mobile_phone,
|
||||
},
|
||||
"context": context,
|
||||
"name": "Send SMS",
|
||||
"target": "new",
|
||||
}
|
||||
|
||||
@@ -503,15 +503,10 @@ class Channel(models.Model):
|
||||
notifications.append([(self._cr.dbname, 'res.partner', partner.id), channel_info])
|
||||
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
|
||||
if not msg_vals or msg_vals.get('moderation_status') != 'pending_moderation':
|
||||
super(Channel, self)._notify_thread(
|
||||
message,
|
||||
msg_vals=msg_vals,
|
||||
model_description=model_description,
|
||||
mail_auto_delete=mail_auto_delete,
|
||||
)
|
||||
super(Channel, self)._notify_thread(message, msg_vals=msg_vals, **kwargs)
|
||||
else:
|
||||
message._notify_pending_by_chat()
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ class Followers(models.Model):
|
||||
# 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.
|
||||
Purpose of this method is to fetch all data necessary to notify recipients
|
||||
in a single query. It fetches data from
|
||||
@@ -92,6 +92,7 @@ class Followers(models.Model):
|
||||
* channels if cids is given;
|
||||
|
||||
: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 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;
|
||||
|
||||
@@ -154,24 +154,24 @@ class MailMail(models.Model):
|
||||
notif_mails_ids = [mail.id for mail in self if mail.notification]
|
||||
if notif_mails_ids:
|
||||
notifications = self.env['mail.notification'].search([
|
||||
('is_email', '=', True),
|
||||
('notification_type', '=', 'email'),
|
||||
('mail_id', 'in', notif_mails_ids),
|
||||
('email_status', 'not in', ('sent', 'canceled'))
|
||||
('notification_status', 'not in', ('sent', 'canceled'))
|
||||
])
|
||||
if notifications:
|
||||
#find all notification linked to a failure
|
||||
# find all notification linked to a failure
|
||||
failed = self.env['mail.notification']
|
||||
if failure_type:
|
||||
failed = notifications.filtered(lambda notif: notif.res_partner_id not in success_pids)
|
||||
failed.sudo().write({
|
||||
'email_status': 'exception',
|
||||
'notification_status': 'exception',
|
||||
'failure_type': failure_type,
|
||||
'failure_reason': failure_reason,
|
||||
})
|
||||
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({
|
||||
'email_status': 'sent',
|
||||
'notification_status': 'sent',
|
||||
'failure_type': '',
|
||||
'failure_reason': '',
|
||||
})
|
||||
@@ -335,14 +335,14 @@ class MailMail(models.Model):
|
||||
# update in case an email bounces while sending all emails related to current
|
||||
# mail record.
|
||||
notifs = self.env['mail.notification'].search([
|
||||
('is_email', '=', True),
|
||||
('notification_type', '=', 'email'),
|
||||
('mail_id', 'in', mail.ids),
|
||||
('email_status', 'not in', ('sent', 'canceled'))
|
||||
('notification_status', 'not in', ('sent', 'canceled'))
|
||||
])
|
||||
if notifs:
|
||||
notif_msg = _('Error without exception. Probably due do concurrent access update of notification records. Please see with an administrator.')
|
||||
notifs.sudo().write({
|
||||
'email_status': 'exception',
|
||||
'notification_status': 'exception',
|
||||
'failure_type': 'UNKNOWN',
|
||||
'failure_reason': notif_msg,
|
||||
})
|
||||
|
||||
@@ -5,6 +5,7 @@ import logging
|
||||
import re
|
||||
|
||||
from binascii import Error as binascii_error
|
||||
from collections import defaultdict
|
||||
from operator import itemgetter
|
||||
from email.utils import formataddr
|
||||
from openerp.http import request
|
||||
@@ -148,15 +149,15 @@ class Message(models.Model):
|
||||
def _compute_has_error(self):
|
||||
error_from_notification = self.env['mail.notification'].sudo().search([
|
||||
('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:
|
||||
message.has_error = message in error_from_notification
|
||||
|
||||
@api.multi
|
||||
def _search_has_error(self, operator, operand):
|
||||
if operator == '=' and operand:
|
||||
return [('notification_ids.email_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'))]
|
||||
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')
|
||||
def _get_starred(self):
|
||||
@@ -373,25 +374,22 @@ class Message(models.Model):
|
||||
partner_ids = []
|
||||
if message.subtype_id:
|
||||
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:
|
||||
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
|
||||
customer_email_status = (
|
||||
(all(n.email_status == 'sent' for n in message.notification_ids) and 'sent') or
|
||||
(any(n.email_status == 'exception' for n in message.notification_ids) and 'exception') or
|
||||
(any(n.email_status == 'bounce' for n in message.notification_ids) and 'bounce') or
|
||||
(all(n.notification_status == 'sent' for n in message.notification_ids if n.notification_type == 'email') and 'sent') or
|
||||
(any(n.notification_status == 'exception' for n in message.notification_ids if n.notification_type == 'email') and 'exception') or
|
||||
(any(n.notification_status == 'bounce' for n in message.notification_ids if n.notification_type == 'email') and 'bounce') or
|
||||
'ready'
|
||||
)
|
||||
customer_email_data = []
|
||||
def filter_notification(notif):
|
||||
return (
|
||||
(notif.email_status in ('bounce', 'exception', 'canceled') or notif.res_partner_id.partner_share) and
|
||||
notif.res_partner_id.active
|
||||
)
|
||||
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))
|
||||
for notification in message.notification_ids.filtered(
|
||||
lambda n: n.notification_type == 'email' and n.res_partner_id.active and
|
||||
(n.notification_status in ('bounce', 'exception', 'canceled') or n.res_partner_id.partner_share)):
|
||||
customer_email_data.append((partner_tree[notification.res_partner_id.id][0], partner_tree[notification.res_partner_id.id][1], notification.notification_status))
|
||||
|
||||
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:
|
||||
@@ -512,7 +510,7 @@ class Message(models.Model):
|
||||
|
||||
# fetch notification status
|
||||
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:
|
||||
mid = notif.mail_message_id.id
|
||||
if not notif_dict.get(mid):
|
||||
@@ -540,13 +538,45 @@ class Message(models.Model):
|
||||
'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
|
||||
def _format_mail_failures(self):
|
||||
"""
|
||||
A shorter message to notify a failure update
|
||||
"""
|
||||
""" A shorter message to notify a failure update """
|
||||
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 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:
|
||||
# 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
|
||||
@@ -558,24 +588,10 @@ class Message(models.Model):
|
||||
record.check_access_rule('read')
|
||||
except AccessError:
|
||||
continue
|
||||
info = {
|
||||
'message_id': message.id,
|
||||
'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
|
||||
else:
|
||||
messages |= message
|
||||
|
||||
@api.multi
|
||||
def _notify_failure_update(self):
|
||||
authors = {}
|
||||
for author, author_messages in groupby(self, itemgetter('author_id')):
|
||||
for author, author_messages in groupby(messages, itemgetter('author_id')):
|
||||
self.env['bus.bus'].sendone(
|
||||
(self._cr.dbname, 'res.partner', author.id),
|
||||
{'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', 'Message', index=True, ondelete='cascade', required=True)
|
||||
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_email = fields.Boolean('Sent by Email', index=True)
|
||||
email_status = fields.Selection([
|
||||
notification_type = fields.Selection([
|
||||
('inbox', 'Inbox'), ('email', 'Email')], string='Notification Type',
|
||||
default='inbox', index=True, required=True)
|
||||
notification_status = fields.Selection([
|
||||
('ready', 'Ready to Send'),
|
||||
('sent', 'Sent'),
|
||||
('bounce', 'Bounced'),
|
||||
@@ -37,10 +39,17 @@ class Notification(models.Model):
|
||||
], string='Failure type')
|
||||
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):
|
||||
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():
|
||||
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
|
||||
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'))
|
||||
else:
|
||||
return _("Unknown error") + ": %s" % (self.failure_reason or '')
|
||||
|
||||
|
||||
|
||||
@@ -226,7 +226,7 @@ class MailThread(models.AbstractModel):
|
||||
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.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'
|
||||
GROUP BY msg.res_id""",
|
||||
(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),
|
||||
('res_partner_id', 'in', partners.ids)])
|
||||
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:
|
||||
@@ -1679,7 +1679,7 @@ class MailThread(models.AbstractModel):
|
||||
email_from=False, author_id=None, parent_id=False,
|
||||
subtype_id=False, subtype=None, partner_ids=None, channel_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):
|
||||
""" Post a new message in an existing thread, returning the new
|
||||
mail.message ID.
|
||||
@@ -1707,11 +1707,14 @@ class MailThread(models.AbstractModel):
|
||||
:return int: ID of newly created mail.message
|
||||
"""
|
||||
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':
|
||||
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")
|
||||
|
||||
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.
|
||||
# Note that with sudo we will match message with internal subtypes.
|
||||
parent_id = parent_message.id if parent_message else False
|
||||
elif parent_id:
|
||||
elif parent_id:
|
||||
old_parent_id = parent_id
|
||||
parent_message = MailMessage_sudo.search([('id', '=', parent_id), ('parent_id', '!=', False)], limit=1)
|
||||
# avoid loops when finding ancestors
|
||||
@@ -1757,7 +1760,8 @@ class MailThread(models.AbstractModel):
|
||||
processed_list.append(new_parent_id)
|
||||
parent_message = parent_message.parent_id
|
||||
parent_id = parent_message.id
|
||||
values = dict(kwargs)
|
||||
|
||||
values = dict(msg_kwargs)
|
||||
values.update({
|
||||
'author_id': author_id,
|
||||
'model': self._name,
|
||||
@@ -1776,19 +1780,19 @@ class MailThread(models.AbstractModel):
|
||||
attachments = attachments or []
|
||||
attachment_ids = attachment_ids or []
|
||||
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
|
||||
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 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_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
|
||||
|
||||
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()
|
||||
|
||||
def message_notify(self, partner_ids=False, parent_id=False, model=False, res_id=False,
|
||||
author_id=False, body='', subject=False, model_description=False,
|
||||
mail_auto_delete=True, **kwargs):
|
||||
author_id=False, body='', subject=False, **kwargs):
|
||||
""" Shortcut allowing to notify partners of messages that shouldn't be
|
||||
displayed on a document. It pushes notifications on inbox or by email depending
|
||||
on the user configuration, like other notifications. """
|
||||
|
||||
if self:
|
||||
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:
|
||||
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],
|
||||
'message_id': tools.generate_tracking_message_id('message-notify'),
|
||||
}
|
||||
values.update(kwargs)
|
||||
values.update(msg_kwargs)
|
||||
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
|
||||
|
||||
def _message_log(self, body='', author_id=None, subject=False, message_type='notification', **kwargs):
|
||||
@@ -1972,24 +1977,27 @@ class MailThread(models.AbstractModel):
|
||||
# ------------------------------------------------------
|
||||
|
||||
@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
|
||||
|
||||
* call ``_notify_compute_recipients`` that computes recipients to
|
||||
notify based on message record or message creation values if given
|
||||
(to optimize performance if we already have data computed);
|
||||
* performs the notification process;
|
||||
Can be overridden to intercept and postpone notification mecanism (mail.channel moderation)
|
||||
: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;
|
||||
"""
|
||||
* performs the notification process by calling the various notification
|
||||
methods implemented;
|
||||
|
||||
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 {}
|
||||
rdata = self._notify_compute_recipients(message, msg_vals)
|
||||
if not rdata:
|
||||
@@ -1998,59 +2006,78 @@ class MailThread(models.AbstractModel):
|
||||
message_values = {}
|
||||
if 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']
|
||||
partner_email_rdata = [r for r in rdata['partners'] if r['notif'] == 'email']
|
||||
channel_ids = [r['id'] for r in rdata['channels']]
|
||||
self._notify_record_by_inbox(message, rdata, msg_vals=msg_vals, **kwargs)
|
||||
self._notify_record_by_email(message, rdata, msg_vals=msg_vals, **kwargs)
|
||||
|
||||
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:
|
||||
message_values = False
|
||||
message_format_values = False
|
||||
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):
|
||||
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:
|
||||
notifications += self.env['mail.channel'].sudo().browse(channel_ids)._channel_message_notifications(message, message_values)
|
||||
if partner_email_rdata:
|
||||
self._notify_record_by_email(message, partner_email_rdata, msg_vals=msg_vals, model_description=model_description, mail_auto_delete=mail_auto_delete)
|
||||
if notifications:
|
||||
self.env['bus.bus'].sudo().sendmany(notifications)
|
||||
return True
|
||||
bus_notifications += self.env['mail.channel'].sudo().browse(channel_ids)._channel_message_notifications(message, message_format_values)
|
||||
if bus_notifications:
|
||||
self.env['bus.bus'].sudo().sendmany(bus_notifications)
|
||||
|
||||
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.
|
||||
|
||||
:param message: mail.message record to notify;
|
||||
:param partners_data: partner to notify by email coming from _notify_compute_recipients
|
||||
:param msg_vals: message creation values if available
|
||||
:param recipients_data: see ``_notify_thread``;
|
||||
: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
|
||||
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_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)
|
||||
|
||||
if not recipients_groups_data:
|
||||
return True
|
||||
|
||||
force_send = self.env.context.get('mail_notify_force_send', True)
|
||||
force_send = self.env.context.get('mail_notify_force_send', force_send)
|
||||
|
||||
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))
|
||||
base_template = False
|
||||
|
||||
|
||||
mail_subject = message.subject or (message.record_name and 'Re: %s' % message.record_name) # in cache, no queries
|
||||
# prepare notification mail values
|
||||
base_mail_values = {
|
||||
@@ -2080,6 +2106,7 @@ class MailThread(models.AbstractModel):
|
||||
emails = self.env['mail.mail'].sudo()
|
||||
|
||||
# loop on groups (customer, portal, user, ... + model specific like group_sale_salesman)
|
||||
notif_create_values = []
|
||||
recipients_max = 50
|
||||
for recipients_group_data in recipients_groups_data:
|
||||
# generate notification email content
|
||||
@@ -2093,7 +2120,8 @@ class MailThread(models.AbstractModel):
|
||||
else:
|
||||
mail_body = message.body
|
||||
mail_body = self._replace_local_links(mail_body)
|
||||
# send email
|
||||
|
||||
# create email
|
||||
for recipients_ids_chunk in split_every(recipients_max, recipients_ids):
|
||||
recipient_values = self._notify_email_recipient_values(recipients_ids_chunk)
|
||||
email_to = recipient_values['email_to']
|
||||
@@ -2110,22 +2138,32 @@ class MailThread(models.AbstractModel):
|
||||
email = Mail.create(create_values)
|
||||
|
||||
if email and recipient_ids:
|
||||
notifications = self.env['mail.notification'].sudo().search([
|
||||
('mail_message_id', '=', email.mail_message_id.id),
|
||||
('res_partner_id', 'in', list(recipient_ids)) # not sure to check.
|
||||
# TODO XDO what if recipient_ids are empty because of _notify_email_recipient_values
|
||||
# should we use recipients_ids_chunk?
|
||||
# should we unlink recipients_ids_chunk - 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)
|
||||
])
|
||||
notifications.write({
|
||||
'is_email': True,
|
||||
tocreate_recipient_ids = list(recipient_ids)
|
||||
if check_existing:
|
||||
existing_notifications = self.env['mail.notification'].sudo().search([
|
||||
('mail_message_id', '=', message.id),
|
||||
('notification_type', '=', 'email'),
|
||||
('res_partner_id', 'in', tocreate_recipient_ids)
|
||||
])
|
||||
if existing_notifications:
|
||||
tocreate_recipient_ids = [rid for rid in recipient_ids if rid not in existing_notifications.mapped('res_partner_id.id')]
|
||||
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,
|
||||
'is_read': True, # handle by email discards Inbox notification
|
||||
'email_status': 'ready',
|
||||
})
|
||||
'is_read': True, # discard Inbox notification
|
||||
'notification_status': 'ready',
|
||||
} for recipient_id in tocreate_recipient_ids]
|
||||
emails |= email
|
||||
|
||||
if notif_create_values:
|
||||
self.env['mail.notification'].sudo().create(notif_create_values)
|
||||
|
||||
# NOTE:
|
||||
# 1. for more than 50 followers, use the queue system
|
||||
# 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
|
||||
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
|
||||
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
|
||||
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
|
||||
@@ -2222,13 +2260,14 @@ class MailThread(models.AbstractModel):
|
||||
# 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
|
||||
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
|
||||
# is it possible to have record but no subtype_id ?
|
||||
recipient_data = {
|
||||
'partners': [],
|
||||
'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:
|
||||
return recipient_data
|
||||
|
||||
@@ -2243,11 +2282,11 @@ class MailThread(models.AbstractModel):
|
||||
if notif == 'inbox':
|
||||
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
|
||||
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
|
||||
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
|
||||
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:
|
||||
recipient_data['channels'].append({'id': cid, 'notif': notif, 'type': ctype})
|
||||
|
||||
@@ -2514,9 +2553,6 @@ class MailThread(models.AbstractModel):
|
||||
'email_to': False,
|
||||
'recipient_ids': recipient_ids,
|
||||
}
|
||||
@api.multi
|
||||
def _notify_customize_recipients(self, message, msg_vals):
|
||||
return {}
|
||||
|
||||
# ------------------------------------------------------
|
||||
# Followers API
|
||||
|
||||
@@ -75,8 +75,8 @@
|
||||
<tree>
|
||||
<field name="res_partner_id"/>
|
||||
<field name="is_read"/>
|
||||
<field name="is_email"/>
|
||||
<field name="email_status"/>
|
||||
<field name="notification_type"/>
|
||||
<field name="notification_status"/>
|
||||
</tree>
|
||||
</field>
|
||||
</group>
|
||||
|
||||
@@ -72,13 +72,13 @@ class Invite(models.TransientModel):
|
||||
'no_auto_thread': True,
|
||||
'add_sign': True,
|
||||
})
|
||||
partners_data = [{
|
||||
'id': pid,
|
||||
'share': True,
|
||||
'notif': 'email',
|
||||
'type': 'customer',
|
||||
recipients_data = {'partners': [{
|
||||
'id': pid,
|
||||
'share': True,
|
||||
'notif': 'email',
|
||||
'type': 'customer',
|
||||
'groups': []
|
||||
} for pid in new_partners.ids]
|
||||
document._notify_record_by_email(message, partners_data, send_after_commit=False)
|
||||
} for pid in new_partners.ids]}
|
||||
document._notify_record_by_email(message, recipients_data, send_after_commit=False)
|
||||
message.unlink()
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
from odoo import _, api, fields, models
|
||||
|
||||
|
||||
class MailCancelResend(models.TransientModel):
|
||||
class MailResendCancel(models.TransientModel):
|
||||
_name = 'mail.resend.cancel'
|
||||
_description = 'Dismiss notification for resend by model'
|
||||
|
||||
@@ -26,7 +26,7 @@ class MailCancelResend(models.TransientModel):
|
||||
FROM mail_message_res_partner_needaction_rel notif
|
||||
JOIN mail_message mes
|
||||
ON notif.mail_message_id = mes.id
|
||||
WHERE notif.email_status IN ('bounce', 'exception')
|
||||
WHERE notif.notification_status IN ('bounce', 'exception')
|
||||
AND mes.model = %s
|
||||
AND mes.author_id = %s
|
||||
""", (wizard.model, author_id))
|
||||
@@ -34,6 +34,6 @@ class MailCancelResend(models.TransientModel):
|
||||
notif_ids = [row[0] for row in res]
|
||||
messages_ids = list(set([row[1] for row in res]))
|
||||
if notif_ids:
|
||||
self.env["mail.notification"].browse(notif_ids).sudo().write({'email_status': 'canceled'})
|
||||
self.env["mail.message"].browse(messages_ids)._notify_failure_update()
|
||||
self.env["mail.notification"].browse(notif_ids).sudo().write({'notification_status': 'canceled'})
|
||||
self.env["mail.message"].browse(messages_ids)._notify_mail_failure_update()
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
||||
@@ -28,16 +28,14 @@ class MailResendMessage(models.TransientModel):
|
||||
message_id = self._context.get('mail_message_to_resend')
|
||||
if 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'))
|
||||
partner_ids = [(0, 0,
|
||||
{
|
||||
"partner_id": notif.res_partner_id.id,
|
||||
"name": notif.res_partner_id.name,
|
||||
"email": notif.res_partner_id.email,
|
||||
"resend": True,
|
||||
"message": notif.format_failure_reason(),
|
||||
}
|
||||
) for notif in notification_ids]
|
||||
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_id": notif.res_partner_id.id,
|
||||
"name": notif.res_partner_id.name,
|
||||
"email": notif.res_partner_id.email,
|
||||
"resend": True,
|
||||
"message": notif.format_failure_reason(),
|
||||
}) for notif in notification_ids]
|
||||
has_user = any([notif.res_partner_id.user_ids for notif in notification_ids])
|
||||
if has_user:
|
||||
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"
|
||||
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")
|
||||
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.sudo().write({'email_status': 'canceled'})
|
||||
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({'notification_status': 'canceled'})
|
||||
if to_send:
|
||||
message = wizard.mail_message_id
|
||||
record = self.env[message.model].browse(message.res_id) if message.is_thread_message() else self.env['mail.thread']
|
||||
|
||||
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:
|
||||
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
|
||||
@@ -76,17 +74,17 @@ class MailResendMessage(models.TransientModel):
|
||||
else: # has no user, is therefore 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'}
|
||||
|
||||
@api.multi
|
||||
def cancel_mail_action(self):
|
||||
for wizard in self:
|
||||
for notif in wizard.notification_ids:
|
||||
notif.filtered(lambda notif: notif.email_status in ('exception', 'bounce')).sudo().write({'email_status': 'canceled'})
|
||||
wizard.mail_message_id._notify_failure_update()
|
||||
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_mail_failure_update()
|
||||
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'):
|
||||
valid, invalid, void_count = [], [], 0
|
||||
result = dict.fromkeys(numbers, False)
|
||||
for number in numbers:
|
||||
if not number:
|
||||
void_count += 1
|
||||
result[number] = {'sanitized': False, 'code': 'empty', 'msg': False}
|
||||
continue
|
||||
try:
|
||||
sanitized = phone_format(
|
||||
number, country_code, country_phone_code,
|
||||
force_format=force_format, raise_exception=True)
|
||||
except Exception as e:
|
||||
invalid.append(number)
|
||||
result[number] = {'sanitized': False, 'code': 'invalid', 'msg': e}
|
||||
else:
|
||||
valid.append(sanitized)
|
||||
return valid, invalid, void_count
|
||||
result[number] = {'sanitized': sanitized, 'code': False, 'msg': False}
|
||||
return result
|
||||
|
||||
|
||||
def phone_sanitize_numbers_w_record(numbers, country_code, country_phone_code, record, record_country_fname='country_id', force_format='E164'):
|
||||
if not country_code or not country_phone_code:
|
||||
country = False
|
||||
if record and record_country_fname in record and record[record_country_fname]:
|
||||
def phone_sanitize_numbers_w_record(numbers, record, country=False, record_country_fname='country_id', force_format='E164'):
|
||||
if not country:
|
||||
if record and hasattr(record, record_country_fname) and record[record_country_fname]:
|
||||
country = record[record_country_fname]
|
||||
elif record:
|
||||
country = record.env.company.country_id
|
||||
if country:
|
||||
country_code = country_code if country_code else country.code
|
||||
country_phone_code = country_phone_code if country_phone_code else country.phone_code
|
||||
country_code = country.code if country else None
|
||||
country_phone_code = country.phone_code if country else None
|
||||
return phone_sanitize_numbers(numbers, country_code, country_phone_code, force_format=force_format)
|
||||
|
||||
|
||||
def phone_sanitize_numbers_string_w_record(numbers_str, country_code, country_phone_code, record, record_country_fname='country_id', force_format='E164'):
|
||||
def phone_sanitize_numbers_string_w_record(numbers_str, record, country=False, record_country_fname='country_id', force_format='E164'):
|
||||
found_numbers = [number.strip() for number in numbers_str.split(',')]
|
||||
return phone_sanitize_numbers_w_record(found_numbers, country_code, country_phone_code, record, record_country_fname, force_format=force_format)
|
||||
return phone_sanitize_numbers_w_record(found_numbers, record, country=country, record_country_fname=record_country_fname, force_format=force_format)
|
||||
|
||||
|
||||
def phone_get_sanitized_records_number(records, number_fname='mobile', country_fname='country_id', force_format='E164'):
|
||||
res = dict.fromkeys(records.ids, False)
|
||||
for record in records:
|
||||
number = record[number_fname]
|
||||
valid, invalid, void_count = phone_sanitize_numbers_w_record([number], None, None, records, country_fname,force_format=force_format)
|
||||
if valid:
|
||||
res[record.id] = valid[0]
|
||||
elif void_count:
|
||||
res[record.id] = False
|
||||
else:
|
||||
res[record.id] = False
|
||||
res[record.id] = phone_sanitize_numbers_w_record([number], records, record_country_fname=country_fname,force_format=force_format)[number]['sanitized']
|
||||
return res
|
||||
|
||||
|
||||
|
||||
@@ -12,13 +12,23 @@ The service is provided by the In App Purchase Odoo platform.
|
||||
""",
|
||||
'depends': ['base', 'iap', 'mail', 'phone_validation'],
|
||||
'data': [
|
||||
'data/ir_cron_data.xml',
|
||||
'wizard/sms_cancel_views.xml',
|
||||
'wizard/sms_composer_views.xml',
|
||||
'wizard/sms_resend_views.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/res_partner_views.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': [
|
||||
'static/src/xml/sms_widget.xml',
|
||||
'static/src/xml/thread.xml',
|
||||
],
|
||||
'installable': 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 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import mail_followers
|
||||
from . import mail_message
|
||||
from . import mail_notification
|
||||
from . import mail_thread
|
||||
from . import res_partner
|
||||
from . import sms_api
|
||||
from . import sms_sms
|
||||
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
|
||||
|
||||
from odoo import models, _
|
||||
|
||||
from odoo.addons.iap.models.iap import InsufficientCreditError
|
||||
from odoo import api, models, fields
|
||||
from odoo.addons.phone_validation.tools import phone_validation
|
||||
from odoo.tools import html2plaintext
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -13,7 +13,30 @@ _logger = logging.getLogger(__name__)
|
||||
class MailThread(models.AbstractModel):
|
||||
_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.
|
||||
:returns partners: recordset of res.partner
|
||||
"""
|
||||
@@ -24,36 +47,250 @@ class MailThread(models.AbstractModel):
|
||||
partners |= self.mapped('partner_ids')
|
||||
return partners
|
||||
|
||||
def message_post_send_sms(self, sms_message, numbers=None, partners=None, note_msg=None, log_error=False):
|
||||
""" Send an SMS text message and post an internal note in the chatter if successfull
|
||||
:param sms_message: plaintext message to send by sms
|
||||
:param partners: the numbers to send to, if none are given it will take those
|
||||
from partners or _get_default_sms_recipients
|
||||
:param partners: the recipients partners, if none are given it will take those
|
||||
from _get_default_sms_recipients, this argument
|
||||
is ignored if numbers is defined
|
||||
:param note_msg: message to log in the chatter, if none is given a default one
|
||||
containing the sms_message is logged
|
||||
def _sms_get_number_fields(self):
|
||||
""" This method returns the fields to use to find the number to use to
|
||||
send an SMS on a record. """
|
||||
return ['mobile']
|
||||
|
||||
def _sms_get_recipients_info(self, force_field=False):
|
||||
"""" Get SMS recipient information on current record set. This method
|
||||
checks for numbers and sanitation in order to centralize computation.
|
||||
|
||||
Example of use cases
|
||||
|
||||
* click on a field -> number is actually forced from field, find customer
|
||||
linked to record, force its number to field or fallback on customer fields;
|
||||
* contact -> find numbers from all possible phone fields on record, find
|
||||
customer, force its number to found field number or fallback on customer fields;
|
||||
|
||||
:return dict: record.id: {
|
||||
'partner': a res.partner recordset that is the customer (void or singleton);
|
||||
'sanitized': sanitized number to use (coming from record's field or partner's mobile
|
||||
or phone). Set to False is number impossible to parse and format;
|
||||
'number': original number before sanitation;
|
||||
} for each record in self
|
||||
"""
|
||||
if not numbers:
|
||||
if not partners:
|
||||
partners = self._get_default_sms_recipients()
|
||||
result = dict.fromkeys(self.ids, False)
|
||||
number_fields = self._sms_get_number_fields()
|
||||
for record in self:
|
||||
tocheck_fields = [force_field] if force_field else number_fields
|
||||
all_numbers = [record[fname] for fname in tocheck_fields if fname in record]
|
||||
all_partners = record._sms_get_default_partners()
|
||||
|
||||
# Collect numbers, we will consider the message to be sent if at least one number can be found
|
||||
numbers = list(set([i.mobile for i in partners if i.mobile]))
|
||||
valid_number = False
|
||||
for fname in [f for f in tocheck_fields if f in record]:
|
||||
valid_number = phone_validation.phone_get_sanitized_record_number(record, number_fname=fname)
|
||||
if valid_number:
|
||||
break
|
||||
|
||||
if numbers:
|
||||
try:
|
||||
self.env['sms.api']._send_sms(numbers, sms_message)
|
||||
mail_message = note_msg or _('SMS message sent: %s') % sms_message
|
||||
if valid_number:
|
||||
result[record.id] = {
|
||||
'partner': all_partners[0] if all_partners else self.env['res.partner'],
|
||||
'sanitized': valid_number, 'number': valid_number,
|
||||
}
|
||||
elif all_partners:
|
||||
partner_number, partner = False, self.env['res.partner']
|
||||
for partner in all_partners:
|
||||
partner_number = partner.mobile or partner.phone
|
||||
if partner_number:
|
||||
partner_number = phone_validation.phone_sanitize_numbers_string_w_record(partner_number, record)[partner_number]['sanitized']
|
||||
if partner_number:
|
||||
break
|
||||
|
||||
except InsufficientCreditError as e:
|
||||
if not log_error:
|
||||
raise e
|
||||
mail_message = _('Insufficient credit, unable to send SMS message: %s') % sms_message
|
||||
if partner_number:
|
||||
result[record.id] = {'partner': partner, 'sanitized': partner_number, 'number': partner_number}
|
||||
else:
|
||||
result[record.id] = {'partner': partner, 'sanitized': False, 'number': partner.mobile or partner.phone}
|
||||
elif all_numbers:
|
||||
result[record.id] = {'partner': self.env['res.partner'], 'sanitized': False, 'number': all_numbers[0]}
|
||||
else:
|
||||
result[record.id] = {'partner': self.env['res.partner'], 'sanitized': False, 'number': False}
|
||||
return result
|
||||
|
||||
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:
|
||||
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:
|
||||
thread.message_post(body=mail_message)
|
||||
return False
|
||||
composer = self.env['sms.composer'].with_context(**composer_context).create({})
|
||||
return composer._action_send_sms()
|
||||
|
||||
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):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
def _get_default_sms_recipients(self):
|
||||
def _sms_get_default_partners(self):
|
||||
""" Override of mail.thread method.
|
||||
SMS recipients on partners are the partners themselves.
|
||||
"""
|
||||
|
||||
@@ -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 core = require('web.core');
|
||||
var fieldRegistry = require('web.field_registry');
|
||||
var dom = require('web.dom');
|
||||
var framework = require('web.framework');
|
||||
|
||||
|
||||
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
|
||||
* the number of SMS and the number of characters. This text is computed every
|
||||
* time the user changes the body.
|
||||
*/
|
||||
var SmsWidget = InputField.extend({
|
||||
var SmsWidget = FieldText.extend({
|
||||
className: 'o_field_text',
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
@@ -26,12 +22,31 @@ var SmsWidget = InputField.extend({
|
||||
this._super.apply(this, arguments);
|
||||
this.nbrChar = 0;
|
||||
this.nbrSMS = 0;
|
||||
this.encoding = "GSM7";
|
||||
this.tagName = 'div';
|
||||
this.encoding = 'GSM7';
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// 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.match(/\n/g) || []).length;
|
||||
this.nbrSMS = this._countSMS(this.nbrChar, this.encoding);
|
||||
this._renderSMS();
|
||||
},
|
||||
|
||||
/**
|
||||
* Count the number of SMS of the content
|
||||
* @private
|
||||
@@ -66,16 +81,7 @@ var SmsWidget = InputField.extend({
|
||||
}
|
||||
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
|
||||
* @private
|
||||
@@ -88,17 +94,47 @@ var SmsWidget = InputField.extend({
|
||||
}
|
||||
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.
|
||||
* @private
|
||||
*/
|
||||
_renderSMS: function () {
|
||||
this.$('.o_sms_count').text(_.str.sprintf(_t('%s chars, fits in %s SMS (%s) '), this.nbrChar, this.nbrSMS, this.encoding));
|
||||
_renderSMSInfo: function () {
|
||||
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
|
||||
//--------------------------------------------------------------------------
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
@@ -106,15 +142,16 @@ var SmsWidget = InputField.extend({
|
||||
*/
|
||||
_onChange: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this._compute();
|
||||
this._updateSMSInfo();
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_onInput: function () {
|
||||
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 unittest.mock import patch
|
||||
|
||||
from odoo import exceptions
|
||||
from odoo import exceptions, tools
|
||||
from odoo.addons.phone_validation.tools import phone_validation
|
||||
from odoo.tests import common
|
||||
from odoo.addons.sms.models.sms_api import SmsApi
|
||||
|
||||
@@ -54,18 +55,106 @@ class MockSMS(common.BaseCase):
|
||||
finally:
|
||||
pass
|
||||
|
||||
def _clear_sms_sent(self):
|
||||
self._sms = []
|
||||
|
||||
def assertSMSSent(self, numbers, content):
|
||||
""" Check sent SMS. Order is not checked. Each number should have received
|
||||
the same content. Usefull to check batch sending.
|
||||
the same content. Useful to check batch sending.
|
||||
|
||||
:param numbers: list of numbers;
|
||||
:param content: content to check for each number;
|
||||
"""
|
||||
self.assertEqual(len(self._sms), len(numbers))
|
||||
for number in numbers:
|
||||
sent_sms = next((sms for sms in self._sms if sms['number'] == number), None)
|
||||
self.assertTrue(bool(sent_sms), 'Number %s not found in %s' % (number, repr([s['number'] for s in self._sms])))
|
||||
self.assertEqual(sent_sms['body'], content)
|
||||
|
||||
def _clear_sms_sent(self):
|
||||
self._sms = []
|
||||
def assertSMSCanceled(self, partner, number, error_code, content=None):
|
||||
""" Check canceled SMS. Search is done for a pair partner / number where
|
||||
partner can be an empty recordset. """
|
||||
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>
|
||||
<template id="assets_backend" name="sms_assets" inherit_id="web.assets_backend">
|
||||
<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>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -11,34 +11,40 @@
|
||||
<xpath expr="//field[@name='phone']" position="replace">
|
||||
<label for="phone"/>
|
||||
<div class="o_row">
|
||||
<field name="phone" widget="phone"/>
|
||||
<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'}"
|
||||
/>
|
||||
<field name="phone" widget="phone" options="{'enable_sms': True}"/>
|
||||
</div>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='mobile']" position="replace">
|
||||
<label for="mobile"/>
|
||||
<div class="o_row">
|
||||
<field name="mobile" widget="phone"/>
|
||||
<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':[('mobile', '=', False)]}"
|
||||
context="{'field_name': 'mobile'}"
|
||||
/>
|
||||
</div>
|
||||
</xpath>
|
||||
<field name="mobile" widget="phone" options="{'enable_sms': True}"/>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</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>
|
||||
|
||||
@@ -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 -*-
|
||||
|
||||
from . import sms_cancel
|
||||
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 -*-
|
||||
# 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.addons.phone_validation.tools import phone_validation
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.addons.iap.models import iap
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
import phonenumbers
|
||||
_sms_phonenumbers_lib_imported = True
|
||||
|
||||
except ImportError:
|
||||
_sms_phonenumbers_lib_imported = False
|
||||
_logger.info(
|
||||
"The `phonenumbers` Python module is not available. "
|
||||
"Phone number validation will be skipped. "
|
||||
"Try `pip3 install phonenumbers` to install it."
|
||||
)
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
|
||||
|
||||
class SendSMS(models.TransientModel):
|
||||
_name = 'sms.composer'
|
||||
_description = 'Send SMS'
|
||||
|
||||
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
|
||||
_description = 'Send SMS Wizard'
|
||||
_RECIPIENTS_DISPLAY_NBR = 3
|
||||
|
||||
@api.model
|
||||
def default_get(self, 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'):
|
||||
model = self.env[active_model]
|
||||
records = self._get_records(model)
|
||||
partners = records._get_default_sms_recipients()
|
||||
phone_numbers = []
|
||||
no_phone_partners = []
|
||||
for partner in partners:
|
||||
number = self._sms_sanitization(partner, self.env.context.get('field_name') or 'mobile')
|
||||
if number:
|
||||
phone_numbers.append(number)
|
||||
else:
|
||||
no_phone_partners.append(partner.name)
|
||||
if len(partners) > 1:
|
||||
if no_phone_partners:
|
||||
raise UserError(_('Missing mobile number for %s.') % ', '.join(no_phone_partners))
|
||||
result['recipients'] = ', '.join(phone_numbers)
|
||||
result['res_model'] = result.get('res_model') or self.env.context.get('active_model')
|
||||
result['composition_mode'] = result.get('composition_mode') or 'comment'
|
||||
|
||||
if result['composition_mode'] == 'comment' and not result.get('res_id'):
|
||||
result['res_id'] = self.env.context.get('active_id')
|
||||
if result['composition_mode'] != 'comment':
|
||||
if result.get('use_active_domain') and not result.get('active_domain'):
|
||||
result['active_domain'] = self.env.context['active_domain']
|
||||
elif not result.get('res_ids'):
|
||||
result['res_ids'] = repr(self.env.context.get('active_ids'))
|
||||
|
||||
if result['res_model']:
|
||||
result.update(
|
||||
self._get_composer_values(
|
||||
result['composition_mode'], result['res_model'], result.get('res_id'),
|
||||
result.get('body'), result.get('template_id')
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
def action_send_sms(self):
|
||||
numbers = [number.strip() for number in self.recipients.split(',') if number.strip()]
|
||||
# documents
|
||||
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:
|
||||
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
|
||||
|
||||
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">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="recipients"/>
|
||||
<field name="message" widget="sms_widget"/>
|
||||
<field name="composition_mode" invisible="1"/>
|
||||
<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>
|
||||
</sheet>
|
||||
<footer>
|
||||
<group>
|
||||
<span>
|
||||
<button string="Send" type="object" class="oe_highlight" name="action_send_sms"/>
|
||||
<button string="Cancel" class="oe_link" special="cancel" />
|
||||
</span>
|
||||
</group>
|
||||
<button string="Send SMS" type="object" class="oe_highlight" name="action_send_sms"/>
|
||||
<button string="Close" class="btn btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<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="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</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>
|
||||
|
||||
@@ -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' %
|
||||
(partner.name, real, expected))
|
||||
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),
|
||||
'Invalid read status for %s' % partner.name)
|
||||
|
||||
# for simplification, limitate to single message asserts
|
||||
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):
|
||||
""" Check for bus notifications. Basic check is about used channels.
|
||||
@@ -174,13 +174,13 @@ class TestRecipients(common.SavepointCase):
|
||||
'name': 'Valid Lelitre',
|
||||
'email': 'valid.lelitre@agrolait.com',
|
||||
'country_id': cls.env.ref('base.be').id,
|
||||
'mobile': '0475001122',
|
||||
'mobile': '0456001122',
|
||||
})
|
||||
cls.partner_2 = Partner.create({
|
||||
'name': 'Valid Poilvache',
|
||||
'email': 'valid.other@gmail.com',
|
||||
'country_id': cls.env.ref('base.be').id,
|
||||
'mobile': '+32 475 22 11 00',
|
||||
'mobile': '+32 456 22 11 00',
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from contextlib import closing
|
||||
import psycopg2
|
||||
|
||||
from odoo import api
|
||||
from odoo.addons.test_mail.tests import common as mail_common
|
||||
from odoo.tests import common
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
class TestMailRace(common.TransactionCase, mail_common.MockEmails):
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
@@ -26,15 +27,15 @@ class TestMailRace(common.TransactionCase, mail_common.MockEmails):
|
||||
'subject': 'S',
|
||||
'body': 'B',
|
||||
'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.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
|
||||
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'):
|
||||
try:
|
||||
# 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:
|
||||
# record already locked by send, all good
|
||||
bounce_deferred.append(True)
|
||||
@@ -55,14 +56,14 @@ class TestMailRace(common.TransactionCase, mail_common.MockEmails):
|
||||
# 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
|
||||
# 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']
|
||||
self.env['ir.mail_server']._patch_method('send_email', send_email)
|
||||
|
||||
mail.send()
|
||||
|
||||
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
|
||||
notif.unlink()
|
||||
|
||||
@@ -50,7 +50,7 @@ class TestMailResend(common.BaseFunctionalTest, common.MockEmails):
|
||||
|
||||
def assertNotifStates(self, states, message):
|
||||
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
|
||||
|
||||
def assertBusMessage(self, partners):
|
||||
|
||||
@@ -200,7 +200,7 @@ class TestAdvMailPerformance(BaseMailPerformance):
|
||||
def test_message_assignation_email(self):
|
||||
self.user_test.write({'notification_type': 'email'})
|
||||
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({
|
||||
'user_id': self.user_test.id,
|
||||
})
|
||||
@@ -209,7 +209,7 @@ class TestAdvMailPerformance(BaseMailPerformance):
|
||||
@warmup
|
||||
def test_message_assignation_inbox(self):
|
||||
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({
|
||||
'user_id': self.user_test.id,
|
||||
})
|
||||
@@ -253,7 +253,7 @@ class TestAdvMailPerformance(BaseMailPerformance):
|
||||
def test_message_post_one_email_notification(self):
|
||||
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(
|
||||
body='<p>Test Post Performances with an email ping</p>',
|
||||
partner_ids=self.customer.ids,
|
||||
@@ -265,7 +265,7 @@ class TestAdvMailPerformance(BaseMailPerformance):
|
||||
def test_message_post_one_inbox_notification(self):
|
||||
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(
|
||||
body='<p>Test Post Performances with an inbox ping</p>',
|
||||
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)
|
||||
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(
|
||||
body='<p>Test Post Performances</p>',
|
||||
message_type='comment',
|
||||
@@ -390,7 +390,7 @@ class TestHeavyMailPerformance(BaseMailPerformance):
|
||||
record = self.umbrella.with_user(self.env.user)
|
||||
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')
|
||||
|
||||
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,
|
||||
})
|
||||
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})
|
||||
self.assertEqual(rec.message_partner_ids, self.partners | self.env.user.partner_id | self.user_portal.partner_id)
|
||||
# write tracking message
|
||||
@@ -479,7 +479,7 @@ class TestHeavyMailPerformance(BaseMailPerformance):
|
||||
customer_id = self.customer.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({
|
||||
'name': 'Test',
|
||||
'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(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({
|
||||
'name': 'Test2',
|
||||
'umbrella_id': self.umbrella.id,
|
||||
@@ -542,7 +542,7 @@ class TestHeavyMailPerformance(BaseMailPerformance):
|
||||
})
|
||||
self.assertEqual(rec.message_partner_ids, self.user_portal.partner_id | self.env.user.partner_id)
|
||||
|
||||
with self.assertQueryCount(__system__=100, emp=105): # test_mail only: 100 - 105
|
||||
with self.assertQueryCount(__system__=105, emp=111): # test_mail only: 105 - 111
|
||||
rec.write({
|
||||
'name': 'Test2',
|
||||
'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]
|
||||
channel_ids = [self.channel_inbox.id, self.channel_email.id]
|
||||
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 2', "attachement tupple content 2", {'cid': 'cid1'}),
|
||||
('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
|
||||
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
|
||||
record.with_context({}).message_post(
|
||||
body='<p>Test body <img src="cid:cid1"> <img src="cid:cid2"></p>',
|
||||
|
||||
@@ -15,7 +15,7 @@ real applications. """,
|
||||
'mail',
|
||||
'mail_bot',
|
||||
# 'snailmail',
|
||||
'mass_mailing',
|
||||
# 'mass_mailing',
|
||||
'phone_validation',
|
||||
'sms',
|
||||
],
|
||||
|
||||
@@ -10,6 +10,7 @@ class MailTestSMS(models.Model):
|
||||
_description = 'Chatter Model for SMS Gateway'
|
||||
_name = 'mail.test.sms'
|
||||
_inherit = ['mail.thread']
|
||||
_order = 'name asc, id asc'
|
||||
|
||||
name = fields.Char()
|
||||
subject = fields.Char()
|
||||
@@ -18,6 +19,19 @@ class MailTestSMS(models.Model):
|
||||
mobile_nbr = fields.Char()
|
||||
customer_id = fields.Many2one('res.partner', 'Customer')
|
||||
|
||||
@api.multi
|
||||
def _get_default_sms_recipients(self):
|
||||
def _sms_get_default_partners(self):
|
||||
return self.mapped('customer_id')
|
||||
|
||||
def _sms_get_number_fields(self):
|
||||
return ['phone_nbr', 'mobile_nbr']
|
||||
|
||||
|
||||
class MailTestSMSSoLike(models.Model):
|
||||
""" A model like sale order having only a customer, not specific phone
|
||||
or mobile fields. """
|
||||
_description = 'Chatter Model for SMS Gateway (Partner only)'
|
||||
_name = 'mail.test.sms.partner'
|
||||
_inherit = ['mail.thread']
|
||||
|
||||
name = fields.Char()
|
||||
partner_id = fields.Many2one('res.partner', 'Customer')
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_mail_test_sms_all,mail.test.sms.all,model_mail_test_sms,,0,0,0,0
|
||||
access_mail_test_sms_user,mail.test.sms.user,model_mail_test_sms,base.group_user,1,1,1,1
|
||||
access_mail_test_sms_partner_all,mail.test.sms.partner.all,model_mail_test_sms_partner,,0,0,0,0
|
||||
access_mail_test_sms_partner_user,mail.test.sms.partner.user,model_mail_test_sms_partner,base.group_user,1,1,1,1
|
||||
|
||||
|
@@ -2,4 +2,8 @@
|
||||
|
||||
from . import common
|
||||
from . import test_sms_composer
|
||||
from . import test_sms_management
|
||||
from . import test_sms_performance
|
||||
from . import test_sms_post
|
||||
from . import test_sms_sms
|
||||
from . import test_sms_template
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo.addons.phone_validation.tools import phone_validation
|
||||
from odoo.addons.test_mail.tests import common as test_mail_common
|
||||
|
||||
|
||||
@@ -12,3 +13,10 @@ class BaseFunctionalTest(test_mail_common.BaseFunctionalTest):
|
||||
|
||||
# update country to belgium in order to test sanitization of numbers
|
||||
cls.user_employee.company_id.write({'country_id': cls.env.ref('base.be').id})
|
||||
|
||||
# some numbers for testing
|
||||
cls.random_numbers_str = '+32456998877, 0456665544'
|
||||
cls.random_numbers = cls.random_numbers_str.split(', ')
|
||||
cls.random_numbers_san = [phone_validation.phone_format(number, 'BE', '32', force_format='E164') for number in cls.random_numbers]
|
||||
cls.test_numbers = ['+32456010203', '0456 04 05 06']
|
||||
cls.test_numbers_san = [phone_validation.phone_format(number, 'BE', '32', force_format='E164') for number in cls.test_numbers]
|
||||
|
||||
@@ -1,74 +1,309 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.phone_validation.tools import phone_validation
|
||||
from odoo.addons.sms.tests import common as sms_common
|
||||
from odoo.addons.test_mail.tests import common as test_mail_common
|
||||
from odoo.addons.test_mail_full.tests import common as test_mail_full_common
|
||||
from odoo.addons.phone_validation.tools.phone_validation import phone_format
|
||||
|
||||
|
||||
class TestSMSComposer(test_mail_full_common.BaseFunctionalTest, sms_common.MockSMS, test_mail_common.MockEmails, test_mail_common.TestRecipients):
|
||||
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
|
||||
def setUpClass(cls):
|
||||
super(TestSMSComposer, cls).setUpClass()
|
||||
super(TestSMSComposerComment, cls).setUpClass()
|
||||
cls._test_body = 'VOID CONTENT'
|
||||
|
||||
cls.partner_numbers = [
|
||||
phone_format(partner.mobile, partner.country_id.code, partner.country_id.phone_code, force_format='E164')
|
||||
phone_validation.phone_format(partner.mobile, partner.country_id.code, partner.country_id.phone_code, force_format='E164')
|
||||
for partner in (cls.partner_1 | cls.partner_2)
|
||||
]
|
||||
cls.random_numbers_str = '+32475998877, 0475997788'
|
||||
cls.random_numbers = [phone_format(number, 'BE', '32', force_format='E164') for number in ['+32475998877', '0475997788']]
|
||||
|
||||
def test_composer_no_model(self):
|
||||
composer = self.env['sms.composer'].with_context().create({
|
||||
'message': self._test_body,
|
||||
'recipients': '+32475998877, 0475997788'
|
||||
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)
|
||||
|
||||
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():
|
||||
composer.action_send_sms()
|
||||
# self.assertSMSSent(self.random_numbers, self._test_body) # TDE FIXME: actually sanitizer is not called on numbers in current master (saas 12.3)
|
||||
self.assertSMSSent('+32475998877, 0475997788'.split(', '), self._test_body)
|
||||
|
||||
def test_composer_no_mail_thread(self):
|
||||
composer = self.env['sms.composer'].with_context(active_model='test_performance.base').create({
|
||||
'message': self._test_body,
|
||||
'recipients': '+32475998877, 0475997788'
|
||||
})
|
||||
|
||||
with self.mockSMSGateway():
|
||||
composer.action_send_sms()
|
||||
# self.assertSMSSent(self.random_numbers, self._test_body) # TDE FIXME: actually sanitizer is not called on numbers in current master (saas 12.3)
|
||||
self.assertSMSSent('+32475998877, 0475997788'.split(', '), self._test_body)
|
||||
|
||||
def test_composer_partners_active_domain(self):
|
||||
partners = self.partner_1 | self.partner_2
|
||||
composer = self.env['sms.composer'].with_context(
|
||||
active_model='res.partner',
|
||||
active_domain=[('id', 'in', partners.ids)]
|
||||
).create({
|
||||
'message': self._test_body,
|
||||
})
|
||||
|
||||
with self.mockSMSGateway():
|
||||
composer.action_send_sms()
|
||||
self.assertSMSSent(self.partner_numbers, self._test_body)
|
||||
|
||||
def test_composer_partners_sanitize(self):
|
||||
partner_incorrect = self.env['res.partner'].create({
|
||||
'name': 'Jean-Claude Incorrect',
|
||||
'email': 'jean.claude@example.com',
|
||||
'mobile': 'coincoin',
|
||||
def test_composer_comment_not_mail_thread(self):
|
||||
with self.sudo('employee'):
|
||||
record = self.env['test_performance.base'].create({'name': 'TestBase'})
|
||||
composer = self.env['sms.composer'].with_context(
|
||||
active_model='test_performance.base', active_id=record.id
|
||||
).create({
|
||||
'body': self._test_body,
|
||||
'numbers': ','.join(self.random_numbers),
|
||||
})
|
||||
partners = self.partner_1 | self.partner_2 | partner_incorrect
|
||||
composer = self.env['sms.composer'].with_context(
|
||||
active_model='res.partner',
|
||||
active_domain=[('id', 'in', partners.ids)]
|
||||
).create({
|
||||
'message': self._test_body,
|
||||
|
||||
with self.mockSMSGateway():
|
||||
composer._action_send_sms()
|
||||
|
||||
self.assertSMSSent(self.random_numbers_san, 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():
|
||||
composer.action_send_sms()
|
||||
# self.assertSMSSent((self.partner_1 | self.partner_2).mapped('mobile'), test_body) # TDE FIXME: actually sanitizer does not work in current master (saas 12.23))
|
||||
def test_composer_mass_active_domain(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)]),
|
||||
).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 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.phone_validation.tools import phone_validation
|
||||
from odoo.addons.sms.tests import common as sms_common
|
||||
from odoo.addons.test_mail.tests import common as test_mail_common
|
||||
from odoo.addons.test_mail_full.tests import common as test_mail_full_common
|
||||
|
||||
|
||||
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
|
||||
def setUpClass(cls):
|
||||
super(TestSMSPost, cls).setUpClass()
|
||||
cls._test_body = 'VOID CONTENT'
|
||||
|
||||
cls.partner_numbers = [
|
||||
phone_validation.phone_format(partner.mobile, partner.country_id.code, partner.country_id.phone_code, force_format='E164')
|
||||
for partner in (cls.partner_1 | cls.partner_2)
|
||||
]
|
||||
|
||||
cls.test_record = cls.env['mail.test.sms'].with_context(**cls._test_context).create({
|
||||
'name': 'Test',
|
||||
'customer_id': cls.partner_1.id,
|
||||
'mobile_nbr': cls.test_numbers[0],
|
||||
'phone_nbr': cls.test_numbers[1],
|
||||
})
|
||||
cls.test_record = cls._reset_mail_context(cls.test_record)
|
||||
|
||||
def test_message_sms_internals_body(self):
|
||||
with self.sudo('employee'), self.mockSMSGateway():
|
||||
test_record = self.env['mail.test.sms'].browse(self.test_record.id)
|
||||
messages = test_record._message_sms('<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({
|
||||
'name': 'Test',
|
||||
'customer_id': cls.partner_1.id,
|
||||
})
|
||||
cls.test_record = cls._reset_mail_context(cls.test_record)
|
||||
cls.partner_3 = cls.env['res.partner'].with_context({
|
||||
'mail_create_nolog': True,
|
||||
'mail_create_nosubscribe': True,
|
||||
'mail_notrack': True,
|
||||
'no_reset_password': True,
|
||||
}).create({
|
||||
'name': 'Ernestine Loubine',
|
||||
'email': 'ernestine.loubine@agrolait.com',
|
||||
'country_id': cls.env.ref('base.be').id,
|
||||
'mobile': '0475556644',
|
||||
})
|
||||
|
||||
def test_message_post_with_sms_on_partners(self):
|
||||
test_body = 'Void body'
|
||||
with self.mockSMSGateway():
|
||||
(self.partner_1 | self.partner_2).message_post_send_sms(test_body)
|
||||
self.assertSMSSent((self.partner_1 | self.partner_2).mapped('mobile'), test_body)
|
||||
def test_message_sms_w_numbers_invalid(self):
|
||||
random_numbers = self.random_numbers + ['6988754']
|
||||
with self.sudo('employee'), self.mockSMSGateway():
|
||||
test_record = self.env['mail.test.sms'].browse(self.test_record.id)
|
||||
messages = test_record._message_sms(self._test_body, sms_numbers=random_numbers)
|
||||
|
||||
def test_message_post_with_sms_w_default(self):
|
||||
test_body = 'Void body'
|
||||
with self.mockSMSGateway():
|
||||
self.test_record.message_post_send_sms(test_body)
|
||||
self.assertSMSSent(self.partner_1.mapped('mobile'), test_body)
|
||||
self.assertIn(test_body, self.test_record.message_ids.body)
|
||||
# invalid numbers are still given to IAP currently as they are
|
||||
self.assertSMSNotification([{'number': self.random_numbers_san[0]}, {'number': self.random_numbers_san[1]}, {'number': random_numbers[2]}], self._test_body, messages)
|
||||
|
||||
def test_message_post_with_sms_w_numbers(self):
|
||||
test_body = 'Void body'
|
||||
test_numbers = ['0475114477', '0475225588']
|
||||
with self.mockSMSGateway():
|
||||
self.test_record.message_post_send_sms(test_body, numbers=test_numbers)
|
||||
self.assertSMSSent(test_numbers, test_body)
|
||||
self.assertIn(test_body, self.test_record.message_ids.body)
|
||||
def test_message_sms_w_partners_nocountry(self):
|
||||
self.test_record.customer_id.write({
|
||||
'mobile': self.random_numbers[0],
|
||||
'phone': self.random_numbers[1],
|
||||
'country_id': False,
|
||||
})
|
||||
with self.sudo('employee'), self.mockSMSGateway():
|
||||
test_record = self.env['mail.test.sms'].browse(self.test_record.id)
|
||||
messages = test_record._message_sms(self._test_body, partner_ids=self.test_record.customer_id.ids)
|
||||
|
||||
def test_message_post_with_sms_w_numbers_duplicate(self):
|
||||
test_body = 'Void body'
|
||||
test_numbers = ['0475114477', '0475225588', '0475114477']
|
||||
with self.mockSMSGateway():
|
||||
self.test_record.message_post_send_sms(test_body, numbers=test_numbers)
|
||||
self.assertSMSSent(test_numbers, test_body)
|
||||
self.assertIn(test_body, self.test_record.message_ids.body)
|
||||
self.assertSMSNotification([{'partner': self.test_record.customer_id}], self._test_body, messages)
|
||||
|
||||
def test_message_post_with_sms_w_partners(self):
|
||||
test_body = 'Void body'
|
||||
with self.mockSMSGateway():
|
||||
self.test_record.message_post_send_sms(test_body, partners=self.partner_1 | self.partner_2)
|
||||
self.assertSMSSent((self.partner_1 | self.partner_2).mapped('mobile'), test_body)
|
||||
self.assertIn(test_body, self.test_record.message_ids.body)
|
||||
def test_message_sms_w_partners_falsy(self):
|
||||
# TDE FIXME: currently sent to IAP
|
||||
self.test_record.customer_id.write({
|
||||
'mobile': 'youpie',
|
||||
'phone': 'youpla',
|
||||
})
|
||||
with self.sudo('employee'), self.mockSMSGateway():
|
||||
test_record = self.env['mail.test.sms'].browse(self.test_record.id)
|
||||
messages = test_record._message_sms(self._test_body, partner_ids=self.test_record.customer_id.ids)
|
||||
|
||||
# self.assertSMSNotification({self.test_record.customer_id: {}}, {}, self._test_body, messages)
|
||||
|
||||
def test_message_sms_w_numbers_sanitization_duplicate(self):
|
||||
pass
|
||||
# TDE FIXME: not sure
|
||||
# random_numbers = self.random_numbers + [self.random_numbers[1]]
|
||||
# random_numbers_san = self.random_numbers_san + [self.random_numbers_san[1]]
|
||||
# with self.sudo('employee'), self.mockSMSGateway():
|
||||
# messages = self.test_record._message_sms(self._test_body, sms_numbers=random_numbers)
|
||||
# self.assertSMSNotification({}, {random_numbers_san[0]: {}, random_numbers_san[1]: {}, random_numbers_san[2]: {}}, self._test_body, messages)
|
||||
|
||||
def test_message_sms_crash_credit(self):
|
||||
with self.sudo('employee'), self.mockSMSGateway(sim_error='credit'):
|
||||
test_record = self.env['mail.test.sms'].browse(self.test_record.id)
|
||||
messages = test_record._message_sms(self._test_body, partner_ids=(self.partner_1 | self.partner_2).ids)
|
||||
|
||||
self.assertSMSNotification([
|
||||
{'partner': self.partner_1, 'state': 'exception', 'failure_type': 'sms_credit'},
|
||||
{'partner': self.partner_2, 'state': 'exception', 'failure_type': 'sms_credit'},
|
||||
], self._test_body, messages)
|
||||
|
||||
def test_message_sms_crash_credit_single(self):
|
||||
with self.sudo('employee'), self.mockSMSGateway(nbr_t_error={phone_validation.phone_get_sanitized_record_number(self.partner_2): 'credit'}):
|
||||
test_record = self.env['mail.test.sms'].browse(self.test_record.id)
|
||||
messages = test_record._message_sms(self._test_body, partner_ids=(self.partner_1 | self.partner_2 | self.partner_3).ids)
|
||||
|
||||
self.assertSMSNotification([
|
||||
{'partner': self.partner_1, 'state': 'sent'},
|
||||
{'partner': self.partner_2, 'state': 'exception', 'failure_type': 'sms_credit'},
|
||||
{'partner': self.partner_3, 'state': 'sent'},
|
||||
], self._test_body, messages)
|
||||
|
||||
def test_message_sms_crash_server_crash(self):
|
||||
with self.sudo('employee'), self.mockSMSGateway(sim_error='jsonrpc_exception'):
|
||||
test_record = self.env['mail.test.sms'].browse(self.test_record.id)
|
||||
messages = test_record._message_sms(self._test_body, partner_ids=(self.partner_1 | self.partner_2 | self.partner_3).ids)
|
||||
|
||||
self.assertSMSNotification([
|
||||
{'partner': self.partner_1, 'state': 'exception', 'failure_type': 'sms_server'},
|
||||
{'partner': self.partner_2, 'state': 'exception', 'failure_type': 'sms_server'},
|
||||
{'partner': self.partner_3, 'state': 'exception', 'failure_type': 'sms_server'},
|
||||
], self._test_body, messages)
|
||||
|
||||
def test_message_sms_crash_wrong_number(self):
|
||||
with self.sudo('employee'), self.mockSMSGateway(sim_error='wrong_format_number'):
|
||||
test_record = self.env['mail.test.sms'].browse(self.test_record.id)
|
||||
messages = test_record._message_sms(self._test_body, partner_ids=(self.partner_1 | self.partner_2).ids)
|
||||
|
||||
self.assertSMSNotification([
|
||||
{'partner': self.partner_1, 'state': 'exception', 'failure_type': 'sms_number_format'},
|
||||
{'partner': self.partner_2, 'state': 'exception', 'failure_type': 'sms_number_format'},
|
||||
], self._test_body, messages)
|
||||
|
||||
def test_message_sms_crash_wrong_number_single(self):
|
||||
with self.sudo('employee'), self.mockSMSGateway(nbr_t_error={phone_validation.phone_get_sanitized_record_number(self.partner_2): 'wrong_format_number'}):
|
||||
test_record = self.env['mail.test.sms'].browse(self.test_record.id)
|
||||
messages = test_record._message_sms(self._test_body, partner_ids=(self.partner_1 | self.partner_2 | self.partner_3).ids)
|
||||
|
||||
self.assertSMSNotification([
|
||||
{'partner': self.partner_1, 'state': 'sent'},
|
||||
{'partner': self.partner_2, 'state': 'exception', 'failure_type': 'sms_number_format'},
|
||||
{'partner': self.partner_3, 'state': 'sent'},
|
||||
], self._test_body, messages)
|
||||
|
||||
@@ -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
|
||||
|
||||
@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.
|
||||
We avoid tracking needaction on post comments. Only emails should be
|
||||
sufficient. """
|
||||
msg_type = msg_vals.get('message_type') or message.message_type
|
||||
if msg_type == 'comment':
|
||||
return {'needaction_partner_ids': []}
|
||||
return super(BlogPost, self)._notify_customize_recipients(message, msg_vals)
|
||||
if msg_vals.get('message_type', message.message_type) == 'comment':
|
||||
return
|
||||
return super(BlogPost, self)._notify_record_by_inbox(message, recipients_data, msg_vals=msg_vals, **kwargs)
|
||||
|
||||
def _default_website_meta(self):
|
||||
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)
|
||||
|
||||
@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.
|
||||
We avoid tracking needaction on post comments. Only emails should be
|
||||
sufficient. """
|
||||
msg_type = msg_vals.get('message_type') or message.message_type
|
||||
if msg_type == 'comment':
|
||||
return {'needaction_partner_ids': [], 'partner_ids': []}
|
||||
return super(Post, self)._notify_customize_recipients(message, msg_vals)
|
||||
if msg_vals.get('message_type', message.message_type) == 'comment':
|
||||
return
|
||||
return super(Post, self)._notify_record_by_inbox(message, recipients_data, msg_vals=msg_vals, **kwargs)
|
||||
|
||||
|
||||
class PostReason(models.Model):
|
||||
|
||||
Reference in New Issue
Block a user