Make that the module could be installed and works :-)

This commit is contained in:
Bernat Brunet Torruella 2014-05-21 17:10:11 +02:00
parent 92f315cf10
commit 9f66e89be9
9 changed files with 642 additions and 3 deletions

View File

@ -1,7 +1,15 @@
#This file is part asterisk module for Tryton.
#The COPYRIGHT file at the top level of this repository contains
#the full copyright notices and license terms.
from trytond.pool import Pool
from .asterisk import *
from .party import *
from .user import *
def register():
pass
Pool.register(
AsteriskConfiguration,
AsteriskConfigurationCompany,
Party,
User,
module='asterisk', type_='model')

443
asterisk.py Normal file
View File

@ -0,0 +1,443 @@
#This file is part asterisk module for Tryton.
#The COPYRIGHT file at the top level of this repository contains
#the full copyright notices and license terms.
from trytond.model import ModelView, ModelSQL, ModelSingleton, fields
from trytond.pool import Pool
from trytond.transaction import Transaction
import logging
import socket
import unicodedata
__all__ = [ 'AsteriskConfiguration', 'AsteriskConfigurationCompany']
class AsteriskConfiguration(ModelSingleton, ModelSQL, ModelView):
'Asterisk Configuration'
__name__ = 'asterisk.configuration'
name = fields.Function(fields.Char('Asterisk server name', required=True,
help="Asterisk server name."), 'get_fields', setter='set_fields')
ip_address = fields.Function(fields.Char('Asterisk IP addr. or DNS',
required=True,
help="IPv4 address or DNS name of the Asterisk server."),
'get_fields', setter='set_fields')
port = fields.Function(fields.Char('Port', required=True,
help="TCP port on which the Asterisk Manager Interface listens. "
"Defined in /etc/asterisk/manager.conf on Asterisk."),
'get_fields', setter='set_fields')
out_prefix = fields.Function(fields.Char('Out prefix',
help="Prefix to dial to place outgoing calls. If you don't use a "
"prefix to place outgoing calls, leave empty."),
'get_fields', setter='set_fields')
national_prefix = fields.Function(fields.Char('National prefix',
help="Prefix for national phone calls (don't include the 'out"
" prefix'). For example, in France, the phone numbers looks like "
"'01 41 98 12 42': the National prefix is '0'."),
'get_fields', setter='set_fields')
international_prefix = fields.Function(fields.Char('International prefix',
help="Prefix to add to make international phone calls (don't "
"include the 'out prefix'). For example, in France, the "
"International prefix is '00'."),
'get_fields', setter='set_fields')
country_prefix = fields.Function(fields.Char('My country prefix',
required=True,
help="Prefix to add to make international phone calls (don't "
"include the 'out prefix'). For example, the phone prefix for "
"France is '33'. If the phone number to dial starts with the 'My "
"country prefix', Tryton 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', Tryton will add the 'out "
"prefix' followed by the 'international prefix'."),
'get_fields', setter='set_fields')
national_format_allowed = fields.Function(fields.Boolean(
'National format allowed?',
help="Do we allow to use click2dial on phone numbers written in "
"national format, e.g. 01 41 98 12 42, or only in the "
"international format, e.g. +34 1 41 98 12 42 ?"),
'get_fields', setter='set_fields')
login = fields.Function(fields.Char('AMI login', required=True,
help="Login that Tryton will use to communicate with the Asterisk "
"Manager Interface. Refer to /etc/asterisk/manager.conf on "
"your Asterisk server."),
'get_fields', setter='set_fields')
password = fields.Function(fields.Char('AMI password', required=True,
help="Password that Asterisk will use to communicate with the "
"Asterisk Manager Interface. Refer to /etc/asterisk/manager.conf "
"on your Asterisk server."),
'get_fields', setter='set_fields')
context = fields.Function(fields.Char('Dialplan context', required=True,
help="Asterisk dialplan context from which the calls will be "
"made. Refer to /etc/asterisk/extensions.conf on your Asterisk "
"server."),
'get_fields', setter='set_fields')
wait_time = fields.Function(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."),
'get_fields', setter='set_fields')
extension_priority = fields.Function(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."),
'get_fields', setter='set_fields')
alert_info = fields.Function(fields.Char('Alert-Info SIP header',
help="Set Alert-Info header in SIP request to user's IP Phone. If "
"empty, the Alert-Info header will not be added. You can use "
"it to have a special ring tone for click2dial, for example "
"you could choose a silent ring tone."),
'get_fields', setter='set_fields')
@classmethod
def get_fields(cls, configurations, names):
res = {}
ConfigurationCompany = Pool().get('asterisk.configuration.company')
company_id = Transaction().context.get('company')
conf_id = configurations[0].id
if company_id:
confs = ConfigurationCompany.search([
('company', '=', company_id),
], limit=1)
for conf in confs:
for field_name in names:
value = getattr(conf, field_name)
res[field_name] = {conf_id: value}
return res
@classmethod
def set_fields(cls, configurations, name, value):
if value:
ConfigurationCompany = Pool().get('asterisk.configuration.company')
company_id = Transaction().context.get('company')
if company_id:
configuration = ConfigurationCompany.search([
('company', '=', company_id),
], limit=1)
if not configuration:
ConfigurationCompany.create([{
'company': company_id,
name: value,
}])
else:
ConfigurationCompany.write([configuration[0]], {
name: value
})
def _only_digits(self, prefix, can_be_empty):
prefix_to_check = self.read([self.id], [prefix])[0]
if prefix_to_check:
prefix_to_check = prefix_to_check[prefix]
if not prefix_to_check:
if not can_be_empty:
return False
else:
if not prefix_to_check.isdigit():
return False
return True
def _only_digits_port(self):
return self._only_digits('port', False)
def _only_digits_out_prefix(self):
return self._only_digits('out_prefix', True)
def _only_digits_country_prefix(self):
return self._only_digits('country_prefix', False)
def _only_digits_national_prefix(self):
return self._only_digits('national_prefix', True)
def _only_digits_international_prefix(self):
return self._only_digits('international_prefix', False)
def _check_wait_time(self):
wait_time_to_check = self.read([self.id], ['wait_time'])[0]\
['wait_time']
if wait_time_to_check < 1 or wait_time_to_check > 120:
return False
return True
def _check_extension_priority(self):
extension_priority_to_check = self.read([self.id],
['extension_priority'])[0]['extension_priority']
if extension_priority_to_check < 1:
return False
return True
def _check_port(self):
port_to_check = self.read([self.id], ['port'])[0]['port']
if int(port_to_check) > 65535 or int(port_to_check) < 1:
return False
return True
@classmethod
def __setup__(cls):
super(AsteriskConfiguration, cls).__setup__()
cls._constraints += [
('_only_digits_port', 'port'),
('_only_digits_out_prefix', 'out_prefix'),
('_only_digits_country_prefix', 'country_prefix'),
('_only_digits_national_prefix', 'national_prefix'),
('_only_digits_international_prefix', 'international_prefix'),
('_check_wait_time', 'wait_time'),
('_check_extension_priority', 'extension_priority'),
('_check_port', 'port'),
]
cls._error_messages.update({
'out_prefix': "Use only digits for the 'Out prefix' or leave "
"it empty.",
'country_prefix': "Use only digits for the 'Country prefix'.",
'national_prefix': "Use only digits for the 'National prefix' "
"or leave it empty.",
'international_prefix': "Use only digits for 'International "
"prefix'.",
'wait_time': "You should enter a 'Wait time' value between 1 "
"and 120 seconds.",
'extension_priority': "The 'Extension priority' must be a "
"positive value.",
'port': 'TCP ports range from 1 to 65535.',
'error': 'Error',
'invalid_phone': 'Invalid phone number',
'invalid_international_format': "The phone number is not "
"written in a valid international format. Example of valid "
"international format: +33 1 41 98 12 42.",
'invalid_national_format': "The phone number is not written "
"in a valid national format.",
'invalid_format': "The phone number is not written in a valid "
"format.",
'no_phone_number': "There is no phone number.",
'no_asterisk_configuration': "Not available Asterisk Server "
"configured for the current user.",
'no_channel_type': "There isn't a channel type configured for "
"the current user",
'no_internal_phone': "There isn't a internal phone number "
"configured for the current user",
'cant_resolve_dns': "Can't resolve the DNS of the Asterisk "
"server:",
'connection_failed': "The connection from Tryton to the "
"Asterisk server has failed. Please check the configuration "
"on Tryton and Asterisk.",
})
@staticmethod
def default_port():
return '5038'
@staticmethod
def default_out_prefix():
return '0'
@staticmethod
def default_national_prefix():
return '0'
@staticmethod
def default_international_prefix():
return '00'
@staticmethod
def default_extension_priority():
return 1
@staticmethod
def default_wait_time():
return 5
@staticmethod
def unaccent(text):
if isinstance(text, str):
text = unicode(text, 'utf-8')
return unicodedata.normalize('NFKD', text).encode('ASCII',
'ignore')
@classmethod
def reformat_number(cls, tryton_number, ast_server):
'''
This method transforms the number available in Tryton to the number
that Asterisk should dial.
'''
logger = logging.getLogger('asterisk')
# Let's call the variable tmp_number now
tmp_number = tryton_number
logger.debug('Number before reformat = %s' % tmp_number)
# Check if empty
if not tmp_number:
cls.raise_user_error(error='invalid_phone',
error_description='invalid_format')
# First, we remove all stupid characters and spaces
for i in [' ', '.', '(', ')', '[', ']', '-', '/']:
tmp_number = tmp_number.replace(i, '')
# 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 '')
# International format
if tmp_number[0] == '+':
# 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():
cls.raise_user_error(error='invalid_phone',
error_description='invalid_format_msg')
logger.debug('Country prefix = ' + 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))
# National format, allowed
elif ast_server.national_format_allowed:
# No treatment required
if not tmp_number.isdigit():
cls.raise_user_error(error='invalid_phone',
error_description='invalid_national_format')
# National format, disallowed
elif not ast_server.national_format_allowed:
cls.raise_user_error(error='invalid_phone',
error_description='invalid_international_format')
# 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
@classmethod
def dial(cls, party, tryton_number):
'''
Open the socket to the Asterisk Manager Interface (AMI)
and send instructions to Dial to Asterisk.
'''
logger = logging.getLogger('asterisk')
User = Pool().get('res.user')
user_id = Transaction().user
if user_id == 0 and 'user' in Transaction().context:
user_id = Transaction().context['user']
user = User(user_id)
# Check if the number to dial is not empty
if not tryton_number:
cls.raise_user_error(error='error',
error_description='no_phone_number')
# We check if the user has an Asterisk server configured
if not user.asterisk_server:
cls.raise_user_error(error='error',
error_description='no_asterisk_configuration')
else:
ast_server = user.asterisk_server
# We check if the current user has a chan type
if not user.asterisk_chan_type:
cls.raise_user_error(error='error',
error_description='no_channel_type')
# We check if the current user has an internal number
if not user.internal_number:
cls.raise_user_error(error='error',
error_description='no_internal_phone')
# The user should also have a CallerID, but in Spain that will
# be the name of the address that we call.
if not user.callerid:
#Party = Pool().get('party.party')
#callerid = Party.search(party).get_name_for_display
callerid = party.display_name
else:
callerid = user.CallerId
# Convert the phone number in the format that will be sent to Asterisk
ast_number = cls.reformat_number(tryton_number, ast_server)
logger.info('User dialing: channel = %s/%s - Callerid = %s' % \
(user.asterisk_chan_type, user.internal_number, user.callerid))
logger.info('Asterisk server = %s:%s' % \
(ast_server.ip_address, ast_server.port))
# Connect to the Asterisk Manager Interface, using IPv6-ready code
try:
res = socket.getaddrinfo(ast_server.ip_address, ast_server.port,
socket.AF_UNSPEC, socket.SOCK_STREAM)
except:
logger.error("Can't resolve the DNS of the Asterisk server : %s" %\
str(ast_server.ip_address))
cls.raise_user_error(error='error',
error_description='cant_resolve_dns')
for result in res:
af, socktype, proto, _, sockaddr = result
try:
sock = socket.socket(af, socktype, proto)
sock.connect(sockaddr)
sock.send('Action: login\r\n')
sock.send('Events: off\r\n')
sock.send('Username: %s\r\n' % str(ast_server.login))
sock.send('Secret: %s\r\n\r\n' % str(ast_server.password))
sock.send('Action: originate\r\n')
sock.send('Channel: %s/%s\r\n' % (str(user.asterisk_chan_type),
str(user.internal_number)))
sock.send('Timeout: %s\r\n' % str(ast_server.wait_time*1000))
sock.send('CallerId: %s\r\n' % cls.unaccent(callerid))
sock.send('Exten: %s\r\n' % str(ast_number))
sock.send('Context: %s\r\n' % str(ast_server.context))
if ast_server.alert_info and user.asterisk_chan_type == 'SIP':
sock.send('Variable: SIPAddHeader=Alert-Info: %s\r\n' % \
str(ast_server.alert_info))
sock.send('Priority: %s\r\n\r\n' % \
str(ast_server.extension_priority))
sock.send('Action: Logoff\r\n\r\n')
sock.close()
except:
logger.debug("Click2dial failed: unable to connect to "
"Asterisk")
cls.raise_user_error(error='error',
error_description='connection_failed')
logger.info("Asterisk Click2Dial from %s to %s" % \
(user.internal_number, ast_number))
class AsteriskConfigurationCompany(ModelSQL, ModelView):
'Asterisk Configuration Company'
__name__ = 'asterisk.configuration.company'
company = fields.Many2One('company.company', 'Company', readonly=True)
name = fields.Char('Asterisk server name')
ip_address = fields.Char('Asterisk IP addr. or DNS')
port = fields.Char('Port')
out_prefix = fields.Char('Out prefix')
national_prefix = fields.Char('National prefix')
international_prefix = fields.Char('International prefix')
country_prefix = fields.Char('My country prefix')
national_format_allowed = fields.Boolean('National format allowed?')
login = fields.Char('AMI login')
#TODO: Make not visible the password
password = fields.Char('AMI password')
context = fields.Char('Dialplan context')
wait_time = fields.Integer('Wait time (sec)')
extension_priority = fields.Integer('Extension priority')
alert_info = fields.Char('Alert-Info SIP header')
@classmethod
def __setup__(cls):
super(AsteriskConfigurationCompany, cls).__setup__()
cls._sql_constraints += [
('company_uniq', 'UNIQUE(company)',
'There is already one configuration for this company.'),
]

