Also impacts mass_mailing_sms, test_mail_sms, test_mass_mailing This PR adds support to receive sms delivery reports. Before this PR, an SMS was considered 'sent' when successfully handled by the third party. The user couldn't know if/when an SMS was actually sent for delivery or delivered to the recipient's device. This was similar to the behavior for emails as delivery reports are not commonly used (and not supported in Odoo). With this work, the SMS `pending` state is introduced in mail, mass_mailing, sms and mass_mailing_sms contexts although only fully used in the latter two modules (+tests of course). Because of the huge cost related to upgrading very large existing databases, the following compromises were made: 1. An email and sms notification/trace SENT means DELIVERED. Those that are sent but NOT DELIVERED are PENDING. The difference between email and sms traces reinforced with this PR is that an email sent will be counted as "sent" ~ "delivered" unless an error is returned for emails while for SMS it can only be reached if a delivery report is received. 2. The Link between an SMS uuid (shared with trusted parties) and the tracking records (notifications or traces) is done via an explicit relationship table (sms_tracker) instead of via a new field. This however allowed to nicely concentrate the state update logic. A `process` state is added to represent an intermediate step in the sending process, such as held at IAP for SMS. A few adjustments are also included to update for IAP api v3. Also, adapts and includes new tests. Task-2560666 Part-of: odoo/odoo#133392
140 lines
6.2 KiB
Python
140 lines
6.2 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from dateutil.relativedelta import relativedelta
|
|
|
|
from odoo import api, fields, models
|
|
from odoo.exceptions import AccessError
|
|
from odoo.tools.translate import _
|
|
|
|
|
|
class MailNotification(models.Model):
|
|
_name = 'mail.notification'
|
|
_table = 'mail_notification'
|
|
_rec_name = 'res_partner_id'
|
|
_log_access = False
|
|
_description = 'Message Notifications'
|
|
|
|
# origin
|
|
author_id = fields.Many2one('res.partner', 'Author', ondelete='set null')
|
|
mail_message_id = fields.Many2one('mail.message', 'Message', index=True, ondelete='cascade', required=True)
|
|
mail_mail_id = fields.Many2one('mail.mail', 'Mail', index=True, help='Optional mail_mail ID. Used mainly to optimize searches.')
|
|
# recipient
|
|
res_partner_id = fields.Many2one('res.partner', 'Recipient', index=True, ondelete='cascade')
|
|
# status
|
|
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'),
|
|
('process', 'Processing'), # being checked by intermediary like IAP for sms
|
|
('pending', 'Sent'), # used with SMS; mail does not differentiate sent from delivered
|
|
('sent', 'Delivered'),
|
|
('bounce', 'Bounced'),
|
|
('exception', 'Exception'),
|
|
('canceled', 'Canceled')
|
|
], string='Status', default='ready', index=True)
|
|
is_read = fields.Boolean('Is Read', index=True)
|
|
read_date = fields.Datetime('Read Date', copy=False)
|
|
failure_type = fields.Selection(selection=[
|
|
# generic
|
|
("unknown", "Unknown error"),
|
|
# mail
|
|
("mail_bounce", "Bounce"),
|
|
("mail_email_invalid", "Invalid email address"),
|
|
("mail_email_missing", "Missing email address"),
|
|
("mail_from_invalid", "Invalid from address"),
|
|
("mail_from_missing", "Missing from address"),
|
|
("mail_smtp", "Connection failed (outgoing mail server problem)"),
|
|
], 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'),
|
|
]
|
|
|
|
# ------------------------------------------------------------
|
|
# CRUD
|
|
# ------------------------------------------------------------
|
|
|
|
def init(self):
|
|
self._cr.execute("""
|
|
CREATE INDEX IF NOT EXISTS mail_notification_res_partner_id_is_read_notification_status_mail_message_id
|
|
ON mail_notification (res_partner_id, is_read, notification_status, mail_message_id);
|
|
CREATE INDEX IF NOT EXISTS mail_notification_author_id_notification_status_failure
|
|
ON mail_notification (author_id, notification_status)
|
|
WHERE notification_status IN ('bounce', 'exception');
|
|
""")
|
|
self.env.cr.execute(
|
|
"""CREATE UNIQUE INDEX IF NOT EXISTS unique_mail_message_id_res_partner_id_if_set
|
|
ON %s (mail_message_id, res_partner_id)
|
|
WHERE res_partner_id IS NOT NULL""" % self._table
|
|
)
|
|
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
messages = self.env['mail.message'].browse(vals['mail_message_id'] for vals in vals_list)
|
|
messages.check_access_rights('read')
|
|
messages.check_access_rule('read')
|
|
for vals in vals_list:
|
|
if vals.get('is_read'):
|
|
vals['read_date'] = fields.Datetime.now()
|
|
return super(MailNotification, self).create(vals_list)
|
|
|
|
def write(self, vals):
|
|
if ('mail_message_id' in vals or 'res_partner_id' in vals) and not self.env.is_admin():
|
|
raise AccessError(_("Can not update the message or recipient of a notification."))
|
|
if vals.get('is_read'):
|
|
vals['read_date'] = fields.Datetime.now()
|
|
return super(MailNotification, self).write(vals)
|
|
|
|
@api.model
|
|
def _gc_notifications(self, max_age_days=180):
|
|
domain = [
|
|
('is_read', '=', True),
|
|
('read_date', '<', fields.Datetime.now() - relativedelta(days=max_age_days)),
|
|
('res_partner_id.partner_share', '=', False),
|
|
('notification_status', 'in', ('sent', 'canceled'))
|
|
]
|
|
return self.search(domain).unlink()
|
|
|
|
# ------------------------------------------------------------
|
|
# TOOLS
|
|
# ------------------------------------------------------------
|
|
|
|
def format_failure_reason(self):
|
|
self.ensure_one()
|
|
if self.failure_type != 'unknown':
|
|
return dict(type(self).failure_type.selection).get(self.failure_type, _('No Error'))
|
|
else:
|
|
return _("Unknown error") + ": %s" % (self.failure_reason or '')
|
|
|
|
# ------------------------------------------------------------
|
|
# DISCUSS
|
|
# ------------------------------------------------------------
|
|
|
|
def _filtered_for_web_client(self):
|
|
"""Returns only the notifications to show on the web client."""
|
|
def _filter_unimportant_notifications(notif):
|
|
if notif.notification_status in ['bounce', 'exception', 'canceled'] \
|
|
or notif.res_partner_id.partner_share:
|
|
return True
|
|
subtype = notif.mail_message_id.subtype_id
|
|
return not subtype or subtype.track_recipients
|
|
|
|
return self.filtered(_filter_unimportant_notifications)
|
|
|
|
def _notification_format(self):
|
|
"""Returns the current notifications in the format expected by the web
|
|
client."""
|
|
return [{
|
|
'id': notif.id,
|
|
'notification_type': notif.notification_type,
|
|
'notification_status': notif.notification_status,
|
|
'failure_type': notif.failure_type,
|
|
'persona': {'id': notif.res_partner_id.id, 'displayName': notif.res_partner_id.display_name, 'type': "partner"} if notif.res_partner_id else False,
|
|
} for notif in self]
|