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.

472 lines
20 KiB

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