You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
418 lines
23 KiB
418 lines
23 KiB
# -*- encoding: utf-8 -*-
|
|
##############################################################################
|
|
#
|
|
# Asterisk Click2dial module for OpenERP
|
|
# Copyright (C) 2010-2013 Alexis de Lattre <alexis@via.ecp.fr>
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as
|
|
# published by the Free Software Foundation, either version 3 of the
|
|
# License, or (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
##############################################################################
|
|
|
|
from openerp.osv import fields, orm
|
|
from openerp.tools.translate import _
|
|
import logging
|
|
# Lib for phone number reformating -> pip install phonenumbers
|
|
import phonenumbers
|
|
# Lib py-asterisk from http://code.google.com/p/py-asterisk/
|
|
# We need a version which has this commit : http://code.google.com/p/py-asterisk/source/detail?r=8d0e1c941cce727c702582f3c9fcd49beb4eeaa4
|
|
# so a version after Nov 20th, 2012
|
|
from Asterisk import Manager
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
class asterisk_server(orm.Model):
|
|
'''Asterisk server object, to store all the parameters of the Asterisk IPBXs'''
|
|
_name = "asterisk.server"
|
|
_description = "Asterisk Servers"
|
|
_columns = {
|
|
'name': fields.char('Asterisk server name', size=50, required=True, help="Asterisk server name."),
|
|
'active': fields.boolean('Active', help="The active field allows you to hide the Asterisk server without deleting it."),
|
|
'ip_address': fields.char('Asterisk IP addr. or DNS', size=50, required=True, help="IP address or DNS name of the Asterisk server."),
|
|
'port': fields.integer('Port', required=True, help="TCP port on which the Asterisk Manager Interface listens. Defined in /etc/asterisk/manager.conf on Asterisk."),
|
|
'out_prefix': fields.char('Out prefix', size=4, help="Prefix to dial to place outgoing calls. If you don't use a prefix to place outgoing calls, leave empty."),
|
|
'national_prefix': fields.char('National prefix', size=4, help="Prefix for national phone calls (don't include the 'out prefix'). For e.g., in France, the phone numbers look like '01 41 98 12 42' : the National prefix is '0'."),
|
|
'international_prefix': fields.char('International prefix', required=True, size=4, help="Prefix to add to make international phone calls (don't include the 'out prefix'). For e.g., in France, the International prefix is '00'."),
|
|
'country_prefix': fields.char('My country prefix', required=True, size=4, help="Phone prefix of the country where the Asterisk server is located. For e.g. the phone prefix for France is '33'. If the phone number to dial starts with the 'My country prefix', OpenERP will remove the country prefix from the phone number and add the 'out prefix' followed by the 'national prefix'. If the phone number to dial doesn't start with the 'My country prefix', OpenERP will add the 'out prefix' followed by the 'international prefix'."),
|
|
'login': fields.char('AMI login', size=30, required=True, help="Login that OpenERP will use to communicate with the Asterisk Manager Interface. Refer to /etc/asterisk/manager.conf on your Asterisk server."),
|
|
'password': fields.char('AMI password', size=30, required=True, help="Password that OpenERP will use to communicate with the Asterisk Manager Interface. Refer to /etc/asterisk/manager.conf on your Asterisk server."),
|
|
'context': fields.char('Dialplan context', size=50, required=True, help="Asterisk dialplan context from which the calls will be made. Refer to /etc/asterisk/extensions.conf on your Asterisk server."),
|
|
'wait_time': fields.integer('Wait time (sec)', required=True, help="Amount of time (in seconds) Asterisk will try to reach the user's phone before hanging up."),
|
|
'extension_priority': fields.integer('Extension priority', required=True, help="Priority of the extension in the Asterisk dialplan. Refer to /etc/asterisk/extensions.conf on your Asterisk server."),
|
|
'alert_info': fields.char('Alert-Info SIP header', size=255, help="Set Alert-Info header in SIP request to user's IP Phone for the click2dial feature. If empty, the Alert-Info header will not be added. You can use it to have a special ring tone for click2dial (a silent one !) or to activate auto-answer for example."),
|
|
'company_id': fields.many2one('res.company', 'Company', help="Company who uses the Asterisk server."),
|
|
}
|
|
|
|
def _get_prefix_from_country(self, cr, uid, context=None):
|
|
user = self.pool['res.users'].browse(cr, uid, uid, context=context)
|
|
country_code = user.company_id and user.company_id.partner_id and user.company_id.partner_id.country_id and user.company_id.partner_id.country_id.code or False
|
|
default_country_prefix = False
|
|
if country_code:
|
|
default_country_prefix = phonenumbers.country_code_for_region(country_code)
|
|
return default_country_prefix
|
|
|
|
_defaults = {
|
|
'active': True,
|
|
'port': 5038, # Default AMI port
|
|
'national_prefix': '0',
|
|
'international_prefix': '00',
|
|
'country_prefix': _get_prefix_from_country,
|
|
'extension_priority': 1,
|
|
'wait_time': 15,
|
|
'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'asterisk.server', context=context),
|
|
}
|
|
|
|
def _check_validity(self, cr, uid, ids):
|
|
for server in self.browse(cr, uid, ids):
|
|
country_prefix = ('Country prefix', server.country_prefix)
|
|
international_prefix = ('International prefix', server.international_prefix)
|
|
out_prefix = ('Out prefix', server.out_prefix)
|
|
national_prefix = ('National prefix', server.national_prefix)
|
|
dialplan_context = ('Dialplan context', server.context)
|
|
alert_info = ('Alert-Info SIP header', server.alert_info)
|
|
login = ('AMI login', server.login)
|
|
password = ('AMI password', server.password)
|
|
|
|
for digit_prefix in [country_prefix, international_prefix, out_prefix, national_prefix]:
|
|
if digit_prefix[1] and not digit_prefix[1].isdigit():
|
|
raise orm.except_orm(_('Error :'), _("Only use digits for the '%s' on the Asterisk server '%s'" % (digit_prefix[0], server.name)))
|
|
if server.wait_time < 1 or server.wait_time > 120:
|
|
raise orm.except_orm(_('Error :'), _("You should set a 'Wait time' value between 1 and 120 seconds for the Asterisk server '%s'" % server.name))
|
|
if server.extension_priority < 1:
|
|
raise orm.except_orm(_('Error :'), _("The 'extension priority' must be a positive value for the Asterisk server '%s'" % server.name))
|
|
if server.port > 65535 or server.port < 1:
|
|
raise orm.except_orm(_('Error :'), _("You should set a TCP port between 1 and 65535 for the Asterisk server '%s'" % server.name))
|
|
for check_string in [dialplan_context, alert_info, login, password]:
|
|
if check_string[1]:
|
|
try:
|
|
string = check_string[1].encode('ascii')
|
|
except UnicodeEncodeError:
|
|
raise orm.except_orm(_('Error :'), _("The '%s' should only have ASCII caracters for the Asterisk server '%s'" % (check_string[0], server.name)))
|
|
return True
|
|
|
|
|
|
_constraints = [
|
|
(_check_validity, "Error message in raise", ['out_prefix', 'country_prefix', 'national_prefix', 'international_prefix', 'wait_time', 'extension_priority', 'port', 'context', 'alert_info', 'login', 'password']),
|
|
]
|
|
|
|
|
|
def _reformat_number(self, cr, uid, erp_number, ast_server, context=None):
|
|
'''
|
|
This function is dedicated to the transformation of the number
|
|
available in OpenERP to the number that Asterisk should dial.
|
|
You may have to inherit this function in another module specific
|
|
for your company if you are not happy with the way I reformat
|
|
the OpenERP numbers.
|
|
'''
|
|
|
|
error_title_msg = _("Invalid phone number")
|
|
invalid_international_format_msg = _("The phone number is not written in valid international format. Example of valid international format : +33 1 41 98 12 42")
|
|
invalid_national_format_msg = _("The phone number is not written in valid national format.")
|
|
invalid_format_msg = _("The phone number is not written in valid format.")
|
|
|
|
# Let's call the variable tmp_number now
|
|
tmp_number = erp_number
|
|
_logger.debug('Number before reformat = %s' % tmp_number)
|
|
|
|
# Check if empty
|
|
if not tmp_number:
|
|
raise orm.except_orm(error_title_msg, invalid_format_msg)
|
|
|
|
# Before starting to use prefix, we convert empty prefix whose value
|
|
# is False to an empty string
|
|
country_prefix = ast_server.country_prefix or ''
|
|
national_prefix = ast_server.national_prefix or ''
|
|
international_prefix = ast_server.international_prefix or ''
|
|
out_prefix = ast_server.out_prefix or ''
|
|
|
|
# Maybe one day we will use
|
|
# phonenumbers.format_out_of_country_calling_number(phonenumbers.parse('<phone_number_e164', None), 'FR')
|
|
# The country code seems to be OK with the ones of OpenERP
|
|
# But it returns sometimes numbers with '-'... we have to investigate this first
|
|
# International format
|
|
if tmp_number[0] != '+':
|
|
raise # This should never happen
|
|
# Remove the starting '+' of the number
|
|
tmp_number = tmp_number.replace('+','')
|
|
_logger.debug('Number after removal of special char = %s' % tmp_number)
|
|
|
|
# At this stage, 'tmp_number' should only contain digits
|
|
if not tmp_number.isdigit():
|
|
raise orm.except_orm(error_title_msg, invalid_format_msg)
|
|
|
|
_logger.debug('Country prefix = %s' % country_prefix)
|
|
if country_prefix == tmp_number[0:len(country_prefix)]:
|
|
# If the number is a national number,
|
|
# remove 'my country prefix' and add 'national prefix'
|
|
tmp_number = (national_prefix) + tmp_number[len(country_prefix):len(tmp_number)]
|
|
_logger.debug('National prefix = %s - Number with national prefix = %s' % (national_prefix, tmp_number))
|
|
|
|
else:
|
|
# If the number is an international number,
|
|
# add 'international prefix'
|
|
tmp_number = international_prefix + tmp_number
|
|
_logger.debug('International prefix = %s - Number with international prefix = %s' % (international_prefix, tmp_number))
|
|
|
|
# Add 'out prefix' to all numbers
|
|
tmp_number = out_prefix + tmp_number
|
|
_logger.debug('Out prefix = %s - Number to be sent to Asterisk = %s' % (out_prefix, tmp_number))
|
|
return tmp_number
|
|
|
|
|
|
# TODO : one day, we will use phonenumbers.format_out_of_country_calling_number() ?
|
|
# if yes, then we can trash the fields international_prefix, national_prefix
|
|
# country_prefix and this kind of code
|
|
def _convert_number_to_international_format(self, cr, uid, number, ast_server, context=None):
|
|
'''Convert the number presented by the phone network to a number
|
|
in international format e.g. +33141981242'''
|
|
if number and number.isdigit() and len(number) > 5:
|
|
if ast_server.international_prefix and number[0:len(ast_server.international_prefix)] == ast_server.international_prefix:
|
|
number = number[len(ast_server.international_prefix):]
|
|
number = '+' + number
|
|
elif ast_server.national_prefix and number[0:len(ast_server.national_prefix)] == ast_server.national_prefix:
|
|
number = number[len(ast_server.national_prefix):]
|
|
number = '+' + ast_server.country_prefix + number
|
|
return number
|
|
|
|
|
|
def _get_asterisk_server_from_user(self, cr, uid, context=None):
|
|
'''Returns an asterisk.server browse object'''
|
|
# We check if the user has an Asterisk server configured
|
|
user = self.pool['res.users'].browse(cr, uid, uid, context=context)
|
|
if user.asterisk_server_id.id:
|
|
ast_server = user.asterisk_server_id
|
|
else:
|
|
asterisk_server_ids = self.search(cr, uid, [('company_id', '=', user.company_id.id)], context=context)
|
|
# If no asterisk server is configured on the user, we take the first one
|
|
if not asterisk_server_ids:
|
|
raise orm.except_orm(_('Error :'), _("No Asterisk server configured for the company '%s'.") % user.company_id.name)
|
|
else:
|
|
ast_server = self.browse(cr, uid, asterisk_server_ids[0], context=context)
|
|
return ast_server
|
|
|
|
def _connect_to_asterisk(self, cr, uid, context=None):
|
|
'''
|
|
Open the connection to the Asterisk Manager
|
|
Returns an instance of the Asterisk Manager
|
|
|
|
'''
|
|
user = self.pool['res.users'].browse(cr, uid, uid, context=context)
|
|
|
|
# Note : if I write 'Error' without ' :', it won't get translated...
|
|
# I don't understand why !
|
|
|
|
ast_server = self._get_asterisk_server_from_user(cr, uid, context=context)
|
|
# We check if the current user has a chan type
|
|
if not user.asterisk_chan_type:
|
|
raise orm.except_orm(_('Error :'), _('No channel type configured for the current user.'))
|
|
|
|
# We check if the current user has an internal number
|
|
if not user.resource:
|
|
raise orm.except_orm(_('Error :'), _('No resource name configured for the current user'))
|
|
|
|
|
|
_logger.debug("User's phone : %s/%s" % (user.asterisk_chan_type, user.resource))
|
|
_logger.debug("Asterisk server = %s:%d" % (ast_server.ip_address, ast_server.port))
|
|
|
|
# Connect to the Asterisk Manager Interface
|
|
try:
|
|
ast_manager = Manager.Manager((ast_server.ip_address, ast_server.port), ast_server.login, ast_server.password)
|
|
except Exception, e:
|
|
_logger.error("Error in the request to the Asterisk Manager Interface %s" % ast_server.ip_address)
|
|
_logger.error("Here is the error message: %s" % e)
|
|
raise orm.except_orm(_('Error :'), _("Problem in the request from OpenERP to Asterisk. Here is the error message: %s" % e))
|
|
return False
|
|
|
|
return (user, ast_server, ast_manager)
|
|
|
|
def test_ami_connection(self, cr, uid, ids, context=None):
|
|
assert len(ids) == 1, 'Only 1 ID'
|
|
ast_server = self.browse(cr, uid, ids[0], context=context)
|
|
try:
|
|
ast_manager = Manager.Manager(
|
|
(ast_server.ip_address, ast_server.port),
|
|
ast_server.login,
|
|
ast_server.password)
|
|
except Exception, e:
|
|
raise orm.except_orm(
|
|
_("Connection Test Failed!"),
|
|
_("Here is the error message: %s" % e))
|
|
finally:
|
|
try:
|
|
if ast_manager:
|
|
ast_manager.Logoff()
|
|
except Exception:
|
|
pass
|
|
raise orm.except_orm(
|
|
_("Connection Test Successfull!"),
|
|
_("OpenERP can successfully login to the Asterisk Manager "
|
|
"Interface."))
|
|
|
|
def _get_calling_number(self, cr, uid, context=None):
|
|
|
|
user, ast_server, ast_manager = self._connect_to_asterisk(cr, uid, context=context)
|
|
calling_party_number = False
|
|
try:
|
|
list_chan = ast_manager.Status()
|
|
#from pprint import pprint
|
|
#pprint(list_chan)
|
|
_logger.debug("Result of Status AMI request: %s", list_chan)
|
|
for chan in list_chan.values():
|
|
sip_account = user.asterisk_chan_type + '/' + user.resource
|
|
if chan.get('ChannelState') == '4' and chan.get('ConnectedLineNum') == user.internal_number: # 4 = Ring
|
|
_logger.debug("Found a matching Event in 'Ring' state")
|
|
calling_party_number = chan.get('CallerIDNum')
|
|
break
|
|
if chan.get('ChannelState') == '6' and sip_account in chan.get('BridgedChannel', ''): # 6 = Up
|
|
_logger.debug("Found a matching Event in 'Up' state")
|
|
calling_party_number = chan.get('CallerIDNum')
|
|
break
|
|
# Compatibility with Asterisk 1.4
|
|
if chan.get('State') == 'Up' and sip_account in chan.get('Link', ''):
|
|
_logger.debug("Found a matching Event in 'Up' state")
|
|
calling_party_number = chan.get('CallerIDNum')
|
|
break
|
|
except Exception, e:
|
|
_logger.error("Error in the Status request to Asterisk server %s" % ast_server.ip_address)
|
|
_logger.error("Here is the detail of the error : '%s'" % unicode(e))
|
|
raise orm.except_orm(_('Error :'), _("Can't get calling number from Asterisk.\nHere is the error: '%s'" % unicode(e)))
|
|
|
|
finally:
|
|
ast_manager.Logoff()
|
|
|
|
_logger.debug("The calling party number is '%s'" % calling_party_number)
|
|
|
|
return calling_party_number
|
|
|
|
|
|
|
|
# Parameters specific for each user
|
|
class res_users(orm.Model):
|
|
_inherit = "res.users"
|
|
|
|
_columns = {
|
|
'internal_number': fields.char('Internal number', size=15,
|
|
help="User's internal phone number."),
|
|
'dial_suffix': fields.char('User-specific dial suffix', size=15,
|
|
help="User-specific dial suffix such as aa=2wb for SCCP auto answer."),
|
|
'callerid': fields.char('Caller ID', size=50,
|
|
help="Caller ID used for the calls initiated by this user."),
|
|
# You'd probably think : Asterisk should reuse the callerID of sip.conf !
|
|
# But it cannot, cf http://lists.digium.com/pipermail/asterisk-users/2012-January/269787.html
|
|
'cdraccount': fields.char('CDR Account', size=50,
|
|
help="Call Detail Record (CDR) account used for billing this user."),
|
|
'asterisk_chan_type': fields.selection([
|
|
('SIP', 'SIP'),
|
|
('IAX2', 'IAX2'),
|
|
('DAHDI', 'DAHDI'),
|
|
('Zap', 'Zap'),
|
|
('Skinny', 'Skinny'),
|
|
('MGCP', 'MGCP'),
|
|
('mISDN', 'mISDN'),
|
|
('H323', 'H323'),
|
|
('SCCP', 'SCCP'),
|
|
('Local', 'Local'),
|
|
], 'Asterisk channel type',
|
|
help="Asterisk channel type, as used in the Asterisk dialplan. If the user has a regular IP phone, the channel type is 'SIP'."),
|
|
'resource': fields.char('Resource name', size=64,
|
|
help="Resource name for the channel type selected. For example, if you use 'Dial(SIP/phone1)' in your Asterisk dialplan to ring the SIP phone of this user, then the resource name for this user is 'phone1'. For a SIP phone, the phone number is often used as resource name, but not always."),
|
|
'alert_info': fields.char('User-specific Alert-Info SIP header', size=255, help="Set a user-specific Alert-Info header in SIP request to user's IP Phone for the click2dial feature. If empty, the Alert-Info header will not be added. You can use it to have a special ring tone for click2dial (a silent one !) or to activate auto-answer for example."),
|
|
'variable': fields.char('User-specific Variable', size=255, help="Set a user-specific 'Variable' field in the Asterisk Manager Interface 'originate' request for the click2dial feature. If you want to have several variable headers, separate them with '|'."),
|
|
'asterisk_server_id': fields.many2one('asterisk.server', 'Asterisk server',
|
|
help="Asterisk server on which the user's phone is connected. If you leave this field empty, it will use the first Asterisk server of the user's company."),
|
|
}
|
|
|
|
_defaults = {
|
|
'asterisk_chan_type': 'SIP',
|
|
}
|
|
|
|
def _check_validity(self, cr, uid, ids):
|
|
for user in self.browse(cr, uid, ids):
|
|
for check_string in [('Resource name', user.resource), ('Internal number', user.internal_number), ('Caller ID', user.callerid)]:
|
|
if check_string[1]:
|
|
try:
|
|
plom = check_string[1].encode('ascii')
|
|
except UnicodeEncodeError:
|
|
raise orm.except_orm(_('Error :'), _("The '%s' for the user '%s' should only have ASCII caracters" % (check_string[0], user.name)))
|
|
return True
|
|
|
|
_constraints = [
|
|
(_check_validity, "Error message in raise", ['resource', 'internal_number', 'callerid']),
|
|
]
|
|
|
|
|
|
class phone_common(orm.AbstractModel):
|
|
_inherit = 'phone.common'
|
|
|
|
def click2dial(self, cr, uid, erp_number, context=None):
|
|
if not erp_number:
|
|
orm.except_orm(
|
|
_('Error:'),
|
|
_('Missing phone number'))
|
|
|
|
user, ast_server, ast_manager = self.pool['asterisk.server']._connect_to_asterisk(cr, uid, context=context)
|
|
ast_number = self.pool['asterisk.server']._reformat_number(
|
|
cr, uid, erp_number, ast_server, context=context)
|
|
|
|
# The user should have a CallerID
|
|
if not user.callerid:
|
|
raise orm.except_orm(_('Error :'), _('No callerID configured for the current user'))
|
|
|
|
variable = []
|
|
if user.asterisk_chan_type == 'SIP':
|
|
# We can only have one alert-info header in a SIP request
|
|
if user.alert_info:
|
|
variable.append('SIPAddHeader=Alert-Info: ' + user.alert_info)
|
|
elif ast_server.alert_info:
|
|
variable.append('SIPAddHeader=Alert-Info: ' + ast_server.alert_info)
|
|
if user.variable:
|
|
for user_variable in user.variable.split('|'):
|
|
variable.append(user_variable.strip())
|
|
|
|
try:
|
|
ast_manager.Originate(
|
|
user.asterisk_chan_type + '/' + user.resource + ( ('/' + user.dial_suffix) if user.dial_suffix else ''),
|
|
context = ast_server.context,
|
|
extension = ast_number,
|
|
priority = str(ast_server.extension_priority),
|
|
timeout = str(ast_server.wait_time*1000),
|
|
caller_id = user.callerid,
|
|
account = user.cdraccount,
|
|
variable = variable)
|
|
except Exception, e:
|
|
_logger.error("Error in the Originate request to Asterisk server %s" % ast_server.ip_address)
|
|
_logger.error("Here is the detail of the error : '%s'" % unicode(e))
|
|
raise orm.except_orm(_('Error :'), _("Click to dial with Asterisk failed.\nHere is the error: '%s'" % unicode(e)))
|
|
|
|
finally:
|
|
ast_manager.Logoff()
|
|
|
|
return True
|
|
|
|
def _prepare_incall_pop_action(
|
|
self, cr, uid, record_res, number, context=None):
|
|
# Not executed because this module doesn't depend on base_phone_popup
|
|
# TODO move to a dedicated module asterisk_popup ?
|
|
action = super(phone_common, self)._prepare_incall_pop_action(
|
|
cr, uid, record_res, number, context=context)
|
|
if not action:
|
|
action = {
|
|
'name': _('No Partner Found'),
|
|
'type': 'ir.actions.act_window',
|
|
'res_model': 'wizard.open.calling.partner',
|
|
'view_mode': 'form',
|
|
'views': [[False, 'form']], # Beurk, but needed
|
|
'target': 'new',
|
|
'context': {'incall_number_popup': number}
|
|
}
|
|
return action
|