From 06056069f7027b3ab0751bbe189c638439ccbdc3 Mon Sep 17 00:00:00 2001 From: geomer198 Date: Tue, 8 Aug 2023 23:49:18 +0300 Subject: [PATCH] [ADD] pos_partner_location_google_map: Module added. --- pos_partner_location_google_map/README.rst | 0 pos_partner_location_google_map/__init__.py | 1 + .../__manifest__.py | 18 +++ .../models/__init__.py | 3 + .../models/address_google_struct.py | 137 ++++++++++++++++++ .../models/base_geocoder.py | 26 ++++ .../models/pos_config.py | 23 +++ .../readme/CONFIGURATION.rst | 2 + .../readme/CONTRIBUTORS.rst | 1 + .../readme/DESCRIPTION.rst | 1 + .../readme/USAGE.rst | 3 + .../src/js/PartnerDetailsMapGoogleEdit.esm.js | 22 +++ .../static/src/js/PartnerMapGoogleEdit.esm.js | 113 +++++++++++++++ .../tests/__init__.py | 3 + .../tests/common.py | 62 ++++++++ .../tests/test_address_google_struct.py | 104 +++++++++++++ .../tests/test_base_geocoder.py | 37 +++++ .../addons/pos_partner_location_google_map | 1 + .../pos_partner_location_google_map/setup.py | 6 + 19 files changed, 563 insertions(+) create mode 100644 pos_partner_location_google_map/README.rst create mode 100644 pos_partner_location_google_map/__init__.py create mode 100644 pos_partner_location_google_map/__manifest__.py create mode 100644 pos_partner_location_google_map/models/__init__.py create mode 100644 pos_partner_location_google_map/models/address_google_struct.py create mode 100644 pos_partner_location_google_map/models/base_geocoder.py create mode 100644 pos_partner_location_google_map/models/pos_config.py create mode 100644 pos_partner_location_google_map/readme/CONFIGURATION.rst create mode 100644 pos_partner_location_google_map/readme/CONTRIBUTORS.rst create mode 100644 pos_partner_location_google_map/readme/DESCRIPTION.rst create mode 100644 pos_partner_location_google_map/readme/USAGE.rst create mode 100644 pos_partner_location_google_map/static/src/js/PartnerDetailsMapGoogleEdit.esm.js create mode 100644 pos_partner_location_google_map/static/src/js/PartnerMapGoogleEdit.esm.js create mode 100644 pos_partner_location_google_map/tests/__init__.py create mode 100644 pos_partner_location_google_map/tests/common.py create mode 100644 pos_partner_location_google_map/tests/test_address_google_struct.py create mode 100644 pos_partner_location_google_map/tests/test_base_geocoder.py create mode 120000 setup/pos_partner_location_google_map/odoo/addons/pos_partner_location_google_map create mode 100644 setup/pos_partner_location_google_map/setup.py diff --git a/pos_partner_location_google_map/README.rst b/pos_partner_location_google_map/README.rst new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pos_partner_location_google_map/__init__.py b/pos_partner_location_google_map/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/pos_partner_location_google_map/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/pos_partner_location_google_map/__manifest__.py b/pos_partner_location_google_map/__manifest__.py new file mode 100644 index 0000000000..ca5b51e45b --- /dev/null +++ b/pos_partner_location_google_map/__manifest__.py @@ -0,0 +1,18 @@ +{ + "name": "POS Partner Location Google Map", + "version": "16.0.1.0.0", + "category": "Point Of Sale", + "summary": "POS Partner Location Google Map", + "author": "Cetmix, Odoo Community Association (OCA)", + "maintainers": ["geomer198", "CetmixGitDrone"], + "website": "https://github.com/OCA/pos", + "license": "AGPL-3", + "depends": ["pos_partner_location_abstract"], + "data": [], + "assets": { + "point_of_sale.assets": [ + "pos_partner_location_google_map/static/src/js/*.js", + ], + }, + "installable": True, +} diff --git a/pos_partner_location_google_map/models/__init__.py b/pos_partner_location_google_map/models/__init__.py new file mode 100644 index 0000000000..a8858868ce --- /dev/null +++ b/pos_partner_location_google_map/models/__init__.py @@ -0,0 +1,3 @@ +from . import address_google_struct +from . import base_geocoder +from . import pos_config diff --git a/pos_partner_location_google_map/models/address_google_struct.py b/pos_partner_location_google_map/models/address_google_struct.py new file mode 100644 index 0000000000..7461158eca --- /dev/null +++ b/pos_partner_location_google_map/models/address_google_struct.py @@ -0,0 +1,137 @@ +from odoo.osv.expression import AND, OR + +from odoo.addons.pos_partner_location_abstract.models.address_struct import ( + AddressStruct, +) + + +class AddressGoogleStruct(AddressStruct): + ADDR_FIELDS = { + "number": ["street_number"], + "street": ["street_address", "route", "plus_code"], + "city": [ + "locality", + "sublocality", + "sublocality_level_1", + "sublocality_level_2", + "sublocality_level_3", + "sublocality_level_4", + ], + "state_id": [ + "administrative_area_level_1", + "administrative_area_level_2", + "administrative_area_level_3", + "administrative_area_level_4", + "administrative_area_level_5", + ], + "country_id": ["country"], + "zip": ["postal_code"], + } + SERVICE_URL = "https://maps.googleapis.com/maps/api/place/details/json" + + def __init__(self, odoo_env): + super(AddressGoogleStruct, self).__init__(odoo_env) + self.api_key = ( + self.env["ir.config_parameter"] + .sudo() + .get_param("base_geolocalize.google_map_api_key", False) + ) + + def has_token(self): + """Checking exists Google API key in settings""" + return bool(self.api_key) + + def _get_fields_value(self, addr_key): + """ + Get fields value for preparing values address + :param str addr_key: field name + :return list: list of fields values + """ + return [self._result.get(key, False) for key in self.ADDR_FIELDS[addr_key]] + + @property + def street(self): + street_number = self._result.get("street_number") + result = [] + if street_number: + result.append(street_number) + for address in self._get_fields_value("street"): + if address: + result.append(address) + return " ".join(result) + return "" + + @property + def city(self): + for city in self._get_fields_value("city"): + if city: + return city + return False + + @property + def state_id(self): + domain = [] + codes = [] + for state in self._get_fields_value("state_id"): + if state: + domain = OR([domain, [("name", "like", state.get("name"))]]) + codes.append(state.get("code")) + if codes: + domain = OR([domain, [("code", "in", codes)]]) + if len(domain) == 0: + return False + country_id = self.country_id + if country_id: + domain = AND([domain, [("country_id", "=", country_id[0])]]) + state = self.env["res.country.state"].search(domain, limit=1) + return state.name_get()[0] if state else False + + @property + def country_id(self): + country_item = self._result.get("country") + if not country_item: + return False + country = self.env["res.country"].search( + [ + "|", + ("name", "like", country_item.get("name")), + ("code", "=", country_item.get("code")), + ] + ) + return country.name_get()[0] if country else False + + @property + def zip(self): + return self._result.get("postal_code") + + def query_addr(self, params, timeout=5): + params.update(key=self.api_key) + response = super(AddressGoogleStruct, self).query_addr(params, timeout=timeout) + if response: + if response.get("status") == "OK": + self._prepare_components_data(response["result"]["address_components"]) + return True + return False + + def _prepare_components_data(self, components): + """ + Preparing component values for class result + :param dict components: list of components + :return: None + :rtype: NoneType + """ + state_country_fields = [ + *self.ADDR_FIELDS.get("state_id", []), + *self.ADDR_FIELDS.get("country_id", []), + ] + for component in components: + for type_ in component["types"]: + if type_ in state_country_fields: + self._result[type_] = { + "code": component["short_name"], + "name": component["long_name"], + } + elif type_ in self.ADDR_FIELDS.get("street", []): + self._result[type_] = component["short_name"] + else: + self._result[type_] = component["long_name"] diff --git a/pos_partner_location_google_map/models/base_geocoder.py b/pos_partner_location_google_map/models/base_geocoder.py new file mode 100644 index 0000000000..2657c3a2f1 --- /dev/null +++ b/pos_partner_location_google_map/models/base_geocoder.py @@ -0,0 +1,26 @@ +from odoo import _, api, models + +from .address_google_struct import AddressGoogleStruct + + +class GeoProvider(models.AbstractModel): + _inherit = "base.geocoder" + + @api.model + def prepare_geo_address_googlemap(self, place_id): + """ + Prepare Address values by place id + :param str place_id: Google map place id + :return dict: address fields values + """ + google = AddressGoogleStruct(self.env) + if not google.has_token(): + raise models.UserError( + _( + "API key for GeoCoding (Places) required.\n" + "Visit https://developers.google.com/maps/documentation/geocoding/get-api-key " # noqa + "for more information." + ) + ) + status = google.query_addr({"place_id": place_id}) + return google.get_result() if status else {} diff --git a/pos_partner_location_google_map/models/pos_config.py b/pos_partner_location_google_map/models/pos_config.py new file mode 100644 index 0000000000..19801c22c5 --- /dev/null +++ b/pos_partner_location_google_map/models/pos_config.py @@ -0,0 +1,23 @@ +from odoo import api, fields, models + + +class PosConfig(models.Model): + _inherit = "pos.config" + + googlemap_api_key = fields.Char(compute="_compute_geolocalize") + + @api.model + def _set_extended_data(self): + data = super(PosConfig, self)._set_extended_data() + ICPSudo = self.env["ir.config_parameter"].sudo() + data.update( + api_key=ICPSudo.get_param("base_geolocalize.google_map_api_key", False) + ) + return data + + def _set_pos_config_parameter(self, tech_name, ext_vals=None): + super(PosConfig, self)._set_pos_config_parameter(tech_name, ext_vals) + key = ext_vals.get("api_key", "") if tech_name == "googlemap" else "" + for config in self: + config.googlemap_api_key = key + return diff --git a/pos_partner_location_google_map/readme/CONFIGURATION.rst b/pos_partner_location_google_map/readme/CONFIGURATION.rst new file mode 100644 index 0000000000..85f8f88f73 --- /dev/null +++ b/pos_partner_location_google_map/readme/CONFIGURATION.rst @@ -0,0 +1,2 @@ +In General Settings -> Integrations enable the Geo Localisation checkbox. +Select provided and add API key. NB: only google maps are currently supported. diff --git a/pos_partner_location_google_map/readme/CONTRIBUTORS.rst b/pos_partner_location_google_map/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..9d697ca0a3 --- /dev/null +++ b/pos_partner_location_google_map/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Cetmix diff --git a/pos_partner_location_google_map/readme/DESCRIPTION.rst b/pos_partner_location_google_map/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..a1a3ee0d59 --- /dev/null +++ b/pos_partner_location_google_map/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This module allows to select partner address directly on map. diff --git a/pos_partner_location_google_map/readme/USAGE.rst b/pos_partner_location_google_map/readme/USAGE.rst new file mode 100644 index 0000000000..4e88eb8c07 --- /dev/null +++ b/pos_partner_location_google_map/readme/USAGE.rst @@ -0,0 +1,3 @@ +In POS open customer list, select a customer and click "Details". +On the customer form click on the "globe" icon and select a location on map. +Click "Save" to save the location. diff --git a/pos_partner_location_google_map/static/src/js/PartnerDetailsMapGoogleEdit.esm.js b/pos_partner_location_google_map/static/src/js/PartnerDetailsMapGoogleEdit.esm.js new file mode 100644 index 0000000000..f3dd3b9bb6 --- /dev/null +++ b/pos_partner_location_google_map/static/src/js/PartnerDetailsMapGoogleEdit.esm.js @@ -0,0 +1,22 @@ +/** @odoo-module **/ + +import PartnerDetailsEdit from "point_of_sale.PartnerDetailsEdit"; +import Registries from "point_of_sale.Registries"; + +const PartnerDetailsMapGoogleEdit = (PartnerDetailsEdit) => + class PartnerDetailsMapGoogleEdit extends PartnerDetailsEdit { + get accessToMap() { + this.config = this.env.pos.config; + if ( + this.config.geolocalize_tech_name === "googlemap" && + this.config.googlemap_api_key + ) { + return true; + } + return super.accessToMap; + } + }; + +Registries.Component.extend(PartnerDetailsEdit, PartnerDetailsMapGoogleEdit); + +export default PartnerDetailsMapGoogleEdit; diff --git a/pos_partner_location_google_map/static/src/js/PartnerMapGoogleEdit.esm.js b/pos_partner_location_google_map/static/src/js/PartnerMapGoogleEdit.esm.js new file mode 100644 index 0000000000..e5ad67222f --- /dev/null +++ b/pos_partner_location_google_map/static/src/js/PartnerMapGoogleEdit.esm.js @@ -0,0 +1,113 @@ +/** @odoo-module **/ + +import PartnerMapEdit from "pos_partner_location_abstract.PartnerMapEdit"; +import Registries from "point_of_sale.Registries"; +import {loadJS} from "@web/core/assets"; +import {onMounted, onWillStart} from "@odoo/owl"; + +/* eslint no-undef: "warn"*/ +const PartnerMapGoogleEdit = (PartnerMapEdit) => + class PartnerMapGoogleEdit extends PartnerMapEdit { + onHandleMap() { + if ( + this.config.geolocalize_tech_name === "googlemap" && + this.config.googlemap_api_key + ) { + this.provider = "googlemap"; + onWillStart(async () => + loadJS( + `https://maps.googleapis.com/maps/api/js?key=${this.config.googlemap_api_key}&libraries=places` + ) + ); + onMounted(() => this.googleMapConfigure()); + } else { + super.onHandleMap(); + } + } + + googleMapConfigure() { + // Default latLng + // Config + this.geocoder = new google.maps.Geocoder(); + const latLng = new google.maps.LatLng(this.lat, this.lng); + const mapOptions = { + zoom: 12, + center: latLng, + }; + // Show Map + this.map = new google.maps.Map(this.mapContainerRef.el, mapOptions); + + if (this.lat && this.lng) { + this.setAddressByLatLng(this.lat, this.lng); + } else { + this.setAddressByLocation(this.partner.contact_address); + } + + this.marker = new google.maps.Marker({ + position: latLng, + map: this.map, + draggable: true, + }); + + this.addrInput.el.value = this.partner.contact_address; + + this.map.addListener("click", (event) => { + const lat = event.latLng.lat(); + const lng = event.latLng.lng(); + this.update_marker(lat, lng); + this.setAddressByLatLng(lat, lng); + }); + } + + setAddressByLatLng(lat, lng) { + if (lat && lng) { + const latLng = new google.maps.LatLng(lat, lng); + this.geocoder.geocode({location: latLng}, (results, status) => { + if (status === google.maps.GeocoderStatus.OK) { + this.getFormattedAddress(results[0].place_id); + this.addrInput.el.value = results[0].formatted_address; + } + }); + } + } + + update_marker(lat, lng) { + super.update_marker(lat, lng); + if (this.provider === "googlemap") { + const latLng = new google.maps.LatLng(lat, lng); + this.map.setCenter(latLng); + this.marker.setPosition(latLng); + google.maps.event.trigger(this.map, "resize"); + } + } + + setAddressByLocation(address) { + if (address && this.provider === "googlemap") { + this.geocoder.geocode({address: address}, (results, status) => { + if (status === google.maps.GeocoderStatus.OK) { + this.lat = results[0].geometry.location.lat(); + this.lng = results[0].geometry.location.lng(); + this.getFormattedAddress(results[0].place_id); + this.addrInput.el.value = results[0].formatted_address; + this.update_marker(this.lat, this.lng); + } + }); + } else { + super.setAddressByLocation(address); + } + } + + getFormattedAddress(place_id) { + this.rpc({ + model: "base.geocoder", + method: "prepare_geo_address_googlemap", + args: [place_id], + }).then((resp) => { + this.address = resp; + }); + } + }; + +Registries.Component.extend(PartnerMapEdit, PartnerMapGoogleEdit); + +export default PartnerMapGoogleEdit; diff --git a/pos_partner_location_google_map/tests/__init__.py b/pos_partner_location_google_map/tests/__init__.py new file mode 100644 index 0000000000..c7a0ef4b65 --- /dev/null +++ b/pos_partner_location_google_map/tests/__init__.py @@ -0,0 +1,3 @@ +from . import common +from . import test_address_google_struct +from . import test_base_geocoder diff --git a/pos_partner_location_google_map/tests/common.py b/pos_partner_location_google_map/tests/common.py new file mode 100644 index 0000000000..c950b1996d --- /dev/null +++ b/pos_partner_location_google_map/tests/common.py @@ -0,0 +1,62 @@ +RESPONSE_MAP = { + "result": { + "address_components": [ + {"long_name": "277", "short_name": "277", "types": ["street_number"]}, + { + "long_name": "Bedford Avenue", + "short_name": "Bedford Ave", + "types": ["route"], + }, + { + "long_name": "Williamsburg", + "short_name": "Williamsburg", + "types": ["neighborhood", "political"], + }, + { + "long_name": "Brooklyn", + "short_name": "Brooklyn", + "types": ["sublocality", "political"], + }, + { + "long_name": "Kings", + "short_name": "Kings", + "types": ["administrative_area_level_2", "political"], + }, + { + "long_name": "New York", + "short_name": "NY", + "types": ["administrative_area_level_1", "political"], + }, + { + "long_name": "United States", + "short_name": "US", + "types": ["country", "political"], + }, + {"long_name": "11211", "short_name": "11211", "types": ["postal_code"]}, + ], + "formatted_address": "277 Bedford Avenue, Brooklyn, NY 11211, USA", + "geometry": { + "location": {"lat": 40.714232, "lng": -73.9612889}, + "location_type": "ROOFTOP", + "viewport": { + "northeast": {"lat": 40.7155809802915, "lng": -73.9599399197085}, + "southwest": {"lat": 40.7128830197085, "lng": -73.96263788029151}, + }, + }, + "place_id": "ChIJd8BlQ2BZwokRAFUEcm_qrcA", + "types": ["street_address"], + }, + "status": "OK", +} + +FORMATTED_RESULT = { + "street_number": "277", + "route": "Bedford Ave", + "neighborhood": "Williamsburg", + "political": "United States", + "sublocality": "Brooklyn", + "administrative_area_level_2": {"code": "Kings", "name": "Kings"}, + "administrative_area_level_1": {"code": "NY", "name": "New York"}, + "country": {"code": "US", "name": "United States"}, + "postal_code": "11211", +} diff --git a/pos_partner_location_google_map/tests/test_address_google_struct.py b/pos_partner_location_google_map/tests/test_address_google_struct.py new file mode 100644 index 0000000000..5416322e7e --- /dev/null +++ b/pos_partner_location_google_map/tests/test_address_google_struct.py @@ -0,0 +1,104 @@ +from unittest import mock + +from odoo.tests import TransactionCase + +from odoo.addons.pos_partner_location_google_map.models.address_google_struct import ( + AddressGoogleStruct, +) + +from .common import FORMATTED_RESULT, RESPONSE_MAP + + +class TestAddressGoogleStruct(TransactionCase): + def setUp(self): + super(TestAddressGoogleStruct, self).setUp() + self.env["ir.config_parameter"].set_param( + "base_geolocalize.google_map_api_key", "GoogleMapKey" + ) + self.state_ny = self.env["res.country.state"].search( + [("code", "=", "NY")], limit=1 + ) + self.country_us = self.env["res.country"].search([("code", "=", "US")], limit=1) + self.google_struct = AddressGoogleStruct(self.env) + + def test_init(self): + self.assertEqual(self.google_struct.api_key, "GoogleMapKey") + + def test_has_token(self): + self.env["ir.config_parameter"].set_param( + "base_geolocalize.google_map_api_key", "" + ) + google = AddressGoogleStruct(self.env) + self.assertFalse(google.has_token()) + self.env["ir.config_parameter"].set_param( + "base_geolocalize.google_map_api_key", "GoogleMapKey" + ) + google = AddressGoogleStruct(self.env) + self.assertTrue(google.has_token()) + + def test_get_fields_value(self): + result = self.google_struct._get_fields_value("number") + self.assertListEqual(result, [False]) + result = self.google_struct._get_fields_value("street") + self.assertListEqual(result, [False, False, False]) + + @mock.patch("requests.get") + def test_query_addr_valid(self, mock_get): + mock_response = mock.Mock(status_code=200) + mock_response.json.return_value = { + "result": {"address_components": []}, + "status": "OK", + } + mock_get.return_value = mock_response + response = self.google_struct.query_addr({}) + self.assertTrue(response) + + @mock.patch("requests.get") + def test_query_addr_invalid(self, mock_get): + mock_response = mock.Mock(status_code=200) + mock_response.json.return_value = { + "result": {"address_components": []}, + "status": "ERR", + } + mock_get.return_value = mock_response + response = self.google_struct.query_addr({}) + self.assertFalse(response) + + def test_street(self): + self.assertEqual(self.google_struct.street, "") + self.google_struct._result.update( + street_number="1", street_address="Test Address" + ) + self.assertEqual(self.google_struct.street, "1 Test Address") + + def test_city(self): + self.assertFalse(self.google_struct.city) + self.google_struct._result.update(sublocality="Tallin") + self.assertEqual(self.google_struct.city, "Tallin") + + def test_zip(self): + self.assertFalse(self.google_struct.zip) + self.google_struct._result.update(postal_code="524124") + self.assertEqual(self.google_struct.zip, "524124") + + def test_prepare_components_data(self): + self.google_struct._prepare_components_data( + RESPONSE_MAP["result"]["address_components"] + ) + self.assertDictEqual(self.google_struct._result, FORMATTED_RESULT) + + def test_country_id(self): + self.assertFalse(self.google_struct.country_id) + self.google_struct._result.update( + country={"code": "US", "name": "United States"} + ) + self.assertEqual(self.google_struct.country_id, self.country_us.name_get()[0]) + + def test_state_id(self): + self.assertFalse(self.google_struct.state_id) + self.google_struct._result.update( + country={"code": "US", "name": "United States"}, + administrative_area_level_2={"code": "Kings", "name": "Kings"}, + administrative_area_level_1={"code": "NY", "name": "New York"}, + ) + self.assertEqual(self.google_struct.state_id, self.state_ny.name_get()[0]) diff --git a/pos_partner_location_google_map/tests/test_base_geocoder.py b/pos_partner_location_google_map/tests/test_base_geocoder.py new file mode 100644 index 0000000000..455b56a34d --- /dev/null +++ b/pos_partner_location_google_map/tests/test_base_geocoder.py @@ -0,0 +1,37 @@ +from unittest import mock + +from odoo.exceptions import UserError +from odoo.tests import TransactionCase + +from .common import RESPONSE_MAP + + +class TestBaseGeocoder(TransactionCase): + def test_prepare_geo_address_googlemap_invalid(self): + self.env["ir.config_parameter"].set_param( + "base_geolocalize.google_map_api_key", "" + ) + with self.assertRaises(UserError): + self.env["base.geocoder"].prepare_geo_address_googlemap("test_place") + + @mock.patch("requests.get") + def test_prepare_geo_address_googlemap_valid(self, mock_get): + self.env["ir.config_parameter"].set_param( + "base_geolocalize.google_map_api_key", "GoogleMapKey" + ) + self.state_ny = self.env["res.country.state"].search( + [("code", "=", "NY")], limit=1 + ) + self.country_us = self.env["res.country"].search([("code", "=", "US")], limit=1) + mock_response = mock.Mock(status_code=200) + mock_response.json.return_value = RESPONSE_MAP + mock_get.return_value = mock_response + response = self.env["base.geocoder"].prepare_geo_address_googlemap("test_place") + expected_value = { + "city": "Brooklyn", + "country_id": self.country_us.name_get()[0], + "state_id": self.state_ny.name_get()[0], + "street": "277 Bedford Ave", + "zip": "11211", + } + self.assertDictEqual(response, expected_value) diff --git a/setup/pos_partner_location_google_map/odoo/addons/pos_partner_location_google_map b/setup/pos_partner_location_google_map/odoo/addons/pos_partner_location_google_map new file mode 120000 index 0000000000..3ce50cfe7d --- /dev/null +++ b/setup/pos_partner_location_google_map/odoo/addons/pos_partner_location_google_map @@ -0,0 +1 @@ +../../../../pos_partner_location_google_map \ No newline at end of file diff --git a/setup/pos_partner_location_google_map/setup.py b/setup/pos_partner_location_google_map/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/pos_partner_location_google_map/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)