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.

291 lines
13 KiB

10 years ago
  1. # -*- encoding: utf-8 -*-
  2. ##############################################################################
  3. #
  4. # Base Phone module for Odoo/OpenERP
  5. # Copyright (C) 2010-2014 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 orm, fields
  22. from openerp.tools.safe_eval import safe_eval
  23. from openerp.tools.translate import _
  24. import logging
  25. # Lib for phone number reformating -> pip install phonenumbers
  26. import phonenumbers
  27. _logger = logging.getLogger(__name__)
  28. class phone_common(orm.AbstractModel):
  29. _name = 'phone.common'
  30. def _generic_reformat_phonenumbers(
  31. self, cr, uid, ids, vals, context=None):
  32. """Reformat phone numbers in E.164 format i.e. +33141981242"""
  33. assert isinstance(self._country_field, (str, unicode, type(None))),\
  34. 'Wrong self._country_field'
  35. assert isinstance(self._partner_field, (str, unicode, type(None))),\
  36. 'Wrong self._partner_field'
  37. assert isinstance(self._phone_fields, list),\
  38. 'self._phone_fields must be a list'
  39. if context is None:
  40. context = {}
  41. if ids and isinstance(ids, (int, long)):
  42. ids = [ids]
  43. if any([vals.get(field) for field in self._phone_fields]):
  44. user = self.pool['res.users'].browse(cr, uid, uid, context=context)
  45. # country_id on res.company is a fields.function that looks at
  46. # company_id.partner_id.addres(default).country_id
  47. countrycode = None
  48. if self._country_field:
  49. if vals.get(self._country_field):
  50. country = self.pool['res.country'].browse(
  51. cr, uid, vals[self._country_field], context=context)
  52. countrycode = country.code
  53. elif ids:
  54. rec = self.browse(cr, uid, ids[0], context=context)
  55. country = safe_eval(
  56. 'rec.' + self._country_field, {'rec': rec})
  57. countrycode = country and country.code or None
  58. elif self._partner_field:
  59. if vals.get(self._partner_field):
  60. partner = self.pool['res.partner'].browse(
  61. cr, uid, vals[self._partner_field], context=context)
  62. countrycode = partner.country_id and\
  63. partner.country_id.code or None
  64. elif ids:
  65. rec = self.browse(cr, uid, ids[0], context=context)
  66. partner = safe_eval(
  67. 'rec.' + self._partner_field, {'rec': rec})
  68. if partner:
  69. countrycode = partner.country_id and\
  70. partner.country_id.code or None
  71. if not countrycode:
  72. if user.company_id.country_id:
  73. countrycode = user.company_id.country_id.code
  74. else:
  75. _logger.error(
  76. _("You should set a country on the company '%s' "
  77. "to allow the reformat of phone numbers")
  78. % user.company_id.name)
  79. countrycode = None
  80. # with country code = None, phonenumbers.parse() will work
  81. # with phonenumbers formatted in E164, but will fail with
  82. # phone numbers in national format
  83. for field in self._phone_fields:
  84. if vals.get(field):
  85. init_value = vals.get(field)
  86. try:
  87. res_parse = phonenumbers.parse(
  88. vals.get(field), countrycode)
  89. vals[field] = phonenumbers.format_number(
  90. res_parse, phonenumbers.PhoneNumberFormat.E164)
  91. if init_value != vals[field]:
  92. _logger.info(
  93. "%s initial value: '%s' updated value: '%s'"
  94. % (field, init_value, vals[field]))
  95. except Exception, e:
  96. # I do BOTH logger and raise, because:
  97. # raise is usefull when the record is created/written
  98. # by a user via the Web interface
  99. # logger is usefull when the record is created/written
  100. # via the webservices
  101. _logger.error(
  102. "Cannot reformat the phone number '%s' to "
  103. "international format with region=%s"
  104. % (vals.get(field), countrycode))
  105. if context.get('raise_if_phone_parse_fails'):
  106. raise Warning(
  107. _("Cannot reformat the phone number '%s' to "
  108. "international format. Error message: %s")
  109. % (vals.get(field), e))
  110. return vals
  111. def get_name_from_phone_number(
  112. self, cr, uid, presented_number, context=None):
  113. '''Function to get name from phone number. Usefull for use from IPBX
  114. to add CallerID name to incoming calls.'''
  115. res = self.get_record_from_phone_number(
  116. cr, uid, presented_number, context=context)
  117. if res:
  118. return res[2]
  119. else:
  120. return False
  121. def get_record_from_phone_number(
  122. self, cr, uid, presented_number, context=None):
  123. '''If it finds something, it returns (object name, ID, record name)
  124. For example : ('res.partner', 42, u'Alexis de Lattre (Akretion)')
  125. '''
  126. if context is None:
  127. context = {}
  128. ctx_phone = context.copy()
  129. ctx_phone['callerid'] = True
  130. _logger.debug(
  131. u"Call get_name_from_phone_number with number = %s"
  132. % presented_number)
  133. if not isinstance(presented_number, (str, unicode)):
  134. _logger.warning(
  135. u"Number '%s' should be a 'str' or 'unicode' but it is a '%s'"
  136. % (presented_number, type(presented_number)))
  137. return False
  138. if not presented_number.isdigit():
  139. _logger.warning(
  140. u"Number '%s' should only contain digits." % presented_number)
  141. user = self.pool['res.users'].browse(cr, uid, uid, context=context)
  142. nr_digits_to_match_from_end = \
  143. user.company_id.number_of_digits_to_match_from_end
  144. if len(presented_number) >= nr_digits_to_match_from_end:
  145. end_number_to_match = presented_number[
  146. -nr_digits_to_match_from_end:len(presented_number)]
  147. else:
  148. end_number_to_match = presented_number
  149. phoneobjects = self._get_phone_fields(cr, uid, context=context)
  150. phonefieldslist = [] # [('res.parter', 10), ('crm.lead', 20)]
  151. for objname in phoneobjects:
  152. if (
  153. '_phone_name_sequence' in dir(self.pool[objname]) and
  154. self.pool[objname]._phone_name_sequence):
  155. phonefieldslist.append(
  156. (objname, self.pool[objname]._phone_name_sequence))
  157. phonefieldslist_sorted = sorted(
  158. phonefieldslist,
  159. key=lambda element: element[1])
  160. _logger.debug('phonefieldslist_sorted=%s' % phonefieldslist_sorted)
  161. for (objname, prio) in phonefieldslist_sorted:
  162. obj = self.pool[objname]
  163. pg_search_number = str('%' + end_number_to_match)
  164. _logger.debug(
  165. "Will search phone and mobile numbers in %s ending with '%s'"
  166. % (objname, end_number_to_match))
  167. domain = []
  168. for phonefield in obj._phone_fields:
  169. domain.append((phonefield, '=like', pg_search_number))
  170. if len(obj._phone_fields) > 1:
  171. domain = ['|'] * (len(obj._phone_fields) - 1) + domain
  172. res_ids = obj.search(cr, uid, domain, context=context)
  173. if len(res_ids) > 1:
  174. _logger.warning(
  175. u"There are several %s (IDS = %s) with a phone number "
  176. "ending with '%s'. Taking the first one."
  177. % (objname, res_ids, end_number_to_match))
  178. if res_ids:
  179. name = obj.name_get(
  180. cr, uid, res_ids[0], context=ctx_phone)[0][1]
  181. res = (objname, res_ids[0], name)
  182. _logger.debug(
  183. u"Answer get_record_from_phone_number: (%s, %d, %s)"
  184. % (res[0], res[1], res[2]))
  185. return res
  186. else:
  187. _logger.debug(
  188. u"No match on %s for end of phone number '%s'"
  189. % (objname, end_number_to_match))
  190. return False
  191. def _get_phone_fields(self, cr, uid, context=None):
  192. '''Returns a dict with key = object name
  193. and value = list of phone fields'''
  194. model_ids = self.pool['ir.model'].search(
  195. cr, uid, [], context=context)
  196. res = []
  197. for model in self.pool['ir.model'].browse(
  198. cr, uid, model_ids, context=context):
  199. senv = False
  200. try:
  201. senv = self.pool[model.model]
  202. except:
  203. continue
  204. if (
  205. '_phone_fields' in dir(senv) and
  206. isinstance(senv._phone_fields, list)):
  207. res.append(model.model)
  208. return res
  209. def click2dial(self, cr, uid, erp_number, context=None):
  210. '''This function is designed to be overridden in IPBX-specific
  211. modules, such as asterisk_click2dial'''
  212. return {'dialed_number': erp_number}
  213. class res_partner(orm.Model):
  214. _name = 'res.partner'
  215. _inherit = ['res.partner', 'phone.common']
  216. _phone_fields = ['phone', 'mobile', 'fax']
  217. _phone_name_sequence = 10
  218. _country_field = 'country_id'
  219. _partner_field = None
  220. def create(self, cr, uid, vals, context=None):
  221. vals_reformated = self._generic_reformat_phonenumbers(
  222. cr, uid, None, vals, context=context)
  223. return super(res_partner, self).create(
  224. cr, uid, vals_reformated, context=context)
  225. def write(self, cr, uid, ids, vals, context=None):
  226. vals_reformated = self._generic_reformat_phonenumbers(
  227. cr, uid, ids, vals, context=context)
  228. return super(res_partner, self).write(
  229. cr, uid, ids, vals_reformated, context=context)
  230. def name_get(self, cr, uid, ids, context=None):
  231. if context is None:
  232. context = {}
  233. if context.get('callerid'):
  234. res = []
  235. if isinstance(ids, (int, long)):
  236. ids = [ids]
  237. for partner in self.browse(cr, uid, ids, context=context):
  238. if partner.parent_id and partner.parent_id.is_company:
  239. name = u'%s (%s)' % (partner.name, partner.parent_id.name)
  240. else:
  241. name = partner.name
  242. res.append((partner.id, name))
  243. return res
  244. else:
  245. return super(res_partner, self).name_get(
  246. cr, uid, ids, context=context)
  247. class res_company(orm.Model):
  248. _inherit = 'res.company'
  249. _columns = {
  250. 'number_of_digits_to_match_from_end': fields.integer(
  251. 'Number of Digits To Match From End',
  252. help="In several situations, OpenERP will have to find a "
  253. "Partner/Lead/Employee/... from a phone number presented by the "
  254. "calling party. As the phone numbers presented by your phone "
  255. "operator may not always be displayed in a standard format, "
  256. "the best method to find the related Partner/Lead/Employee/... "
  257. "in OpenERP is to try to match the end of the phone number in "
  258. "OpenERP with the N last digits of the phone number presented "
  259. "by the calling party. N is the value you should enter in this "
  260. "field."),
  261. }
  262. _defaults = {
  263. 'number_of_digits_to_match_from_end': 8,
  264. }
  265. _sql_constraints = [(
  266. 'number_of_digits_to_match_from_end_positive',
  267. 'CHECK (number_of_digits_to_match_from_end > 0)',
  268. "The value of the field 'Number of Digits To Match From End' must "
  269. "be positive."),
  270. ]