commit
bf634fe91e
|
|
@ -0,0 +1,8 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# SMS Retry Module
|
||||||
|
# Copyright (C) 2024 Expert Co. Ltd. (<http://exp-sa.com>).
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
from . import models
|
||||||
|
|
@ -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',
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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'
|
||||||
|
)
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
Loading…
Reference in New Issue