From 34a6d7ec907dacb281e04785416f7cd49c95b2ea Mon Sep 17 00:00:00 2001 From: Abdurrahman Saber Date: Fri, 9 Jan 2026 00:19:25 +0400 Subject: [PATCH] [ADD] sms_retry --- odex25_base/sms_retry/__init__.py | 8 + odex25_base/sms_retry/__manifest__.py | 44 +++++ odex25_base/sms_retry/data/ir_cron_data.xml | 17 ++ odex25_base/sms_retry/models/__init__.py | 9 + .../sms_retry/models/res_config_settings.py | 51 ++++++ odex25_base/sms_retry/models/sms_sms.py | 173 ++++++++++++++++++ .../sms_retry/security/ir.model.access.csv | 2 + .../views/res_config_settings_views.xml | 56 ++++++ odex25_base/sms_retry/views/sms_sms_views.xml | 35 ++++ 9 files changed, 395 insertions(+) create mode 100644 odex25_base/sms_retry/__init__.py create mode 100644 odex25_base/sms_retry/__manifest__.py create mode 100644 odex25_base/sms_retry/data/ir_cron_data.xml create mode 100644 odex25_base/sms_retry/models/__init__.py create mode 100644 odex25_base/sms_retry/models/res_config_settings.py create mode 100644 odex25_base/sms_retry/models/sms_sms.py create mode 100644 odex25_base/sms_retry/security/ir.model.access.csv create mode 100644 odex25_base/sms_retry/views/res_config_settings_views.xml create mode 100644 odex25_base/sms_retry/views/sms_sms_views.xml diff --git a/odex25_base/sms_retry/__init__.py b/odex25_base/sms_retry/__init__.py new file mode 100644 index 000000000..534b25d0e --- /dev/null +++ b/odex25_base/sms_retry/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# SMS Retry Module +# Copyright (C) 2024 Expert Co. Ltd. (). +# +############################################################################## +from . import models diff --git a/odex25_base/sms_retry/__manifest__.py b/odex25_base/sms_retry/__manifest__.py new file mode 100644 index 000000000..800a28b64 --- /dev/null +++ b/odex25_base/sms_retry/__manifest__.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# SMS Retry Module +# Copyright (C) 2024 Expert Co. Ltd. (). +# +############################################################################## +{ + 'name': 'SMS Retry', + 'version': '14.0.1.0.0', + 'category': 'Odex25-base/Communication', + 'author': 'Expert Co. Ltd.', + 'company': 'Expert Co. Ltd.', + 'maintainer': 'Expert Co. Ltd.', + 'website': 'http://www.exp-sa.com', + 'summary': 'Retry failed SMS messages with configurable intervals', + 'description': """ +SMS Retry Module +================ +This module allows automatic retry of failed SMS messages with configurable: +* Retry intervals (in minutes) +* Maximum retry count +* Next retry time tracking per SMS record + +Features: +--------- +* Automatically retries failed SMS messages +* Configurable retry interval +* Configurable maximum retry attempts +* Stores next retry time in each SMS record +* Cron job processes retries based on next_retry_time + """, + 'depends': ['sms', 'base'], + 'data': [ + 'security/ir.model.access.csv', + 'data/ir_cron_data.xml', + 'views/res_config_settings_views.xml', + 'views/sms_sms_views.xml', + ], + 'installable': True, + 'auto_install': False, + 'application': False, + 'license': 'LGPL-3', +} diff --git a/odex25_base/sms_retry/data/ir_cron_data.xml b/odex25_base/sms_retry/data/ir_cron_data.xml new file mode 100644 index 000000000..bd17134f8 --- /dev/null +++ b/odex25_base/sms_retry/data/ir_cron_data.xml @@ -0,0 +1,17 @@ + + + + + SMS: Process Retry Queue + + code + model._process_retry_queue() + + 15 + minutes + -1 + + + + + diff --git a/odex25_base/sms_retry/models/__init__.py b/odex25_base/sms_retry/models/__init__.py new file mode 100644 index 000000000..257927690 --- /dev/null +++ b/odex25_base/sms_retry/models/__init__.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# SMS Retry Module +# Copyright (C) 2024 Expert Co. Ltd. (). +# +############################################################################## +from . import sms_sms +from . import res_config_settings diff --git a/odex25_base/sms_retry/models/res_config_settings.py b/odex25_base/sms_retry/models/res_config_settings.py new file mode 100644 index 000000000..4203123b2 --- /dev/null +++ b/odex25_base/sms_retry/models/res_config_settings.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# SMS Retry Module +# Copyright (C) 2024 Expert Co. Ltd. (). +# +############################################################################## +from odoo import api, fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + sms_retry_enabled = fields.Boolean( + string='Enable SMS Retry', + related='company_id.sms_retry_enabled', + readonly=False, + help='Enable automatic retry of failed SMS messages' + ) + sms_retry_interval_minutes = fields.Integer( + string='Retry Interval (minutes)', + related='company_id.sms_retry_interval_minutes', + readonly=False, + help='Time interval in minutes between retry attempts' + ) + sms_max_retry_count = fields.Integer( + string='Max Retry Count', + related='company_id.sms_max_retry_count', + readonly=False, + help='Maximum number of retry attempts per SMS' + ) + + +class ResCompany(models.Model): + _inherit = 'res.company' + + sms_retry_enabled = fields.Boolean( + string='Enable SMS Retry', + default=False, + help='Enable automatic retry of failed SMS messages for this company' + ) + sms_retry_interval_minutes = fields.Integer( + string='Retry Interval (minutes)', + default=60, + help='Time interval in minutes between retry attempts. Default: 60 minutes' + ) + sms_max_retry_count = fields.Integer( + string='Max Retry Count', + default=3, + help='Maximum number of retry attempts per SMS. Default: 3 attempts' + ) diff --git a/odex25_base/sms_retry/models/sms_sms.py b/odex25_base/sms_retry/models/sms_sms.py new file mode 100644 index 000000000..f9138c3f6 --- /dev/null +++ b/odex25_base/sms_retry/models/sms_sms.py @@ -0,0 +1,173 @@ +# -*- 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 diff --git a/odex25_base/sms_retry/security/ir.model.access.csv b/odex25_base/sms_retry/security/ir.model.access.csv new file mode 100644 index 000000000..28d6c1120 --- /dev/null +++ b/odex25_base/sms_retry/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_sms_sms_user,sms.sms.user,model_sms_sms,base.group_user,1,1,1,1 diff --git a/odex25_base/sms_retry/views/res_config_settings_views.xml b/odex25_base/sms_retry/views/res_config_settings_views.xml new file mode 100644 index 000000000..48d460181 --- /dev/null +++ b/odex25_base/sms_retry/views/res_config_settings_views.xml @@ -0,0 +1,56 @@ + + + + + res.config.settings.view.form.inherit.sms.retry + res.config.settings + + + +
+

SMS Retry Configuration

+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/odex25_base/sms_retry/views/sms_sms_views.xml b/odex25_base/sms_retry/views/sms_sms_views.xml new file mode 100644 index 000000000..f8f314864 --- /dev/null +++ b/odex25_base/sms_retry/views/sms_sms_views.xml @@ -0,0 +1,35 @@ + + + + + sms.sms.view.form.inherit.sms.retry + sms.sms + + + + + + + + + + + + + + + + + + sms.sms.view.tree.inherit.sms.retry + sms.sms + + + + + + + + + +