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.

276 lines
16 KiB

  1. # -*- encoding: utf-8 -*-
  2. ##############################################################################
  3. #
  4. # Asterisk Click2dial module for OpenERP
  5. # Copyright (C) 2010 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. # TODO list :
  22. # Wait time don't seem to be taken into account
  23. from osv import osv, fields
  24. # Lib required to open a socket towards Asterisk
  25. import socket
  26. # Lib required to print logs
  27. import netsvc
  28. class asterisk_server(osv.osv):
  29. _name = "asterisk.server"
  30. _description = "Asterisk Servers"
  31. _columns = {
  32. 'name' : fields.char('Asterisk server name', size=50, required=True, help="Asterisk server name."),
  33. 'ip_address' : fields.char('Asterisk IP addr. or DNS', size=50, required=True, help="IPv4 address or DNS name of the Asterisk server."),
  34. 'port' : fields.integer('Port', required=True, help="TCP port on which the Asterisk Manager Interface listens. Defined in /etc/asterisk/manager.conf on Asterisk."),
  35. 'out_prefix' : fields.char('Out prefix', size=4, help="Prefix to dial to place outgoing calls. If you don't use a prefix to place outgoing calls, leave empty."),
  36. 'national_prefix' : fields.char('National prefix', size=4, help="Prefix for national phone calls (don't include the 'out prefix'). For e.g., in France, the phone numbers look like '01 41 98 12 42' : the National prefix is '0'."),
  37. 'international_prefix' : fields.char('International prefix', size=4, help="Prefix to add to make international phone calls (don't include the 'out prefix'). For e.g., in France, the International prefix is '00'."),
  38. 'country_prefix' : fields.char('My country prefix', size=4, help="Phone prefix of the country where the Asterisk server is located. For e.g. the phone prefix for France is '33'. If the phone number to dial starts with the 'My country prefix', OpenERP will remove the country prefix from the phone number and add the 'out prefix' followed by the 'national prefix'. If the phone number to dial doesn't start with the 'My country prefix', OpenERP will add the 'out prefix' followed by the 'international prefix'."),
  39. 'login' : fields.char('AMI login', size=30, required=True, help="Login that OpenERP will use to communicate with the Asterisk Manager Interface. Refer to /etc/asterisk/manager.conf on your Asterisk server."),
  40. 'password' : fields.char('AMI password', size=30, required=True, help="Password that Asterisk will use to communicate with the Asterisk Manager Interface. Refer to /etc/asterisk/manager.conf on your Asterisk server."),
  41. 'context' : fields.char('Dialplan context', size=50, required=True, help="Asterisk dialplan context from which the calls will be made. Refer to /etc/asterisk/extensions.conf on your Asterisk server."),
  42. 'wait_time' : fields.integer('Wait time (sec)', required=True, help="Amount of time (in seconds) Asterisk will try to reach the user's phone before hanging up."),
  43. 'extension_priority' : fields.integer('Extension priority', required=True, help="Priority of the extension in the Asterisk dialplan. Refer to /etc/asterisk/extensions.conf on your Asterisk server."),
  44. 'alert-info' : fields.char('Alert-Info SIP header', size=40, help="Set Alert-Info header in SIP request to user's IP Phone. If empty, the Alert-Info header will not be added. You can use it to have a special ring tone for click2dial, for example you could choose a silent ring tone."),
  45. 'company_id' : fields.many2one('res.company', 'Company', help="Company who uses the Asterisk server."),
  46. }
  47. _defaults = {
  48. 'port': lambda *a: 5038, # Default AMI port
  49. 'out_prefix': lambda *a: '0',
  50. 'national_prefix': lambda *a: '0',
  51. 'international_prefix': lambda *a: '00',
  52. 'extension_priority': lambda *a: 1,
  53. 'wait_time': lambda *a: 15,
  54. }
  55. def _only_digits(self, cr, uid, ids, prefix, can_be_empty):
  56. for i in ids:
  57. prefix_to_check = self.read(cr, uid, i, [prefix])[prefix]
  58. if prefix_to_check == False:
  59. if can_be_empty:
  60. return True
  61. else:
  62. return False
  63. else:
  64. if not prefix_to_check.isdigit():
  65. return False
  66. return True
  67. def _only_digits_out_prefix(self, cr, uid, ids):
  68. return self._only_digits(cr, uid, ids, 'out_prefix', True)
  69. def _only_digits_country_prefix(self, cr, uid, ids):
  70. return self._only_digits(cr, uid, ids, 'country_prefix', False)
  71. def _only_digits_national_prefix(self, cr, uid, ids):
  72. return self._only_digits(cr, uid, ids, 'national_prefix', True)
  73. def _only_digits_international_prefix(self, cr, uid, ids):
  74. return self._only_digits(cr, uid, ids, 'international_prefix', False)
  75. def _check_wait_time(self, cr, uid, ids):
  76. for i in ids:
  77. wait_time_to_check = self.read(cr, uid, i, ['wait_time'])['wait_time']
  78. if wait_time_to_check < 1 or wait_time_to_check > 120:
  79. return False
  80. return True
  81. def _check_extension_priority(self, cr, uid, ids):
  82. for i in ids:
  83. extension_priority_to_check = self.read(cr, uid, i, ['extension_priority'])['extension_priority']
  84. if extension_priority_to_check < 1:
  85. return False
  86. return True
  87. def _check_port(self, cr, uid, ids):
  88. for i in ids:
  89. port_to_check = self.read(cr, uid, i, ['port'])['port']
  90. if port_to_check > 65535 or port_to_check < 1:
  91. return False
  92. return True
  93. # TODO : is it possible to read the field name inside the constraint
  94. # function in order to avoid using as many functions as fields to
  95. # check prefix ?
  96. _constraints = [
  97. (_only_digits_out_prefix, "Only use digits for the 'out prefix' or leave empty", ['out_prefix']),
  98. (_only_digits_country_prefix, "Only use digits for the 'country prefix'", ['country_prefix']),
  99. (_only_digits_national_prefix, "Only use digits for the 'national prefix' or leave empty", ['national_prefix']),
  100. (_only_digits_international_prefix, "Only use digits for 'international prefix'", ['international_prefix']),
  101. (_check_wait_time, "You should enter a 'Wait time' value between 1 and 120 seconds", ['wait_time']),
  102. (_check_extension_priority, "The 'extension priority' must be a positive value", ['extension_priority']),
  103. (_check_port, 'TCP ports range from 1 to 65535', ['port']),
  104. ]
  105. # This function is dedicated to the transformation of the number
  106. # available in OpenERP to the number that Asterisk should dial.
  107. # You may have to inherit this function in another module specific
  108. # for your company if you are not happy with the way I reformat
  109. # the OpenERP numbers.
  110. def reformat_number(self, cr, uid, ids, erp_number, ast_server, context):
  111. logger = netsvc.Logger()
  112. invalid_format_msg = "The phone number is not written in valid international format. Example of valid international format : +33 1 41 98 12 42"
  113. # Let's call the variable tmp_number now
  114. tmp_number = erp_number
  115. logger.notifyChannel('asterisk_click2dial', netsvc.LOG_DEBUG, 'Number before reformat = ' + tmp_number)
  116. # First, we remove all stupid caracters and spaces
  117. try:
  118. for i in [' ', '.', '(', ')', '[', ']', '-', '/']:
  119. tmp_number = tmp_number.replace(i, '')
  120. except:
  121. raise osv.except_osv('Invalid phone number', invalid_format_msg)
  122. # Remove the starting '+' of the number
  123. if tmp_number[0]=='+':
  124. tmp_number = tmp_number.replace('+','')
  125. logger.notifyChannel('asterisk_click2dial', netsvc.LOG_DEBUG, 'Number after removal of special char = ' + tmp_number)
  126. else:
  127. raise osv.except_osv('Invalid phone number', invalid_format_msg)
  128. # At this stage, 'tmp_number' should only contain digits
  129. if not tmp_number.isdigit():
  130. raise osv.except_osv('Invalid phone number', invalid_format_msg)
  131. # Before starting to use prefix, we convert empty prefix whose value
  132. # is False to an empty string
  133. country_prefix = (ast_server.country_prefix or '')
  134. national_prefix = (ast_server.national_prefix or '')
  135. international_prefix = (ast_server.international_prefix or '')
  136. out_prefix = (ast_server.out_prefix or '')
  137. logger.notifyChannel('asterisk_click2dial', netsvc.LOG_DEBUG, 'Country prefix = ' + country_prefix)
  138. if country_prefix == tmp_number[0:len(country_prefix)]:
  139. # If the number is a national number,
  140. # remove 'my country prefix' and add 'national prefix'
  141. tmp_number = (national_prefix) + tmp_number[len(country_prefix):len(tmp_number)]
  142. logger.notifyChannel('asterisk_click2dial', netsvc.LOG_DEBUG, 'National prefix = ' + national_prefix + ' - Number with national prefix = ' + tmp_number)
  143. else:
  144. # If the number is an international number,
  145. # add 'international prefix'
  146. tmp_number = international_prefix + tmp_number
  147. logger.notifyChannel('asterisk_click2dial', netsvc.LOG_DEBUG, 'International prefix = ' + international_prefix + ' - Number with international prefix = ' + tmp_number)
  148. # Add 'out prefix' to all numbers - Caution : out prefix can be False
  149. tmp_number = out_prefix + tmp_number
  150. logger.notifyChannel('asterisk_click2dial', netsvc.LOG_DEBUG, 'Out prefix = ' + out_prefix)
  151. logger.notifyChannel('asterisk_click2dial', netsvc.LOG_DEBUG, 'Number that will be sent to Asterisk = ' + tmp_number)
  152. return tmp_number
  153. # Open the socket to the Asterisk Manager Interface
  154. # and send instructions to Dial to Asterisk
  155. def dial(self, cr, uid, ids, erp_number, context):
  156. logger = netsvc.Logger()
  157. user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
  158. # Check if the number to dial is not empty
  159. if not erp_number:
  160. raise osv.except_osv('Error', 'There is no phone number !')
  161. # We check if the user has an Asterisk server configured
  162. if not user.asterisk_server_id.id:
  163. raise osv.except_osv('No Asterisk server configured for the current user', "You must associate an Asterisk server to the current user.")
  164. else:
  165. ast_server = user.asterisk_server_id
  166. # We check if the current user has a chan type
  167. if not user.asterisk_chan_type:
  168. raise osv.except_osv('No channel type configured for the current user', "You must configure an Asterisk channel type for the current user.")
  169. # We check if the current user has an internal number
  170. if not user.internal_number:
  171. raise osv.except_osv('No internal phone number configured for the current user', "You must configure an internal phone number for the current user.")
  172. # Convert the phone number in the format that will be sent to Asterisk
  173. ast_number = self.reformat_number(cr, uid, ids, erp_number, ast_server, context=context)
  174. logger.notifyChannel('asterisk_click2dial', netsvc.LOG_DEBUG, 'User dialing : channel = ' + user.asterisk_chan_type + '/' + user.internal_number + ' - Callerid = ' + user.callerid)
  175. logger.notifyChannel('asterisk_click2dial', netsvc.LOG_DEBUG, 'Asterisk server = ' + ast_server.ip_address + ':' + str(ast_server.port))
  176. # Connect to the Asterisk Manager Interface
  177. try:
  178. ast_ip = socket.gethostbyname(str(ast_server.ip_address))
  179. except:
  180. raise osv.except_osv('Wrong DNS', "Can't resolve the DNS name of the Asterisk server.")
  181. try:
  182. sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  183. sock.connect((ast_ip, ast_server.port))
  184. sock.send('Action: login\r\n')
  185. sock.send('Events: off\r\n')
  186. sock.send('Username: '+str(ast_server.login)+'\r\n')
  187. sock.send('Secret: '+str(ast_server.password)+'\r\n\r\n')
  188. sock.send('Action: originate\r\n')
  189. sock.send('Channel: ' + str(user.asterisk_chan_type) + '/' + str(user.internal_number)+'\r\n')
  190. sock.send('WaitTime: '+str(ast_server.wait_time)+'\r\n')
  191. sock.send('CallerId: '+str(user.callerid)+'\r\n')
  192. sock.send('Exten: '+str(ast_number)+'\r\n')
  193. sock.send('Context: '+str(ast_server.context)+'\r\n')
  194. if not ast_server.alert-info and user.asterisk_chan_type == 'SIP':
  195. sock.send('Variable: SIPAddHeader=Alert-Info: '+str(ast_server.alert-info)+'\r\n')
  196. sock.send('Priority: '+str(ast_server.extension_priority)+'\r\n\r\n')
  197. sock.send('Action: Logoff\r\n\r\n')
  198. sock.close()
  199. except:
  200. logger.notifyChannel('asterisk_click2dial', netsvc.LOG_WARNING, "Click2dial failed : unable to connect to Asterisk")
  201. raise osv.except_osv('Asterisk server unreachable', "The connection from OpenERP to the Asterisk server failed. Please check the configuration on OpenERP and on Asterisk.")
  202. logger.notifyChannel('asterisk_click2dial', netsvc.LOG_INFO, "Asterisk Click2Dial from " + user.internal_number + ' to ' + ast_number)
  203. asterisk_server()
  204. class res_users(osv.osv):
  205. _name = "res.users"
  206. _inherit = "res.users"
  207. _columns = {
  208. 'internal_number' : fields.char('Internal number', size=15, help="User's internal phone number."),
  209. 'callerid' : fields.char('Caller ID', size=50, help="Caller ID used for the calls initiated by this user."),
  210. 'asterisk_chan_type' : fields.selection([('SIP', 'SIP'), ('IAX2', 'IAX2'), ('DAHDI', 'DAHDI'), ('Zap', 'Zap'), ('Skinny', 'Skinny'), ('MGCP', 'MGCP'), ('mISDN', 'mISDN'), ('H323', 'H323')], 'Asterisk channel type', help="Asterisk channel type, as used in the Asterisk dialplan. If the user has a regular IP phone, the channel type is 'SIP'."),
  211. 'asterisk_server_id' : fields.many2one('asterisk.server', 'Asterisk server', help="Asterisk server on which the user's phone is connected."),
  212. }
  213. _defaults = {
  214. 'asterisk_chan_type': lambda *a: 'SIP',
  215. }
  216. res_users()
  217. class res_partner_address(osv.osv):
  218. _name = "res.partner.address"
  219. _inherit ="res.partner.address"
  220. # Functions called by the button "Dial" in the partner address view
  221. def action_dial_phone(self, cr, uid, ids, context):
  222. erp_number = self.read(cr, uid, ids, ['phone'], context=context)[0]['phone']
  223. self.pool.get('asterisk.server').dial(cr, uid, ids, erp_number, context)
  224. def action_dial_mobile(self, cr, uid, ids, context):
  225. erp_number = self.read(cr, uid, ids, ['mobile'], context=context)[0]['mobile']
  226. self.pool.get('asterisk.server').dial(cr, uid, ids, erp_number, context)
  227. res_partner_address()
  228. class res_company(osv.osv):
  229. _name = "res.company"
  230. _inherit = "res.company"
  231. _columns = {
  232. 'asterisk_server_ids' : fields.one2many('asterisk.server', 'company_id', 'Asterisk servers', help="List of Asterisk servers.")
  233. }
  234. res_company()