# -*- coding: utf-8 -*- ############################################################################## # # SMS Retry Module # Copyright (C) 2024 Expert Co. Ltd. (). # ############################################################################## 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