diff --git a/web_view_google_map/README.rst b/web_view_google_map/README.rst new file mode 100644 index 000000000..0ba0e9c9a --- /dev/null +++ b/web_view_google_map/README.rst @@ -0,0 +1,165 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +=============== +Google Map View +=============== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:9a53bf4aef9d09814005307b9a3ba76588d68994ec466f879a4d0d0aa4350451 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fgeospatial-lightgray.png?logo=github + :target: https://github.com/OCA/geospatial/tree/19.0/web_view_google_map + :alt: OCA/geospatial +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/geospatial-19-0/geospatial-19-0-web_view_google_map + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/geospatial&target_branch=19.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module adds a Google Maps view type and Places autocomplete field +widgets to the Odoo web client. + +Features: + +- ``google_map`` view mode to display geolocated records on Google Maps +- Marker colors from view attributes (``color`` / ``colors``) +- Marker clustering +- Field widgets ``gplaces_autocomplete`` and ``gplaces_address_form`` + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +1. Install **Google Maps Integration** (``base_google_map``). +2. Go to *Settings → Google Maps Integration*. +3. Set your Google Maps API key and enable the **Places** / **Geometry** + libraries as needed. +4. Restrict the key in Google Cloud Console to your Odoo origins and the + Maps JavaScript API / Places API. + +Usage +===== + +Google Map view +--------------- + +Add a ``google_map`` view on any model that stores latitude/longitude: + +.. code:: xml + + + res.partner.google_map + res.partner + + + + + + + + + + +Include ``google_map`` in the action ``view_mode``, for example +``list,form,google_map``. + +Contacts already expose a Google Map mode on the partner action. + +Places widgets +-------------- + +.. code:: xml + + + + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Open Source Integrators +* Gray Matter Logic + +Contributors +------------ + +- Yopi Angi +- Wolfgang Hall +- Serpent Consulting Services Pvt. Ltd. +- Ammar Officewala +- `Gray Matter Logic `__: + + - Maxime Chambreuil + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-gityopie| image:: https://github.com/gityopie.png?size=40px + :target: https://github.com/gityopie + :alt: gityopie +.. |maintainer-wolfhall| image:: https://github.com/wolfhall.png?size=40px + :target: https://github.com/wolfhall + :alt: wolfhall + +Current `maintainers `__: + +|maintainer-gityopie| |maintainer-wolfhall| + +This module is part of the `OCA/geospatial `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/web_view_google_map/__init__.py b/web_view_google_map/__init__.py new file mode 100644 index 000000000..3440e1e0e --- /dev/null +++ b/web_view_google_map/__init__.py @@ -0,0 +1,4 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import models +from .hooks import uninstall_hook diff --git a/web_view_google_map/__manifest__.py b/web_view_google_map/__manifest__.py new file mode 100644 index 000000000..f1f6719fc --- /dev/null +++ b/web_view_google_map/__manifest__.py @@ -0,0 +1,36 @@ +# Copyright (C) 2019, Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Google Map View", + "summary": "Add a Google Map view type to the Odoo web client", + "version": "19.0.1.0.0", + "author": "Open Source Integrators, Gray Matter Logic, " + "Odoo Community Association (OCA)", + "website": "https://github.com/OCA/geospatial", + "license": "AGPL-3", + "category": "Extra Tools", + "depends": ["base_google_map", "contacts"], + "data": ["views/res_partner.xml"], + "images": ["static/description/thumbnails.png"], + "assets": { + "web.assets_backend": [ + "web_view_google_map/static/lib/markerclusterer/markerclusterer.min.js", + "web_view_google_map/static/src/views/google_map/map_themes.esm.js", + "web_view_google_map/static/src/views/google_map/google_maps_loader.esm.js", + "web_view_google_map/static/src/views/google_map/google_map_renderer.scss", + "web_view_google_map/static/src/views/google_map/google_map_renderer.esm.js", + "web_view_google_map/static/src/views/google_map/google_map_renderer.xml", + "web_view_google_map/static/src/views/google_map/google_map_controller.esm.js", + "web_view_google_map/static/src/views/google_map/google_map_controller.xml", + "web_view_google_map/static/src/views/google_map/google_map_view.esm.js", + "web_view_google_map/static/src/fields/gplaces_autocomplete.esm.js", + "web_view_google_map/static/src/fields/gplaces_autocomplete.xml", + "web_view_google_map/static/src/fields/gplaces_address_form.esm.js", + "web_view_google_map/static/src/fields/gplaces_address_form.xml", + ], + }, + "installable": True, + "uninstall_hook": "uninstall_hook", + "maintainers": ["gityopie", "wolfhall"], +} diff --git a/web_view_google_map/hooks.py b/web_view_google_map/hooks.py new file mode 100644 index 000000000..ce4ebe175 --- /dev/null +++ b/web_view_google_map/hooks.py @@ -0,0 +1,16 @@ +# Copyright (C) 2019, Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + + +def uninstall_hook(env): + env.cr.execute( + "UPDATE ir_act_window " + "SET view_mode=replace(view_mode, ',google_map', '') " + "WHERE view_mode LIKE '%,google_map%'" + ) + env.cr.execute( + "UPDATE ir_act_window " + "SET view_mode=replace(view_mode, 'google_map,', '') " + "WHERE view_mode LIKE '%google_map,%'" + ) + env.cr.execute("DELETE FROM ir_act_window WHERE view_mode = 'google_map'") diff --git a/web_view_google_map/i18n/it.po b/web_view_google_map/i18n/it.po new file mode 100644 index 000000000..54b41b2c8 --- /dev/null +++ b/web_view_google_map/i18n/it.po @@ -0,0 +1,93 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * web_view_google_map +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 19.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2025-05-20 17:26+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.10.4\n" + +#. module: web_view_google_map +#. openerp-web +#: code:addons/web_view_google_map/static/src/js/widgets/gplaces_autocomplete.js:0 +#: code:addons/web_view_google_map/static/src/js/widgets/gplaces_autocomplete.js:0 +#, python-format +msgid "
" +msgstr "" + +#. module: web_view_google_map +#: model:ir.model,name:web_view_google_map.model_ir_actions_act_window_view +msgid "Action Window View" +msgstr "" + +#. module: web_view_google_map +#: model:ir.model.fields,field_description:web_view_google_map.field_ir_actions_act_window_view__display_name +#: model:ir.model.fields,field_description:web_view_google_map.field_ir_ui_view__display_name +msgid "Display Name" +msgstr "" + +#. module: web_view_google_map +#. openerp-web +#: code:addons/web_view_google_map/static/src/js/view/map/map_view.js:0 +#: model_terms:ir.ui.view,arch_db:web_view_google_map.view_res_partner_google_map +#, python-format +msgid "Google Map" +msgstr "" + +#. module: web_view_google_map +#: model:ir.model.fields.selection,name:web_view_google_map.selection__ir_actions_act_window_view__view_mode__google_map +#: model:ir.model.fields.selection,name:web_view_google_map.selection__ir_ui_view__type__google_map +msgid "Google Maps" +msgstr "" + +#. module: web_view_google_map +#: model:ir.model.fields,field_description:web_view_google_map.field_ir_actions_act_window_view__id +#: model:ir.model.fields,field_description:web_view_google_map.field_ir_ui_view__id +msgid "ID" +msgstr "ID" + +#. module: web_view_google_map +#: model:ir.model.fields,field_description:web_view_google_map.field_ir_actions_act_window_view____last_update +#: model:ir.model.fields,field_description:web_view_google_map.field_ir_ui_view____last_update +msgid "Last Modified on" +msgstr "" + +#. module: web_view_google_map +#. openerp-web +#: code:addons/web_view_google_map/static/src/js/view/map/map_renderer.js:0 +#, python-format +msgid "Styled Map" +msgstr "" + +#. module: web_view_google_map +#. openerp-web +#: code:addons/web_view_google_map/static/src/js/widgets/gplaces_autocomplete.js:0 +#: code:addons/web_view_google_map/static/src/js/widgets/gplaces_autocomplete.js:0 +#, python-format +msgid "The following fields are invalid:" +msgstr "" + +#. module: web_view_google_map +#: model:ir.model,name:web_view_google_map.model_ir_ui_view +msgid "View" +msgstr "" + +#. module: web_view_google_map +#: model:ir.model.fields,field_description:web_view_google_map.field_ir_actions_act_window_view__view_mode +#: model:ir.model.fields,field_description:web_view_google_map.field_ir_ui_view__type +msgid "View Type" +msgstr "" + +#. module: web_view_google_map +#: model_terms:ir.ui.view,arch_db:web_view_google_map.view_res_partner_google_map +msgid "at" +msgstr "" diff --git a/web_view_google_map/i18n/web_view_google_map.pot b/web_view_google_map/i18n/web_view_google_map.pot new file mode 100644 index 000000000..a94335bcc --- /dev/null +++ b/web_view_google_map/i18n/web_view_google_map.pot @@ -0,0 +1,90 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * web_view_google_map +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 19.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: web_view_google_map +#. openerp-web +#: code:addons/web_view_google_map/static/src/js/widgets/gplaces_autocomplete.js:0 +#: code:addons/web_view_google_map/static/src/js/widgets/gplaces_autocomplete.js:0 +#, python-format +msgid "
" +msgstr "" + +#. module: web_view_google_map +#: model:ir.model,name:web_view_google_map.model_ir_actions_act_window_view +msgid "Action Window View" +msgstr "" + +#. module: web_view_google_map +#: model:ir.model.fields,field_description:web_view_google_map.field_ir_actions_act_window_view__display_name +#: model:ir.model.fields,field_description:web_view_google_map.field_ir_ui_view__display_name +msgid "Display Name" +msgstr "" + +#. module: web_view_google_map +#. openerp-web +#: code:addons/web_view_google_map/static/src/js/view/map/map_view.js:0 +#: model_terms:ir.ui.view,arch_db:web_view_google_map.view_res_partner_google_map +#, python-format +msgid "Google Map" +msgstr "" + +#. module: web_view_google_map +#: model:ir.model.fields.selection,name:web_view_google_map.selection__ir_actions_act_window_view__view_mode__google_map +#: model:ir.model.fields.selection,name:web_view_google_map.selection__ir_ui_view__type__google_map +msgid "Google Maps" +msgstr "" + +#. module: web_view_google_map +#: model:ir.model.fields,field_description:web_view_google_map.field_ir_actions_act_window_view__id +#: model:ir.model.fields,field_description:web_view_google_map.field_ir_ui_view__id +msgid "ID" +msgstr "" + +#. module: web_view_google_map +#: model:ir.model.fields,field_description:web_view_google_map.field_ir_actions_act_window_view____last_update +#: model:ir.model.fields,field_description:web_view_google_map.field_ir_ui_view____last_update +msgid "Last Modified on" +msgstr "" + +#. module: web_view_google_map +#. openerp-web +#: code:addons/web_view_google_map/static/src/js/view/map/map_renderer.js:0 +#, python-format +msgid "Styled Map" +msgstr "" + +#. module: web_view_google_map +#. openerp-web +#: code:addons/web_view_google_map/static/src/js/widgets/gplaces_autocomplete.js:0 +#: code:addons/web_view_google_map/static/src/js/widgets/gplaces_autocomplete.js:0 +#, python-format +msgid "The following fields are invalid:" +msgstr "" + +#. module: web_view_google_map +#: model:ir.model,name:web_view_google_map.model_ir_ui_view +msgid "View" +msgstr "" + +#. module: web_view_google_map +#: model:ir.model.fields,field_description:web_view_google_map.field_ir_actions_act_window_view__view_mode +#: model:ir.model.fields,field_description:web_view_google_map.field_ir_ui_view__type +msgid "View Type" +msgstr "" + +#. module: web_view_google_map +#: model_terms:ir.ui.view,arch_db:web_view_google_map.view_res_partner_google_map +msgid "at" +msgstr "" diff --git a/web_view_google_map/models/__init__.py b/web_view_google_map/models/__init__.py new file mode 100644 index 000000000..74a0f227e --- /dev/null +++ b/web_view_google_map/models/__init__.py @@ -0,0 +1,4 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import ir_act_window_view +from . import ir_ui_view diff --git a/web_view_google_map/models/ir_act_window_view.py b/web_view_google_map/models/ir_act_window_view.py new file mode 100644 index 000000000..eb609bc98 --- /dev/null +++ b/web_view_google_map/models/ir_act_window_view.py @@ -0,0 +1,13 @@ +# Copyright (C) 2019, Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class IrActionsActWindowView(models.Model): + _inherit = "ir.actions.act_window.view" + + view_mode = fields.Selection( + selection_add=[("google_map", "Google Maps")], + ondelete={"google_map": "cascade"}, + ) diff --git a/web_view_google_map/models/ir_ui_view.py b/web_view_google_map/models/ir_ui_view.py new file mode 100644 index 000000000..fb0d6bd61 --- /dev/null +++ b/web_view_google_map/models/ir_ui_view.py @@ -0,0 +1,13 @@ +# Copyright (C) 2019, Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class IrUiView(models.Model): + _inherit = "ir.ui.view" + + type = fields.Selection(selection_add=[("google_map", "Google Maps")]) + + def _get_view_info(self): + return {"google_map": {"icon": "fa fa-map-o"}} | super()._get_view_info() diff --git a/web_view_google_map/pyproject.toml b/web_view_google_map/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/web_view_google_map/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/web_view_google_map/readme/CONFIGURE.md b/web_view_google_map/readme/CONFIGURE.md new file mode 100644 index 000000000..390056de9 --- /dev/null +++ b/web_view_google_map/readme/CONFIGURE.md @@ -0,0 +1,6 @@ +1. Install **Google Maps Integration** (`base_google_map`). +2. Go to *Settings → Google Maps Integration*. +3. Set your Google Maps API key and enable the **Places** / **Geometry** + libraries as needed. +4. Restrict the key in Google Cloud Console to your Odoo origins and the + Maps JavaScript API / Places API. diff --git a/web_view_google_map/readme/CONTRIBUTORS.md b/web_view_google_map/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..231684033 --- /dev/null +++ b/web_view_google_map/readme/CONTRIBUTORS.md @@ -0,0 +1,6 @@ +- Yopi Angi \<\> +- Wolfgang Hall \<\> +- Serpent Consulting Services Pvt. Ltd. \<\> +- Ammar Officewala \<\> +- [Gray Matter Logic](https://www.graymatterlogic.com): + - Maxime Chambreuil \<\> diff --git a/web_view_google_map/readme/DESCRIPTION.md b/web_view_google_map/readme/DESCRIPTION.md new file mode 100644 index 000000000..1c4cedba1 --- /dev/null +++ b/web_view_google_map/readme/DESCRIPTION.md @@ -0,0 +1,9 @@ +This module adds a Google Maps view type and Places autocomplete field +widgets to the Odoo web client. + +Features: + +- `google_map` view mode to display geolocated records on Google Maps +- Marker colors from view attributes (`color` / `colors`) +- Marker clustering +- Field widgets `gplaces_autocomplete` and `gplaces_address_form` diff --git a/web_view_google_map/readme/USAGE.md b/web_view_google_map/readme/USAGE.md new file mode 100644 index 000000000..48eb06b04 --- /dev/null +++ b/web_view_google_map/readme/USAGE.md @@ -0,0 +1,43 @@ +## Google Map view + +Add a `google_map` view on any model that stores latitude/longitude: + +```xml + + res.partner.google_map + res.partner + + + + + + + + + +``` + +Include `google_map` in the action `view_mode`, for example +`list,form,google_map`. + +Contacts already expose a Google Map mode on the partner action. + +## Places widgets + +```xml + + +``` diff --git a/web_view_google_map/static/description/icon.png b/web_view_google_map/static/description/icon.png new file mode 100644 index 000000000..3a0328b51 Binary files /dev/null and b/web_view_google_map/static/description/icon.png differ diff --git a/web_view_google_map/static/description/index.html b/web_view_google_map/static/description/index.html new file mode 100644 index 000000000..582ac92ff --- /dev/null +++ b/web_view_google_map/static/description/index.html @@ -0,0 +1,508 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Google Map View

