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.

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