61
asterisk.xml Normal file
View File

@ -0,0 +1,61 @@
<?xml version="1.0"?>
<!-- This file is part asterisk module for Tryton.
The COPYRIGHT file at the top level of this repository contains the full copyright notices and license terms. -->
<tryton>
<data>
<record model="res.group" id="group_asterisk">
<field name="name">Asterisk Administration</field>
</record>
<record model="res.user-res.group" id="user_admin_group_asterisk">
<field name="user" ref="res.user_admin"/>
<field name="group" ref="group_asterisk"/>
</record>
<menuitem
id="menu_asterisk"
name="Asterisk"
parent="ir.menu_administration"/>
<!-- asterisk.configuration -->
<record model="ir.ui.view" id="asterisk_configuration_view_form">
<field name="model">asterisk.configuration</field>
<field name="type">form</field>
<field name="name">asterisk_configuration_form</field>
</record>
<record model="ir.action.act_window" id="act_asterisk_configuration_form">
<field name="name">Server</field>
<field name="res_model">asterisk.configuration</field>
</record>
<record model="ir.action.act_window.view" id="act_asterisk_configuration_form_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="asterisk_configuration_view_form"/>
<field name="act_window" ref="act_asterisk_configuration_form"/>
</record>
<menuitem
id="menu_asterisk_configuration"
name="Asterisk Configuration"
parent="menu_asterisk"
action="act_asterisk_configuration_form"/>
<record model="ir.model.access" id="access_asterisk_asterisk_administration">
<field name="model" search="[('model', '=', 'asterisk.configuration.company')]"/>
<field name="group" ref="group_asterisk"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
<record model="ir.model.access" id="access_asterisk_asterisk">
<field name="model" search="[('model', '=', 'asterisk.configuration.company')]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
</data>
</tryton>