+ +

Beta License: AGPL-3 OCA/geospatial Translate me on Weblate Try me on Runboat

+

This module adds a Google Maps view type and Places autocomplete field +widgets to the Odoo web client.

+

Features:

+
    +
  • google_map view mode to display geolocated records on Google Maps
  • +
  • Marker colors from view attributes (color / colors)
  • +
  • Marker clustering
  • +
  • Field widgets gplaces_autocomplete and gplaces_address_form
  • +
+

Table of contents

+ +
+

Configuration

+
    +
  1. Install Google Maps Integration (base_google_map).
  2. +
  3. Go to Settings → Google Maps Integration.
  4. +
  5. Set your Google Maps API key and enable the Places / Geometry +libraries as needed.
  6. +
  7. Restrict the key in Google Cloud Console to your Odoo origins and the +Maps JavaScript API / Places API.
  8. +
+
+
+

Usage

+
+

Google Map view

+

Add a google_map view on any model that stores latitude/longitude:

+
+<record id="view_partner_google_map" model="ir.ui.view">
+    <field name="name">res.partner.google_map</field>
+    <field name="model">res.partner</field>
+    <field name="arch" type="xml">
+        <google_map
+            string="Map"
+            lat="partner_latitude"
+            lng="partner_longitude"
+            colors="blue:company_type=='person';green:company_type=='company';"
+        >
+            <field name="partner_latitude" />
+            <field name="partner_longitude" />
+            <field name="display_name" />
+            <field name="company_type" />
+        </google_map>
+    </field>
+</record>
+
+

Include google_map in the action view_mode, for example +list,form,google_map.

+

Contacts already expose a Google Map mode on the partner action.

+
+
+

Places widgets

+
+<field
+    name="street"
+    widget="gplaces_address_form"
+    options="{'lat': 'partner_latitude', 'lng': 'partner_longitude'}"
+/>
+<field
+    name="name"
+    widget="gplaces_autocomplete"
+    options="{'lat': 'partner_latitude', 'lng': 'partner_longitude'}"
+/>
+
+
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Open Source Integrators
  • +
  • Gray Matter Logic
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainers:

+

gityopie wolfhall

+

