mirror of
https://github.com/bringout/oca-ocb-mail.git
synced 2026-04-22 00:42:08 +02:00
19.0 vanilla
This commit is contained in:
parent
5df8c07b59
commit
daa394e8b0
2114 changed files with 564841 additions and 299642 deletions
|
|
@ -1,10 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
from odoo import api, fields, models, tools, _
|
||||
from odoo.addons.sms.tools.sms_api import SmsApi
|
||||
from odoo.tools.urls import urljoin as url_join
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -15,93 +17,111 @@ class SmsSms(models.Model):
|
|||
_rec_name = 'number'
|
||||
_order = 'id DESC'
|
||||
|
||||
IAP_TO_SMS_STATE = {
|
||||
'success': 'sent',
|
||||
IAP_TO_SMS_STATE_SUCCESS = {
|
||||
'processing': 'process',
|
||||
'success': 'pending',
|
||||
# These below are not returned in responses from IAP API in _send but are received via webhook events.
|
||||
'sent': 'pending',
|
||||
'delivered': 'sent',
|
||||
}
|
||||
IAP_TO_SMS_FAILURE_TYPE = { # TODO RIGR remove me in master
|
||||
'insufficient_credit': 'sms_credit',
|
||||
'wrong_number_format': 'sms_number_format',
|
||||
'country_not_supported': 'sms_country_not_supported',
|
||||
'server_error': 'sms_server',
|
||||
'unregistered': 'sms_acc'
|
||||
}
|
||||
|
||||
BOUNCE_DELIVERY_ERRORS = {'sms_invalid_destination', 'sms_not_allowed', 'sms_rejected'}
|
||||
DELIVERY_ERRORS = {'sms_expired', 'sms_not_delivered', *BOUNCE_DELIVERY_ERRORS}
|
||||
|
||||
uuid = fields.Char('UUID', copy=False, readonly=True, default=lambda self: uuid4().hex,
|
||||
help='Alternate way to identify a SMS record, used for delivery reports')
|
||||
number = fields.Char('Number')
|
||||
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'),
|
||||
('process', 'Processing'),
|
||||
('pending', 'Sent'),
|
||||
('sent', 'Delivered'), # As for notifications and traces
|
||||
('error', 'Error'),
|
||||
('canceled', 'Canceled')
|
||||
('canceled', 'Cancelled')
|
||||
], 'SMS Status', readonly=True, copy=False, default='outgoing', required=True)
|
||||
failure_type = fields.Selection([
|
||||
("unknown", "Unknown error"),
|
||||
('sms_number_missing', 'Missing Number'),
|
||||
('sms_number_format', 'Wrong Number Format'),
|
||||
('sms_country_not_supported', 'Country Not Supported'),
|
||||
('sms_registration_needed', 'Country-specific Registration Required'),
|
||||
('sms_credit', 'Insufficient Credit'),
|
||||
('sms_server', 'Server Error'),
|
||||
('sms_acc', 'Unregistered Account'),
|
||||
# mass mode specific codes
|
||||
# mass mode specific codes, generated internally, not returned by IAP.
|
||||
('sms_blacklist', 'Blacklisted'),
|
||||
('sms_duplicate', 'Duplicate'),
|
||||
('sms_optout', 'Opted Out'),
|
||||
], copy=False)
|
||||
sms_tracker_id = fields.Many2one('sms.tracker', string='SMS trackers', compute='_compute_sms_tracker_id')
|
||||
to_delete = fields.Boolean(
|
||||
'Marked for deletion', default=False,
|
||||
help='Will automatically be deleted, while notifications will not be deleted in any case.'
|
||||
)
|
||||
|
||||
_uuid_unique = models.Constraint(
|
||||
'unique(uuid)',
|
||||
'UUID must be unique',
|
||||
)
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
self.env.ref('sms.ir_cron_sms_scheduler_action')._trigger()
|
||||
return super().create(vals_list)
|
||||
|
||||
@api.depends('uuid')
|
||||
def _compute_sms_tracker_id(self):
|
||||
self.sms_tracker_id = False
|
||||
existing_trackers = self.env['sms.tracker'].search([('sms_uuid', 'in', self.filtered('uuid').mapped('uuid'))])
|
||||
tracker_ids_by_sms_uuid = {tracker.sms_uuid: tracker.id for tracker in existing_trackers}
|
||||
for sms in self.filtered(lambda s: s.uuid in tracker_ids_by_sms_uuid):
|
||||
sms.sms_tracker_id = tracker_ids_by_sms_uuid[sms.uuid]
|
||||
|
||||
def action_set_canceled(self):
|
||||
self.state = 'canceled'
|
||||
notifications = self.env['mail.notification'].sudo().search([
|
||||
('sms_id', 'in', self.ids),
|
||||
# sent is sent -> cannot reset
|
||||
('notification_status', 'not in', ['canceled', 'sent']),
|
||||
])
|
||||
if notifications:
|
||||
notifications.write({'notification_status': 'canceled'})
|
||||
if not self._context.get('sms_skip_msg_notification', False):
|
||||
notifications.mail_message_id._notify_message_notification_update()
|
||||
self._update_sms_state_and_trackers('canceled')
|
||||
|
||||
def action_set_error(self, failure_type):
|
||||
self.state = 'error'
|
||||
self.failure_type = failure_type
|
||||
notifications = self.env['mail.notification'].sudo().search([
|
||||
('sms_id', 'in', self.ids),
|
||||
# sent can be set to error due to IAP feedback
|
||||
('notification_status', '!=', 'exception'),
|
||||
])
|
||||
if notifications:
|
||||
notifications.write({'notification_status': 'exception', 'failure_type': failure_type})
|
||||
if not self._context.get('sms_skip_msg_notification', False):
|
||||
notifications.mail_message_id._notify_message_notification_update()
|
||||
self._update_sms_state_and_trackers('error', failure_type=failure_type)
|
||||
|
||||
def action_set_outgoing(self):
|
||||
self.write({
|
||||
'state': 'outgoing',
|
||||
'failure_type': False
|
||||
})
|
||||
notifications = self.env['mail.notification'].sudo().search([
|
||||
('sms_id', 'in', self.ids),
|
||||
# sent is sent -> cannot reset
|
||||
('notification_status', 'not in', ['ready', 'sent']),
|
||||
])
|
||||
if notifications:
|
||||
notifications.write({'notification_status': 'ready', 'failure_type': False})
|
||||
if not self._context.get('sms_skip_msg_notification', False):
|
||||
notifications.mail_message_id._notify_message_notification_update()
|
||||
self._update_sms_state_and_trackers('outgoing', failure_type=False)
|
||||
|
||||
def send(self, unlink_failed=False, unlink_sent=True, auto_commit=False, raise_exception=False):
|
||||
def send(self, unlink_failed=False, unlink_sent=True, raise_exception=False):
|
||||
""" Main API method to send SMS.
|
||||
|
||||
This contacts an external server. If the transaction fails, it may be
|
||||
retried which can result in sending multiple SMS messages!
|
||||
|
||||
:param unlink_failed: unlink failed SMS after IAP feedback;
|
||||
:param unlink_sent: unlink sent SMS after IAP feedback;
|
||||
:param auto_commit: commit after each batch of SMS;
|
||||
:param raise_exception: raise if there is an issue contacting IAP;
|
||||
"""
|
||||
self = self.filtered(lambda sms: sms.state == 'outgoing')
|
||||
for batch_ids in self._split_batch():
|
||||
self.browse(batch_ids)._send(unlink_failed=unlink_failed, unlink_sent=unlink_sent, raise_exception=raise_exception)
|
||||
# auto-commit if asked except in testing mode
|
||||
if auto_commit is True and not getattr(threading.current_thread(), 'testing', False):
|
||||
self._cr.commit()
|
||||
domain = [('state', '=', 'outgoing'), ('to_delete', '!=', True)]
|
||||
to_send = self.try_lock_for_update().filtered_domain(domain)
|
||||
|
||||
for sms_api, sms in to_send._split_by_api():
|
||||
for batch_ids in sms._split_batch():
|
||||
self.browse(batch_ids).with_context(sms_api=sms_api)._send(
|
||||
unlink_failed=unlink_failed,
|
||||
unlink_sent=unlink_sent,
|
||||
raise_exception=raise_exception,
|
||||
)
|
||||
|
||||
def _split_by_api(self):
|
||||
yield SmsApi(self.env), self
|
||||
|
||||
def resend_failed(self):
|
||||
sms_to_send = self.filtered(lambda sms: sms.state == 'error')
|
||||
sms_to_send = self.filtered(lambda sms: sms.state == 'error' and not sms.to_delete)
|
||||
sms_to_send.state = 'outgoing'
|
||||
notification_title = _('Warning')
|
||||
notification_type = 'danger'
|
||||
|
|
@ -112,7 +132,7 @@ class SmsSms(models.Model):
|
|||
if success_sms > 0:
|
||||
notification_title = _('Success')
|
||||
notification_type = 'success'
|
||||
notification_message = _('%s out of the %s selected SMS Text Messages have successfully been resent.', success_sms, len(self))
|
||||
notification_message = _('%(count)s out of the %(total)s selected SMS Text Messages have successfully been resent.', count=success_sms, total=len(self))
|
||||
else:
|
||||
notification_message = _('The SMS Text Messages could not be resent.')
|
||||
else:
|
||||
|
|
@ -128,90 +148,93 @@ class SmsSms(models.Model):
|
|||
}
|
||||
|
||||
@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!
|
||||
def _process_queue(self):
|
||||
""" CRON job to send queued SMS messages. """
|
||||
domain = [('state', '=', 'outgoing'), ('to_delete', '!=', True)]
|
||||
|
||||
: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')]
|
||||
batch_size = self._get_send_batch_size()
|
||||
records = self.search(domain, limit=batch_size, order='id').try_lock_for_update()
|
||||
if not records:
|
||||
return
|
||||
|
||||
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()
|
||||
records._send(unlink_failed=False, unlink_sent=True, raise_exception=False)
|
||||
self.env['ir.cron']._commit_progress(len(records), remaining=self.search_count(domain) if len(records) == batch_size else 0)
|
||||
|
||||
res = None
|
||||
try:
|
||||
# auto-commit except in testing mode
|
||||
auto_commit = not getattr(threading.current_thread(), 'testing', False)
|
||||
res = self.browse(ids).send(unlink_failed=False, unlink_sent=True, auto_commit=auto_commit, raise_exception=False)
|
||||
except Exception:
|
||||
_logger.exception("Failed processing SMS queue")
|
||||
return res
|
||||
def _get_send_batch_size(self):
|
||||
return int(self.env['ir.config_parameter'].sudo().get_param('sms.session.batch.size', 500))
|
||||
|
||||
def _get_sms_company(self):
|
||||
return self.mail_message_id.record_company_id or self.env.company
|
||||
|
||||
def _split_batch(self):
|
||||
batch_size = int(self.env['ir.config_parameter'].sudo().get_param('sms.session.batch.size', 500))
|
||||
for sms_batch in tools.split_every(batch_size, self.ids):
|
||||
yield sms_batch
|
||||
batch_size = self._get_send_batch_size()
|
||||
yield from tools.split_every(batch_size, self.ids)
|
||||
|
||||
def _send(self, unlink_failed=False, unlink_sent=True, 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]
|
||||
"""Send SMS after checking the number (presence and formatting)."""
|
||||
sms_api = self.env.context.get('sms_api')
|
||||
if not sms_api:
|
||||
company = self._get_sms_company()
|
||||
company.ensure_one() # This should always be the case since the grouping is done in `send`
|
||||
sms_api = company._get_sms_api_class()(self.env)
|
||||
|
||||
return self._send_with_api(
|
||||
sms_api,
|
||||
unlink_failed=unlink_failed,
|
||||
unlink_sent=unlink_sent,
|
||||
raise_exception=raise_exception,
|
||||
)
|
||||
|
||||
def _send_with_api(self, sms_api, unlink_failed=False, unlink_sent=True, raise_exception=False):
|
||||
"""Send SMS after checking the number (presence and formatting)."""
|
||||
messages = [{
|
||||
'content': body,
|
||||
'numbers': [{'number': sms.number, 'uuid': sms.uuid} for sms in body_sms_records],
|
||||
} for body, body_sms_records in self.grouped('body').items()]
|
||||
|
||||
delivery_reports_url = url_join(self[0].get_base_url(), '/sms/status')
|
||||
try:
|
||||
iap_results = self.env['sms.api']._send_sms_batch(iap_data)
|
||||
results = sms_api._send_sms_batch(messages, delivery_reports_url=delivery_reports_url)
|
||||
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_iap_sent_sms(
|
||||
[{'res_id': sms.id, 'state': 'server_error'} for sms in self],
|
||||
unlink_failed=unlink_failed, unlink_sent=unlink_sent)
|
||||
results = [{'uuid': sms.uuid, 'state': 'server_error'} for sms in self]
|
||||
else:
|
||||
_logger.info('Send batch %s SMS: %s: gave %s', len(self.ids), self.ids, iap_results)
|
||||
self._postprocess_iap_sent_sms(iap_results, unlink_failed=unlink_failed, unlink_sent=unlink_sent)
|
||||
_logger.info('Send batch %s SMS: %s: gave %s', len(self.ids), self.ids, results)
|
||||
|
||||
def _postprocess_iap_sent_sms(self, iap_results, failure_reason=None, unlink_failed=False, unlink_sent=True):
|
||||
todelete_sms_ids = []
|
||||
if unlink_failed:
|
||||
todelete_sms_ids += [item['res_id'] for item in iap_results if item['state'] != 'success']
|
||||
if unlink_sent:
|
||||
todelete_sms_ids += [item['res_id'] for item in iap_results if item['state'] == 'success']
|
||||
results_uuids = [result['uuid'] for result in results]
|
||||
all_sms_sudo = self.env['sms.sms'].sudo().search([('uuid', 'in', results_uuids)]).with_context(sms_skip_msg_notification=True)
|
||||
|
||||
for state in self.IAP_TO_SMS_STATE.keys():
|
||||
sms_ids = [item['res_id'] for item in iap_results if item['state'] == state]
|
||||
if sms_ids:
|
||||
if state != 'success' and not unlink_failed:
|
||||
self.env['sms.sms'].sudo().browse(sms_ids).write({
|
||||
'state': 'error',
|
||||
'failure_type': self.IAP_TO_SMS_STATE[state],
|
||||
})
|
||||
if state == 'success' and not unlink_sent:
|
||||
self.env['sms.sms'].sudo().browse(sms_ids).write({
|
||||
'state': 'sent',
|
||||
'failure_type': False,
|
||||
})
|
||||
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': self.IAP_TO_SMS_STATE[state] if state != 'success' else False,
|
||||
'failure_reason': failure_reason if failure_reason else False,
|
||||
})
|
||||
self.mail_message_id._notify_message_notification_update()
|
||||
for (iap_state, failure_reason), results_group in tools.groupby(results, key=lambda result: (result['state'], result.get('failure_reason'))):
|
||||
sms_sudo = all_sms_sudo.filtered(lambda s: s.uuid in {result['uuid'] for result in results_group})
|
||||
if success_state := self.IAP_TO_SMS_STATE_SUCCESS.get(iap_state):
|
||||
sms_sudo.sms_tracker_id._action_update_from_sms_state(success_state)
|
||||
to_delete = {'to_delete': True} if unlink_sent else {}
|
||||
sms_sudo.write({'state': success_state, 'failure_type': False, **to_delete})
|
||||
else:
|
||||
failure_type = sms_api.PROVIDER_TO_SMS_FAILURE_TYPE.get(iap_state, 'unknown')
|
||||
if failure_type != 'unknown':
|
||||
sms_sudo.sms_tracker_id._action_update_from_sms_state('error', failure_type=failure_type, failure_reason=failure_reason)
|
||||
else:
|
||||
sms_sudo.sms_tracker_id.with_context(sms_known_failure_reason=failure_reason)._action_update_from_provider_error(iap_state)
|
||||
to_delete = {'to_delete': True} if unlink_failed else {}
|
||||
sms_sudo.write({'state': 'error', 'failure_type': failure_type, **to_delete})
|
||||
|
||||
if todelete_sms_ids:
|
||||
self.browse(todelete_sms_ids).sudo().unlink()
|
||||
all_sms_sudo._handle_call_result_hook(results)
|
||||
all_sms_sudo.mail_message_id._notify_message_notification_update()
|
||||
|
||||
def _update_sms_state_and_trackers(self, new_state, failure_type=None):
|
||||
"""Update sms state update and related tracking records (notifications, traces)."""
|
||||
self.write({'state': new_state, 'failure_type': failure_type})
|
||||
# Use sudo on mail.notification to allow writing other users' notifications; rights are already checked by sms write
|
||||
self.sms_tracker_id.sudo()._action_update_from_sms_state(new_state, failure_type=failure_type)
|
||||
|
||||
def _handle_call_result_hook(self, results):
|
||||
"""Further process SMS sending API results."""
|
||||
pass
|
||||
|
||||
@api.autovacuum
|
||||
def _gc_device(self):
|
||||
self.env.cr.execute("DELETE FROM sms_sms WHERE to_delete = TRUE")
|
||||
_logger.info("GC'd %d sms marked for deletion", self.env.cr.rowcount)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue