main.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. # -*- encoding: utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. import requests
  4. from odoo import http
  5. from odoo.http import request
  6. from odoo.tools import html2plaintext
  7. import logging
  8. _logger = logging.getLogger(__name__)
  9. FIELDS_MAPPING = {
  10. 'country': ['country'],
  11. 'street_number': ['number'],
  12. 'locality': ['city'], # If locality exists, use it instead of the more general administrative area
  13. 'route': ['street'],
  14. 'postal_code': ['zip'],
  15. 'administrative_area_level_1': ['state', 'city'],
  16. 'administrative_area_level_2': ['state', 'country']
  17. }
  18. # If a google fields may correspond to multiple standard fields, the first occurrence in the list will overwrite following entries.
  19. FIELDS_PRIORITY = ['country', 'street_number', 'neighborhood', 'locality', 'route', 'postal_code',
  20. 'administrative_area_level_1', 'administrative_area_level_2']
  21. GOOGLE_PLACES_ENDPOINT = 'https://maps.googleapis.com/maps/api/place'
  22. TIMEOUT = 2.5
  23. class AutoCompleteController(http.Controller):
  24. def _translate_google_to_standard(self, google_fields):
  25. standard_data = {}
  26. for google_field in google_fields:
  27. fields_standard = FIELDS_MAPPING[google_field['type']] if google_field['type'] in FIELDS_MAPPING else []
  28. for field_standard in fields_standard:
  29. if field_standard in standard_data: # if a value is already assigned, do not overwrite it.
  30. continue
  31. # Convert state and countries to odoo ids
  32. if field_standard == 'country':
  33. standard_data[field_standard] = request.env['res.country'].search(
  34. [('code', '=', google_field['short_name'].upper())])[0].id
  35. elif field_standard == 'state':
  36. state = request.env['res.country.state'].search(
  37. [('code', '=', google_field['short_name'].upper()),
  38. ('country_id.id', '=', standard_data['country'])])
  39. if len(state) == 1:
  40. standard_data[field_standard] = state.id
  41. else:
  42. standard_data[field_standard] = google_field['long_name']
  43. return standard_data
  44. def _guess_number_from_input(self, source_input, standard_address):
  45. """
  46. Google might not send the house number in case the address
  47. does not exist in their database.
  48. We try to guess the number from the user's input to avoid losing the info.
  49. """
  50. # Remove other parts from address to make better guesses
  51. guessed_house_number = source_input \
  52. .replace(standard_address.get('zip', ''), '') \
  53. .replace(standard_address.get('street', ''), '') \
  54. .replace(standard_address.get('city', ''), '')
  55. guessed_house_number = guessed_house_number.split(',')[0].strip()
  56. return guessed_house_number
  57. def _perform_place_search(self, partial_address, api_key=None, session_id=None, language_code=None, country_code=None):
  58. if len(partial_address) <= 5:
  59. return {
  60. 'results': [],
  61. 'session_id': session_id
  62. }
  63. params = {
  64. 'key': api_key,
  65. 'fields': 'formatted_address,name',
  66. 'inputtype': 'textquery',
  67. 'types': 'address',
  68. 'input': partial_address
  69. }
  70. if country_code:
  71. params['components'] = f'country:{country_code}'
  72. if language_code:
  73. params['language'] = language_code
  74. if session_id:
  75. params['sessiontoken'] = session_id
  76. try:
  77. results = requests.get(f'{GOOGLE_PLACES_ENDPOINT}/autocomplete/json', params=params, timeout=TIMEOUT).json()
  78. except (TimeoutError, ValueError) as e:
  79. _logger.error(e)
  80. return {
  81. 'results': [],
  82. 'session_id': session_id
  83. }
  84. if results.get('error_message'):
  85. _logger.error(results['error_message'])
  86. results = results.get('predictions', [])
  87. # Convert google specific format to standard format.
  88. return {
  89. 'results': [{
  90. 'formatted_address': result['description'],
  91. 'google_place_id': result['place_id'],
  92. } for result in results],
  93. 'session_id': session_id
  94. }
  95. def _perform_complete_place_search(self, address, api_key=None, google_place_id=None, language_code=None, session_id=None):
  96. params = {
  97. 'key': api_key,
  98. 'place_id': google_place_id,
  99. 'fields': 'address_component,adr_address'
  100. }
  101. if language_code:
  102. params['language'] = language_code
  103. if session_id:
  104. params['sessiontoken'] = session_id
  105. try:
  106. results = requests.get(f'{GOOGLE_PLACES_ENDPOINT}/details/json', params=params, timeout=TIMEOUT).json()
  107. except (TimeoutError, ValueError) as e:
  108. _logger.error(e)
  109. return {'address': None}
  110. if results.get('error_message'):
  111. _logger.error(results['error_message'])
  112. try:
  113. html_address = results['result']['adr_address']
  114. results = results['result']['address_components'] # Get rid of useless extra data
  115. except KeyError:
  116. return {'address': None}
  117. # Keep only the first type from the list of types
  118. for res in results:
  119. res['type'] = res.pop('types')[0]
  120. # Sort the result by their priority.
  121. results.sort(key=lambda r: FIELDS_PRIORITY.index(r['type']) if r['type'] in FIELDS_PRIORITY else 100)
  122. standard_address = self._translate_google_to_standard(results)
  123. if 'number' not in standard_address:
  124. standard_address['number'] = self._guess_number_from_input(address, standard_address)
  125. standard_address['formatted_street_number'] = f'{standard_address["number"]} {standard_address.get("street", "")}'
  126. else:
  127. formatted_from_html = html2plaintext(html_address.split(',')[0])
  128. formatted_manually = f'{standard_address["number"]} {standard_address.get("street", "")}'
  129. # Sometimes, the google api sends back abbreviated data :
  130. # "52 High Road Street" becomes "52 HR St" for example. We usually take the result from google, but if it's an abbreviation, take our guess instead.
  131. if len(formatted_from_html) >= len(formatted_manually):
  132. standard_address['formatted_street_number'] = formatted_from_html
  133. else:
  134. standard_address['formatted_street_number'] = formatted_manually
  135. return standard_address
  136. @http.route('/autocomplete/address', methods=['POST'], type='json', auth='public', website=True)
  137. def _autocomplete_address(self, partial_address, session_id=None):
  138. api_key = request.env['website'].get_current_website().sudo().google_places_api_key
  139. return self._perform_place_search(partial_address, session_id=session_id, api_key=api_key)
  140. @http.route('/autocomplete/address_full', methods=['POST'], type='json', auth='public', website=True)
  141. def _autocomplete_address_full(self, address, session_id=None, google_place_id=None, **kwargs):
  142. api_key = request.env['website'].get_current_website().sudo().google_places_api_key
  143. return self._perform_complete_place_search(address, google_place_id=google_place_id,
  144. session_id=session_id, api_key=api_key, **kwargs)