Initial commit
This commit is contained in:
5
fusion_voip_ringcentral/__init__.py
Normal file
5
fusion_voip_ringcentral/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
from . import models
|
||||
46
fusion_voip_ringcentral/__manifest__.py
Normal file
46
fusion_voip_ringcentral/__manifest__.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
{
|
||||
'name': 'Phone - RingCentral',
|
||||
'version': '19.0.1.0.0',
|
||||
'category': 'Productivity/Phone',
|
||||
'summary': 'RingCentral VoIP provider for Odoo Phone. Make and receive calls via RingCentral.',
|
||||
'description': """
|
||||
Phone - RingCentral
|
||||
===================
|
||||
|
||||
Integrates RingCentral as a VoIP provider for Odoo's built-in Phone app.
|
||||
|
||||
Features:
|
||||
---------
|
||||
* One-click SIP provisioning from RingCentral API
|
||||
* Auto-configures WebSocket URL, SIP domain, username, and password
|
||||
* Uses RingCentral's authorization ID for SIP authentication
|
||||
* Works with all Odoo Phone modules (CRM, Helpdesk, HR, etc.)
|
||||
* JWT authentication for server-to-server credential provisioning
|
||||
|
||||
Copyright 2026 Nexa Systems Inc. All rights reserved.
|
||||
""",
|
||||
'author': 'Nexa Systems Inc.',
|
||||
'website': 'https://www.nexasystems.ca',
|
||||
'license': 'OPL-1',
|
||||
'depends': [
|
||||
'voip',
|
||||
],
|
||||
'external_dependencies': {
|
||||
'python': ['ringcentral'],
|
||||
},
|
||||
'data': [
|
||||
'views/voip_provider_views.xml',
|
||||
'views/res_users_views.xml',
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'voip_ringcentral/static/src/**/*',
|
||||
],
|
||||
},
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
}
|
||||
7
fusion_voip_ringcentral/models/__init__.py
Normal file
7
fusion_voip_ringcentral/models/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
from . import voip_provider
|
||||
from . import res_users_settings
|
||||
from . import res_users
|
||||
27
fusion_voip_ringcentral/models/res_users.py
Normal file
27
fusion_voip_ringcentral/models/res_users.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class ResUsers(models.Model):
|
||||
_inherit = 'res.users'
|
||||
|
||||
rc_authorization_id = fields.Char(
|
||||
string='RingCentral Authorization ID',
|
||||
compute='_compute_rc_authorization_id',
|
||||
inverse='_reflect_change_in_res_users_settings',
|
||||
groups='base.group_user',
|
||||
)
|
||||
|
||||
@api.depends('res_users_settings_id.rc_authorization_id')
|
||||
def _compute_rc_authorization_id(self):
|
||||
for user in self:
|
||||
user.rc_authorization_id = user.res_users_settings_id.rc_authorization_id
|
||||
|
||||
@api.model
|
||||
def _get_voip_user_configuration_fields(self):
|
||||
return super()._get_voip_user_configuration_fields() + [
|
||||
'rc_authorization_id',
|
||||
]
|
||||
11
fusion_voip_ringcentral/models/res_users_settings.py
Normal file
11
fusion_voip_ringcentral/models/res_users_settings.py
Normal file
@@ -0,0 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResUsersSettings(models.Model):
|
||||
_inherit = 'res.users.settings'
|
||||
|
||||
rc_authorization_id = fields.Char('RingCentral Authorization ID')
|
||||
138
fusion_voip_ringcentral/models/voip_provider.py
Normal file
138
fusion_voip_ringcentral/models/voip_provider.py
Normal file
@@ -0,0 +1,138 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
|
||||
import logging
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VoipProvider(models.Model):
|
||||
_inherit = 'voip.provider'
|
||||
|
||||
rc_client_id = fields.Char(
|
||||
string='RingCentral Client ID',
|
||||
groups='base.group_system',
|
||||
)
|
||||
rc_client_secret = fields.Char(
|
||||
string='RingCentral Client Secret',
|
||||
groups='base.group_system',
|
||||
)
|
||||
rc_jwt_token = fields.Char(
|
||||
string='RingCentral JWT Token',
|
||||
groups='base.group_system',
|
||||
)
|
||||
rc_server_url = fields.Char(
|
||||
string='RingCentral Server URL',
|
||||
default='https://platform.ringcentral.com',
|
||||
groups='base.group_system',
|
||||
)
|
||||
rc_device_id = fields.Char(
|
||||
string='RingCentral Device ID',
|
||||
readonly=True,
|
||||
groups='base.group_system',
|
||||
help='Device ID assigned by RingCentral during SIP provisioning. '
|
||||
'Use this to find the device in RingCentral Admin > Phones & Devices and configure its Caller ID.',
|
||||
)
|
||||
|
||||
def action_provision_sip(self):
|
||||
"""Call RingCentral SIP Provision API and auto-configure provider + user credentials."""
|
||||
self.ensure_one()
|
||||
|
||||
if not all([self.rc_client_id, self.rc_client_secret, self.rc_jwt_token]):
|
||||
raise UserError(_(
|
||||
'Please fill in the RingCentral Client ID, Client Secret, and JWT Token before provisioning.'
|
||||
))
|
||||
|
||||
try:
|
||||
from ringcentral import SDK
|
||||
except ImportError:
|
||||
raise UserError(_(
|
||||
'The ringcentral Python package is not installed. Run: pip install ringcentral'
|
||||
))
|
||||
|
||||
try:
|
||||
server_url = self.rc_server_url or 'https://platform.ringcentral.com'
|
||||
sdk = SDK(self.rc_client_id, self.rc_client_secret, server_url)
|
||||
platform = sdk.platform()
|
||||
platform.login(jwt=self.rc_jwt_token)
|
||||
|
||||
response = platform.post(
|
||||
'/restapi/v1.0/client-info/sip-provision',
|
||||
body={'sipInfo': [{'transport': 'WSS'}]},
|
||||
)
|
||||
data = response.json()
|
||||
|
||||
sip_info_list = getattr(data, 'sipInfo', None) or (data.get('sipInfo') if isinstance(data, dict) else None)
|
||||
if not sip_info_list:
|
||||
raise UserError(_('No SIP info returned from RingCentral.'))
|
||||
|
||||
sip_info = sip_info_list[0]
|
||||
|
||||
device_obj = getattr(data, 'device', None) or (data.get('device') if isinstance(data, dict) else None)
|
||||
device_id = ''
|
||||
if device_obj:
|
||||
device_id = str(getattr(device_obj, 'id', '') or
|
||||
(device_obj.get('id', '') if isinstance(device_obj, dict) else ''))
|
||||
|
||||
outbound_proxy = getattr(sip_info, 'outboundProxy', '') or ''
|
||||
domain = getattr(sip_info, 'domain', '') or ''
|
||||
username = getattr(sip_info, 'username', '') or ''
|
||||
password = getattr(sip_info, 'password', '') or ''
|
||||
auth_id = str(getattr(sip_info, 'authorizationId', '') or '')
|
||||
|
||||
if not all([outbound_proxy, domain, username, password]):
|
||||
raise UserError(_('Incomplete SIP credentials returned from RingCentral.'))
|
||||
|
||||
provider_vals = {
|
||||
'ws_server': f'wss://{outbound_proxy}',
|
||||
'pbx_ip': domain,
|
||||
'mode': 'prod',
|
||||
}
|
||||
if device_id:
|
||||
provider_vals['rc_device_id'] = device_id
|
||||
self.write(provider_vals)
|
||||
|
||||
# Auto-configure the current user's SIP credentials
|
||||
user_settings = self.env['res.users.settings']._find_or_create_for_user(self.env.user)
|
||||
user_settings.write({
|
||||
'voip_provider_id': self.id,
|
||||
'voip_username': username,
|
||||
'voip_secret': password,
|
||||
'rc_authorization_id': auth_id,
|
||||
})
|
||||
|
||||
_logger.info(
|
||||
"RingCentral SIP provisioned: WSS=%s, domain=%s, user=%s, authId=%s, deviceId=%s",
|
||||
outbound_proxy, domain, username, auth_id, device_id,
|
||||
)
|
||||
|
||||
device_msg = ''
|
||||
if device_id:
|
||||
device_msg = f'\nDevice ID: {device_id} (visible in RingCentral Admin > Phones & Devices)'
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _('SIP Provisioned Successfully'),
|
||||
'message': _(
|
||||
'RingCentral SIP credentials configured.\n'
|
||||
'WebSocket: wss://%(proxy)s\n'
|
||||
'Domain: %(domain)s\n'
|
||||
'SIP User: %(user)s%(device_msg)s',
|
||||
proxy=outbound_proxy, domain=domain, user=username, device_msg=device_msg,
|
||||
),
|
||||
'type': 'success',
|
||||
'sticky': True,
|
||||
},
|
||||
}
|
||||
|
||||
except UserError:
|
||||
raise
|
||||
except Exception as e:
|
||||
_logger.exception("RingCentral SIP provisioning failed")
|
||||
raise UserError(_('RingCentral SIP provisioning failed: %s') % str(e))
|
||||
14
fusion_voip_ringcentral/static/src/voip_service_patch.js
Normal file
14
fusion_voip_ringcentral/static/src/voip_service_patch.js
Normal file
@@ -0,0 +1,14 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Voip } from "@voip/core/voip_service";
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(Voip.prototype, {
|
||||
get areCredentialsSet() {
|
||||
return Boolean(this.store.settings.rc_authorization_id) && super.areCredentialsSet;
|
||||
},
|
||||
get authorizationUsername() {
|
||||
return this.store.settings.rc_authorization_id || "";
|
||||
},
|
||||
});
|
||||
32
fusion_voip_ringcentral/views/res_users_views.xml
Normal file
32
fusion_voip_ringcentral/views/res_users_views.xml
Normal file
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- Add RingCentral Authorization ID to user VoIP settings (admin view) -->
|
||||
<record id="res_users_form_inherit_rc" model="ir.ui.view">
|
||||
<field name="name">res.users.form.inherit.voip_ringcentral</field>
|
||||
<field name="model">res.users</field>
|
||||
<field name="inherit_id" ref="voip.res_user_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='voip_secret']" position="after">
|
||||
<field name="rc_authorization_id" readonly="1"
|
||||
string="RC Auth ID"
|
||||
invisible="not voip_provider_id"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Add to preferences form too -->
|
||||
<record id="res_users_prefs_form_inherit_rc" model="ir.ui.view">
|
||||
<field name="name">res.users.prefs.form.inherit.voip_ringcentral</field>
|
||||
<field name="model">res.users</field>
|
||||
<field name="inherit_id" ref="voip.res_users_view_form_preferences"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='voip_secret']" position="after">
|
||||
<field name="rc_authorization_id" readonly="1"
|
||||
string="RC Auth ID"
|
||||
invisible="not voip_provider_id"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
65
fusion_voip_ringcentral/views/voip_provider_views.xml
Normal file
65
fusion_voip_ringcentral/views/voip_provider_views.xml
Normal file
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- Extend VoIP Provider form to add RingCentral credentials and Provision button -->
|
||||
<record id="voip_provider_view_form_inherit_rc" model="ir.ui.view">
|
||||
<field name="name">voip.provider.form.inherit.ringcentral</field>
|
||||
<field name="model">voip.provider</field>
|
||||
<field name="inherit_id" ref="voip.voip_provider_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//sheet" position="inside">
|
||||
<separator string="RingCentral Integration"/>
|
||||
<group>
|
||||
<group>
|
||||
<field name="rc_client_id" placeholder="Client ID from RingCentral Developer Console"/>
|
||||
<field name="rc_client_secret" password="True"
|
||||
placeholder="Client Secret"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="rc_jwt_token" password="True"
|
||||
placeholder="JWT Token from RingCentral Credentials"/>
|
||||
<field name="rc_server_url"
|
||||
placeholder="https://platform.ringcentral.com"/>
|
||||
</group>
|
||||
</group>
|
||||
<group invisible="not rc_device_id">
|
||||
<group>
|
||||
<field name="rc_device_id"/>
|
||||
</group>
|
||||
<group>
|
||||
<div class="text-muted small">
|
||||
Find this device in RingCentral Admin Portal under
|
||||
Phones & Devices to configure its outbound Caller ID.
|
||||
</div>
|
||||
</group>
|
||||
</group>
|
||||
<div class="mt-2 mb-4">
|
||||
<button name="action_provision_sip" type="object"
|
||||
string="Provision SIP Credentials"
|
||||
class="btn-primary"
|
||||
icon="fa-plug"
|
||||
confirm="This will fetch SIP credentials from RingCentral and auto-configure the provider and your user settings. Continue?"/>
|
||||
<span class="text-muted ms-3">
|
||||
Fetches WSS endpoint, SIP domain, username, password, and authorization ID from RingCentral.
|
||||
</span>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Override list view: remove editable so clicking opens form view -->
|
||||
<record id="voip_provider_list_inherit_rc" model="ir.ui.view">
|
||||
<field name="name">voip.provider.list.inherit.ringcentral</field>
|
||||
<field name="model">voip.provider</field>
|
||||
<field name="inherit_id" ref="voip.voip_provider_tree_view"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//list" position="attributes">
|
||||
<attribute name="editable"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='mode']" position="after">
|
||||
<field name="rc_client_id" optional="hide" string="RC Client ID"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user