14
party.py Normal file
View File

@ -0,0 +1,14 @@
#This file is part asterisk module for Tryton.
#The COPYRIGHT file at the top level of this repository contains
#the full copyright notices and license terms.
from trytond.pool import PoolMeta
__all__ = ['Party']
__metaclass__ = PoolMeta
class Party:
__name__ = 'party.party'
@property
def display_name(self):
return self.trade_name or self.name or "UNKNOWN NAME"

View File

@ -1,6 +1,10 @@
[tryton]
version=3.0.0
depends:
company
ir
party
res
xml:
asterisk.xml
user.xml

43
user.py Normal file
View File

@ -0,0 +1,43 @@
#This file is part asterisk module for Tryton.
#The COPYRIGHT file at the top level of this repository contains
#the full copyright notices and license terms.
from trytond.model import fields
from trytond.pool import Pool, PoolMeta
__all__ = ['User']
__metaclass__ = PoolMeta
class User():
__name__ = "res.user"
internal_number = fields.Char('Internal number',
help="User's internal phone number.")
callerid = fields.Char('Caller ID',
help="Caller ID used for the calls initiated by this user.")
asterisk_chan_type = fields.Selection([
('SIP', 'SIP'),
('IAX2', 'IAX2'),
('DAHDI', 'DAHDI'),
('Zap', 'Zap'),
('Skinny', 'Skinny'),
('MGCP', 'MGCP'),
('mISDN', 'mISDN'),
('H323', 'H323'),
], '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'.")
asterisk_server = fields.Function(fields.Many2One('asterisk.configuration',
'Asterisk server',
help="Asterisk server on which the user's phone is connected."),
getter='get_asterisk_server')
def get_asterisk_server(self, name=None):
ConfigurationCompany = Pool().get('asterisk.configuration.company')
if self.company:
confs = ConfigurationCompany.search([
('company', '=', self.company.id),
], limit=1)
if confs:
return confs[0].id
return None

14
user.xml Normal file
View File

@ -0,0 +1,14 @@
<?xml version="1.0"?>
<!-- This file is part asterisk module for Tryton.
The COPYRIGHT file at the top level of this repository contains the full copyright notices and license terms. -->
<tryton>
<data>
<record model="ir.ui.view" id="user_view_form">
<field name="model">res.user</field>
<field name="type">form</field>
<field name="name">user_form</field>
<field name="inherit" ref="res.user_view_form"/>
</record>
</data>
</tryton>

View File

@ -0,0 +1,35 @@
<?xml version="1.0"?>
<!-- This file is part asterisk module for Tryton.
The COPYRIGHT file at the top level of this repository contains the full copyright notices and license terms. -->
<form string="Asterisk">
<label name="name"/>
<field name="name"/>
<label name="ip_address"/>
<field name="ip_address"/>
<label name="port"/>
<field name="port"/>
<separator string="Prefix" colspan="4" id="prefix"/>
<label name="out_prefix"/>
<field name="out_prefix"/>
<label name="national_prefix"/>
<field name="national_prefix"/>
<label name="international_prefix"/>
<field name="international_prefix"/>
<label name="country_prefix"/>
<field name="country_prefix"/>
<label name="national_format_allowed"/>
<field name="national_format_allowed"/>
<separator string="AMI" colspan="4" id="ami"/>
<label name="login"/>
<field name="login"/>
<label name="password"/>
<field name="password"/>
<label name="context"/>
<field name="context"/>
<label name="wait_time"/>
<field name="wait_time"/>
<label name="extension_priority"/>
<field name="extension_priority"/>
<label name="alert_info"/>
<field name="alert_info"/>
</form>

17
view/user_form.xml Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0"?>
<!-- This file is part asterisk module for Tryton.
The COPYRIGHT file at the top level of this repository contains the full copyright notices and license terms. -->
<data>
<xpath expr="/form/notebook/page[@id='preferences']" position="after">
<page string="Asterisk" col="4" id="asterisk">
<label name="internal_number"/>
<field name="internal_number"/>
<label name="callerid"/>
<field name="callerid"/>
<label name="asterisk_chan_type"/>
<field name="asterisk_chan_type"/>
<label name="asterisk_server"/>
<field name="asterisk_server"/>
</page>
</xpath>
</data>