odex25_standard/odex25_base/sms_retry/models/sms_sms.py

174 lines
7.1 KiB
Python

# -*- coding: utf-8 -*-
##############################################################################
#
# SMS Retry Module
# Copyright (C) 2024 Expert Co. Ltd. (<http://exp-sa.com>).
#
##############################################################################
from datetime import datetime, timedelta
from odoo import api, fields, models, _
import logging
_logger = logging.getLogger(__name__)
class SmsSms(models.Model):
_inherit = 'sms.sms'
retry_count = fields.Integer(
string='Retry Count',
default=0,
help='Number of retry attempts made for this SMS'
)
max_retry_count = fields.Integer(
string='Max Retry Count',
default=3,
help='Maximum number of retry attempts allowed'
)
next_retry_time = fields.Datetime(
string='Next Retry Time',
index=True,
help='Scheduled time for the next retry attempt'
)
def _is_retry_enabled(self):
"""Check if SMS retry is enabled."""
company = self.env.user.company_id or self.env['res.company'].sudo().search([('sms_retry_enabled', '=', True)], limit=1)
if company and company.sms_retry_enabled:
return True
# Fallback to system parameter
return self.env['ir.config_parameter'].sudo().get_param('sms.retry.enabled', 'False') == 'True'
def _get_retry_interval(self):
"""Get retry interval in minutes from system parameters or company settings."""
company = self.env.user.company_id or self.env['res.company'].sudo().search([('sms_retry_enabled', '=', True)], limit=1)
if company and company.sms_retry_enabled:
return company.sms_retry_interval_minutes
# Fallback to system parameter
return int(self.env['ir.config_parameter'].sudo().get_param('sms.retry.interval_minutes', 60))
def _get_max_retry_count(self):
"""Get max retry count from company settings or system parameters."""
company = self.env.user.company_id or self.env['res.company'].sudo().search([('sms_retry_enabled', '=', True)], limit=1)
if company and company.sms_retry_enabled:
return company.sms_max_retry_count
# Fallback to system parameter
return int(self.env['ir.config_parameter'].sudo().get_param('sms.retry.max_count', 3))
def _should_retry(self, error_code=None):
"""Determine if SMS should be retried based on error code and retry settings."""
self.ensure_one()
if not self._is_retry_enabled():
return False
# Don't retry if max retry count reached
if self.retry_count >= self.max_retry_count:
return False
# Don't retry certain error codes that won't resolve with retry
non_retryable_errors = ['sms_number_missing', 'sms_number_format', 'sms_blacklist', 'sms_duplicate']
if error_code and error_code in non_retryable_errors:
return False
return True
def _schedule_retry(self, error_code=None):
"""Schedule a retry for failed SMS."""
self.ensure_one()
if not self._should_retry(error_code):
_logger.info("SMS %s will not be retried. Error: %s, Retry count: %s/%s",
self.id, error_code, self.retry_count, self.max_retry_count)
return False
interval_minutes = self._get_retry_interval()
next_retry = fields.Datetime.now() + timedelta(minutes=interval_minutes)
self.write({
'next_retry_time': next_retry,
'max_retry_count': self._get_max_retry_count(),
})
_logger.info("SMS %s scheduled for retry at %s (attempt %s/%s)",
self.id, next_retry, self.retry_count + 1, self.max_retry_count)
return True
def _retry_sms(self):
"""Retry sending failed SMS by resetting state to outgoing."""
self.ensure_one()
if self.state != 'error':
return False
if self.retry_count >= self.max_retry_count:
_logger.info("SMS %s has reached max retry count (%s)", self.id, self.max_retry_count)
return False
# Reset to outgoing state to trigger normal send process
self.write({
'state': 'outgoing',
'retry_count': self.retry_count + 1,
'next_retry_time': False,
})
_logger.info("SMS %s retry attempt %s initiated", self.id, self.retry_count)
# Trigger immediate send
try:
self.send(delete_all=False, auto_commit=True, raise_exception=False)
except Exception as e:
_logger.exception("Error during SMS %s retry: %s", self.id, e)
# State will be updated by _postprocess_iap_sent_sms
return False
return True
def _postprocess_iap_sent_sms(self, iap_results, failure_reason=None, delete_all=False):
"""Override to schedule retries for failed SMS."""
# Call parent method first
super(SmsSms, self)._postprocess_iap_sent_sms(iap_results, failure_reason=failure_reason, delete_all=delete_all)
# Process retries for failed SMS (only if retry is enabled)
if not self._is_retry_enabled():
return
for result in iap_results:
if result.get('state') != 'success':
sms_id = result.get('res_id')
if sms_id:
sms = self.browse(sms_id)
if sms.exists() and sms.state == 'error':
error_code = self.IAP_TO_SMS_STATE.get(result.get('state'), 'sms_server')
sms._schedule_retry(error_code=error_code)
@api.model
def _process_retry_queue(self):
"""Process SMS records that are ready for retry.
Called by cron job to find and retry SMS messages."""
company_ids = self.env['res.company'].search([('sms_retry_enabled', '=', True)])
if not company_ids:
_logger.debug("SMS retry is not enabled for any company")
return
now = fields.Datetime.now()
domain = [
('state', '=', 'error'),
('next_retry_time', '<=', now),
('next_retry_time', '!=', False),
]
sms_to_retry = self.search(domain, limit=1000)
if sms_to_retry:
_logger.info("Processing %s SMS records for retry", len(sms_to_retry))
for sms in sms_to_retry:
try:
# Check if retry is still enabled and valid before processing
# This will also check retry_count < max_retry_count
if sms._is_retry_enabled() and sms._should_retry():
sms._retry_sms()
else:
# Clear next_retry_time if retry is no longer valid
sms.write({'next_retry_time': False})
except Exception as e:
_logger.exception("Error retrying SMS %s: %s", sms.id, e)
# Note: Commit is handled automatically by the cron job framework
# Do not commit here as it breaks test savepoints