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.

680 lines
37 KiB

13 years ago
  1. # -*- encoding: utf-8 -*-
  2. ##############################################################################
  3. #
  4. # Asterisk Click2dial module for OpenERP
  5. # Copyright (C) 2010-2012 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 osv import osv, fields
  22. # Lib required to open a socket (needed to communicate with Asterisk server)
  23. import socket
  24. # Lib required to print logs
  25. import logging
  26. # Lib to translate error messages
  27. from tools.translate import _
  28. # Lib for regexp
  29. import re
  30. _logger = logging.getLogger(__name__)
  31. class asterisk_server(osv.osv):
  32. '''Asterisk server object, to store all 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, help="Asterisk server name."),
  37. 'active': fields.boolean('Active', help="The active field allows you to hide the Asterisk server without deleting it."),
  38. 'ip_address': fields.char('Asterisk IP addr. or DNS', size=50, required=True, help="IP address or DNS name of the Asterisk server."),
  39. 'port': fields.integer('Port', required=True, help="TCP port on which the Asterisk Manager Interface listens. Defined in /etc/asterisk/manager.conf on Asterisk."),
  40. '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."),
  41. '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'."),
  42. 'international_prefix': fields.char('International prefix', required=True, 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'."),
  43. 'country_prefix': fields.char('My country prefix', required=True, 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'."),
  44. 'national_format_allowed': fields.boolean('National format allowed ?', help="Do we allow to use click2dial on phone numbers written in national format, e.g. 01 41 98 12 42, or only in the international format, e.g. +33 1 41 98 12 42 ?"),
  45. '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."),
  46. '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."),
  47. '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."),
  48. '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."),
  49. '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."),
  50. 'alert_info': fields.char('Alert-Info SIP header', size=255, help="Set Alert-Info header in SIP request to user's IP Phone for the click2dial feature. If empty, the Alert-Info header will not be added. You can use it to have a special ring tone for click2dial (a silent one !) or to activate auto-answer for example. If you want to have several variable headers, separate them with '|'."),
  51. 'company_id': fields.many2one('res.company', 'Company', help="Company who uses the Asterisk server."),
  52. }
  53. _defaults = {
  54. 'active': True,
  55. 'port': 5038, # Default AMI port
  56. 'out_prefix': '0',
  57. 'national_prefix': '0',
  58. 'international_prefix': '00',
  59. 'extension_priority': 1,
  60. 'wait_time': 15,
  61. }
  62. def _check_validity(self, cr, uid, ids):
  63. for server in self.browse(cr, uid, ids):
  64. country_prefix = ('Country prefix', server.country_prefix)
  65. international_prefix = ('International prefix', server.international_prefix)
  66. out_prefix = ('Out prefix', server.out_prefix)
  67. national_prefix = ('National prefix', server.national_prefix)
  68. dialplan_context = ('Dialplan context', server.context)
  69. alert_info = ('Alert-Info SIP header', server.alert_info)
  70. login = ('AMI login', server.login)
  71. password = ('AMI password', server.password)
  72. for digit_prefix in [country_prefix, international_prefix, out_prefix, national_prefix]:
  73. if digit_prefix[1] and not digit_prefix[1].isdigit():
  74. raise osv.except_osv(_('Error :'), _("Only use digits for the '%s' on the Asterisk server '%s'" % (digit_prefix[0], server.name)))
  75. if server.wait_time < 1 or server.wait_time > 120:
  76. raise osv.except_osv(_('Error :'), _("You should set a 'Wait time' value between 1 and 120 seconds for the Asterisk server '%s'" % server.name))
  77. if server.extension_priority < 1:
  78. raise osv.except_osv(_('Error :'), _("The 'extension priority' must be a positive value for the Asterisk server '%s'" % server.name))
  79. if server.port > 65535 or server.port < 1:
  80. raise osv.except_osv(_('Error :'), _("You should set a TCP port between 1 and 65535 for the Asterik server '%s'" % server.name))
  81. for check_string in [dialplan_context, alert_info, login, password]:
  82. if check_string[1]:
  83. try:
  84. string = check_string[1].encode('ascii')
  85. except UnicodeEncodeError:
  86. raise osv.except_osv(_('Error :'), _("The '%s' should only have ASCII caracters for the Asterisk server '%s'" % (check_string[0], server.name)))
  87. return True
  88. _constraints = [
  89. (_check_validity, "Error message in raise", ['out_prefix', 'country_prefix', 'national_prefix', 'international_prefix', 'wait_time', 'extension_priority', 'port', 'context', 'alert_info', 'login', 'password']),
  90. ]
  91. def _reformat_number(self, cr, uid, erp_number, ast_server, context=None):
  92. '''
  93. This function is dedicated to the transformation of the number
  94. available in OpenERP to the number that Asterisk should dial.
  95. You may have to inherit this function in another module specific
  96. for your company if you are not happy with the way I reformat
  97. the OpenERP numbers.
  98. '''
  99. error_title_msg = _("Invalid phone number")
  100. invalid_international_format_msg = _("The phone number is not written in valid international format. Example of valid international format : +33 1 41 98 12 42")
  101. invalid_national_format_msg = _("The phone number is not written in valid national format.")
  102. invalid_format_msg = _("The phone number is not written in valid format.")
  103. # Let's call the variable tmp_number now
  104. tmp_number = erp_number
  105. _logger.debug('Number before reformat = %s' % tmp_number)
  106. # Check if empty
  107. if not tmp_number:
  108. raise osv.except_osv(error_title_msg, invalid_format_msg)
  109. # treat (0) as a special condition as we dont want an extra 0 to be inserted in the number
  110. # FIXME all 0s after the country prefix should be stripped off
  111. tmp_number = tmp_number.replace('(0)','')
  112. # First, we remove all stupid caracters and spaces
  113. for char_to_remove in [' ', '.', '(', ')', '[', ']', '-', '/']:
  114. tmp_number = tmp_number.replace(char_to_remove, '')
  115. # Before starting to use prefix, we convert empty prefix whose value
  116. # is False to an empty string
  117. country_prefix = (ast_server.country_prefix or '')
  118. national_prefix = (ast_server.national_prefix or '')
  119. international_prefix = (ast_server.international_prefix or '')
  120. out_prefix = (ast_server.out_prefix or '')
  121. # International format
  122. if tmp_number[0] == '+':
  123. # Remove the starting '+' of the number
  124. tmp_number = tmp_number.replace('+','')
  125. _logger.debug('Number after removal of special char = %s' % tmp_number)
  126. # At this stage, 'tmp_number' should only contain digits
  127. if not tmp_number.isdigit():
  128. raise osv.except_osv(error_title_msg, invalid_format_msg)
  129. _logger.debug('Country prefix = %s' % country_prefix)
  130. if country_prefix == tmp_number[0:len(country_prefix)]:
  131. # If the number is a national number,
  132. # remove 'my country prefix' and add 'national prefix'
  133. tmp_number = (national_prefix) + tmp_number[len(country_prefix):len(tmp_number)]
  134. _logger.debug('National prefix = %s - Number with national prefix = %s' % (national_prefix, tmp_number))
  135. else:
  136. # If the number is an international number,
  137. # add 'international prefix'
  138. tmp_number = international_prefix + tmp_number
  139. _logger.debug('International prefix = %s - Number with international prefix = %s' % (international_prefix, tmp_number))
  140. # National format, allowed
  141. elif ast_server.national_format_allowed:
  142. # No treatment required
  143. if not tmp_number.isdigit():
  144. raise osv.except_osv(error_title_msg, invalid_national_format_msg)
  145. # National format, disallowed
  146. elif not ast_server.national_format_allowed:
  147. raise osv.except_osv(error_title_msg, invalid_international_format_msg)
  148. # Add 'out prefix' to all numbers
  149. tmp_number = out_prefix + tmp_number
  150. _logger.debug('Out prefix = %s - Number to be sent to Asterisk = %s' % (out_prefix, tmp_number))
  151. return tmp_number
  152. def _convert_number_to_international_format(self, cr, uid, number, ast_server, context=None):
  153. '''Convert the number presented by the phone network to a number
  154. in international format e.g. +33141981242'''
  155. if number and number.isdigit() and len(number) > 5:
  156. if ast_server.international_prefix and number[0:len(ast_server.international_prefix)] == ast_server.international_prefix:
  157. number = number[len(ast_server.international_prefix):]
  158. number = '+' + number
  159. elif ast_server.national_prefix and number[0:len(ast_server.national_prefix)] == ast_server.national_prefix:
  160. number = number[len(ast_server.national_prefix):]
  161. number = '+' + ast_server.country_prefix + number
  162. return number
  163. def _get_asterisk_server_from_user(self, cr, uid, user, context=None):
  164. '''Returns an asterisk.server browse object'''
  165. # We check if the user has an Asterisk server configured
  166. if user.asterisk_server_id.id:
  167. ast_server = user.asterisk_server_id
  168. else:
  169. asterisk_server_ids = self.search(cr, uid, [('company_id', '=', user.company_id.id)], context=context)
  170. # If no asterisk server is configured on the user, we take the first one
  171. if not asterisk_server_ids:
  172. raise osv.except_osv(_('Error :'), _("No Asterisk server configured for the company '%s'.") % user.company_id.name)
  173. else:
  174. ast_server = self.browse(cr, uid, asterisk_server_ids[0], context=context)
  175. return ast_server
  176. def _parse_asterisk_answer(self, cr, uid, sock, end_string='\r\n\r\n', context=None):
  177. '''Parse the answer of the Asterisk Manager Interface'''
  178. answer = ''
  179. data = ''
  180. while end_string not in data:
  181. data = sock.recv(1024)
  182. if data:
  183. answer += data
  184. # remove end_string from answer
  185. if answer[-len(end_string):] == end_string:
  186. answer = answer[:-len(end_string)]
  187. return answer
  188. def _connect_to_asterisk(self, cr, uid, method='dial', options=None, context=None):
  189. '''
  190. Open the socket to the Asterisk Manager Interface (AMI)
  191. and send instructions to Dial to Asterisk. That's the important function !
  192. '''
  193. user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
  194. # Note : if I write 'Error' without ' :', it won't get translated...
  195. # I don't understand why !
  196. ast_server = self._get_asterisk_server_from_user(cr, uid, user, context=context)
  197. # We check if the current user has a chan type
  198. if not user.asterisk_chan_type:
  199. raise osv.except_osv(_('Error :'), _('No channel type configured for the current user.'))
  200. # We check if the current user has an internal number
  201. if not user.internal_number:
  202. raise osv.except_osv(_('Error :'), _('No internal phone number configured for the current user'))
  203. _logger.debug("User's phone : %s/%s" % (user.asterisk_chan_type, user.internal_number))
  204. _logger.debug("Asterisk server = %s:%d" % (ast_server.ip_address, ast_server.port))
  205. # Connect to the Asterisk Manager Interface, using IPv6-ready code
  206. try:
  207. res = socket.getaddrinfo(str(ast_server.ip_address), ast_server.port, socket.AF_UNSPEC, socket.SOCK_STREAM)
  208. except:
  209. _logger.warning("Can't resolve the DNS of the Asterisk server '%s'" % ast_server.ip_address)
  210. raise osv.except_osv(_('Error :'), _("Can't resolve the DNS of the Asterisk server : '%s'" % ast_server.ip_address))
  211. for result in res:
  212. af, socktype, proto, canonname, sockaddr = result
  213. sock = socket.socket(af, socktype, proto)
  214. try:
  215. sock.connect(sockaddr)
  216. header_received = sock.recv(1024)
  217. _logger.debug('Header received from Asterisk : %s' % header_received)
  218. # Login to Asterisk
  219. login_act = 'Action: login\r\n' + \
  220. 'Events: off\r\n' + \
  221. 'Username: ' + ast_server.login + '\r\n' + \
  222. 'Secret: ' + ast_server.password + '\r\n\r\n'
  223. sock.send(login_act.encode('ascii'))
  224. login_answer = self._parse_asterisk_answer(cr, uid, sock, context=context)
  225. if 'Response: Success' in login_answer:
  226. _logger.debug("Successful authentification to Asterisk :\n%s" % login_answer)
  227. else:
  228. raise osv.except_osv(_('Error :'), _("Authentification to Asterisk failed :\n%s" % login_answer))
  229. if method == 'dial':
  230. # Convert the phone number in the format that will be sent to Asterisk
  231. erp_number = options.get('erp_number')
  232. if not erp_number:
  233. raise osv.except_osv(_('Error :'), "Hara kiri : you must call the function with erp_number in the options")
  234. ast_number = self._reformat_number(cr, uid, erp_number, ast_server, context=context)
  235. # The user should have a CallerID
  236. if not user.callerid:
  237. raise osv.except_osv(_('Error :'), _('No callerID configured for the current user'))
  238. # Dial with Asterisk
  239. originate_act = 'Action: originate\r\n' + \
  240. 'Channel: ' + user.asterisk_chan_type + '/' + user.internal_number + ( ('/' + user.dial_suffix) if user.dial_suffix else '') + '\r\n' + \
  241. 'Priority: ' + str(ast_server.extension_priority) + '\r\n' + \
  242. 'Timeout: ' + str(ast_server.wait_time*1000) + '\r\n' + \
  243. 'CallerId: ' + user.callerid + '\r\n' + \
  244. 'Exten: ' + ast_number + '\r\n' + \
  245. 'Context: ' + ast_server.context + '\r\n'
  246. if ast_server.alert_info and user.asterisk_chan_type == 'SIP':
  247. for server_alertinfo in ast_server.alert_info.split('|'):
  248. originate_act += 'Variable: SIPAddHeader=Alert-Info: ' + server_alertinfo.strip() + '\r\n'
  249. if user.alert_info and user.asterisk_chan_type == 'SIP':
  250. for user_alertinfo in user.alert_info.split('|'):
  251. originate_act += 'Variable: SIPAddHeader=Alert-Info: ' + user_alertinfo.strip() + '\r\n'
  252. if user.variable:
  253. for user_variable in user.variable.split('|'):
  254. originate_act += 'Variable: ' + user_variable.strip() + '\r\n'
  255. originate_act += '\r\n'
  256. sock.send(originate_act.encode('ascii'))
  257. originate_answer = self._parse_asterisk_answer(cr, uid, sock, context=context)
  258. if 'Response: Success' in originate_answer:
  259. _logger.debug('Successfull originate command : %s' % originate_answer)
  260. else:
  261. raise osv.except_osv(_('Error :'), _("Click to dial with Asterisk failed :\n%s" % originate_answer))
  262. elif method == "get_calling_number":
  263. status_act = 'Action: Status\r\n\r\n' # TODO : add ActionID
  264. sock.send(status_act.encode('ascii'))
  265. status_answer = self._parse_asterisk_answer(cr, uid, sock, end_string='Event: StatusComplete', context=context)
  266. if 'Response: Success' in status_answer:
  267. _logger.debug('Successfull Status command :\n%s' % status_answer)
  268. else:
  269. raise osv.except_osv(_('Error :'), _("Status command to Asterisk failed :\n%s" % status_answer))
  270. # Parse answer
  271. calling_party_number = False
  272. status_answer_split = status_answer.split('\r\n\r\n')
  273. for event in status_answer_split:
  274. string_bridge_match = 'BridgedChannel: ' + user.asterisk_chan_type + '/' + user.internal_number
  275. string_link_match = 'Link: ' + user.asterisk_chan_type + '/' + user.internal_number # Asterisk 1.4 ? Or is it related to the fact that it's an IAX trunk ?
  276. if not string_bridge_match in event and not string_link_match in event:
  277. continue
  278. event_split = event.split('\r\n')
  279. for event_line in event_split:
  280. if not 'CallerIDNum' in event_line:
  281. continue
  282. line_detail = event_line.split(': ')
  283. if len(line_detail) <> 2:
  284. raise osv.except_osv('Error :', "Hara kiri... this is not possible")
  285. calling_party_number = line_detail[1]
  286. # Logout of Asterisk
  287. sock.send(('Action: Logoff\r\n\r\n').encode('ascii'))
  288. logout_answer = self._parse_asterisk_answer(cr, uid, sock, context=context)
  289. if 'Response: Goodbye' in logout_answer:
  290. _logger.debug('Successfull logout from Asterisk :\n%s' % logout_answer)
  291. else:
  292. _logger.warning('Logout from Asterisk failed :\n%s' % logout_answer)
  293. # we catch only network problems here
  294. except socket.error:
  295. _logger.warning("Unable to connect to the Asterisk server '%s' IP '%s:%d'" % (ast_server.name, ast_server.ip_address, ast_server.port))
  296. raise osv.except_osv(_('Error :'), _("The connection from OpenERP to the Asterisk server failed. Please check the configuration on OpenERP and on Asterisk."))
  297. finally:
  298. sock.close()
  299. if method == 'dial':
  300. _logger.info("Asterisk Click2Dial from %s/%s to %s" % (user.asterisk_chan_type, user.internal_number, ast_number))
  301. return True
  302. elif method == "get_calling_number":
  303. _logger.debug("Calling party number: %s" % calling_party_number)
  304. return calling_party_number
  305. else:
  306. return False
  307. asterisk_server()
  308. # Parameters specific for each user
  309. class res_users(osv.osv):
  310. _inherit = "res.users"
  311. _columns = {
  312. 'internal_number': fields.char('Internal number', size=15,
  313. help="User's internal phone number."),
  314. 'dial_suffix': fields.char('User-specific dial suffix', size=15,
  315. help="User-specific dial suffix such as aa=2wb for SCCP auto answer."),
  316. 'callerid': fields.char('Caller ID', size=50,
  317. help="Caller ID used for the calls initiated by this user."),
  318. # You'd probably think : Asterisk should reuse the callerID of sip.conf !
  319. # But it cannot, cf http://lists.digium.com/pipermail/asterisk-users/2012-January/269787.html
  320. 'asterisk_chan_type': fields.selection([
  321. ('SIP', 'SIP'),
  322. ('IAX2', 'IAX2'),
  323. ('DAHDI', 'DAHDI'),
  324. ('Zap', 'Zap'),
  325. ('Skinny', 'Skinny'),
  326. ('MGCP', 'MGCP'),
  327. ('mISDN', 'mISDN'),
  328. ('H323', 'H323'),
  329. ('SCCP', 'SCCP'),
  330. ], 'Asterisk channel type',
  331. help="Asterisk channel type, as used in the Asterisk dialplan. If the user has a regular IP phone, the channel type is 'SIP'."),
  332. 'alert_info': fields.char('User-specific Alert-Info SIP header', size=255, help="Set a user-specific Alert-Info header in SIP request to user's IP Phone for the click2dial feature. If empty, the Alert-Info header will not be added. You can use it to have a special ring tone for click2dial (a silent one !) or to activate auto-answer for example. If you want to have several variable headers, separate them with '|'."),
  333. 'variable': fields.char('User-specific Variable', size=255, help="Set a user-specific 'Variable' field in the Asterisk Manager Interface 'originate' request for the click2dial feature. If you want to have several variable headers, separate them with '|'."),
  334. 'asterisk_server_id': fields.many2one('asterisk.server', 'Asterisk server',
  335. help="Asterisk server on which the user's phone is connected. If you leave this field empty, it will use the first Asterisk server of the user's company."),
  336. }
  337. _defaults = {
  338. 'asterisk_chan_type': 'SIP',
  339. }
  340. def _check_validity(self, cr, uid, ids):
  341. for user in self.browse(cr, uid, ids):
  342. for check_string in [('Internal number', user.internal_number), ('Caller ID', user.callerid)]:
  343. if check_string[1]:
  344. try:
  345. plom = check_string[1].encode('ascii')
  346. except UnicodeEncodeError:
  347. raise osv.except_osv(_('Error :'), _("The '%s' for the user '%s' should only have ASCII caracters" % (check_string[0], user.name)))
  348. return True
  349. _constraints = [
  350. (_check_validity, "Error message in raise", ['internal_number', 'callerid']),
  351. ]
  352. res_users()
  353. class res_partner_address(osv.osv):
  354. _inherit = "res.partner.address"
  355. def dial(self, cr, uid, ids, phone_field='phone', context=None):
  356. '''Read the number to dial and call _connect_to_asterisk the right way'''
  357. erp_number = self.read(cr, uid, ids, [phone_field], context=context)[0][phone_field]
  358. # Check if the number to dial is not empty
  359. if not erp_number:
  360. raise osv.except_osv(_('Error :'), _('There is no phone number !'))
  361. options = {'erp_number': erp_number}
  362. return self.pool.get('asterisk.server')._connect_to_asterisk(cr, uid, method='dial', options=options, context=context)
  363. def action_dial_phone(self, cr, uid, ids, context=None):
  364. '''Function called by the button 'Dial' next to the 'phone' field
  365. in the partner address view'''
  366. return self.dial(cr, uid, ids, phone_field='phone', context=context)
  367. def action_dial_mobile(self, cr, uid, ids, context=None):
  368. '''Function called by the button 'Dial' next to the 'mobile' field
  369. in the partner address view'''
  370. return self.dial(cr, uid, ids, phone_field='mobile', context=context)
  371. def get_name_from_phone_number(self, cr, uid, number, context=None):
  372. '''Function to get name from phone number. Usefull for use from Asterisk
  373. to add CallerID name to incoming calls.
  374. The "scripts/" subdirectory of this module has an AGI script that you can
  375. install on your Asterisk IPBX : the script will be called from the Asterisk
  376. dialplan via the AGI() function and it will use this function via an XML-RPC
  377. request.
  378. '''
  379. res = self.get_partner_from_phone_number(cr, uid, number, context=context)
  380. if res:
  381. return res[2]
  382. else:
  383. return False
  384. def get_partner_from_phone_number(self, cr, uid, number, context=None):
  385. res = {}
  386. # We check that "number" is really a number
  387. if not isinstance(number, str):
  388. return False
  389. if not number.isdigit():
  390. return False
  391. _logger.debug(u"Call get_name_from_phone_number with number = %s" % number)
  392. # Get all the partner addresses :
  393. all_ids = self.search(cr, uid, [], context=context)
  394. # For each partner address, we check if the number matches on the "phone" or "mobile" fields
  395. for entry in self.browse(cr, uid, all_ids, context=context):
  396. if entry.phone:
  397. # We use a regexp on the phone field to remove non-digit caracters
  398. if re.sub(r'\D', '', entry.phone).endswith(number):
  399. _logger.debug(u"Answer get_name_from_phone_number with name = %s" % entry.name)
  400. return (entry.id, entry.partner_id.id, entry.name)
  401. if entry.mobile:
  402. if re.sub(r'\D', '', entry.mobile).endswith(number):
  403. _logger.debug(u"Answer get_name_from_phone_number with name = %s" % entry.name)
  404. return (entry.id, entry.partner_id.id, entry.name)
  405. _logger.debug(u"No match for phone number %s" % number)
  406. return False
  407. res_partner_address()
  408. class wizard_open_calling_partner(osv.osv_memory):
  409. _name = "wizard.open.calling.partner"
  410. _description = "Open calling partner"
  411. _columns = {
  412. # I can't set any field to readonly, because otherwize it would call
  413. # default_get (and thus connect to Asterisk) a second time when the user
  414. # clicks on one of the buttons
  415. 'calling_number': fields.char('Calling number', size=30, help="Phone number of calling party that has been obtained from Asterisk."),
  416. 'partner_address_id': fields.many2one('res.partner.address', 'Contact name', help="Partner contact related to the calling number. If there is none and you want to update an existing partner"),
  417. 'partner_id': fields.many2one('res.partner', 'Partner', help="Partner related to the calling number."),
  418. 'to_update_partner_address_id': fields.many2one('res.partner.address', 'Contact to update', help="Partner contact on which the phone or mobile number will be written"),
  419. 'current_phone': fields.related('to_update_partner_address_id', 'phone', type='char', relation='res.partner.address', string='Current phone'),
  420. 'current_mobile': fields.related('to_update_partner_address_id', 'mobile', type='char', relation='res.partner.address', string='Current mobile'),
  421. }
  422. def default_get(self, cr, uid, fields, context=None):
  423. '''Thanks to the default_get method, we are able to query Asterisk and
  424. get the corresponding partner when we launch the wizard'''
  425. res = {}
  426. calling_number = self.pool.get('asterisk.server')._connect_to_asterisk(cr, uid, method='get_calling_number', context=context)
  427. #To test the code without Asterisk server
  428. #calling_number = "0141981242"
  429. if calling_number:
  430. res['calling_number'] = calling_number
  431. # We match only on the end of the phone number
  432. if len(calling_number) >= 9:
  433. number_to_search = calling_number[-9:len(calling_number)]
  434. else:
  435. number_to_search = calling_number
  436. partner = self.pool.get('res.partner.address').get_partner_from_phone_number(cr, uid, number_to_search, context=context)
  437. if partner:
  438. res['partner_address_id'] = partner[0]
  439. res['partner_id'] = partner[1]
  440. else:
  441. res['partner_id'] = False
  442. res['partner_address_id'] = False
  443. res['to_update_partner_address_id'] = False
  444. else:
  445. _logger.debug("Could not get the calling number from Asterisk.")
  446. raise osv.except_osv(_('Error :'), _("Could not get the calling number from Asterisk. Are you currently on the phone ? If yes, check your setup and look at the OpenERP debug logs."))
  447. return res
  448. def open_filtered_object(self, cr, uid, ids, oerp_object, context=None):
  449. '''Returns the action that opens the list view of the 'oerp_object'
  450. given as argument filtered on the partner'''
  451. # This module only depends on "base"
  452. # and I don't want to add a dependancy on "sale" or "account"
  453. # So I just check here that the model exists, to avoid a crash
  454. if not self.pool.get('ir.model').search(cr, uid, [('model', '=', oerp_object._name)], context=context):
  455. raise osv.except_osv(_('Error :'), _("The object '%s' is not found in your OpenERP database, probably because the related module is not installed." % oerp_object._description))
  456. partner = self.read(cr, uid, ids[0], ['partner_id'], context=context)['partner_id']
  457. if partner:
  458. action = {
  459. 'name': oerp_object._description,
  460. 'view_type': 'form',
  461. 'view_mode': 'tree,form',
  462. 'res_model': oerp_object._name,
  463. 'type': 'ir.actions.act_window',
  464. 'nodestroy': False, # close the pop-up wizard after action
  465. 'target': 'current',
  466. 'domain': [('partner_id', '=', partner[0])],
  467. }
  468. return action
  469. else:
  470. return False
  471. def open_sale_orders(self, cr, uid, ids, context=None):
  472. '''Function called by the related button of the wizard'''
  473. return self.open_filtered_object(cr, uid, ids, self.pool.get('sale.order'), context=context)
  474. def open_invoices(self, cr, uid, ids, context=None):
  475. '''Function called by the related button of the wizard'''
  476. return self.open_filtered_object(cr, uid, ids, self.pool.get('account.invoice'), context=context)
  477. def simple_open(self, cr, uid, ids, object_name='res.partner', context=None):
  478. if object_name == 'res.partner':
  479. field = 'partner_id'
  480. label = 'Partner'
  481. elif object_name == 'res.partner.address':
  482. field = 'partner_address_id'
  483. label = 'Contact'
  484. else:
  485. raise osv.except_osv(_('Error :'), "This object '%s' is not supported" % object_name)
  486. record_to_open = self.read(cr, uid, ids[0], [field], context=context)[field]
  487. if record_to_open:
  488. return {
  489. 'name': label,
  490. 'view_type': 'form',
  491. 'view_mode': 'form,tree',
  492. 'res_model': object_name,
  493. 'type': 'ir.actions.act_window',
  494. 'nodestroy': False, # close the pop-up wizard after action
  495. 'target': 'current',
  496. 'res_id': record_to_open[0],
  497. }
  498. else:
  499. return False
  500. def open_partner(self, cr, uid, ids, context=None):
  501. '''Function called by the related button of the wizard'''
  502. return self.simple_open(cr, uid, ids, object_name='res.partner', context=context)
  503. def open_partner_address(self, cr, uid, ids, context=None):
  504. '''Function called by the related button of the wizard'''
  505. return self.simple_open(cr, uid, ids, object_name='res.partner.address', context=context)
  506. def create_partner_address(self, cr, uid, ids, phone_type='phone', context=None):
  507. '''Function called by the related button of the wizard'''
  508. calling_number = self.read(cr, uid, ids[0], ['calling_number'], context=context)['calling_number']
  509. user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
  510. ast_server = self.pool.get('asterisk.server')._get_asterisk_server_from_user(cr, uid, user, context=context)
  511. # Convert the number to the international format
  512. number_to_write = self.pool.get('asterisk.server')._convert_number_to_international_format(cr, uid, calling_number, ast_server, context=context)
  513. new_partner_address_id = self.pool.get('res.partner.address').create(cr, uid, {phone_type: number_to_write}, context=context)
  514. action = {
  515. 'name': 'Create new contact',
  516. 'view_type': 'form',
  517. 'view_mode': 'form,tree',
  518. 'res_model': 'res.partner.address',
  519. 'type': 'ir.actions.act_window',
  520. 'nodestroy': False,
  521. 'target': 'current',
  522. 'res_id': new_partner_address_id,
  523. }
  524. return action
  525. def create_partner_address_phone(self, cr, uid, ids, context=None):
  526. return self.create_partner_address(cr, uid, ids, phone_type='phone', context=context)
  527. def create_partner_address_mobile(self, cr, uid, ids, context=None):
  528. return self.create_partner_address(cr, uid, ids, phone_type='mobile', context=context)
  529. def update_partner_address(self, cr, uid, ids, phone_type='mobile', context=None):
  530. cur_wizard = self.browse(cr, uid, ids[0], context=context)
  531. if not cur_wizard.to_update_partner_address_id:
  532. raise osv.except_osv(_('Error :'), _("Select the contact to update."))
  533. user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
  534. ast_server = self.pool.get('asterisk.server')._get_asterisk_server_from_user(cr, uid, user, context=context)
  535. number_to_write = self.pool.get('asterisk.server')._convert_number_to_international_format(cr, uid, cur_wizard.calling_number, ast_server, context=context)
  536. self.pool.get('res.partner.address').write(cr, uid, cur_wizard.to_update_partner_address_id.id, {phone_type: number_to_write}, context=context)
  537. action = {
  538. 'name': 'Contact: ' + cur_wizard.to_update_partner_address_id.name,
  539. 'view_type': 'form',
  540. 'view_mode': 'form,tree',
  541. 'res_model': 'res.partner.address',
  542. 'type': 'ir.actions.act_window',
  543. 'nodestroy': False,
  544. 'target': 'current',
  545. 'res_id': cur_wizard.to_update_partner_address_id.id
  546. }
  547. return action
  548. def update_partner_address_phone(self, cr, uid, ids, context=None):
  549. return self.update_partner_address(cr, uid, ids, phone_type='phone', context=context)
  550. def update_partner_address_mobile(self, cr, uid, ids, context=None):
  551. return self.update_partner_address(cr, uid, ids, phone_type='mobile', context=context)
  552. def onchange_to_update_partner_address(self, cr, uid, ids, to_update_partner_address_id, context=None):
  553. res = {}
  554. res['value'] = {}
  555. if to_update_partner_address_id:
  556. to_update_partner_address = self.pool.get('res.partner.address').browse(cr, uid, to_update_partner_address_id, context=context)
  557. res['value'].update({'current_phone': to_update_partner_address.phone,
  558. 'current_mobile': to_update_partner_address.mobile})
  559. else:
  560. res['value'].update({'current_phone': False, 'current_mobile': False})
  561. return res
  562. wizard_open_calling_partner()
  563. # This module supports multi-company
  564. class res_company(osv.osv):
  565. _inherit = "res.company"
  566. _columns = {
  567. 'asterisk_server_ids': fields.one2many('asterisk.server', 'company_id', 'Asterisk servers', help="List of Asterisk servers.")
  568. }
  569. res_company()