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.

488 lines
20 KiB

10 years ago
10 years ago
10 years ago
  1. # -*- encoding: utf-8 -*-
  2. ##############################################################################
  3. #
  4. # Asterisk Click2dial module for OpenERP
  5. # Copyright (C) 2010-2013 Alexis de Lattre <alexis@via.ecp.fr>
  6. #
  7. # This program is free software: you can redistribute it and/or modify
  8. # it under the terms of the GNU Affero General Public License as
  9. # published by the Free Software Foundation, either version 3 of the
  10. # License, or (at your option) any later version.
  11. #
  12. # This program is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU Affero General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU Affero General Public License
  18. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  19. #
  20. ##############################################################################
  21. from openerp.osv import fields, orm
  22. from openerp.tools.translate import _
  23. import logging
  24. # Lib for phone number reformating -> pip install phonenumbers
  25. import phonenumbers
  26. try:
  27. # Lib py-asterisk from http://code.google.com/p/py-asterisk/
  28. # -> pip install py-Asterisk
  29. from Asterisk import Manager
  30. except ImportError:
  31. Manager = None
  32. _logger = logging.getLogger(__name__)
  33. class asterisk_server(orm.Model):
  34. '''Asterisk server object, stores the parameters of the Asterisk IPBXs'''
  35. _name = "asterisk.server"
  36. _description = "Asterisk Servers"
  37. _columns = {
  38. 'name': fields.char('Asterisk Server Name', size=50, required=True),
  39. 'active': fields.boolean(
  40. 'Active', help="The active field allows you to hide the Asterisk "
  41. "server without deleting it."),
  42. 'ip_address': fields.char(
  43. 'Asterisk IP address or DNS', size=50, required=True,
  44. help="IP address or DNS name of the Asterisk server."),
  45. 'port': fields.integer(
  46. 'Port', required=True,
  47. help="TCP port on which the Asterisk Manager Interface listens. "
  48. "Defined in /etc/asterisk/manager.conf on Asterisk."),
  49. 'out_prefix': fields.char(
  50. 'Out Prefix', size=4, help="Prefix to dial to make outgoing "
  51. "calls. If you don't use a prefix to make outgoing calls, "
  52. "leave empty."),
  53. 'login': fields.char(
  54. 'AMI Login', size=30, required=True,
  55. help="Login that OpenERP will use to communicate with the "
  56. "Asterisk Manager Interface. Refer to /etc/asterisk/manager.conf "
  57. "on your Asterisk server."),
  58. 'password': fields.char(
  59. 'AMI Password', size=30, required=True,
  60. help="Password that OpenERP will use to communicate with the "
  61. "Asterisk Manager Interface. Refer to /etc/asterisk/manager.conf "
  62. "on your Asterisk server."),
  63. 'context': fields.char(
  64. 'Dialplan Context', size=50, required=True,
  65. help="Asterisk dialplan context from which the calls will be "
  66. "made. Refer to /etc/asterisk/extensions.conf on your Asterisk "
  67. "server."),
  68. 'wait_time': fields.integer(
  69. 'Wait Time (sec)', required=True,
  70. help="Amount of time (in seconds) Asterisk will try to reach "
  71. "the user's phone before hanging up."),
  72. 'extension_priority': fields.integer(
  73. 'Extension Priority', required=True,
  74. help="Priority of the extension in the Asterisk dialplan. Refer "
  75. "to /etc/asterisk/extensions.conf on your Asterisk server."),
  76. 'alert_info': fields.char(
  77. 'Alert-Info SIP Header', size=255,
  78. help="Set Alert-Info header in SIP request to user's IP Phone "
  79. "for the click2dial feature. If empty, the Alert-Info header "
  80. "will not be added. You can use it to have a special ring tone "
  81. "for click2dial (a silent one !) or to activate auto-answer "
  82. "for example."),
  83. 'company_id': fields.many2one(
  84. 'res.company', 'Company',
  85. help="Company who uses the Asterisk server."),
  86. }
  87. _defaults = {
  88. 'active': True,
  89. 'port': 5038, # Default AMI port
  90. 'extension_priority': 1,
  91. 'wait_time': 15,
  92. 'company_id': lambda self, cr, uid, context:
  93. self.pool['res.company']._company_default_get(
  94. cr, uid, 'asterisk.server', context=context),
  95. }
  96. def _check_validity(self, cr, uid, ids):
  97. for server in self.browse(cr, uid, ids):
  98. out_prefix = ('Out prefix', server.out_prefix)
  99. dialplan_context = ('Dialplan context', server.context)
  100. alert_info = ('Alert-Info SIP header', server.alert_info)
  101. login = ('AMI login', server.login)
  102. password = ('AMI password', server.password)
  103. if out_prefix[1] and not out_prefix[1].isdigit():
  104. raise orm.except_orm(
  105. _('Error:'),
  106. _("Only use digits for the '%s' on the Asterisk server "
  107. "'%s'" % (out_prefix[0], server.name)))
  108. if server.wait_time < 1 or server.wait_time > 120:
  109. raise orm.except_orm(
  110. _('Error:'),
  111. _("You should set a 'Wait time' value between 1 and 120 "
  112. "seconds for the Asterisk server '%s'" % server.name))
  113. if server.extension_priority < 1:
  114. raise orm.except_orm(
  115. _('Error:'),
  116. _("The 'extension priority' must be a positive value for "
  117. "the Asterisk server '%s'" % server.name))
  118. if server.port > 65535 or server.port < 1:
  119. raise orm.except_orm(
  120. _('Error:'),
  121. _("You should set a TCP port between 1 and 65535 for the "
  122. "Asterisk server '%s'" % server.name))
  123. for check_str in [dialplan_context, alert_info, login, password]:
  124. if check_str[1]:
  125. try:
  126. check_str[1].encode('ascii')
  127. except UnicodeEncodeError:
  128. raise orm.except_orm(
  129. _('Error:'),
  130. _("The '%s' should only have ASCII caracters for "
  131. "the Asterisk server '%s'"
  132. % (check_str[0], server.name)))
  133. return True
  134. _constraints = [(
  135. _check_validity,
  136. "Error message in raise",
  137. [
  138. 'out_prefix', 'wait_time', 'extension_priority', 'port',
  139. 'context', 'alert_info', 'login', 'password']
  140. )]
  141. def _reformat_number(
  142. self, cr, uid, erp_number, ast_server=None, context=None):
  143. '''
  144. This function is dedicated to the transformation of the number
  145. available in OpenERP to the number that Asterisk should dial.
  146. You may have to inherit this function in another module specific
  147. for your company if you are not happy with the way I reformat
  148. the OpenERP numbers.
  149. '''
  150. assert(erp_number), 'Missing phone number'
  151. _logger.debug('Number before reformat = %s' % erp_number)
  152. if not ast_server:
  153. ast_server = self._get_asterisk_server_from_user(
  154. cr, uid, context=context)
  155. # erp_number are supposed to be in E.164 format, so no need to
  156. # give a country code here
  157. parsed_num = phonenumbers.parse(erp_number, None)
  158. country_code = ast_server.company_id.country_id.code
  159. assert(country_code), 'Missing country on company'
  160. _logger.debug('Country code = %s' % country_code)
  161. to_dial_number = phonenumbers.format_out_of_country_calling_number(
  162. parsed_num, country_code.upper()).replace(' ', '').replace('-', '')
  163. # Add 'out prefix' to all numbers
  164. if ast_server.out_prefix:
  165. _logger.debug('Out prefix = %s' % ast_server.out_prefix)
  166. to_dial_number = '%s%s' % (ast_server.out_prefix, to_dial_number)
  167. _logger.debug('Number to be sent to Asterisk = %s' % to_dial_number)
  168. return to_dial_number
  169. def _get_asterisk_server_from_user(self, cr, uid, context=None):
  170. '''Returns an asterisk.server browse object'''
  171. # We check if the user has an Asterisk server configured
  172. user = self.pool['res.users'].browse(cr, uid, uid, context=context)
  173. if user.asterisk_server_id.id:
  174. ast_server = user.asterisk_server_id
  175. else:
  176. asterisk_server_ids = self.search(
  177. cr, uid, [('company_id', '=', user.company_id.id)],
  178. context=context)
  179. # If the user doesn't have an asterisk server,
  180. # we take the first one of the user's company
  181. if not asterisk_server_ids:
  182. raise orm.except_orm(
  183. _('Error:'),
  184. _("No Asterisk server configured for the company '%s'.")
  185. % user.company_id.name)
  186. else:
  187. ast_server = self.browse(
  188. cr, uid, asterisk_server_ids[0], context=context)
  189. return ast_server
  190. def _connect_to_asterisk(self, cr, uid, context=None):
  191. '''
  192. Open the connection to the Asterisk Manager
  193. Returns an instance of the Asterisk Manager
  194. '''
  195. user = self.pool['res.users'].browse(cr, uid, uid, context=context)
  196. ast_server = self._get_asterisk_server_from_user(
  197. cr, uid, context=context)
  198. # We check if the current user has a chan type
  199. if not user.asterisk_chan_type:
  200. raise orm.except_orm(
  201. _('Error:'),
  202. _('No channel type configured for the current user.'))
  203. # We check if the current user has an internal number
  204. if not user.resource:
  205. raise orm.except_orm(
  206. _('Error:'),
  207. _('No resource name configured for the current user'))
  208. _logger.debug(
  209. "User's phone: %s/%s" % (user.asterisk_chan_type, user.resource))
  210. _logger.debug(
  211. "Asterisk server: %s:%d"
  212. % (ast_server.ip_address, ast_server.port))
  213. # Connect to the Asterisk Manager Interface
  214. try:
  215. ast_manager = Manager.Manager(
  216. (ast_server.ip_address, ast_server.port),
  217. ast_server.login, ast_server.password)
  218. except Exception, e:
  219. _logger.error(
  220. "Error in the request to the Asterisk Manager Interface %s"
  221. % ast_server.ip_address)
  222. _logger.error("Here is the error message: %s" % e)
  223. raise orm.except_orm(
  224. _('Error:'),
  225. _("Problem in the request from OpenERP to Asterisk. "
  226. "Here is the error message: %s" % e))
  227. return (user, ast_server, ast_manager)
  228. def test_ami_connection(self, cr, uid, ids, context=None):
  229. assert len(ids) == 1, 'Only 1 ID'
  230. ast_server = self.browse(cr, uid, ids[0], context=context)
  231. ast_manager = False
  232. try:
  233. ast_manager = Manager.Manager(
  234. (ast_server.ip_address, ast_server.port),
  235. ast_server.login,
  236. ast_server.password)
  237. except Exception, e:
  238. raise orm.except_orm(
  239. _("Connection Test Failed!"),
  240. _("Here is the error message: %s" % e))
  241. finally:
  242. if ast_manager:
  243. ast_manager.Logoff()
  244. raise orm.except_orm(
  245. _("Connection Test Successfull!"),
  246. _("Odoo can successfully login to the Asterisk Manager "
  247. "Interface."))
  248. def _get_calling_number(self, cr, uid, context=None):
  249. user, ast_server, ast_manager = self._connect_to_asterisk(
  250. cr, uid, context=context)
  251. calling_party_number = False
  252. try:
  253. list_chan = ast_manager.Status()
  254. # from pprint import pprint
  255. # pprint(list_chan)
  256. _logger.debug("Result of Status AMI request: %s", list_chan)
  257. for chan in list_chan.values():
  258. sip_account = user.asterisk_chan_type + '/' + user.resource
  259. # 4 = Ring
  260. if (
  261. chan.get('ChannelState') == '4' and
  262. chan.get('ConnectedLineNum') == user.internal_number):
  263. _logger.debug("Found a matching Event in 'Ring' state")
  264. calling_party_number = chan.get('CallerIDNum')
  265. break
  266. # 6 = Up
  267. if (
  268. chan.get('ChannelState') == '6'
  269. and sip_account in chan.get('BridgedChannel', '')):
  270. _logger.debug("Found a matching Event in 'Up' state")
  271. calling_party_number = chan.get('CallerIDNum')
  272. break
  273. # Compatibility with Asterisk 1.4
  274. if (
  275. chan.get('State') == 'Up'
  276. and sip_account in chan.get('Link', '')):
  277. _logger.debug("Found a matching Event in 'Up' state")
  278. calling_party_number = chan.get('CallerIDNum')
  279. break
  280. except Exception, e:
  281. _logger.error(
  282. "Error in the Status request to Asterisk server %s"
  283. % ast_server.ip_address)
  284. _logger.error(
  285. "Here are the details of the error: '%s'" % unicode(e))
  286. raise orm.except_orm(
  287. _('Error:'),
  288. _("Can't get calling number from Asterisk.\nHere is the "
  289. "error: '%s'" % unicode(e)))
  290. finally:
  291. ast_manager.Logoff()
  292. _logger.debug("Calling party number: '%s'" % calling_party_number)
  293. return calling_party_number
  294. def get_record_from_my_channel(self, cr, uid, context=None):
  295. calling_number = self.pool['asterisk.server']._get_calling_number(
  296. cr, uid, context=context)
  297. # calling_number = "0641981246"
  298. if calling_number:
  299. record = self.pool['phone.common'].get_record_from_phone_number(
  300. cr, uid, calling_number, context=context)
  301. if record:
  302. return record
  303. else:
  304. return calling_number
  305. else:
  306. return False
  307. class res_users(orm.Model):
  308. _inherit = "res.users"
  309. _columns = {
  310. 'internal_number': fields.char(
  311. 'Internal Number', size=15,
  312. help="User's internal phone number."),
  313. 'dial_suffix': fields.char(
  314. 'User-specific Dial Suffix', size=15,
  315. help="User-specific dial suffix such as aa=2wb for SCCP "
  316. "auto answer."),
  317. 'callerid': fields.char(
  318. 'Caller ID', size=50,
  319. help="Caller ID used for the calls initiated by this user."),
  320. # You'd probably think: Asterisk should reuse the callerID of sip.conf!
  321. # But it cannot, cf
  322. # http://lists.digium.com/pipermail/asterisk-users/
  323. # 2012-January/269787.html
  324. 'cdraccount': fields.char(
  325. 'CDR Account', size=50,
  326. help="Call Detail Record (CDR) account used for billing this "
  327. "user."),
  328. 'asterisk_chan_type': fields.selection([
  329. ('SIP', 'SIP'),
  330. ('IAX2', 'IAX2'),
  331. ('DAHDI', 'DAHDI'),
  332. ('Zap', 'Zap'),
  333. ('Skinny', 'Skinny'),
  334. ('MGCP', 'MGCP'),
  335. ('mISDN', 'mISDN'),
  336. ('H323', 'H323'),
  337. ('SCCP', 'SCCP'),
  338. ('Local', 'Local'),
  339. ], 'Asterisk Channel Type',
  340. help="Asterisk channel type, as used in the Asterisk dialplan. "
  341. "If the user has a regular IP phone, the channel type is 'SIP'."),
  342. 'resource': fields.char(
  343. 'Resource Name', size=64,
  344. help="Resource name for the channel type selected. For example, "
  345. "if you use 'Dial(SIP/phone1)' in your Asterisk dialplan to ring "
  346. "the SIP phone of this user, then the resource name for this user "
  347. "is 'phone1'. For a SIP phone, the phone number is often used as "
  348. "resource name, but not always."),
  349. 'alert_info': fields.char(
  350. 'User-specific Alert-Info SIP Header', size=255,
  351. help="Set a user-specific Alert-Info header in SIP request to "
  352. "user's IP Phone for the click2dial feature. If empty, the "
  353. "Alert-Info header will not be added. You can use it to have a "
  354. "special ring tone for click2dial (a silent one !) or to "
  355. "activate auto-answer for example."),
  356. 'variable': fields.char(
  357. 'User-specific Variable', size=255,
  358. help="Set a user-specific 'Variable' field in the Asterisk "
  359. "Manager Interface 'originate' request for the click2dial "
  360. "feature. If you want to have several variable headers, separate "
  361. "them with '|'."),
  362. 'asterisk_server_id': fields.many2one(
  363. 'asterisk.server', 'Asterisk Server',
  364. help="Asterisk server on which the user's phone is connected. "
  365. "If you leave this field empty, it will use the first Asterisk "
  366. "server of the user's company."),
  367. }
  368. _defaults = {
  369. 'asterisk_chan_type': 'SIP',
  370. }
  371. def _check_validity(self, cr, uid, ids):
  372. for user in self.browse(cr, uid, ids):
  373. strings_to_check = [
  374. (_('Resource Name'), user.resource),
  375. (_('Internal Number'), user.internal_number),
  376. (_('Caller ID'), user.callerid),
  377. ]
  378. for check_string in strings_to_check:
  379. if check_string[1]:
  380. try:
  381. check_string[1].encode('ascii')
  382. except UnicodeEncodeError:
  383. raise orm.except_orm(
  384. _('Error:'),
  385. _("The '%s' for the user '%s' should only have "
  386. "ASCII caracters")
  387. % (check_string[0], user.name))
  388. return True
  389. _constraints = [(
  390. _check_validity,
  391. "Error message in raise",
  392. ['resource', 'internal_number', 'callerid']
  393. )]
  394. class phone_common(orm.AbstractModel):
  395. _inherit = 'phone.common'
  396. def click2dial(self, cr, uid, erp_number, context=None):
  397. if not erp_number:
  398. orm.except_orm(
  399. _('Error:'),
  400. _('Missing phone number'))
  401. user, ast_server, ast_manager = \
  402. self.pool['asterisk.server']._connect_to_asterisk(
  403. cr, uid, context=context)
  404. ast_number = self.pool['asterisk.server']._reformat_number(
  405. cr, uid, erp_number, ast_server, context=context)
  406. # The user should have a CallerID
  407. if not user.callerid:
  408. raise orm.except_orm(
  409. _('Error:'),
  410. _('No callerID configured for the current user'))
  411. variable = []
  412. if user.asterisk_chan_type == 'SIP':
  413. # We can only have one alert-info header in a SIP request
  414. if user.alert_info:
  415. variable.append(
  416. 'SIPAddHeader=Alert-Info: %s' % user.alert_info)
  417. elif ast_server.alert_info:
  418. variable.append(
  419. 'SIPAddHeader=Alert-Info: %s' % ast_server.alert_info)
  420. if user.variable:
  421. for user_variable in user.variable.split('|'):
  422. variable.append(user_variable.strip())
  423. channel = '%s/%s' % (user.asterisk_chan_type, user.resource)
  424. if user.dial_suffix:
  425. channel += '/%s' % user.dial_suffix
  426. try:
  427. ast_manager.Originate(
  428. channel,
  429. context=ast_server.context,
  430. extension=ast_number,
  431. priority=str(ast_server.extension_priority),
  432. timeout=str(ast_server.wait_time * 1000),
  433. caller_id=user.callerid,
  434. account=user.cdraccount,
  435. variable=variable)
  436. except Exception, e:
  437. _logger.error(
  438. "Error in the Originate request to Asterisk server %s"
  439. % ast_server.ip_address)
  440. _logger.error(
  441. "Here are the details of the error: '%s'" % unicode(e))
  442. raise orm.except_orm(
  443. _('Error:'),
  444. _("Click to dial with Asterisk failed.\nHere is the error: "
  445. "'%s'")
  446. % unicode(e))
  447. finally:
  448. ast_manager.Logoff()
  449. return {'dialed_number': ast_number}