Skip to content

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/channels for active calls, returns caller's number
  • get_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. The asterisk_click2dial override sends the ARI originate request
  • convert_to_dial_number(erp_number) - Converts Odoo phone format to dialable number using phonenumbers library
  • get_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_sequence and _phone_name_fields attributes

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:

  1. on_dial_button.esm.js onClick() -> calls this.orm.call("phone.common", "click2dial", [phone_num])
  2. asterisk_click2dial/models/phone_common.py click2dial() -> converts number, builds ARI params, sends POST /ari/channels
  3. 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:

  1. asterisk_click2dial.esm.js onOpenCaller() -> RPC to /asterisk_click2dial/get_record_from_my_channel
  2. controller.py -> calls asterisk.server.get_record_from_my_channel()
  3. asterisk_server.py _get_calling_number() -> queries ARI for active channels, matches user's channel name
  4. phone_common.py get_record_from_phone_number() -> SQL search across res.partner and 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:

.o_field_phone a.o_form_uri:not(.o_field_phone_dial) {
    display: none !important;
}

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