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.

406 lines
17 KiB

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