This module is part of the OCA/geospatial project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+
+ + diff --git a/web_view_google_map/static/description/maps.png b/web_view_google_map/static/description/maps.png new file mode 100644 index 000000000..102aa98e7 Binary files /dev/null and b/web_view_google_map/static/description/maps.png differ diff --git a/web_view_google_map/static/description/thumbnails.png b/web_view_google_map/static/description/thumbnails.png new file mode 100644 index 000000000..bb9e32b6f Binary files /dev/null and b/web_view_google_map/static/description/thumbnails.png differ diff --git a/web_view_google_map/static/description/widget_gplaces_address_form.gif b/web_view_google_map/static/description/widget_gplaces_address_form.gif new file mode 100644 index 000000000..77e512391 Binary files /dev/null and b/web_view_google_map/static/description/widget_gplaces_address_form.gif differ diff --git a/web_view_google_map/static/description/widget_gplaces_autocomplete.gif b/web_view_google_map/static/description/widget_gplaces_autocomplete.gif new file mode 100644 index 000000000..f59f412dd Binary files /dev/null and b/web_view_google_map/static/description/widget_gplaces_autocomplete.gif differ diff --git a/web_view_google_map/static/lib/markerclusterer/img/m1.png b/web_view_google_map/static/lib/markerclusterer/img/m1.png new file mode 100755 index 000000000..329ff524c Binary files /dev/null and b/web_view_google_map/static/lib/markerclusterer/img/m1.png differ diff --git a/web_view_google_map/static/lib/markerclusterer/img/m2.png b/web_view_google_map/static/lib/markerclusterer/img/m2.png new file mode 100755 index 000000000..b999cbcf6 Binary files /dev/null and b/web_view_google_map/static/lib/markerclusterer/img/m2.png differ diff --git a/web_view_google_map/static/lib/markerclusterer/img/m3.png b/web_view_google_map/static/lib/markerclusterer/img/m3.png new file mode 100755 index 000000000..9f30b3092 Binary files /dev/null and b/web_view_google_map/static/lib/markerclusterer/img/m3.png differ diff --git a/web_view_google_map/static/lib/markerclusterer/img/m4.png b/web_view_google_map/static/lib/markerclusterer/img/m4.png new file mode 100755 index 000000000..0d3f8263b Binary files /dev/null and b/web_view_google_map/static/lib/markerclusterer/img/m4.png differ diff --git a/web_view_google_map/static/lib/markerclusterer/img/m5.png b/web_view_google_map/static/lib/markerclusterer/img/m5.png new file mode 100755 index 000000000..61387d2ab Binary files /dev/null and b/web_view_google_map/static/lib/markerclusterer/img/m5.png differ diff --git a/web_view_google_map/static/lib/markerclusterer/markerclusterer.min.js b/web_view_google_map/static/lib/markerclusterer/markerclusterer.min.js new file mode 100644 index 000000000..087c12d4d --- /dev/null +++ b/web_view_google_map/static/lib/markerclusterer/markerclusterer.min.js @@ -0,0 +1,836 @@ +/* eslint-disable */ +var MarkerClusterer = (function () { + "use strict"; + function e(t) { + return (e = + typeof Symbol === "function" && typeof Symbol.iterator === "symbol" + ? function (e) { + return typeof e; + } + : function (e) { + return e && + typeof Symbol === "function" && + e.constructor === Symbol && + e !== Symbol.prototype + ? "symbol" + : typeof e; + })(t); + } + function t(e, t) { + if (!(e instanceof t)) throw new TypeError("Cannot call a class as a function"); + } + function i(e, t) { + for (var i = 0; i < t.length; i++) { + var s = t[i]; + (s.enumerable = s.enumerable || !1), + (s.configurable = !0), + "value" in s && (s.writable = !0), + Object.defineProperty(e, s.key, s); + } + } + function s(e, t, s) { + return t && i(e.prototype, t), s && i(e, s), e; + } + var r = (function () { + function e(i, s, r) { + t(this, e), + this.extend(e, google.maps.OverlayView), + (this.map_ = i), + (this.MARKER_CLUSTER_IMAGE_PATH_ = "../images/m"), + (this.MARKER_CLUSTER_IMAGE_EXTENSION_ = "png"), + (this.markers_ = []), + (this.clusters_ = []), + (this.sizes = [53, 56, 66, 78, 90]), + (this.styles_ = []), + (this.ready_ = !1); + var n = r || {}; + (this.zIndex_ = n.zIndex || google.maps.Marker.MAX_ZINDEX + 1), + (this.gridSize_ = n.gridSize || 60), + (this.minClusterSize_ = n.minimumClusterSize || 2), + (this.maxZoom_ = n.maxZoom || null), + (this.styles_ = n.styles || []), + (this.imagePath_ = n.imagePath || this.MARKER_CLUSTER_IMAGE_PATH_), + (this.imageExtension_ = + n.imageExtension || this.MARKER_CLUSTER_IMAGE_EXTENSION_), + (this.zoomOnClick_ = !0), + n.zoomOnClick != null && (this.zoomOnClick_ = n.zoomOnClick), + (this.averageCenter_ = !1), + n.averageCenter != null && (this.averageCenter_ = n.averageCenter), + this.setupStyles_(), + this.setMap(i), + (this.prevZoom_ = this.map_.getZoom()); + var a = this; + google.maps.event.addListener(this.map_, "zoom_changed", function () { + var e = a.map_.getZoom(), + t = a.map_.minZoom || 0, + i = Math.min( + a.map_.maxZoom || 100, + a.map_.mapTypes[a.map_.getMapTypeId()].maxZoom + ); + (e = Math.min(Math.max(e, t), i)), + a.prevZoom_ != e && ((a.prevZoom_ = e), a.resetViewport()); + }), + google.maps.event.addListener(this.map_, "idle", function () { + a.redraw(); + }), + s && (s.length || Object.keys(s).length) && this.addMarkers(s, !1); + } + return ( + s(e, [ + { + key: "extend", + value: function (e, t) { + return function (e) { + for (var t in e.prototype) + this.prototype[t] = e.prototype[t]; + return this; + }.apply(e, [t]); + }, + }, + { + key: "onAdd", + value: function () { + this.setReady_(!0); + }, + }, + {key: "draw", value: function () {}}, + { + key: "setupStyles_", + value: function () { + if (!this.styles_.length) + for (var e, t = 0; (e = this.sizes[t]); t++) + this.styles_.push({ + url: + this.imagePath_ + + (t + 1) + + "." + + this.imageExtension_, + height: e, + width: e, + }); + }, + }, + { + key: "fitMapToMarkers", + value: function () { + for ( + var e, + t = this.getMarkers(), + i = new google.maps.LatLngBounds(), + s = 0; + (e = t[s]); + s++ + ) + i.extend(e.getPosition()); + this.map_.fitBounds(i); + }, + }, + { + key: "setZIndex", + value: function (e) { + this.zIndex_ = e; + }, + }, + { + key: "getZIndex", + value: function () { + return this.zIndex_; + }, + }, + { + key: "setStyles", + value: function (e) { + this.styles_ = e; + }, + }, + { + key: "getStyles", + value: function () { + return this.styles_; + }, + }, + { + key: "isZoomOnClick", + value: function () { + return this.zoomOnClick_; + }, + }, + { + key: "isAverageCenter", + value: function () { + return this.averageCenter_; + }, + }, + { + key: "getMarkers", + value: function () { + return this.markers_; + }, + }, + { + key: "getTotalMarkers", + value: function () { + return this.markers_.length; + }, + }, + { + key: "setMaxZoom", + value: function (e) { + this.maxZoom_ = e; + }, + }, + { + key: "getMaxZoom", + value: function () { + return this.maxZoom_; + }, + }, + { + key: "calculator_", + value: function (e, t) { + for (var i = 0, s = e.length, r = s; r !== 0; ) + (r = parseInt(r / 10, 10)), i++; + return {text: s, index: (i = Math.min(i, t))}; + }, + }, + { + key: "setCalculator", + value: function (e) { + this.calculator_ = e; + }, + }, + { + key: "getCalculator", + value: function () { + return this.calculator_; + }, + }, + { + key: "addMarkers", + value: function (e, t) { + if (e.length) + for (var i, s = 0; (i = e[s]); s++) + this.pushMarkerTo_(i); + else if (Object.keys(e).length) + for (var r in e) this.pushMarkerTo_(e[r]); + t || this.redraw(); + }, + }, + { + key: "pushMarkerTo_", + value: function (e) { + if (((e.isAdded = !1), e.draggable)) { + var t = this; + google.maps.event.addListener( + e, + "dragend", + function () { + (e.isAdded = !1), t.repaint(); + } + ); + } + this.markers_.push(e); + }, + }, + { + key: "addMarker", + value: function (e, t) { + this.pushMarkerTo_(e), t || this.redraw(); + }, + }, + { + key: "removeMarker_", + value: function (e) { + var t = -1; + if (this.markers_.indexOf) t = this.markers_.indexOf(e); + else + for (var i, s = 0; (i = this.markers_[s]); s++) + if (i == e) { + t = s; + break; + } + return ( + t != -1 && + (e.setMap(null), this.markers_.splice(t, 1), !0) + ); + }, + }, + { + key: "removeMarker", + value: function (e, t) { + var i = this.removeMarker_(e); + return ( + !(t || !i) && (this.resetViewport(), this.redraw(), !0) + ); + }, + }, + { + key: "removeMarkers", + value: function (e, t) { + for ( + var i, + s = e === this.getMarkers() ? e.slice() : e, + r = !1, + n = 0; + (i = s[n]); + n++ + ) { + var a = this.removeMarker_(i); + r = r || a; + } + if (!t && r) return this.resetViewport(), this.redraw(), !0; + }, + }, + { + key: "setReady_", + value: function (e) { + this.ready_ || ((this.ready_ = e), this.createClusters_()); + }, + }, + { + key: "getTotalClusters", + value: function () { + return this.clusters_.length; + }, + }, + { + key: "getMap", + value: function () { + return this.map_; + }, + }, + { + key: "setMap", + value: function (e) { + this.map_ = e; + }, + }, + { + key: "getGridSize", + value: function () { + return this.gridSize_; + }, + }, + { + key: "setGridSize", + value: function (e) { + this.gridSize_ = e; + }, + }, + { + key: "getMinClusterSize", + value: function () { + return this.minClusterSize_; + }, + }, + { + key: "setMinClusterSize", + value: function (e) { + this.minClusterSize_ = e; + }, + }, + { + key: "getExtendedBounds", + value: function (e) { + var t = this.getProjection(), + i = new google.maps.LatLng( + e.getNorthEast().lat(), + e.getNorthEast().lng() + ), + s = new google.maps.LatLng( + e.getSouthWest().lat(), + e.getSouthWest().lng() + ), + r = t.fromLatLngToDivPixel(i); + (r.x += this.gridSize_), (r.y -= this.gridSize_); + var n = t.fromLatLngToDivPixel(s); + (n.x -= this.gridSize_), (n.y += this.gridSize_); + var a = t.fromDivPixelToLatLng(r), + o = t.fromDivPixelToLatLng(n); + return e.extend(a), e.extend(o), e; + }, + }, + { + key: "isMarkerInBounds_", + value: function (e, t) { + return t.contains(e.getPosition()); + }, + }, + { + key: "clearMarkers", + value: function () { + this.resetViewport(!0), (this.markers_ = []); + }, + }, + { + key: "resetViewport", + value: function (e) { + for (var t, i = 0; (t = this.clusters_[i]); i++) t.remove(); + for (var s, r = 0; (s = this.markers_[r]); r++) + (s.isAdded = !1), e && s.setMap(null); + this.clusters_ = []; + }, + }, + { + key: "repaint", + value: function () { + var e = this.clusters_.slice(); + (this.clusters_.length = 0), + this.resetViewport(), + this.redraw(), + setTimeout(function () { + for (var t, i = 0; (t = e[i]); i++) t.remove(); + }, 0); + }, + }, + { + key: "redraw", + value: function () { + this.createClusters_(); + }, + }, + { + key: "distanceBetweenPoints_", + value: function (e, t) { + if (!e || !t) return 0; + var i = ((t.lat() - e.lat()) * Math.PI) / 180, + s = ((t.lng() - e.lng()) * Math.PI) / 180, + r = + Math.sin(i / 2) * Math.sin(i / 2) + + Math.cos((e.lat() * Math.PI) / 180) * + Math.cos((t.lat() * Math.PI) / 180) * + Math.sin(s / 2) * + Math.sin(s / 2); + return ( + 6371 * (2 * Math.atan2(Math.sqrt(r), Math.sqrt(1 - r))) + ); + }, + }, + { + key: "addToClosestCluster_", + value: function (e) { + for ( + var t, i = 4e4, s = null, r = 0; + (t = this.clusters_[r]); + r++ + ) { + var a = t.getCenter(); + if (a) { + var o = this.distanceBetweenPoints_( + a, + e.getPosition() + ); + o < i && ((i = o), (s = t)); + } + } + if (s && s.isMarkerInClusterBounds(e)) s.addMarker(e); + else { + var h = new n(this); + h.addMarker(e), this.clusters_.push(h); + } + }, + }, + { + key: "createClusters_", + value: function () { + if (this.ready_) + for ( + var e, + t = new google.maps.LatLngBounds( + this.map_.getBounds().getSouthWest(), + this.map_.getBounds().getNorthEast() + ), + i = this.getExtendedBounds(t), + s = 0; + (e = this.markers_[s]); + s++ + ) + !e.isAdded && + this.isMarkerInBounds_(e, i) && + this.addToClosestCluster_(e); + }, + }, + ]), + e + ); + })(), + n = (function () { + function e(i) { + t(this, e), + (this.markerClusterer_ = i), + (this.map_ = i.getMap()), + (this.gridSize_ = i.getGridSize()), + (this.minClusterSize_ = i.getMinClusterSize()), + (this.averageCenter_ = i.isAverageCenter()), + (this.center_ = null), + (this.markers_ = []), + (this.bounds_ = null), + (this.clusterIcon_ = new a(this, i.getStyles(), i.getGridSize())); + } + return ( + s(e, [ + { + key: "isMarkerAlreadyAdded", + value: function (e) { + if (this.markers_.indexOf) + return this.markers_.indexOf(e) != -1; + for (var t, i = 0; (t = this.markers_[i]); i++) + if (t == e) return !0; + return !1; + }, + }, + { + key: "addMarker", + value: function (e) { + if (this.isMarkerAlreadyAdded(e)) return !1; + if (this.center_) { + if (this.averageCenter_) { + var t = this.markers_.length + 1, + i = + (this.center_.lat() * (t - 1) + + e.getPosition().lat()) / + t, + s = + (this.center_.lng() * (t - 1) + + e.getPosition().lng()) / + t; + (this.center_ = new google.maps.LatLng(i, s)), + this.calculateBounds_(); + } + } else + (this.center_ = e.getPosition()), + this.calculateBounds_(); + (e.isAdded = !0), this.markers_.push(e); + var r = this.markers_.length; + if ( + (r < this.minClusterSize_ && + e.getMap() != this.map_ && + e.setMap(this.map_), + r == this.minClusterSize_) + ) + for (var n = 0; n < r; n++) + this.markers_[n].setMap(null); + return ( + r >= this.minClusterSize_ && e.setMap(null), + this.updateIcon(), + !0 + ); + }, + }, + { + key: "getMarkerClusterer", + value: function () { + return this.markerClusterer_; + }, + }, + { + key: "getBounds", + value: function () { + for ( + var e, + t = new google.maps.LatLngBounds( + this.center_, + this.center_ + ), + i = this.getMarkers(), + s = 0; + (e = i[s]); + s++ + ) + t.extend(e.getPosition()); + return t; + }, + }, + { + key: "remove", + value: function () { + this.clusterIcon_.remove(), + (this.markers_.length = 0), + delete this.markers_; + }, + }, + { + key: "getSize", + value: function () { + return this.markers_.length; + }, + }, + { + key: "getMarkers", + value: function () { + return this.markers_; + }, + }, + { + key: "getCenter", + value: function () { + return this.center_; + }, + }, + { + key: "calculateBounds_", + value: function () { + var e = new google.maps.LatLngBounds( + this.center_, + this.center_ + ); + this.bounds_ = this.markerClusterer_.getExtendedBounds(e); + }, + }, + { + key: "isMarkerInClusterBounds", + value: function (e) { + return this.bounds_.contains(e.getPosition()); + }, + }, + { + key: "getMap", + value: function () { + return this.map_; + }, + }, + { + key: "updateIcon", + value: function () { + var e = this.map_.getZoom(), + t = this.markerClusterer_.getMaxZoom(); + if (t && e > t) + for (var i, s = 0; (i = this.markers_[s]); s++) + i.setMap(this.map_); + else if (this.markers_.length < this.minClusterSize_) + this.clusterIcon_.hide(); + else { + var r = this.markerClusterer_.getStyles().length, + n = this.markerClusterer_.getCalculator()( + this.markers_, + r + ); + this.clusterIcon_.setCenter(this.center_), + this.clusterIcon_.setSums(n), + this.clusterIcon_.show(); + } + }, + }, + ]), + e + ); + })(), + a = (function () { + function i(e, s, r) { + t(this, i), + e.getMarkerClusterer().extend(i, google.maps.OverlayView), + (this.styles_ = s), + (this.padding_ = r || 0), + (this.cluster_ = e), + (this.center_ = null), + (this.map_ = e.getMap()), + (this.div_ = null), + (this.sums_ = null), + (this.visible_ = !1), + this.setMap(this.map_); + } + return ( + s(i, [ + { + key: "triggerClusterClick", + value: function () { + var e = this.cluster_.getBounds(), + t = this.cluster_.getMarkerClusterer(); + google.maps.event.trigger( + t.map_, + "clusterclick", + this.cluster_ + ), + t.isZoomOnClick() && + (this.map_.fitBounds(e), + this.map_.setCenter(e.getCenter())); + }, + }, + { + key: "onAdd", + value: function () { + if ( + ((this.div_ = document.createElement("DIV")), + this.visible_) + ) { + var e = this.getPosFromLatLng_(this.center_); + (this.div_.style.cssText = this.createCss(e)), + (this.div_.innerHTML = this.sums_.text); + } + this.getPanes().overlayMouseTarget.appendChild(this.div_); + var t = this; + google.maps.event.addDomListener( + this.div_, + "click", + function () { + t.triggerClusterClick(); + } + ); + }, + }, + { + key: "getPosFromLatLng_", + value: function (e) { + var t = this.getProjection().fromLatLngToDivPixel(e); + return ( + (t.x -= parseInt(this.width_ / 2, 10)), + (t.y -= parseInt(this.height_ / 2, 10)), + t + ); + }, + }, + { + key: "draw", + value: function () { + if (this.visible_) { + var e = this.getPosFromLatLng_(this.center_); + (this.div_.style.top = e.y + "px"), + (this.div_.style.left = e.x + "px"); + } + }, + }, + { + key: "hide", + value: function () { + this.div_ && (this.div_.style.display = "none"), + (this.visible_ = !1); + }, + }, + { + key: "show", + value: function () { + if (this.div_) { + var e = this.getPosFromLatLng_(this.center_); + (this.div_.style.cssText = this.createCss(e)), + (this.div_.style.display = ""); + } + this.visible_ = !0; + }, + }, + { + key: "remove", + value: function () { + this.setMap(null); + }, + }, + { + key: "onRemove", + value: function () { + this.div_ && + this.div_.parentNode && + (this.hide(), + this.div_.parentNode.removeChild(this.div_), + (this.div_ = null)); + }, + }, + { + key: "setSums", + value: function (e) { + (this.sums_ = e), + (this.text_ = e.text), + (this.index_ = e.index), + this.div_ && (this.div_.innerHTML = e.text), + this.useStyle(); + }, + }, + { + key: "useStyle", + value: function () { + var e = Math.max(0, this.sums_.index - 1); + e = Math.min(this.styles_.length - 1, e); + var t = this.styles_[e]; + (this.url_ = t.url), + (this.height_ = t.height), + (this.width_ = t.width), + (this.textColor_ = t.textColor), + (this.anchor_ = t.anchor), + (this.textSize_ = t.textSize), + (this.backgroundPosition_ = t.backgroundPosition); + }, + }, + { + key: "setCenter", + value: function (e) { + this.center_ = e; + }, + }, + { + key: "createCss", + value: function (t) { + var i = []; + i.push( + "z-index:" + + this.cluster_.markerClusterer_.getZIndex() + + ";" + ), + i.push("background-image:url(" + this.url_ + ");"); + var s = this.backgroundPosition_ + ? this.backgroundPosition_ + : "0 0"; + i.push("background-position:" + s + ";"), + e(this.anchor_) === "object" + ? (typeof this.anchor_[0] === "number" && + this.anchor_[0] > 0 && + this.anchor_[0] < this.height_ + ? i.push( + "height:" + + (this.height_ - this.anchor_[0]) + + "px; padding-top:" + + this.anchor_[0] + + "px;" + ) + : i.push( + "height:" + + this.height_ + + "px; line-height:" + + this.height_ + + "px;" + ), + typeof this.anchor_[1] === "number" && + this.anchor_[1] > 0 && + this.anchor_[1] < this.width_ + ? i.push( + "width:" + + (this.width_ - this.anchor_[1]) + + "px; padding-left:" + + this.anchor_[1] + + "px;" + ) + : i.push( + "width:" + + this.width_ + + "px; text-align:center;" + )) + : i.push( + "height:" + + this.height_ + + "px; line-height:" + + this.height_ + + "px; width:" + + this.width_ + + "px; text-align:center;" + ); + var r = this.textColor_ ? this.textColor_ : "black", + n = this.textSize_ ? this.textSize_ : 11; + return ( + i.push( + "cursor:pointer; top:" + + t.y + + "px; left:" + + t.x + + "px; color:" + + r + + "; position:absolute; font-size:" + + n + + "px; font-family:Arial,sans-serif; font-weight:bold" + ), + i.join("") + ); + }, + }, + ]), + i + ); + })(); + return r; +})(); diff --git a/web_view_google_map/static/src/fields/gplaces_address_form.esm.js b/web_view_google_map/static/src/fields/gplaces_address_form.esm.js new file mode 100644 index 000000000..6cde82250 --- /dev/null +++ b/web_view_google_map/static/src/fields/gplaces_address_form.esm.js @@ -0,0 +1,124 @@ +import {Component, onMounted, onWillStart, useRef} from "@odoo/owl"; +import {_t} from "@web/core/l10n/translation"; +import {loadGoogleMaps} from "../views/google_map/google_maps_loader.esm"; +import {registry} from "@web/core/registry"; +import {standardFieldProps} from "@web/views/fields/standard_field_props"; +import {useInputField} from "@web/views/fields/input_field_hook"; +import {useService} from "@web/core/utils/hooks"; + +/* global google */ + +const DEFAULT_FILLFIELDS = { + street: ["street_number", "route"], + street2: [ + "administrative_area_level_3", + "administrative_area_level_4", + "administrative_area_level_5", + ], + city: ["locality", "administrative_area_level_2"], + zip: "postal_code", + state_id: "administrative_area_level_1", + country_id: "country", +}; + +function getComponentValue(place, type, form = "long_name") { + const component = (place.address_components || []).find((item) => + item.types.includes(type) + ); + return component ? component[form] : ""; +} + +export class GplacesAddressFormField extends Component { + static template = "web_view_google_map.GplacesAddressFormField"; + static props = { + ...standardFieldProps, + placeholder: {type: String, optional: true}, + fillfields: {type: Object, optional: true}, + lat: {type: String, optional: true}, + lng: {type: String, optional: true}, + }; + + setup() { + this.orm = useService("orm"); + this.input = useRef("input"); + this.fillfields = { + ...DEFAULT_FILLFIELDS, + ...(this.props.fillfields || {}), + }; + useInputField({ + getValue: () => this.props.record.data[this.props.name] || "", + }); + + onWillStart(async () => { + await loadGoogleMaps(this.orm); + }); + + onMounted(() => { + this.initAutocomplete(); + }); + } + + get formattedValue() { + return this.props.record.data[this.props.name] || ""; + } + + initAutocomplete() { + if (this.props.readonly || !this.input.el || !window.google?.maps?.places) { + return; + } + this.autocomplete = new google.maps.places.Autocomplete(this.input.el, { + types: ["address"], + fields: ["address_components", "geometry", "formatted_address", "name"], + }); + this.autocomplete.addListener("place_changed", () => this.onPlaceChanged()); + } + + async onPlaceChanged() { + const place = this.autocomplete.getPlace(); + if (!place) { + return; + } + const values = {}; + for (const [fieldName, source] of Object.entries(this.fillfields)) { + if (["state_id", "country_id"].includes(fieldName)) { + continue; + } + if (Array.isArray(source)) { + values[fieldName] = source + .map((type) => getComponentValue(place, type)) + .filter(Boolean) + .join(" "); + } else { + values[fieldName] = getComponentValue(place, source); + } + } + const location = place.geometry?.location; + if (this.props.lat && location) { + values[this.props.lat] = location.lat(); + } + if (this.props.lng && location) { + values[this.props.lng] = location.lng(); + } + values[this.props.name] = + values.street || place.formatted_address || this.input.el.value; + await this.props.record.update(values); + } +} + +export const gplacesAddressFormField = { + component: GplacesAddressFormField, + displayName: _t("Google Places Address Form"), + supportedTypes: ["char"], + extractProps: ({options, placeholder}) => ({ + placeholder, + fillfields: options.fillfields, + lat: options.lat, + lng: options.lng, + }), +}; + +registry.category("fields").add("gplaces_address_form", gplacesAddressFormField); +// Legacy widget name used in older docs / views +registry + .category("fields") + .add("gplaces_address_autocomplete", gplacesAddressFormField); diff --git a/web_view_google_map/static/src/fields/gplaces_address_form.xml b/web_view_google_map/static/src/fields/gplaces_address_form.xml new file mode 100644 index 000000000..f16cb2a1f --- /dev/null +++ b/web_view_google_map/static/src/fields/gplaces_address_form.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/web_view_google_map/static/src/fields/gplaces_autocomplete.esm.js b/web_view_google_map/static/src/fields/gplaces_autocomplete.esm.js new file mode 100644 index 000000000..fb89c7e50 --- /dev/null +++ b/web_view_google_map/static/src/fields/gplaces_autocomplete.esm.js @@ -0,0 +1,175 @@ +import {Component, onMounted, onWillStart, useRef} from "@odoo/owl"; +import {_t} from "@web/core/l10n/translation"; +import {loadGoogleMaps} from "../views/google_map/google_maps_loader.esm"; +import {registry} from "@web/core/registry"; +import {standardFieldProps} from "@web/views/fields/standard_field_props"; +import {useInputField} from "@web/views/fields/input_field_hook"; +import {useService} from "@web/core/utils/hooks"; + +/* global google */ + +const DEFAULT_FILLFIELDS = { + general: { + name: "name", + website: "website", + phone: ["international_phone_number", "formatted_phone_number"], + }, + geolocation: { + partner_latitude: "latitude", + partner_longitude: "longitude", + }, + address: { + street: ["street_number", "route"], + street2: [ + "administrative_area_level_3", + "administrative_area_level_4", + "administrative_area_level_5", + ], + city: ["locality", "administrative_area_level_2"], + zip: "postal_code", + state_id: "administrative_area_level_1", + country_id: "country", + }, +}; + +function getComponentValue(place, type, form = "long_name") { + const component = (place.address_components || []).find((item) => + item.types.includes(type) + ); + return component ? component[form] : ""; +} + +function resolveSource(place, source) { + if (Array.isArray(source)) { + return source.map((key) => place[key]).find(Boolean) || ""; + } + return place[source] || ""; +} + +function resolveAddressSource(place, source) { + if (Array.isArray(source)) { + return source + .map((type) => getComponentValue(place, type)) + .filter(Boolean) + .join(" "); + } + return getComponentValue(place, source); +} + +export class GplacesAutocompleteField extends Component { + static template = "web_view_google_map.GplacesAutocompleteField"; + static props = { + ...standardFieldProps, + placeholder: {type: String, optional: true}, + fillfields: {type: Object, optional: true}, + lat: {type: String, optional: true}, + lng: {type: String, optional: true}, + }; + + setup() { + this.orm = useService("orm"); + this.input = useRef("input"); + this.fillfields = { + ...DEFAULT_FILLFIELDS, + ...(this.props.fillfields || {}), + }; + useInputField({ + getValue: () => this.props.record.data[this.props.name] || "", + }); + + onWillStart(async () => { + await loadGoogleMaps(this.orm); + }); + + onMounted(() => { + this.initAutocomplete(); + }); + } + + get formattedValue() { + return this.props.record.data[this.props.name] || ""; + } + + initAutocomplete() { + if (this.props.readonly || !this.input.el || !window.google?.maps?.places) { + return; + } + this.autocomplete = new google.maps.places.Autocomplete(this.input.el, { + types: ["establishment"], + fields: [ + "address_components", + "geometry", + "name", + "website", + "formatted_phone_number", + "international_phone_number", + ], + }); + this.autocomplete.addListener("place_changed", () => this.onPlaceChanged()); + } + + _fillGeneral(place, values) { + for (const [fieldName, source] of Object.entries( + this.fillfields.general || {} + )) { + values[fieldName] = resolveSource(place, source); + } + } + + _fillGeolocation(place, values) { + const location = place.geometry?.location; + if (!location) { + return; + } + for (const [fieldName, key] of Object.entries( + this.fillfields.geolocation || {} + )) { + values[fieldName] = key === "latitude" ? location.lat() : location.lng(); + } + if (this.props.lat) { + values[this.props.lat] = location.lat(); + } + if (this.props.lng) { + values[this.props.lng] = location.lng(); + } + } + + _fillAddress(place, values) { + for (const [fieldName, source] of Object.entries( + this.fillfields.address || {} + )) { + if (["state_id", "country_id"].includes(fieldName)) { + continue; + } + values[fieldName] = resolveAddressSource(place, source); + } + } + + async onPlaceChanged() { + const place = this.autocomplete.getPlace(); + if (!place) { + return; + } + const values = {}; + this._fillGeneral(place, values); + this._fillGeolocation(place, values); + this._fillAddress(place, values); + values[this.props.name] = + place.name || place.formatted_address || this.input.el.value; + await this.props.record.update(values); + } +} + +export const gplacesAutocompleteField = { + component: GplacesAutocompleteField, + displayName: _t("Google Places Autocomplete"), + supportedTypes: ["char"], + extractProps: ({options, placeholder}) => ({ + placeholder, + fillfields: options.fillfields, + lat: options.lat, + lng: options.lng, + }), +}; + +registry.category("fields").add("gplaces_autocomplete", gplacesAutocompleteField); diff --git a/web_view_google_map/static/src/fields/gplaces_autocomplete.xml b/web_view_google_map/static/src/fields/gplaces_autocomplete.xml new file mode 100644 index 000000000..488a4c2b4 --- /dev/null +++ b/web_view_google_map/static/src/fields/gplaces_autocomplete.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/web_view_google_map/static/src/img/layer_bike.png b/web_view_google_map/static/src/img/layer_bike.png new file mode 100755 index 000000000..a1210bba2 Binary files /dev/null and b/web_view_google_map/static/src/img/layer_bike.png differ diff --git a/web_view_google_map/static/src/img/layer_traffic.png b/web_view_google_map/static/src/img/layer_traffic.png new file mode 100755 index 000000000..d6a236f3f Binary files /dev/null and b/web_view_google_map/static/src/img/layer_traffic.png differ diff --git a/web_view_google_map/static/src/img/layer_transit.png b/web_view_google_map/static/src/img/layer_transit.png new file mode 100755 index 000000000..0f77a4ed0 Binary files /dev/null and b/web_view_google_map/static/src/img/layer_transit.png differ diff --git a/web_view_google_map/static/src/img/markers/black.png b/web_view_google_map/static/src/img/markers/black.png new file mode 100644 index 000000000..23a0250b1 Binary files /dev/null and b/web_view_google_map/static/src/img/markers/black.png differ diff --git a/web_view_google_map/static/src/img/markers/blue.png b/web_view_google_map/static/src/img/markers/blue.png new file mode 100644 index 000000000..fbeb0cc01 Binary files /dev/null and b/web_view_google_map/static/src/img/markers/blue.png differ diff --git a/web_view_google_map/static/src/img/markers/brown.png b/web_view_google_map/static/src/img/markers/brown.png new file mode 100644 index 000000000..6de8d9a87 Binary files /dev/null and b/web_view_google_map/static/src/img/markers/brown.png differ diff --git a/web_view_google_map/static/src/img/markers/cyan.png b/web_view_google_map/static/src/img/markers/cyan.png new file mode 100644 index 000000000..5cb9b996f Binary files /dev/null and b/web_view_google_map/static/src/img/markers/cyan.png differ diff --git a/web_view_google_map/static/src/img/markers/green.png b/web_view_google_map/static/src/img/markers/green.png new file mode 100644 index 000000000..ff666a2da Binary files /dev/null and b/web_view_google_map/static/src/img/markers/green.png differ diff --git a/web_view_google_map/static/src/img/markers/grey.png b/web_view_google_map/static/src/img/markers/grey.png new file mode 100644 index 000000000..df3360d6b Binary files /dev/null and b/web_view_google_map/static/src/img/markers/grey.png differ diff --git a/web_view_google_map/static/src/img/markers/light-green.png b/web_view_google_map/static/src/img/markers/light-green.png new file mode 100644 index 000000000..465d9e543 Binary files /dev/null and b/web_view_google_map/static/src/img/markers/light-green.png differ diff --git a/web_view_google_map/static/src/img/markers/magenta.png b/web_view_google_map/static/src/img/markers/magenta.png new file mode 100644 index 000000000..6e0038b99 Binary files /dev/null and b/web_view_google_map/static/src/img/markers/magenta.png differ diff --git a/web_view_google_map/static/src/img/markers/orange.png b/web_view_google_map/static/src/img/markers/orange.png new file mode 100644 index 000000000..bd64101db Binary files /dev/null and b/web_view_google_map/static/src/img/markers/orange.png differ diff --git a/web_view_google_map/static/src/img/markers/pink.png b/web_view_google_map/static/src/img/markers/pink.png new file mode 100644 index 000000000..466b7c354 Binary files /dev/null and b/web_view_google_map/static/src/img/markers/pink.png differ diff --git a/web_view_google_map/static/src/img/markers/purple.png b/web_view_google_map/static/src/img/markers/purple.png new file mode 100644 index 000000000..3fc502ca0 Binary files /dev/null and b/web_view_google_map/static/src/img/markers/purple.png differ diff --git a/web_view_google_map/static/src/img/markers/red.png b/web_view_google_map/static/src/img/markers/red.png new file mode 100644 index 000000000..553f8c488 Binary files /dev/null and b/web_view_google_map/static/src/img/markers/red.png differ diff --git a/web_view_google_map/static/src/img/markers/white.png b/web_view_google_map/static/src/img/markers/white.png new file mode 100644 index 000000000..cc6fa8a70 Binary files /dev/null and b/web_view_google_map/static/src/img/markers/white.png differ diff --git a/web_view_google_map/static/src/img/markers/yellow.png b/web_view_google_map/static/src/img/markers/yellow.png new file mode 100644 index 000000000..e2ad98c48 Binary files /dev/null and b/web_view_google_map/static/src/img/markers/yellow.png differ diff --git a/web_view_google_map/static/src/img/mode_bicycling.png b/web_view_google_map/static/src/img/mode_bicycling.png new file mode 100755 index 000000000..a1210bba2 Binary files /dev/null and b/web_view_google_map/static/src/img/mode_bicycling.png differ diff --git a/web_view_google_map/static/src/img/mode_driving.png b/web_view_google_map/static/src/img/mode_driving.png new file mode 100755 index 000000000..f98af8d29 Binary files /dev/null and b/web_view_google_map/static/src/img/mode_driving.png differ diff --git a/web_view_google_map/static/src/img/mode_transit.png b/web_view_google_map/static/src/img/mode_transit.png new file mode 100755 index 000000000..0f77a4ed0 Binary files /dev/null and b/web_view_google_map/static/src/img/mode_transit.png differ diff --git a/web_view_google_map/static/src/img/mode_walking.png b/web_view_google_map/static/src/img/mode_walking.png new file mode 100755 index 000000000..fd71f608e Binary files /dev/null and b/web_view_google_map/static/src/img/mode_walking.png differ diff --git a/web_view_google_map/static/src/views/google_map/google_map_controller.esm.js b/web_view_google_map/static/src/views/google_map/google_map_controller.esm.js new file mode 100644 index 000000000..2ab933f27 --- /dev/null +++ b/web_view_google_map/static/src/views/google_map/google_map_controller.esm.js @@ -0,0 +1,26 @@ +import {Component, useRef} from "@odoo/owl"; +import {GoogleMapRenderer} from "./google_map_renderer.esm"; +import {Layout} from "@web/search/layout"; +import {executeButtonCallback} from "@web/views/view_button/view_button_hook"; + +export class GoogleMapController extends Component { + static template = "web_view_google_map.GoogleMapView"; + static components = {Layout, GoogleMapRenderer}; + static props = { + "*": true, + }; + + setup() { + this.rootRef = useRef("root"); + } + + async onClickCreate() { + return executeButtonCallback(this.rootRef.el, () => this.createRecord()); + } + + async createRecord() { + if (this.props.createRecord) { + await this.props.createRecord(); + } + } +} diff --git a/web_view_google_map/static/src/views/google_map/google_map_controller.xml b/web_view_google_map/static/src/views/google_map/google_map_controller.xml new file mode 100644 index 000000000..d466662ce --- /dev/null +++ b/web_view_google_map/static/src/views/google_map/google_map_controller.xml @@ -0,0 +1,21 @@ + + + +
+ + + + + + +
+
+
diff --git a/web_view_google_map/static/src/views/google_map/google_map_renderer.esm.js b/web_view_google_map/static/src/views/google_map/google_map_renderer.esm.js new file mode 100644 index 000000000..03ddd1d90 --- /dev/null +++ b/web_view_google_map/static/src/views/google_map/google_map_renderer.esm.js @@ -0,0 +1,277 @@ +import {Component, onMounted, onPatched, onWillStart, useRef} from "@odoo/owl"; +import {MAP_THEMES} from "./map_themes.esm"; +import {evaluateBooleanExpr} from "@web/core/py_js/py"; +import {loadGoogleMaps} from "./google_maps_loader.esm"; +import {rpc} from "@web/core/network/rpc"; +import {useService} from "@web/core/utils/hooks"; + +/* global google, MarkerClusterer */ + +const MARKER_COLORS = [ + "black", + "blue", + "brown", + "cyan", + "green", + "grey", + "orange", + "pink", + "purple", + "red", + "white", + "yellow", +]; + +function normalizeArch(arch) { + if (arch && typeof arch !== "string") { + return arch; + } + const xml = String(arch || ""); + const doc = new DOMParser().parseFromString(xml, "text/xml"); + return doc.documentElement; +} + +function parseMarkerColors(colorsAttr) { + if (!colorsAttr) { + return []; + } + return colorsAttr + .split(";") + .map((pair) => pair.trim()) + .filter(Boolean) + .map((pair) => { + const idx = pair.indexOf(":"); + if (idx === -1) { + return null; + } + return [pair.slice(0, idx).trim(), pair.slice(idx + 1).trim()]; + }) + .filter(Boolean); +} + +export class GoogleMapRenderer extends Component { + static template = "web_view_google_map.GoogleMapRenderer"; + static props = { + "*": true, + }; + + setup() { + this.orm = useService("orm"); + this.action = useService("action"); + this.mapRef = useRef("mapContainer"); + this.gmap = null; + this.markerCluster = null; + this.markers = []; + this.infoWindow = null; + this.records = []; + this.theme = "default"; + + const archEl = normalizeArch(this.props.archInfo?.arch || this.props.arch); + const attrs = archEl.attributes || {}; + this.fieldLat = attrs.getNamedItem("lat")?.value || "partner_latitude"; + this.fieldLng = attrs.getNamedItem("lng")?.value || "partner_longitude"; + this.markerColor = attrs.getNamedItem("color")?.value || null; + this.markerColors = parseMarkerColors(attrs.getNamedItem("colors")?.value); + this.defaultMarkerColor = "red"; + this.iconUrl = "/web_view_google_map/static/src/img/markers/"; + this.resModel = this.props.resModel; + this.archFields = [...archEl.querySelectorAll(":scope > field")].map((node) => + node.getAttribute("name") + ); + + onWillStart(async () => { + await loadGoogleMaps(this.orm); + await this.loadTheme(); + await this.loadRecords(); + }); + + onMounted(() => { + this.initMap(); + this.renderMarkers(); + }); + + onPatched(() => { + if (this.gmap) { + this.renderMarkers(); + } + }); + } + + async loadTheme() { + try { + const data = await rpc("/web/map_theme"); + if ( + data?.theme && + Object.prototype.hasOwnProperty.call(MAP_THEMES, data.theme) + ) { + this.theme = data.theme; + } + } catch { + this.theme = "default"; + } + } + + async loadRecords() { + const fields = new Set(["id", "display_name", this.fieldLat, this.fieldLng]); + for (const name of this.archFields) { + if (name) { + fields.add(name); + } + } + try { + this.records = await this.orm.searchRead( + this.resModel, + this.props.domain || [], + [...fields], + { + limit: this.props.limit || 80, + context: this.props.context || {}, + } + ); + } catch { + this.records = []; + } + } + + initMap() { + const mapDiv = this.mapRef.el; + if (!mapDiv || !window.google?.maps) { + return; + } + this.infoWindow = new google.maps.InfoWindow(); + this.gmap = new google.maps.Map(mapDiv, { + mapTypeId: google.maps.MapTypeId.ROADMAP, + minZoom: 3, + maxZoom: 20, + fullscreenControl: true, + mapTypeControl: true, + center: {lat: 0, lng: 0}, + zoom: 2, + }); + this.applyTheme(); + this.initMarkerCluster(); + } + + applyTheme() { + if (!this.gmap || this.theme === "default" || !MAP_THEMES[this.theme]) { + return; + } + const styledMapType = new google.maps.StyledMapType(MAP_THEMES[this.theme], { + name: "Styled Map", + }); + this.gmap.mapTypes.set("styled_map", styledMapType); + this.gmap.setMapTypeId("styled_map"); + } + + initMarkerCluster() { + if (typeof MarkerClusterer === "undefined") { + return; + } + this.markerCluster = new MarkerClusterer(this.gmap, [], { + gridSize: 40, + maxZoom: 7, + zoomOnClick: true, + imagePath: "/web_view_google_map/static/lib/markerclusterer/img/m", + }); + } + + clearMarkers() { + for (const marker of this.markers) { + marker.setMap(null); + } + this.markers = []; + if (this.markerCluster) { + this.markerCluster.clearMarkers(); + } + } + + renderMarkers() { + if (!this.gmap) { + return; + } + this.clearMarkers(); + const bounds = new google.maps.LatLngBounds(); + let hasPoint = false; + + for (const record of this.records) { + const lat = Number(record[this.fieldLat]); + const lng = Number(record[this.fieldLng]); + if (!lat || !lng) { + continue; + } + const latLng = new google.maps.LatLng(lat, lng); + const color = this.getIconColor(record); + const marker = new google.maps.Marker({ + position: latLng, + map: this.markerCluster ? null : this.gmap, + animation: google.maps.Animation.DROP, + icon: this.getIconColorPath(color), + title: record.display_name || "", + }); + marker.addListener("click", () => this.onMarkerClick(marker, record)); + this.markers.push(marker); + if (this.markerCluster) { + this.markerCluster.addMarker(marker); + } + bounds.extend(latLng); + hasPoint = true; + } + + if (hasPoint) { + this.gmap.fitBounds(bounds); + } + } + + getIconColor(record) { + if (this.markerColor) { + return this.markerColor; + } + for (const [color, expression] of this.markerColors) { + try { + if (evaluateBooleanExpr(expression, record)) { + return color; + } + } catch { + // Ignore invalid color expressions from the arch + } + } + return this.defaultMarkerColor; + } + + getIconColorPath(color) { + const name = MARKER_COLORS.includes(color) ? color : this.defaultMarkerColor; + return `${this.iconUrl}${name}.png`; + } + + onMarkerClick(marker, record) { + const email = record.email ? `
${record.email}
` : ""; + const city = record.city ? `
${record.city}
` : ""; + this.infoWindow.setContent(` +
+ ${record.display_name || ""} + ${email}${city} + Open +
+ `); + this.infoWindow.open(this.gmap, marker); + google.maps.event.addListenerOnce(this.infoWindow, "domready", () => { + const link = document.querySelector(".o_google_map_open_record"); + if (link) { + link.addEventListener("click", (ev) => { + ev.preventDefault(); + this.openRecord(record); + }); + } + }); + } + + openRecord(record) { + this.action.doAction({ + type: "ir.actions.act_window", + res_model: this.resModel, + res_id: record.id, + views: [[false, "form"]], + target: "current", + }); + } +} diff --git a/web_view_google_map/static/src/views/google_map/google_map_renderer.scss b/web_view_google_map/static/src/views/google_map/google_map_renderer.scss new file mode 100644 index 000000000..717ac77fc --- /dev/null +++ b/web_view_google_map/static/src/views/google_map/google_map_renderer.scss @@ -0,0 +1,19 @@ +.o_google_map_view { + height: 100%; + width: 100%; +} + +.o_google_map_container { + height: 100%; + min-height: 400px; + width: 100%; +} + +.o_google_map_info { + max-width: 240px; + + strong { + display: block; + margin-bottom: 0.25rem; + } +} diff --git a/web_view_google_map/static/src/views/google_map/google_map_renderer.xml b/web_view_google_map/static/src/views/google_map/google_map_renderer.xml new file mode 100644 index 000000000..106d9b474 --- /dev/null +++ b/web_view_google_map/static/src/views/google_map/google_map_renderer.xml @@ -0,0 +1,8 @@ + + + +
+
+
+ + diff --git a/web_view_google_map/static/src/views/google_map/google_map_view.esm.js b/web_view_google_map/static/src/views/google_map/google_map_view.esm.js new file mode 100644 index 000000000..c56621a87 --- /dev/null +++ b/web_view_google_map/static/src/views/google_map/google_map_view.esm.js @@ -0,0 +1,35 @@ +import {GoogleMapController} from "./google_map_controller.esm"; +import {GoogleMapRenderer} from "./google_map_renderer.esm"; +import {registry} from "@web/core/registry"; + +function normalizeArch(arch) { + if (arch && typeof arch !== "string") { + return arch; + } + const xml = String(arch || ""); + const doc = new DOMParser().parseFromString(xml, "text/xml"); + return doc.documentElement; +} + +export const googleMapView = { + type: "google_map", + display_name: "Google Map", + icon: "fa fa-map-o", + multiRecord: true, + Controller: GoogleMapController, + Renderer: GoogleMapRenderer, + searchMenuTypes: ["filter", "favorite"], + + props: (genericProps) => { + const archEl = normalizeArch(genericProps.arch); + return { + ...genericProps, + Renderer: GoogleMapRenderer, + archInfo: { + arch: archEl, + }, + }; + }, +}; + +registry.category("views").add("google_map", googleMapView); diff --git a/web_view_google_map/static/src/views/google_map/google_maps_loader.esm.js b/web_view_google_map/static/src/views/google_map/google_maps_loader.esm.js new file mode 100644 index 000000000..bc18fa1fc --- /dev/null +++ b/web_view_google_map/static/src/views/google_map/google_maps_loader.esm.js @@ -0,0 +1,71 @@ +import {loadJS} from "@web/core/assets"; + +let googleMapsPromise = null; + +/** + * Load the Google Maps JS API using keys/libraries from ir.config_parameter. + * Parameters are managed by base_google_map. + * @param {Object} orm - ORM service (`useService("orm")`) + * @returns {Promise} + */ +export async function loadGoogleMaps(orm) { + if (window.google?.maps) { + return window.google.maps; + } + if (googleMapsPromise) { + return googleMapsPromise; + } + googleMapsPromise = (async () => { + const [apiKey, libraries, lang, region] = await Promise.all([ + orm.call("ir.config_parameter", "get_param", [ + "google.api_key_geocode", + "", + ]), + orm.call("ir.config_parameter", "get_param", [ + "google.maps_libraries", + "geometry,places", + ]), + orm.call("ir.config_parameter", "get_param", [ + "google.lang_localization", + "", + ]), + orm.call("ir.config_parameter", "get_param", [ + "google.region_localization", + "", + ]), + ]); + + const url = new URL("https://maps.googleapis.com/maps/api/js"); + url.searchParams.set("v", "quarterly"); + url.searchParams.set( + "libraries", + String(libraries || "geometry,places").replace(/^,+|,+$/g, "") + ); + if (apiKey) { + url.searchParams.set("key", apiKey); + } + // Lang/region may already include "&language=xx" from base_google_map + const langCode = String(lang || "").includes("=") + ? String(lang).split("=").pop() + : lang; + const regionCode = String(region || "").includes("=") + ? String(region).split("=").pop() + : region; + if (langCode) { + url.searchParams.set("language", langCode); + } + if (regionCode) { + url.searchParams.set("region", regionCode); + } + + await loadJS(url.toString()); + return window.google.maps; + })(); + + try { + return await googleMapsPromise; + } catch (error) { + googleMapsPromise = null; + throw error; + } +} diff --git a/web_view_google_map/static/src/views/google_map/map_themes.esm.js b/web_view_google_map/static/src/views/google_map/map_themes.esm.js new file mode 100644 index 000000000..8983c2345 --- /dev/null +++ b/web_view_google_map/static/src/views/google_map/map_themes.esm.js @@ -0,0 +1,1264 @@ +// Extracted from legacy web_view_google_map Utils +export const MAP_THEMES = { + default: [], + aubergine: [ + { + elementType: "geometry", + stylers: [ + { + color: "#1d2c4d", + }, + ], + }, + { + elementType: "labels.text.fill", + stylers: [ + { + color: "#8ec3b9", + }, + ], + }, + { + elementType: "labels.text.stroke", + stylers: [ + { + color: "#1a3646", + }, + ], + }, + { + featureType: "administrative.country", + elementType: "geometry.stroke", + stylers: [ + { + color: "#4b6878", + }, + ], + }, + { + featureType: "administrative.land_parcel", + elementType: "labels.text.fill", + stylers: [ + { + color: "#64779e", + }, + ], + }, + { + featureType: "administrative.province", + elementType: "geometry.stroke", + stylers: [ + { + color: "#4b6878", + }, + ], + }, + { + featureType: "landscape.man_made", + elementType: "geometry.stroke", + stylers: [ + { + color: "#334e87", + }, + ], + }, + { + featureType: "landscape.natural", + elementType: "geometry", + stylers: [ + { + color: "#023e58", + }, + ], + }, + { + featureType: "poi", + elementType: "geometry", + stylers: [ + { + color: "#283d6a", + }, + ], + }, + { + featureType: "poi", + elementType: "labels.text.fill", + stylers: [ + { + color: "#6f9ba5", + }, + ], + }, + { + featureType: "poi", + elementType: "labels.text.stroke", + stylers: [ + { + color: "#1d2c4d", + }, + ], + }, + { + featureType: "poi.business", + stylers: [ + { + visibility: "off", + }, + ], + }, + { + featureType: "poi.park", + elementType: "geometry.fill", + stylers: [ + { + color: "#023e58", + }, + ], + }, + { + featureType: "poi.park", + elementType: "labels.text", + stylers: [ + { + visibility: "off", + }, + ], + }, + { + featureType: "poi.park", + elementType: "labels.text.fill", + stylers: [ + { + color: "#3C7680", + }, + ], + }, + { + featureType: "road", + elementType: "geometry", + stylers: [ + { + color: "#304a7d", + }, + ], + }, + { + featureType: "road", + elementType: "labels.text.fill", + stylers: [ + { + color: "#98a5be", + }, + ], + }, + { + featureType: "road", + elementType: "labels.text.stroke", + stylers: [ + { + color: "#1d2c4d", + }, + ], + }, + { + featureType: "road.highway", + elementType: "geometry", + stylers: [ + { + color: "#2c6675", + }, + ], + }, + { + featureType: "road.highway", + elementType: "geometry.stroke", + stylers: [ + { + color: "#255763", + }, + ], + }, + { + featureType: "road.highway", + elementType: "labels.text.fill", + stylers: [ + { + color: "#b0d5ce", + }, + ], + }, + { + featureType: "road.highway", + elementType: "labels.text.stroke", + stylers: [ + { + color: "#023e58", + }, + ], + }, + { + featureType: "transit", + elementType: "labels.text.fill", + stylers: [ + { + color: "#98a5be", + }, + ], + }, + { + featureType: "transit", + elementType: "labels.text.stroke", + stylers: [ + { + color: "#1d2c4d", + }, + ], + }, + { + featureType: "transit.line", + elementType: "geometry.fill", + stylers: [ + { + color: "#283d6a", + }, + ], + }, + { + featureType: "transit.station", + elementType: "geometry", + stylers: [ + { + color: "#3a4762", + }, + ], + }, + { + featureType: "water", + elementType: "geometry", + stylers: [ + { + color: "#0e1626", + }, + ], + }, + { + featureType: "water", + elementType: "labels.text.fill", + stylers: [ + { + color: "#4e6d70", + }, + ], + }, + ], + night: [ + { + elementType: "geometry", + stylers: [ + { + color: "#242f3e", + }, + ], + }, + { + elementType: "labels.text.fill", + stylers: [ + { + color: "#746855", + }, + ], + }, + { + elementType: "labels.text.stroke", + stylers: [ + { + color: "#242f3e", + }, + ], + }, + { + featureType: "administrative.locality", + elementType: "labels.text.fill", + stylers: [ + { + color: "#d59563", + }, + ], + }, + { + featureType: "poi", + elementType: "labels.text", + stylers: [ + { + visibility: "off", + }, + ], + }, + { + featureType: "poi", + elementType: "labels.text.fill", + stylers: [ + { + color: "#d59563", + }, + ], + }, + { + featureType: "poi.business", + stylers: [ + { + visibility: "off", + }, + ], + }, + { + featureType: "poi.park", + elementType: "geometry", + stylers: [ + { + color: "#263c3f", + }, + ], + }, + { + featureType: "poi.park", + elementType: "labels.text.fill", + stylers: [ + { + color: "#6b9a76", + }, + ], + }, + { + featureType: "road", + elementType: "geometry", + stylers: [ + { + color: "#38414e", + }, + ], + }, + { + featureType: "road", + elementType: "geometry.stroke", + stylers: [ + { + color: "#212a37", + }, + ], + }, + { + featureType: "road", + elementType: "labels.icon", + stylers: [ + { + visibility: "off", + }, + ], + }, + { + featureType: "road", + elementType: "labels.text.fill", + stylers: [ + { + color: "#9ca5b3", + }, + ], + }, + { + featureType: "road.highway", + elementType: "geometry", + stylers: [ + { + color: "#746855", + }, + ], + }, + { + featureType: "road.highway", + elementType: "geometry.stroke", + stylers: [ + { + color: "#1f2835", + }, + ], + }, + { + featureType: "road.highway", + elementType: "labels.text.fill", + stylers: [ + { + color: "#f3d19c", + }, + ], + }, + { + featureType: "transit", + stylers: [ + { + visibility: "off", + }, + ], + }, + { + featureType: "transit", + elementType: "geometry", + stylers: [ + { + color: "#2f3948", + }, + ], + }, + { + featureType: "transit.station", + elementType: "labels.text.fill", + stylers: [ + { + color: "#d59563", + }, + ], + }, + { + featureType: "water", + elementType: "geometry", + stylers: [ + { + color: "#17263c", + }, + ], + }, + { + featureType: "water", + elementType: "labels.text.fill", + stylers: [ + { + color: "#515c6d", + }, + ], + }, + { + featureType: "water", + elementType: "labels.text.stroke", + stylers: [ + { + color: "#17263c", + }, + ], + }, + ], + dark: [ + { + elementType: "geometry", + stylers: [ + { + color: "#212121", + }, + ], + }, + { + elementType: "labels.icon", + stylers: [ + { + visibility: "off", + }, + ], + }, + { + elementType: "labels.text.fill", + stylers: [ + { + color: "#757575", + }, + ], + }, + { + elementType: "labels.text.stroke", + stylers: [ + { + color: "#212121", + }, + ], + }, + { + featureType: "administrative", + elementType: "geometry", + stylers: [ + { + color: "#757575", + }, + ], + }, + { + featureType: "administrative.country", + elementType: "labels.text.fill", + stylers: [ + { + color: "#9e9e9e", + }, + ], + }, + { + featureType: "administrative.land_parcel", + stylers: [ + { + visibility: "off", + }, + ], + }, + { + featureType: "administrative.locality", + elementType: "labels.text.fill", + stylers: [ + { + color: "#bdbdbd", + }, + ], + }, + { + featureType: "poi", + elementType: "labels.text.fill", + stylers: [ + { + color: "#757575", + }, + ], + }, + { + featureType: "poi.park", + elementType: "geometry", + stylers: [ + { + color: "#181818", + }, + ], + }, + { + featureType: "poi.park", + elementType: "labels.text.fill", + stylers: [ + { + color: "#616161", + }, + ], + }, + { + featureType: "poi.park", + elementType: "labels.text.stroke", + stylers: [ + { + color: "#1b1b1b", + }, + ], + }, + { + featureType: "road", + elementType: "geometry.fill", + stylers: [ + { + color: "#2c2c2c", + }, + ], + }, + { + featureType: "road", + elementType: "labels.text.fill", + stylers: [ + { + color: "#8a8a8a", + }, + ], + }, + { + featureType: "road.arterial", + elementType: "geometry", + stylers: [ + { + color: "#373737", + }, + ], + }, + { + featureType: "road.highway", + elementType: "geometry", + stylers: [ + { + color: "#3c3c3c", + }, + ], + }, + { + featureType: "road.highway.controlled_access", + elementType: "geometry", + stylers: [ + { + color: "#4e4e4e", + }, + ], + }, + { + featureType: "road.local", + elementType: "labels.text.fill", + stylers: [ + { + color: "#616161", + }, + ], + }, + { + featureType: "transit", + elementType: "labels.text.fill", + stylers: [ + { + color: "#757575", + }, + ], + }, + { + featureType: "water", + elementType: "geometry", + stylers: [ + { + color: "#000000", + }, + ], + }, + { + featureType: "water", + elementType: "labels.text.fill", + stylers: [ + { + color: "#3d3d3d", + }, + ], + }, + ], + retro: [ + { + elementType: "geometry", + stylers: [ + { + color: "#ebe3cd", + }, + ], + }, + { + elementType: "labels.text.fill", + stylers: [ + { + color: "#523735", + }, + ], + }, + { + elementType: "labels.text.stroke", + stylers: [ + { + color: "#f5f1e6", + }, + ], + }, + { + featureType: "administrative", + elementType: "geometry.stroke", + stylers: [ + { + color: "#c9b2a6", + }, + ], + }, + { + featureType: "administrative.land_parcel", + elementType: "geometry.stroke", + stylers: [ + { + color: "#dcd2be", + }, + ], + }, + { + featureType: "administrative.land_parcel", + elementType: "labels.text.fill", + stylers: [ + { + color: "#ae9e90", + }, + ], + }, + { + featureType: "landscape.natural", + elementType: "geometry", + stylers: [ + { + color: "#dfd2ae", + }, + ], + }, + { + featureType: "poi", + elementType: "geometry", + stylers: [ + { + color: "#dfd2ae", + }, + ], + }, + { + featureType: "poi", + elementType: "labels.text.fill", + stylers: [ + { + color: "#93817c", + }, + ], + }, + { + featureType: "poi.park", + elementType: "geometry.fill", + stylers: [ + { + color: "#a5b076", + }, + ], + }, + { + featureType: "poi.park", + elementType: "labels.text.fill", + stylers: [ + { + color: "#447530", + }, + ], + }, + { + featureType: "road", + elementType: "geometry", + stylers: [ + { + color: "#f5f1e6", + }, + ], + }, + { + featureType: "road.arterial", + elementType: "geometry", + stylers: [ + { + color: "#fdfcf8", + }, + ], + }, + { + featureType: "road.highway", + elementType: "geometry", + stylers: [ + { + color: "#f8c967", + }, + ], + }, + { + featureType: "road.highway", + elementType: "geometry.stroke", + stylers: [ + { + color: "#e9bc62", + }, + ], + }, + { + featureType: "road.highway.controlled_access", + elementType: "geometry", + stylers: [ + { + color: "#e98d58", + }, + ], + }, + { + featureType: "road.highway.controlled_access", + elementType: "geometry.stroke", + stylers: [ + { + color: "#db8555", + }, + ], + }, + { + featureType: "road.local", + elementType: "labels.text.fill", + stylers: [ + { + color: "#806b63", + }, + ], + }, + { + featureType: "transit.line", + elementType: "geometry", + stylers: [ + { + color: "#dfd2ae", + }, + ], + }, + { + featureType: "transit.line", + elementType: "labels.text.fill", + stylers: [ + { + color: "#8f7d77", + }, + ], + }, + { + featureType: "transit.line", + elementType: "labels.text.stroke", + stylers: [ + { + color: "#ebe3cd", + }, + ], + }, + { + featureType: "transit.station", + elementType: "geometry", + stylers: [ + { + color: "#dfd2ae", + }, + ], + }, + { + featureType: "water", + elementType: "geometry.fill", + stylers: [ + { + color: "#b9d3c2", + }, + ], + }, + { + featureType: "water", + elementType: "labels.text.fill", + stylers: [ + { + color: "#92998d", + }, + ], + }, + ], + silver: [ + { + elementType: "geometry", + stylers: [ + { + color: "#f5f5f5", + }, + ], + }, + { + elementType: "labels.icon", + stylers: [ + { + visibility: "off", + }, + ], + }, + { + elementType: "labels.text.fill", + stylers: [ + { + color: "#616161", + }, + ], + }, + { + elementType: "labels.text.stroke", + stylers: [ + { + color: "#f5f5f5", + }, + ], + }, + { + featureType: "administrative.land_parcel", + elementType: "labels.text.fill", + stylers: [ + { + color: "#bdbdbd", + }, + ], + }, + { + featureType: "poi", + elementType: "geometry", + stylers: [ + { + color: "#eeeeee", + }, + ], + }, + { + featureType: "poi", + elementType: "labels.text.fill", + stylers: [ + { + color: "#757575", + }, + ], + }, + { + featureType: "poi.park", + elementType: "geometry", + stylers: [ + { + color: "#e5e5e5", + }, + ], + }, + { + featureType: "poi.park", + elementType: "labels.text.fill", + stylers: [ + { + color: "#9e9e9e", + }, + ], + }, + { + featureType: "road", + elementType: "geometry", + stylers: [ + { + color: "#ffffff", + }, + ], + }, + { + featureType: "road.arterial", + elementType: "labels.text.fill", + stylers: [ + { + color: "#757575", + }, + ], + }, + { + featureType: "road.highway", + elementType: "geometry", + stylers: [ + { + color: "#dadada", + }, + ], + }, + { + featureType: "road.highway", + elementType: "labels.text.fill", + stylers: [ + { + color: "#616161", + }, + ], + }, + { + featureType: "road.local", + elementType: "labels.text.fill", + stylers: [ + { + color: "#9e9e9e", + }, + ], + }, + { + featureType: "transit.line", + elementType: "geometry", + stylers: [ + { + color: "#e5e5e5", + }, + ], + }, + { + featureType: "transit.station", + elementType: "geometry", + stylers: [ + { + color: "#eeeeee", + }, + ], + }, + { + featureType: "water", + elementType: "geometry", + stylers: [ + { + color: "#c9c9c9", + }, + ], + }, + { + featureType: "water", + elementType: "labels.text.fill", + stylers: [ + { + color: "#9e9e9e", + }, + ], + }, + ], + atlas: [ + { + stylers: [ + { + visibility: "on", + }, + ], + }, + { + featureType: "administrative", + elementType: "geometry.fill", + stylers: [ + { + color: "#f5f5f5", + }, + ], + }, + { + featureType: "administrative", + elementType: "labels.text.fill", + stylers: [ + { + color: "#003975", + }, + ], + }, + { + featureType: "administrative.locality", + elementType: "labels.text.fill", + stylers: [ + { + weight: 1, + }, + ], + }, + { + featureType: "administrative.neighborhood", + elementType: "labels.text.fill", + stylers: [ + { + color: "#90b1d5", + }, + ], + }, + { + featureType: "landscape.man_made", + elementType: "geometry", + stylers: [ + { + color: "#fcfcfc", + }, + { + visibility: "on", + }, + ], + }, + { + featureType: "landscape.natural", + elementType: "geometry", + stylers: [ + { + color: "#fcfcfc", + }, + ], + }, + { + featureType: "landscape.natural.landcover", + elementType: "geometry", + stylers: [ + { + color: "#f2f2f2", + }, + ], + }, + { + featureType: "landscape.natural.terrain", + elementType: "geometry", + stylers: [ + { + color: "#bfeecd", + }, + ], + }, + { + featureType: "poi.attraction", + elementType: "labels.icon", + stylers: [ + { + color: "#32b2c3", + }, + ], + }, + { + featureType: "poi.attraction", + elementType: "labels.text.fill", + stylers: [ + { + color: "#32b2c3", + }, + ], + }, + { + featureType: "poi.business", + elementType: "labels.icon", + stylers: [ + { + color: "#6c87ea", + }, + ], + }, + { + featureType: "poi.business", + elementType: "labels.text.fill", + stylers: [ + { + weight: 2, + }, + ], + }, + { + featureType: "poi.government", + elementType: "geometry", + stylers: [ + { + color: "#ebf7ff", + }, + ], + }, + { + featureType: "poi.park", + elementType: "geometry", + stylers: [ + { + color: "#dafbe2", + }, + ], + }, + { + featureType: "poi.park", + elementType: "labels.icon", + stylers: [ + { + color: "#44ca66", + }, + ], + }, + { + featureType: "poi.park", + elementType: "labels.text.fill", + stylers: [ + { + color: "#33a34f", + }, + ], + }, + { + featureType: "poi.school", + elementType: "geometry", + stylers: [ + { + color: "#f5e0f3", + }, + ], + }, + { + featureType: "road.arterial", + elementType: "geometry.fill", + stylers: [ + { + color: "#ededed", + }, + ], + }, + { + featureType: "road.arterial", + elementType: "geometry.stroke", + stylers: [ + { + color: "#cfcfcf", + }, + ], + }, + { + featureType: "road.highway", + elementType: "geometry.fill", + stylers: [ + { + color: "#90d59c", + }, + ], + }, + { + featureType: "road.highway", + elementType: "geometry.stroke", + stylers: [ + { + color: "#598860", + }, + ], + }, + { + featureType: "road.highway.controlled_access", + elementType: "geometry.fill", + stylers: [ + { + color: "#8ac0f0", + }, + ], + }, + { + featureType: "road.highway.controlled_access", + elementType: "geometry.stroke", + stylers: [ + { + color: "#2e7dc2", + }, + ], + }, + { + featureType: "road.local", + elementType: "geometry.fill", + stylers: [ + { + color: "#fcfcfc", + }, + ], + }, + { + featureType: "road.local", + elementType: "geometry.stroke", + stylers: [ + { + color: "#93d3fb", + }, + ], + }, + { + featureType: "transit", + stylers: [ + { + visibility: "off", + }, + ], + }, + { + featureType: "water", + elementType: "geometry", + stylers: [ + { + color: "#bde6fa", + }, + ], + }, + ], +}; diff --git a/web_view_google_map/tests/__init__.py b/web_view_google_map/tests/__init__.py new file mode 100644 index 000000000..f7a55f0ed --- /dev/null +++ b/web_view_google_map/tests/__init__.py @@ -0,0 +1,4 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import test_google_map_view +from . import test_hooks diff --git a/web_view_google_map/tests/test_google_map_view.py b/web_view_google_map/tests/test_google_map_view.py new file mode 100644 index 000000000..ec5560997 --- /dev/null +++ b/web_view_google_map/tests/test_google_map_view.py @@ -0,0 +1,39 @@ +# Copyright (C) 2026 Gray Matter Logic (). +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.addons.base.tests.common import BaseCommon + + +class TestGoogleMapView(BaseCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.View = cls.env["ir.ui.view"] + cls.ActWindowView = cls.env["ir.actions.act_window.view"] + + def test_view_type_selection(self): + selection = dict(self.View._fields["type"].selection) + self.assertIn("google_map", selection) + self.assertEqual(selection["google_map"], "Google Maps") + + def test_act_window_view_mode_selection(self): + selection = dict(self.ActWindowView._fields["view_mode"].selection) + self.assertIn("google_map", selection) + self.assertEqual(selection["google_map"], "Google Maps") + + def test_get_view_info_contains_google_map(self): + info = self.View._get_view_info() + self.assertIn("google_map", info) + self.assertEqual(info["google_map"]["icon"], "fa fa-map-o") + + def test_partner_google_map_view_exists(self): + view = self.env.ref("web_view_google_map.view_res_partner_google_map") + self.assertEqual(view.type, "google_map") + self.assertEqual(view.model, "res.partner") + self.assertIn("partner_latitude", view.arch) + self.assertIn("partner_longitude", view.arch) + + def test_partner_action_includes_google_map(self): + action = self.env.ref("base.action_partner_form") + self.assertIn("google_map", action.view_mode) diff --git a/web_view_google_map/tests/test_hooks.py b/web_view_google_map/tests/test_hooks.py new file mode 100644 index 000000000..691e0cb39 --- /dev/null +++ b/web_view_google_map/tests/test_hooks.py @@ -0,0 +1,41 @@ +# Copyright (C) 2026 Gray Matter Logic (). +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.addons.base.tests.common import BaseCommon +from odoo.addons.web_view_google_map.hooks import uninstall_hook + + +class TestUninstallHook(BaseCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.Action = cls.env["ir.actions.act_window"] + + def test_uninstall_hook_strips_google_map_view_mode(self): + action_mid = self.Action.create( + { + "name": "Partners Mid Google Map", + "res_model": "res.partner", + "view_mode": "list,google_map,form", + } + ) + action_start = self.Action.create( + { + "name": "Partners Start Google Map", + "res_model": "res.partner", + "view_mode": "google_map,list,form", + } + ) + action_only = self.Action.create( + { + "name": "Partners Only Google Map", + "res_model": "res.partner", + "view_mode": "google_map", + } + ) + uninstall_hook(self.env) + self.env.invalidate_all() + self.assertEqual(action_mid.view_mode, "list,form") + self.assertEqual(action_start.view_mode, "list,form") + self.assertFalse(action_only.exists()) diff --git a/web_view_google_map/views/res_partner.xml b/web_view_google_map/views/res_partner.xml new file mode 100644 index 000000000..6fae6e8b5 --- /dev/null +++ b/web_view_google_map/views/res_partner.xml @@ -0,0 +1,35 @@ + + + + view.res.partner.google_map + res.partner + + + + + + + + + + + + + + + + kanban,list,form,google_map + + + + + google_map + + + +