From ba988f7e6bfd9dd9b62c3f8bbf8402618df671ec Mon Sep 17 00:00:00 2001 From: Bastien Dosimont Date: Wed, 13 Apr 2022 07:23:51 +0000 Subject: [PATCH] [ADD] website_sale_autocomplete : Autocomplete the address fields in website using Google Places. task-id: 2806842 PR : https://github.com/odoo/odoo/pull/87636 - Implement an 'address.provider' structure, designed to be extendable and support multiple providers (not just google) - Add a module website_sale_autocompletion, that could be relocated without too much hassle - Add a module autocompletion_google_places thats adds google functionalities on top of the previous module closes odoo/odoo#88782 Signed-off-by: Yannick Tivisse (yti) --- .../models/res_config_settings.py | 2 + .../views/res_config_settings_views.xml | 14 ++ addons/website_sale_autocomplete/__init__.py | 5 + .../website_sale_autocomplete/__manifest__.py | 30 ++++ .../controllers/__init__.py | 4 + .../controllers/main.py | 153 ++++++++++++++++++ .../models/__init__.py | 5 + .../models/res_config_settings.py | 10 ++ .../models/website.py | 11 ++ .../img/powered_by_google_on_non_white.png | Bin 0 -> 1404 bytes .../src/img/powered_by_google_on_white.png | Bin 0 -> 2133 bytes .../static/src/js/address_form.js | 108 +++++++++++++ .../static/src/xml/autocomplete.xml | 15 ++ .../static/tests/autocomplete_tour.js | 71 ++++++++ .../tests/__init__.py | 4 + .../tests/test_ui.py | 40 +++++ .../views/res_config_settings_views.xml | 31 ++++ .../views/templates.xml | 11 ++ 18 files changed, 514 insertions(+) create mode 100644 addons/website_sale_autocomplete/__init__.py create mode 100644 addons/website_sale_autocomplete/__manifest__.py create mode 100644 addons/website_sale_autocomplete/controllers/__init__.py create mode 100644 addons/website_sale_autocomplete/controllers/main.py create mode 100644 addons/website_sale_autocomplete/models/__init__.py create mode 100644 addons/website_sale_autocomplete/models/res_config_settings.py create mode 100644 addons/website_sale_autocomplete/models/website.py create mode 100644 addons/website_sale_autocomplete/static/src/img/powered_by_google_on_non_white.png create mode 100644 addons/website_sale_autocomplete/static/src/img/powered_by_google_on_white.png create mode 100644 addons/website_sale_autocomplete/static/src/js/address_form.js create mode 100644 addons/website_sale_autocomplete/static/src/xml/autocomplete.xml create mode 100644 addons/website_sale_autocomplete/static/tests/autocomplete_tour.js create mode 100644 addons/website_sale_autocomplete/tests/__init__.py create mode 100644 addons/website_sale_autocomplete/tests/test_ui.py create mode 100644 addons/website_sale_autocomplete/views/res_config_settings_views.xml create mode 100644 addons/website_sale_autocomplete/views/templates.xml diff --git a/addons/website_sale/models/res_config_settings.py b/addons/website_sale/models/res_config_settings.py index d36fcb32e75..2f0c38ae310 100644 --- a/addons/website_sale/models/res_config_settings.py +++ b/addons/website_sale/models/res_config_settings.py @@ -27,6 +27,8 @@ class ResConfigSettings(models.TransientModel): module_website_sale_digital = fields.Boolean("Digital Content") module_website_sale_wishlist = fields.Boolean("Wishlists") module_website_sale_comparison = fields.Boolean("Product Comparison Tool") + module_website_sale_autocomplete = fields.Boolean('Address Autocomplete') + module_account = fields.Boolean("Invoicing") module_website_sale_picking = fields.Boolean('On Site Payments & Picking') diff --git a/addons/website_sale/views/res_config_settings_views.xml b/addons/website_sale/views/res_config_settings_views.xml index 8674ebea9f2..41b626ca0ef 100644 --- a/addons/website_sale/views/res_config_settings_views.xml +++ b/addons/website_sale/views/res_config_settings_views.xml @@ -393,6 +393,20 @@ + + +
+
+ +
+
+
+
+
diff --git a/addons/website_sale_autocomplete/__init__.py b/addons/website_sale_autocomplete/__init__.py new file mode 100644 index 00000000000..c48d23bbb1d --- /dev/null +++ b/addons/website_sale_autocomplete/__init__.py @@ -0,0 +1,5 @@ +# -*- encoding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import controllers +from . import models diff --git a/addons/website_sale_autocomplete/__manifest__.py b/addons/website_sale_autocomplete/__manifest__.py new file mode 100644 index 00000000000..fdf030b657f --- /dev/null +++ b/addons/website_sale_autocomplete/__manifest__.py @@ -0,0 +1,30 @@ +# -*- encoding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'Google places autocompletion', + 'category': 'Website/Website', + 'summary': 'Assist your users with automatic completion & suggestions when filling their address during checkout', + 'version': '1.0', + 'description': "Assist your users with automatic completion & suggestions when filling their address during checkout", + 'depends': [ + 'website_sale' + ], + 'data': [ + 'views/templates.xml', + 'views/res_config_settings_views.xml', + ], + 'assets': { + 'web.assets_frontend': [ + 'website_sale_autocomplete/static/src/js/address_form.js' + ], + 'web.assets_tests': [ + 'website_sale_autocomplete/static/tests/**/*.js' + ], + 'web.assets_qweb': [ + 'website_sale_autocomplete/static/src/xml/autocomplete.xml', + ], + }, + 'installable': True, + 'license': 'LGPL-3', +} diff --git a/addons/website_sale_autocomplete/controllers/__init__.py b/addons/website_sale_autocomplete/controllers/__init__.py new file mode 100644 index 00000000000..e425c18c14b --- /dev/null +++ b/addons/website_sale_autocomplete/controllers/__init__.py @@ -0,0 +1,4 @@ +# -*- encoding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import main diff --git a/addons/website_sale_autocomplete/controllers/main.py b/addons/website_sale_autocomplete/controllers/main.py new file mode 100644 index 00000000000..c45873ba53c --- /dev/null +++ b/addons/website_sale_autocomplete/controllers/main.py @@ -0,0 +1,153 @@ +# -*- encoding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +import requests + +from odoo import http +from odoo.http import request +from odoo.tools import html2plaintext + +FIELDS_MAPPING = { + 'country': ['country'], + 'street_number': ['number'], + 'locality': ['city'], # If locality exists, use it instead of the more general administrative area + 'route': ['street'], + 'postal_code': ['zip'], + 'administrative_area_level_1': ['state', 'city'], + 'administrative_area_level_2': ['state', 'country'] +} + +# If a google fields may correspond to multiple standard fields, the first occurrence in the list will overwrite following entries. +FIELDS_PRIORITY = ['country', 'street_number', 'neighborhood', 'locality', 'route', 'postal_code', + 'administrative_area_level_1', 'administrative_area_level_2'] +GOOGLE_PLACES_ENDPOINT = 'https://maps.googleapis.com/maps/api/place' +TIMEOUT = 2.5 + + +class AutoCompleteController(http.Controller): + + def _translate_google_to_standard(self, google_fields): + standard_data = {} + + for google_field in google_fields: + fields_standard = FIELDS_MAPPING[google_field['type']] if google_field['type'] in FIELDS_MAPPING else [] + + for field_standard in fields_standard: + if field_standard in standard_data: # if a value is already assigned, do not overwrite it. + continue + # Convert state and countries to odoo ids + if field_standard == 'country': + standard_data[field_standard] = request.env['res.country'].search( + [('code', '=', google_field['short_name'].upper())])[0].id + elif field_standard == 'state': + state = request.env['res.country.state'].search( + [('code', '=', google_field['short_name'].upper()), + ('country_id.id', '=', standard_data['country'])]) + if len(state) == 1: + standard_data[field_standard] = state.id + else: + standard_data[field_standard] = google_field['long_name'] + return standard_data + + def _guess_number_from_input(self, source_input, standard_address): + """ + Google might not send the house number in case the address + does not exist in their database. + We try to guess the number from the user's input to avoid losing the info. + """ + # Remove other parts from address to make better guesses + guessed_house_number = source_input \ + .replace(standard_address.get('zip', ''), '') \ + .replace(standard_address.get('street', ''), '') \ + .replace(standard_address.get('city', ''), '') + guessed_house_number = guessed_house_number.split(',')[0].strip() + return guessed_house_number + + def _perform_place_search(self, partial_address, api_key=None, session_id=None, language_code=None, country_code=None): + params = { + 'key': api_key, + 'fields': 'formatted_address,name', + 'inputtype': 'textquery', + 'types': 'address', + 'input': partial_address + } + if country_code: + params['components'] = f'country:{country_code}' + if language_code: + params['language'] = language_code + if session_id: + params['sessiontoken'] = session_id + + try: + results = requests.get(f'{GOOGLE_PLACES_ENDPOINT}/autocomplete/json', params=params, timeout=TIMEOUT).json() + except (TimeoutError, ValueError): + return { + 'results': [], + 'session_id': session_id + } + results = results.get('predictions', []) + + # Convert google specific format to standard format. + return { + 'results': [{ + 'formatted_address': result['description'], + 'google_place_id': result['place_id'], + } for result in results], + 'session_id': session_id + } + + def _perform_complete_place_search(self, address, api_key=None, google_place_id=None, language_code=None, session_id=None): + params = { + 'key': api_key, + 'place_id': google_place_id, + 'fields': 'address_component,adr_address' + } + + if language_code: + params['language'] = language_code + if session_id: + params['sessiontoken'] = session_id + + try: + results = requests.get(f'{GOOGLE_PLACES_ENDPOINT}/details/json', params=params, timeout=TIMEOUT).json() + except (TimeoutError, ValueError): + return {'address': None} + + try: + html_address = results['result']['adr_address'] + results = results['result']['address_components'] # Get rid of useless extra data + except KeyError: + return {'address': None} + + # Keep only the first type from the list of types + for res in results: + res['type'] = res.pop('types')[0] + + # Sort the result by their priority. + results.sort(key=lambda r: FIELDS_PRIORITY.index(r['type']) if r['type'] in FIELDS_PRIORITY else 100) + + standard_address = self._translate_google_to_standard(results) + + if 'number' not in standard_address: + standard_address['number'] = self._guess_number_from_input(address, standard_address) + standard_address['formatted_street_number'] = f'{standard_address["number"]} {standard_address.get("street", "")}' + else: + formatted_from_html = html2plaintext(html_address.split(',')[0]) + formatted_manually = f'{standard_address["number"]} {standard_address.get("street", "")}' + # Sometimes, the google api sends back abbreviated data : + # "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. + if len(formatted_from_html) >= len(formatted_manually): + standard_address['formatted_street_number'] = formatted_from_html + else: + standard_address['formatted_street_number'] = formatted_manually + return standard_address + + @http.route('/autocomplete/address', methods=['POST'], type='json', auth='public', website=True) + def _autocomplete_address(self, partial_address, session_id=None): + api_key = request.env['website'].get_current_website().sudo().google_places_api_key + return self._perform_place_search(partial_address, session_id=session_id, api_key=api_key) + + @http.route('/autocomplete/address_full', methods=['POST'], type='json', auth='public', website=True) + def _autocomplete_address_full(self, address, session_id=None, google_place_id=None, **kwargs): + api_key = request.env['website'].get_current_website().sudo().google_places_api_key + return self._perform_complete_place_search(address, google_place_id=google_place_id, + session_id=session_id, api_key=api_key, **kwargs) diff --git a/addons/website_sale_autocomplete/models/__init__.py b/addons/website_sale_autocomplete/models/__init__.py new file mode 100644 index 00000000000..1626b097a20 --- /dev/null +++ b/addons/website_sale_autocomplete/models/__init__.py @@ -0,0 +1,5 @@ +# -*- encoding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import website +from . import res_config_settings diff --git a/addons/website_sale_autocomplete/models/res_config_settings.py b/addons/website_sale_autocomplete/models/res_config_settings.py new file mode 100644 index 00000000000..13eea8d76de --- /dev/null +++ b/addons/website_sale_autocomplete/models/res_config_settings.py @@ -0,0 +1,10 @@ +from odoo import models, fields + + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + google_places_api_key = fields.Char( + string='Google Places API Key', + related='website_id.google_places_api_key', + readonly=False) diff --git a/addons/website_sale_autocomplete/models/website.py b/addons/website_sale_autocomplete/models/website.py new file mode 100644 index 00000000000..df33f20da68 --- /dev/null +++ b/addons/website_sale_autocomplete/models/website.py @@ -0,0 +1,11 @@ +# -*- encoding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from odoo import models, fields + + +class Website(models.Model): + _inherit = 'website' + + google_places_api_key = fields.Char( + string='Google Places API Key', + groups="base.group_system") diff --git a/addons/website_sale_autocomplete/static/src/img/powered_by_google_on_non_white.png b/addons/website_sale_autocomplete/static/src/img/powered_by_google_on_non_white.png new file mode 100644 index 0000000000000000000000000000000000000000..abe091388933c83693d8309dd9c0cff2ecd507ad GIT binary patch literal 1404 zcmV-?1%vvDP))&-Gw2b zgoDcK=4y&g@MEUQayN$Az5~qZF7F#`m5jFwhQ$Na3KL}6V!mV|%VC|fJ@R21ItJsq zNG#yN)jjaT3??=bgCqcZBniz-$oVxNdf3`Q39*v0hXCnoL`m3-GZN}JG^bk&;y1=M z6=(IleS`wF_&&2?Kb9(`VJSUsBA=HezHfs8GbsWyv4el|eKe8yhn?#TrZJn|GQPRk z#y^a&xx}B%nAZ@E3H-ufngxG6Kz{V$NBjk=v7dMG3BWIVtogr;6qo*- zJJ%rh!4<=N2hHdWXd8UaptyioA=sXW;qKOa#Jdp1{Gm8LBjF4mivP{Vbmnubq%@w$ z83||`J=!l}fYM3WfhZ88`I908uh@{#qp*Z;O8hi%z~LlnnKaoD#aAUh#}_SpIKtdU zEeXHg((BF-U-iOfS;X+opzjrvdks2=?mh7URSUM4T9D<#67*pS9OF%4kv$~L_uiiQ z3xGpb6lR1fL;pzVm4G%y;ybQZJ`B)@5@Jx#K2UriKqvWt)#AN4iY7{hBk184d|F9g z+qc(P%))9*!#d0ac&}04v3Jfk7K#Jt1BrhHeJ`7F?QoEHy06jcxPY+M*;Bm^vXC8> z1u@u;2I1AB+pt{1E*9oNR#WHuzOZo3^RH^ew&1r0i!V&4Ol^97Hj5O%;((tZj9NQ&T6{d zblqLQ49!0CKDrQ@l|sMsS*MM*5VkrR7}!0000< KMNUMnLSTX=_M(&k literal 0 HcmV?d00001 diff --git a/addons/website_sale_autocomplete/static/src/img/powered_by_google_on_white.png b/addons/website_sale_autocomplete/static/src/img/powered_by_google_on_white.png new file mode 100644 index 0000000000000000000000000000000000000000..43a153527476d0bef18d448092891bc0ada777ed GIT binary patch literal 2133 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 { + if (response.results) { + const inputContainer = ev.currentTarget.parentNode; + const dropdown = inputContainer.querySelector('.dropdown-menu'); + if (dropdown) { + dropdown.remove(); + } + inputContainer.appendChild($(QWeb.render("website_sale_autocomplete.AutocompleteDropDown", { + results: response.results + }))[0]); + } + if (response.session_id) { + this.sessionId = response.session_id; + } + } + ); + }, + + _onClickAutocompleteResult: async function(ev) { + const dropDown = ev.currentTarget.parentNode; + + const spinner = document.createElement('div'); + dropDown.innerText = ''; + dropDown.classList.add('d-flex', 'justify-content-center', 'align-items-center'); + spinner.classList.add('spinner-border', 'text-warning', 'text-center', 'm-auto'); + dropDown.appendChild(spinner); + + const address = await this._rpc({ + route: '/autocomplete/address_full', + params: { + address: ev.currentTarget.innerText, + google_place_id: ev.currentTarget.dataset.googlePlaceId, + session_id: this.sessionId || null + } + }); + if (address.formatted_street_number) { + this.streetAndNumberInput.value = address.formatted_street_number; + } + // Text fields, empty if no value in order to avoid the user missing old data. + this.zipInput.value = address.zip || ''; + this.cityInput.value = address.city || ''; + + // Selects based on odoo ids + if (address.country) { + this.countrySelect.value = address.country; + // Let the state select know that the country has changed so that it may fetch the correct states or disappear. + this.countrySelect.dispatchEvent(new Event('change', {bubbles: true})); + } + if (address.state) { + // Waits for the stateSelect to update before setting the state. + new MutationObserver((entries, observer) => { + this.stateSelect.value = address.state; + observer.disconnect(); + }).observe(this.stateSelect, { + childList: true, // Trigger only if the options change + }); + } + dropDown.remove(); + }, +}); diff --git a/addons/website_sale_autocomplete/static/src/xml/autocomplete.xml b/addons/website_sale_autocomplete/static/src/xml/autocomplete.xml new file mode 100644 index 00000000000..a16c42ffd52 --- /dev/null +++ b/addons/website_sale_autocomplete/static/src/xml/autocomplete.xml @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/addons/website_sale_autocomplete/static/tests/autocomplete_tour.js b/addons/website_sale_autocomplete/static/tests/autocomplete_tour.js new file mode 100644 index 00000000000..a749cca4798 --- /dev/null +++ b/addons/website_sale_autocomplete/static/tests/autocomplete_tour.js @@ -0,0 +1,71 @@ +/** @odoo-module */ + +import tour from 'web_tour.tour'; +import tourUtils from 'website_sale.tour_utils'; + + +function fail (errorMessage) { + tour._consume_tour(tour.running_tour, errorMessage); +} + +tour.register('autocomplete_tour', { + test: true, + url: '/shop', // /shop/address is redirected if no sales order +}, [{ + content: "search test product", + trigger: 'form input[name="search"]', + run: "text A test product", +},{ + content: 'Go to the product page', + trigger: '.dropdown-item:contains("A test product")' +}, { + content: 'Add to cart', + trigger: '#add_to_cart' +}, + tourUtils.goToCart(1), +{ + content: 'Go to process checkout', + trigger: 'a:contains("Process Checkout")' +}, { // Actual test + content: 'Input in Street & Number field', + trigger: 'input[name="street"]', + run: 'text This is a test' +}, { + content: 'Check if results have appeared', + trigger: '.js_autocomplete_result', + run: function () {} +}, { + content: 'Input again in street field', + trigger: 'input[name="street"]', + run: 'text add more' +}, { + content: 'Click on the first result', + trigger: '.js_autocomplete_result' +}, { + content: 'Verify the autocomplete box disappeared', + trigger: 'body:not(:has(.js_autocomplete_result))' +}, { // Verify test data has been input + content: 'Check Street & number have been set', + trigger: 'input[name="street"]', + run: function () { + if (this.$anchor.val() !== '42 A fictional Street') { + fail('Street value is not correct : ' + this.$anchor.val()) + } + } +}, { + content: 'Check City is not empty anymore', + trigger: 'input[name="city"]', + run: function () { + if (this.$anchor.val() !== 'A Fictional City') { + fail('Street value is not correct : ' + this.$anchor.val()) + } + } +}, { + content: 'Check Zip code is not empty anymore', + trigger: 'input[name="zip"]', + run: function () { + if (this.$anchor.val() !== '12345') { + fail('Street value is not correct : ' + this.$anchor.val()) + } + } +}]); diff --git a/addons/website_sale_autocomplete/tests/__init__.py b/addons/website_sale_autocomplete/tests/__init__.py new file mode 100644 index 00000000000..e732297c6a6 --- /dev/null +++ b/addons/website_sale_autocomplete/tests/__init__.py @@ -0,0 +1,4 @@ +# -*- encoding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import test_ui diff --git a/addons/website_sale_autocomplete/tests/test_ui.py b/addons/website_sale_autocomplete/tests/test_ui.py new file mode 100644 index 00000000000..08775d42dc0 --- /dev/null +++ b/addons/website_sale_autocomplete/tests/test_ui.py @@ -0,0 +1,40 @@ +# -*- encoding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from odoo.addons.website_sale_autocomplete.controllers.main import AutoCompleteController +from odoo.tests import patch, HttpCase, tagged + +CONTROLLER_PATH = 'odoo.addons.website_sale_autocomplete.controllers.main.AutoCompleteController' +MOCK_GOOGLE_ID = 'aHR0cHM6Ly93d3cueW91dHViZS5jb20vd2F0Y2g/dj1kUXc0dzlXZ1hjUQ==' +MOCK_API_KEY = 'Tm9ib2R5IGV4cGVjdHMgdGhlIFNwYW5pc2ggaW5xdWlzaXRpb24gIQ==' + + +@tagged('post_install', '-at_install') +class TestUI(HttpCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.product = cls.env['product.product'].create({ + 'name': 'A test product', + 'website_published': True, + 'list_price': 1 + }) + + def test_autocomplete(self): + with patch.object(AutoCompleteController, '_perform_complete_place_search', + lambda controller, *args, **kwargs: { + 'country': self.env['res.country'].search([('code', '=', 'USA')]).id, + 'state': self.env['res.country.state'].search([('country_id.code', '=', 'USA')])[0].id, + 'zip': '12345', + 'city': 'A Fictional City', + 'street': 'A fictional Street', + 'number': 42, + 'formatted_street_number': '42 A fictional Street' + }), \ + patch.object(AutoCompleteController, '_perform_place_search', + lambda controller, *args, **kwargs: { + 'results': [{ + 'formatted_address': f'Result {x}', + 'google_place_id': MOCK_GOOGLE_ID + } for x in range(5)]}): + self.env['website'].get_current_website().google_places_api_key = MOCK_API_KEY + self.start_tour('/shop/address', 'autocomplete_tour') diff --git a/addons/website_sale_autocomplete/views/res_config_settings_views.xml b/addons/website_sale_autocomplete/views/res_config_settings_views.xml new file mode 100644 index 00000000000..506074005e4 --- /dev/null +++ b/addons/website_sale_autocomplete/views/res_config_settings_views.xml @@ -0,0 +1,31 @@ + + + + + res.config.settings.view.form.inherit.autocomplete.googleplaces + res.config.settings + + + + + + + + + diff --git a/addons/website_sale_autocomplete/views/templates.xml b/addons/website_sale_autocomplete/views/templates.xml new file mode 100644 index 00000000000..91964079365 --- /dev/null +++ b/addons/website_sale_autocomplete/views/templates.xml @@ -0,0 +1,11 @@ + + + + +