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.

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