Merge pull request #6086 from expsa/sms_retry

[ADD] sms_retry
This commit is contained in:
abdurrahman-saber 2026-01-09 00:20:18 +04:00 committed by GitHub
commit ed75c386b9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 395 additions and 0 deletions

View File

@ -0,0 +1,8 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# SMS Retry Module
# Copyright (C) 2024 Expert Co. Ltd. (<http://exp-sa.com>).
#
##############################################################################
from . import models

View File

@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# SMS Retry Module
# Copyright (C) 2024 Expert Co. Ltd. (<http://exp-sa.com>).
#
##############################################################################
{
'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',
}

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="ir_cron_sms_retry_queue" model="ir.cron">
<field name="name">SMS: Process Retry Queue</field>
<field name="model_id" ref="sms.model_sms_sms"/>
<field name="state">code</field>
<field name="code">model._process_retry_queue()</field>
<field name="user_id" ref="base.user_root"/>
<field name="interval_number">15</field>
<field name="interval_type">minutes</field>
<field name="numbercall">-1</field>
<field name="doall" eval="False"/>
<field name="active" eval="True"/>
</record>
</data>
</odoo>

View File

@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# SMS Retry Module
# Copyright (C) 2024 Expert Co. Ltd. (<http://exp-sa.com>).
#
##############################################################################
from . import sms_sms
from . import res_config_settings

View File

@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# SMS Retry Module
# Copyright (C) 2024 Expert Co. Ltd. (<http://exp-sa.com>).
#
##############################################################################
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'
)

View File

@ -0,0 +1,173 @@
# -*- 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

View File

@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_sms_sms_user sms.sms.user model_sms_sms base.group_user 1 1 1 1

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="res_config_settings_view_form_inherit_sms_retry" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit.sms.retry</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[hasclass('settings')]" position="inside">
<div class="app_settings_block" data-string="SMS Retry" string="SMS Retry" data-key="sms_retry">
<h2>SMS Retry Configuration</h2>
<div class="row mt16 o_settings_container">
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="sms_retry_enabled"/>
</div>
<div class="o_setting_right_pane">
<label for="sms_retry_enabled"/>
<div class="text-muted">
Enable automatic retry of failed SMS messages
</div>
</div>
</div>
</div>
<div class="row mt16 o_settings_container" attrs="{'invisible': [('sms_retry_enabled', '=', False)]}">
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane">
</div>
<div class="o_setting_right_pane">
<label for="sms_retry_interval_minutes"/>
<div class="text-muted">
Time interval in minutes between retry attempts. Default: 60 minutes
</div>
<field name="sms_retry_interval_minutes"/>
</div>
</div>
</div>
<div class="row mt16 o_settings_container" attrs="{'invisible': [('sms_retry_enabled', '=', False)]}">
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane">
</div>
<div class="o_setting_right_pane">
<label for="sms_max_retry_count"/>
<div class="text-muted">
Maximum number of retry attempts per SMS. Default: 3 attempts
</div>
<field name="sms_max_retry_count"/>
</div>
</div>
</div>
</div>
</xpath>
</field>
</record>
</data>
</odoo>

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="sms_sms_view_form_inherit_sms_retry" model="ir.ui.view">
<field name="name">sms.sms.view.form.inherit.sms.retry</field>
<field name="model">sms.sms</field>
<field name="inherit_id" ref="sms.sms_tsms_view_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='error_code']" position="after">
<group string="Retry Information" attrs="{'invisible': [('state', '!=', 'error')]}">
<group>
<field name="retry_count" readonly="1"/>
<field name="max_retry_count" readonly="1"/>
</group>
<group>
<field name="next_retry_time" readonly="1" attrs="{'invisible': [('next_retry_time', '=', False)]}"/>
</group>
</group>
</xpath>
</field>
</record>
<record id="sms_sms_view_tree_inherit_sms_retry" model="ir.ui.view">
<field name="name">sms.sms.view.tree.inherit.sms.retry</field>
<field name="model">sms.sms</field>
<field name="inherit_id" ref="sms.sms_sms_view_tree"/>
<field name="arch" type="xml">
<field name="state" position="after">
<field name="retry_count" optional="hide"/>
<field name="next_retry_time" optional="hide" attrs="{'invisible': [('next_retry_time', '=', False)]}"/>
</field>
</field>
</record>
</data>
</odoo>