Telephony Developer Guide¶
Last Updated: February 2026
Module Overview¶
The telephony integration consists of three OCA modules in oca_telephony/:
| Module | Purpose |
|---|---|
base_phone |
Phone number formatting, Dial button widget, phone lookup engine |
asterisk_click2dial |
ARI connection to Asterisk, call origination, systray caller ID lookup |
voip_oca |
WebRTC softphone in browser (not currently used) |
Key Models¶
asterisk.server¶
File: asterisk_click2dial/models/asterisk_server.py
Stores PBX connection settings. One record per Asterisk/VitalPBX server.
| Field | Type | Purpose |
|---|---|---|
ip_address |
Char | PBX hostname or IP |
port |
Integer | ARI port (default 8088) |
login |
Char | ARI username |
password |
Char | ARI password |
context |
Char | Asterisk dialplan context |
wait_time |
Integer | Ring timeout in seconds |
Key methods:
test_ari_connection()- Tests ARI connectivity, returns notification_get_connect_info(url_path)- Returns(server, auth_tuple, full_url)for ARI requests_get_calling_number()- Queries ARI/ari/channelsfor active calls, returns caller's numberget_record_from_my_channel()- Combines_get_calling_number()with phone lookup to find Odoo record
phone.common¶
File: base_phone/models/phone_common.py
Abstract model inherited by asterisk_click2dial. Provides phone number lookup and formatting.
Key methods:
click2dial(erp_number)- Override point for PBX-specific modules. Theasterisk_click2dialoverride sends the ARI originate requestconvert_to_dial_number(erp_number)- Converts Odoo phone format to dialable number usingphonenumberslibraryget_record_from_phone_number(presented_number)- SQL search across all phone-enabled models, returns(model_name, record_id, display_name)_get_phone_models()- Discovers all models with_phone_name_sequenceand_phone_name_fieldsattributes
res.users (extended)¶
File: asterisk_click2dial/models/res_users.py
Each user has telephony fields:
| Field | Purpose |
|---|---|
internal_number |
Display-only internal extension |
resource |
PJSIP endpoint name (used to build channel) |
asterisk_chan_type |
PJSIP, SIP, IAX2, etc. |
asterisk_chan_name |
Computed: {chan_type}/{resource} |
callerid |
Caller ID string sent with outgoing calls |
asterisk_server_id |
Which PBX this user is on |
Call Flow: Click-to-Dial¶
Browser Odoo Server Asterisk (ARI) SIP Phone
| | | |
|-- Click "Dial" --------->| | |
| |-- POST /ari/channels --->| |
| | endpoint=PJSIP/1001 | |
| | extension=18655399299 | |
| | context=internal | |
| | |-- INVITE 1001 ------>|
| | | | (phone rings)
|<-- "Unhook phone" -------| | |
| | |<-- 200 OK ----------|
| | | | (user answers)
| | |-- Dial extension --->|
| | | 18655399299 |
| | | (via trunk) |
Code path:
on_dial_button.esm.jsonClick()-> callsthis.orm.call("phone.common", "click2dial", [phone_num])asterisk_click2dial/models/phone_common.pyclick2dial()-> converts number, builds ARI params, sendsPOST /ari/channels- Asterisk rings user's phone, then routes to the dialed extension/context
Call Flow: Incoming Caller Lookup¶
Browser Odoo Server Asterisk (ARI)
| | |
|-- Click systray phone -->| |
| |-- GET /ari/channels ---->|
| |<-- [{channel data}] -----|
| | |
| |-- SQL lookup: phone |
| | matching connected.num |
| | |
|<-- Open partner form ----| |
Code path:
asterisk_click2dial.esm.jsonOpenCaller()-> RPC to/asterisk_click2dial/get_record_from_my_channelcontroller.py-> callsasterisk.server.get_record_from_my_channel()asterisk_server.py_get_calling_number()-> queries ARI for active channels, matches user's channel namephone_common.pyget_record_from_phone_number()-> SQL search acrossres.partnerand other phone-enabled models
Making a Model Phone-Searchable¶
To make any Odoo model searchable by phone number (for incoming call lookup), add these class attributes:
class YourModel(models.Model):
_name = 'your.model'
# Sequence determines search priority (lower = searched first)
_phone_name_sequence = 100
# Fields to search for phone numbers
_phone_name_fields = ['phone', 'mobile']
phone = fields.Char()
mobile = fields.Char()
The phone.common._get_phone_models() method auto-discovers all models with these attributes.
Custom Modifications¶
US Phone Number Parsing Fix¶
File: base_phone/models/phone_common.py, line 160
Problem: The original OCA code passes None as the default region to phonenumbers.parse(), which fails for numbers without a + country prefix (e.g., (865) 539-9299).
Fix: Use the company's country code as the fallback region:
# Original (fails for local format numbers):
parsed_num = phonenumbers.parse(erp_number, None)
# Fixed (uses company country as default region):
country_code = self.env.company.country_id.code or None
parsed_num = phonenumbers.parse(erp_number, country_code)
Upstream Dependency
This is a modification to an OCA module. When updating oca_telephony from upstream, this change will be overwritten. Either maintain as a patch or contribute upstream.
Hidden Call Button¶
File: asterisk_click2dial/static/src/scss/asterisk.scss
CSS to hide the default Odoo tel: link, keeping only the OCA Dial button:
Manual ir.asset Registration¶
The OCA module's assets manifest key doesn't auto-register in Odoo 17 under certain addons path configurations. JS assets must be manually inserted into ir.asset. See the Setup Guide for the script.
Frontend Components¶
OnDialButton (base_phone)¶
Files: base_phone/static/src/components/on_dial_button/
OWL component that renders the "Dial" button next to phone fields. Calls phone.common.click2dial() via ORM service.
PhoneField patch (base_phone)¶
Files: base_phone/static/src/components/phone_field/
Patches the standard Odoo PhoneField widget to inject OnDialButton next to every phone number in form views.
Click2DialSystray (asterisk_click2dial)¶
Files: asterisk_click2dial/static/src/components/asterisk_click2dial/
Adds a phone icon to the Odoo systray (top menu bar). On click, queries Asterisk for the current active call and opens the matching Odoo record.
Testing Locally¶
Prerequisites¶
cd /path/to/SR-Odoo/asterisk-dev
docker-compose up -d # Start Asterisk
twinkle -f dan --sip-port 5062 # Start softphone as ext 1001
Test Click-to-Dial via Python¶
import xmlrpc.client
url = "http://localhost:1818"
db = "test1"
common = xmlrpc.client.ServerProxy(f"{url}/xmlrpc/2/common")
uid = common.authenticate(db, "admin", "admin", {})
models = xmlrpc.client.ServerProxy(f"{url}/xmlrpc/2/object")
# This should ring the softphone
result = models.execute_kw(db, uid, "admin", 'phone.common', 'click2dial', ['(865) 539-9299'])
print(result) # {'dialed_number': '18655399299'}
Test Phone Lookup¶
result = models.execute_kw(db, uid, "admin", 'phone.common',
'get_record_from_phone_number', ['8659733067'])
print(result) # ['res.partner', 3563, 'Dan Scott']
Verify Asterisk State¶
# Check registered phones
docker exec asterisk-dev asterisk -rx "pjsip show endpoints"
# Check active calls
docker exec asterisk-dev asterisk -rx "core show channels"
# Test ARI directly
curl -u odoo:odoo_ari_pass http://127.0.0.1:8088/ari/endpoints
File Reference¶
oca_telephony/
base_phone/
models/
phone_common.py # Core: click2dial, phone lookup, number formatting
res_partner.py # Adds _phone_name_fields to res.partner
res_company.py # Adds number_of_digits_to_match_from_end setting
static/src/components/
on_dial_button/ # Dial button widget
phone_field/ # PhoneField patch to inject Dial button
wizard/
number_not_found.py # Popup when caller not in Odoo
reformat_all_phonenumbers.py # Batch reformat utility
asterisk_click2dial/
models/
asterisk_server.py # ARI connection, call origination, caller lookup
phone_common.py # Overrides click2dial with ARI originate
res_users.py # User extension/channel config
controller.py # /asterisk_click2dial/get_record_from_my_channel endpoint
static/src/
components/asterisk_click2dial/ # Systray phone icon
scss/asterisk.scss # Styles + Call button hide
asterisk-dev/
docker-compose.yml # Docker Asterisk for local dev
Dockerfile # Based on andrius/asterisk
config/
pjsip.conf # SIP extensions 1001, 1002
extensions.conf # Dialplan (internal + outgoing stub)
ari.conf # ARI user for Odoo
manager.conf # AMI user for Odoo
http.conf # HTTP server for ARI
modules.conf # Module autoload config