diff --git a/web_view_leaflet_map/README.rst b/web_view_leaflet_map/README.rst new file mode 100644 index 000000000..f7bc8dbce --- /dev/null +++ b/web_view_leaflet_map/README.rst @@ -0,0 +1,180 @@ +================================ +Leaflet Map View (OpenStreetMap) +================================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:52ae60ed2032c73f0ba6fc8ca70e2a2fc73bb12745aeeda793042efb1175c1b9 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |badge2| image:: https://img.shields.io/badge/licence-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/18.0/web_view_leaflet_map + :alt: OCA/geospatial +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/geospatial-18-0/geospatial-18-0-web_view_leaflet_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=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module extends odoo views, to add a new kind of view, named +``leaflet_map`` that is using the Leaflet javascript library to use +maps. (https://leafletjs.com/) This library is for exemple, used in the +OpenStreetMap project. (https://www.openstreetmap.org/) + +You can see a simple usage in the module +``web_view_leaflet_map_partner`` in the same OCA repository that +displays your contact in a map, if latitude and longitude are defined. +(To define latitude and longitude, refer to the Odoo module +``base_geolocalize``) + +|image1| + +|image2| + +.. |image1| image:: https://raw.githubusercontent.com/OCA/geospatial/18.0/web_view_leaflet_map/static/description/view_res_partner_map_1.png +.. |image2| image:: https://raw.githubusercontent.com/OCA/geospatial/18.0/web_view_leaflet_map/static/description/view_res_partner_map_2.png + +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +- See configuration of the module ``web_leaflet_lib``. + +Development +=========== + +Create a new view : + +.. code:: xml + + + my.model + + + + + + + + + + + +1. FIELD_LATITUDE and FIELD_LONGITUDE are the name of the fields that + contains GPS coordinates of the model. +2. FIELD_TITLE will be used when the popup is displayed, as a title. +3. FIELD_ADDRESS will be used when the popup is displayed to display the + adress. +4. (optional) FIELD_MARKER_ICON_IMAGE, is the name of the image field to + place as an icon of the marker. Note: You can set extra settings + ``marker_icon_size_x``, ``marker_icon_size_y``, to define the size of + the image, and ``marker_popup_anchor_x``, ``marker_popup_anchor_y`` + to define the position of the popup. + +Map options : + +- ``default_zoom`` : define the default zoom value. (7 if not defined) +- ``max_zoom`` : define the max zoom value. (19 if not defined) +- ``zoom_snap`` : define the zoom level in each change. (1 if not + defined) +- Create or update an action for the model + +.. code:: xml + + + tree,form,leaflet_map + + +**Default position in the map** + +By default, the position of the map is defined by the user, in the +function ``get_default_leaflet_position``. It returns the position of +the current company, if defined. you can overload this function +globally, or per model. + +Known issues / Roadmap +====================== + +- For the time being, at the start of the map loading, the call of + ``invalidateSize()`` is required. We should investigate why and try to + remove that call. see + https://github.com/Leaflet/Leaflet/issues/3002#issuecomment-93836022 +- For the time being, the map has "Markers" and allow to display odoo + items if longitude and latitude are available. We could imagine other + kind of usages, with Polylines, Polygons, etc... See all the leaflet + options : https://leafletjs.com/reference.html + +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 +------- + +* GRAP + +Contributors +------------ + +- Sylvain LE GAL (https://www.twitter.com/legalsylvain) + +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-legalsylvain| image:: https://github.com/legalsylvain.png?size=40px + :target: https://github.com/legalsylvain + :alt: legalsylvain + +Current `maintainer `__: + +|maintainer-legalsylvain| + +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_leaflet_map/__init__.py b/web_view_leaflet_map/__init__.py new file mode 100644 index 000000000..071962a35 --- /dev/null +++ b/web_view_leaflet_map/__init__.py @@ -0,0 +1,2 @@ +from . import models +from .hooks import uninstall_hook diff --git a/web_view_leaflet_map/__manifest__.py b/web_view_leaflet_map/__manifest__.py new file mode 100644 index 000000000..e27cf2dfb --- /dev/null +++ b/web_view_leaflet_map/__manifest__.py @@ -0,0 +1,28 @@ +# Copyright (C) 2022 - Today: GRAP (http://www.grap.coop) +# @author: Sylvain LE GAL (https://twitter.com/legalsylvain) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +{ + "name": "Leaflet Map View (OpenStreetMap)", + "summary": "Add new 'leaflet_map' view, to display markers.", + "version": "18.0.1.0.0", + "development_status": "Alpha", + "author": "GRAP, Odoo Community Association (OCA)", + "maintainers": ["legalsylvain"], + "website": "https://github.com/OCA/geospatial", + "license": "AGPL-3", + "category": "Extra Tools", + "depends": [ + "base_geolocalize", + "web_leaflet_lib", + ], + "assets": { + "web.assets_backend": [ + "web_view_leaflet_map/static/src/components/map-component/map_view.esm.js", + "web_view_leaflet_map/static/src/components/map-component/map_view.xml", + "web_view_leaflet_map/static/src/components/map-component/web_view_leaflet_map.css", + ], + }, + "installable": True, + "uninstall_hook": "uninstall_hook", +} diff --git a/web_view_leaflet_map/hooks.py b/web_view_leaflet_map/hooks.py new file mode 100644 index 000000000..ed5c52a76 --- /dev/null +++ b/web_view_leaflet_map/hooks.py @@ -0,0 +1,18 @@ +# Copyright (C) 2019, Open Source Integrators +# Copyright (C) 2022 - Today: GRAP (http://www.grap.coop) +# @author: Sylvain LE GAL (https://twitter.com/legalsylvain) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + + +def uninstall_hook(env): + env.cr.execute( + "UPDATE ir_act_window " + "SET view_mode=replace(view_mode, ',leaflet_map', '')" + "WHERE view_mode LIKE '%,leaflet_map%';" + ) + env.cr.execute( + "UPDATE ir_act_window " + "SET view_mode=replace(view_mode, 'leaflet_map,', '')" + "WHERE view_mode LIKE '%leaflet_map,%';" + ) + env.cr.execute("DELETE FROM ir_act_window " "WHERE view_mode = 'leaflet_map';") diff --git a/web_view_leaflet_map/i18n/fr.po b/web_view_leaflet_map/i18n/fr.po new file mode 100644 index 000000000..076fda89d --- /dev/null +++ b/web_view_leaflet_map/i18n/fr.po @@ -0,0 +1,79 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * web_view_leaflet_map +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-09-18 19:56+0000\n" +"PO-Revision-Date: 2022-09-18 19:56+0000\n" +"Last-Translator: <>\n" +"Language-Team: \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: web_view_leaflet_map +#: model:ir.model,name:web_view_leaflet_map.model_ir_actions_act_window_view +msgid "Action Window View" +msgstr "Vue de la Fenêtre d'Action" + +#. module: web_view_leaflet_map +#: model:ir.model.fields.selection,name:web_view_leaflet_map.selection__ir_actions_act_window_view__view_mode__leaflet_map +#: model:ir.model.fields.selection,name:web_view_leaflet_map.selection__ir_ui_view__type__leaflet_map +msgid "Leaflet Map" +msgstr "Carte Leaflet" + +#. module: web_view_leaflet_map +#. odoo-javascript +#: code:addons/web_view_leaflet_map/static/src/js/view/map/map_view.js:0 +#, python-format +msgid "Map" +msgstr "Carte" + +#. module: web_view_leaflet_map +#: model:ir.model,name:web_view_leaflet_map.model_res_users +msgid "User" +msgstr "" + +#. module: web_view_leaflet_map +#: model:ir.model,name:web_view_leaflet_map.model_ir_ui_view +msgid "View" +msgstr "Vue" + +#. module: web_view_leaflet_map +#: model:ir.model.fields,field_description:web_view_leaflet_map.field_ir_actions_act_window_view__view_mode +#: model:ir.model.fields,field_description:web_view_leaflet_map.field_ir_ui_view__type +#: model:ir.model.fields,field_description:web_view_leaflet_map.field_website_page__type +msgid "View Type" +msgstr "Type de Vue" + +#~ msgid "Activity" +#~ msgstr "Activité" + +#~ msgid "Calendar" +#~ msgstr "Calendrier" + +#~ msgid "Diagram" +#~ msgstr "Diagramme" + +#~ msgid "Form" +#~ msgstr "Formulaire" + +#~ msgid "Graph" +#~ msgstr "Graphique" + +#~ msgid "Pivot" +#~ msgstr "Tableau croisé dynamique" + +#~ msgid "Search" +#~ msgstr "Rechercher" + +#~ msgid "Tree" +#~ msgstr "Arborescence" + +#~ msgid "Users" +#~ msgstr "Utilisateurs" diff --git a/web_view_leaflet_map/i18n/it.po b/web_view_leaflet_map/i18n/it.po new file mode 100644 index 000000000..33c0a205b --- /dev/null +++ b/web_view_leaflet_map/i18n/it.po @@ -0,0 +1,52 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * web_view_leaflet_map +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-01-26 08:36+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 4.17\n" + +#. module: web_view_leaflet_map +#: model:ir.model,name:web_view_leaflet_map.model_ir_actions_act_window_view +msgid "Action Window View" +msgstr "Vista maschera azione" + +#. module: web_view_leaflet_map +#: model:ir.model.fields.selection,name:web_view_leaflet_map.selection__ir_actions_act_window_view__view_mode__leaflet_map +#: model:ir.model.fields.selection,name:web_view_leaflet_map.selection__ir_ui_view__type__leaflet_map +msgid "Leaflet Map" +msgstr "Mappa Leaflet" + +#. module: web_view_leaflet_map +#. odoo-javascript +#: code:addons/web_view_leaflet_map/static/src/js/view/map/map_view.js:0 +#, python-format +msgid "Map" +msgstr "Mappa" + +#. module: web_view_leaflet_map +#: model:ir.model,name:web_view_leaflet_map.model_res_users +msgid "User" +msgstr "Utente" + +#. module: web_view_leaflet_map +#: model:ir.model,name:web_view_leaflet_map.model_ir_ui_view +msgid "View" +msgstr "Vista" + +#. module: web_view_leaflet_map +#: model:ir.model.fields,field_description:web_view_leaflet_map.field_ir_actions_act_window_view__view_mode +#: model:ir.model.fields,field_description:web_view_leaflet_map.field_ir_ui_view__type +#: model:ir.model.fields,field_description:web_view_leaflet_map.field_website_page__type +msgid "View Type" +msgstr "Tipo vista" diff --git a/web_view_leaflet_map/i18n/web_view_leaflet_map.pot b/web_view_leaflet_map/i18n/web_view_leaflet_map.pot new file mode 100644 index 000000000..e147c0e35 --- /dev/null +++ b/web_view_leaflet_map/i18n/web_view_leaflet_map.pot @@ -0,0 +1,49 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * web_view_leaflet_map +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.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_leaflet_map +#: model:ir.model,name:web_view_leaflet_map.model_ir_actions_act_window_view +msgid "Action Window View" +msgstr "" + +#. module: web_view_leaflet_map +#: model:ir.model.fields.selection,name:web_view_leaflet_map.selection__ir_actions_act_window_view__view_mode__leaflet_map +#: model:ir.model.fields.selection,name:web_view_leaflet_map.selection__ir_ui_view__type__leaflet_map +msgid "Leaflet Map" +msgstr "" + +#. module: web_view_leaflet_map +#. odoo-javascript +#: code:addons/web_view_leaflet_map/static/src/js/view/map/map_view.js:0 +#, python-format +msgid "Map" +msgstr "" + +#. module: web_view_leaflet_map +#: model:ir.model,name:web_view_leaflet_map.model_res_users +msgid "User" +msgstr "" + +#. module: web_view_leaflet_map +#: model:ir.model,name:web_view_leaflet_map.model_ir_ui_view +msgid "View" +msgstr "" + +#. module: web_view_leaflet_map +#: model:ir.model.fields,field_description:web_view_leaflet_map.field_ir_actions_act_window_view__view_mode +#: model:ir.model.fields,field_description:web_view_leaflet_map.field_ir_ui_view__type +#: model:ir.model.fields,field_description:web_view_leaflet_map.field_website_page__type +msgid "View Type" +msgstr "" diff --git a/web_view_leaflet_map/models/__init__.py b/web_view_leaflet_map/models/__init__.py new file mode 100644 index 000000000..fc23e6d3f --- /dev/null +++ b/web_view_leaflet_map/models/__init__.py @@ -0,0 +1,3 @@ +from . import ir_act_window_view +from . import ir_ui_view +from . import res_users diff --git a/web_view_leaflet_map/models/ir_act_window_view.py b/web_view_leaflet_map/models/ir_act_window_view.py new file mode 100644 index 000000000..f6c72d938 --- /dev/null +++ b/web_view_leaflet_map/models/ir_act_window_view.py @@ -0,0 +1,15 @@ +# Copyright (C) 2019, Open Source Integrators +# Copyright (C) 2022 - Today: GRAP (http://www.grap.coop) +# @author: Sylvain LE GAL (https://twitter.com/legalsylvain) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class IrActionsActWindowView(models.Model): + _inherit = "ir.actions.act_window.view" + + view_mode = fields.Selection( + selection_add=[("leaflet_map", "Leaflet Map")], + ondelete={"leaflet_map": "cascade"}, + ) diff --git a/web_view_leaflet_map/models/ir_ui_view.py b/web_view_leaflet_map/models/ir_ui_view.py new file mode 100644 index 000000000..60cc4dc27 --- /dev/null +++ b/web_view_leaflet_map/models/ir_ui_view.py @@ -0,0 +1,15 @@ +# Copyright (C) 2019, Open Source Integrators +# Copyright (C) 2022 - Today: GRAP (http://www.grap.coop) +# @author: Sylvain LE GAL (https://twitter.com/legalsylvain) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class IrUiView(models.Model): + _inherit = "ir.ui.view" + + type = fields.Selection(selection_add=[("leaflet_map", "Leaflet Map")]) + + def _get_view_info(self): + return {"leaflet_map": {"icon": "fa fa-map-o"}} | super()._get_view_info() diff --git a/web_view_leaflet_map/models/res_users.py b/web_view_leaflet_map/models/res_users.py new file mode 100644 index 000000000..d88bd90d7 --- /dev/null +++ b/web_view_leaflet_map/models/res_users.py @@ -0,0 +1,17 @@ +# Copyright (C) 2022 - Today: GRAP (http://www.grap.coop) +# @author: Sylvain LE GAL (https://twitter.com/legalsylvain) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import api, models + + +class ResUsers(models.Model): + _inherit = "res.users" + + @api.model + def get_default_leaflet_position(self, model_name): + current_partner = self.env.user.company_id.partner_id + return { + "lat": current_partner.partner_latitude, + "lng": current_partner.partner_longitude, + } diff --git a/web_view_leaflet_map/pyproject.toml b/web_view_leaflet_map/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/web_view_leaflet_map/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/web_view_leaflet_map/readme/CONFIGURE.md b/web_view_leaflet_map/readme/CONFIGURE.md new file mode 100644 index 000000000..801711609 --- /dev/null +++ b/web_view_leaflet_map/readme/CONFIGURE.md @@ -0,0 +1 @@ +- See configuration of the module `web_leaflet_lib`. diff --git a/web_view_leaflet_map/readme/CONTRIBUTORS.md b/web_view_leaflet_map/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..4a6b63400 --- /dev/null +++ b/web_view_leaflet_map/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +- Sylvain LE GAL () diff --git a/web_view_leaflet_map/readme/DESCRIPTION.md b/web_view_leaflet_map/readme/DESCRIPTION.md new file mode 100644 index 000000000..7f0d47e8f --- /dev/null +++ b/web_view_leaflet_map/readme/DESCRIPTION.md @@ -0,0 +1,13 @@ +This module extends odoo views, to add a new kind of view, named +`leaflet_map` that is using the Leaflet javascript library to use maps. +() This library is for exemple, used in the +OpenStreetMap project. () + +You can see a simple usage in the module `web_view_leaflet_map_partner` +in the same OCA repository that displays your contact in a map, if +latitude and longitude are defined. (To define latitude and longitude, +refer to the Odoo module `base_geolocalize`) + +![](../static/description/view_res_partner_map_1.png) + +![](../static/description/view_res_partner_map_2.png) diff --git a/web_view_leaflet_map/readme/DEVELOP.md b/web_view_leaflet_map/readme/DEVELOP.md new file mode 100644 index 000000000..a3f3cac93 --- /dev/null +++ b/web_view_leaflet_map/readme/DEVELOP.md @@ -0,0 +1,53 @@ +Create a new view : + +``` xml + + my.model + + + + + + + + + + +``` + +1. FIELD_LATITUDE and FIELD_LONGITUDE are the name of the fields that + contains GPS coordinates of the model. +2. FIELD_TITLE will be used when the popup is displayed, as a title. +3. FIELD_ADDRESS will be used when the popup is displayed to display + the adress. +4. (optional) FIELD_MARKER_ICON_IMAGE, is the name of the image field + to place as an icon of the marker. Note: You can set extra settings + `marker_icon_size_x`, `marker_icon_size_y`, to define the size of + the image, and `marker_popup_anchor_x`, `marker_popup_anchor_y` to + define the position of the popup. + +Map options : + +- `default_zoom` : define the default zoom value. (7 if not defined) +- `max_zoom` : define the max zoom value. (19 if not defined) +- `zoom_snap` : define the zoom level in each change. (1 if not defined) +- Create or update an action for the model + +``` xml + + tree,form,leaflet_map + +``` + +**Default position in the map** + +By default, the position of the map is defined by the user, in the +function `get_default_leaflet_position`. It returns the position of the +current company, if defined. you can overload this function globally, or +per model. diff --git a/web_view_leaflet_map/readme/ROADMAP.md b/web_view_leaflet_map/readme/ROADMAP.md new file mode 100644 index 000000000..2ab5c80a4 --- /dev/null +++ b/web_view_leaflet_map/readme/ROADMAP.md @@ -0,0 +1,8 @@ +- For the time being, at the start of the map loading, the call of + `invalidateSize()` is required. We should investigate why and try to + remove that call. see + +- For the time being, the map has "Markers" and allow to display odoo + items if longitude and latitude are available. We could imagine other + kind of usages, with Polylines, Polygons, etc... See all the leaflet + options : diff --git a/web_view_leaflet_map/static/description/icon.png b/web_view_leaflet_map/static/description/icon.png new file mode 100644 index 000000000..3a0328b51 Binary files /dev/null and b/web_view_leaflet_map/static/description/icon.png differ diff --git a/web_view_leaflet_map/static/description/index.html b/web_view_leaflet_map/static/description/index.html new file mode 100644 index 000000000..801c5b1e9 --- /dev/null +++ b/web_view_leaflet_map/static/description/index.html @@ -0,0 +1,514 @@ + + + + + +Leaflet Map View (OpenStreetMap) + + + +
+

Leaflet Map View (OpenStreetMap)

+ + +

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

+

This module extends odoo views, to add a new kind of view, named +leaflet_map that is using the Leaflet javascript library to use +maps. (https://leafletjs.com/) This library is for exemple, used in the +OpenStreetMap project. (https://www.openstreetmap.org/)

+

You can see a simple usage in the module +web_view_leaflet_map_partner in the same OCA repository that +displays your contact in a map, if latitude and longitude are defined. +(To define latitude and longitude, refer to the Odoo module +base_geolocalize)

+

image1

+

image2

+
+

Important

+

This is an alpha version, the data model and design can change at any time without warning. +Only for development or testing purpose, do not use in production. +More details on development status

+
+

Table of contents

+ +
+

Configuration

+
    +
  • See configuration of the module web_leaflet_lib.
  • +
+
+
+

Development

+

Create a new view :

+
+<record id="view_my_model_map" model="ir.ui.view">
+    <field name="model">my.model</field>
+    <field name="arch" type="xml">
+        <leaflet_map
+                field_latitude="FIELD_LATITUDE"
+                field_longitude="FIELD_LONGITUDE"
+                field_title="FIELD_TITLE"
+                field_address="FIELD_ADDRESS"
+                field_marker_icon_image="FIELD_MARKER_ICON_IMAGE"
+            >
+            <field name="__last_update"/>
+            <field name="FIELD_LATITUDE"/>
+            <field name="FIELD_LONGITUDE"/>
+            <field name="FIELD_TITLE"/>
+            <field name="FIELD_ADDRESS"/>
+        </leaflet_map>
+    </field>
+</record>
+
+
    +
  1. FIELD_LATITUDE and FIELD_LONGITUDE are the name of the fields that +contains GPS coordinates of the model.
  2. +
  3. FIELD_TITLE will be used when the popup is displayed, as a title.
  4. +
  5. FIELD_ADDRESS will be used when the popup is displayed to display the +adress.
  6. +
  7. (optional) FIELD_MARKER_ICON_IMAGE, is the name of the image field to +place as an icon of the marker. Note: You can set extra settings +marker_icon_size_x, marker_icon_size_y, to define the size of +the image, and marker_popup_anchor_x, marker_popup_anchor_y +to define the position of the popup.
  8. +
+

Map options :

+
    +
  • default_zoom : define the default zoom value. (7 if not defined)
  • +
  • max_zoom : define the max zoom value. (19 if not defined)
  • +
  • zoom_snap : define the zoom level in each change. (1 if not +defined)
  • +
  • Create or update an action for the model
  • +
+
+<record id="my_module.action_my_model" model="ir.actions.act_window">
+    <field name="view_mode">tree,form,leaflet_map</field>
+</record>
+
+

Default position in the map

+

By default, the position of the map is defined by the user, in the +function get_default_leaflet_position. It returns the position of +the current company, if defined. you can overload this function +globally, or per model.

+
+
+

Known issues / Roadmap

+ +
+
+

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

+
    +
  • GRAP
  • +
+
+ +
+

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 maintainer:

+

legalsylvain

+

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_leaflet_map/static/description/res_partner_map.png b/web_view_leaflet_map/static/description/res_partner_map.png new file mode 100644 index 000000000..a47b2cf52 Binary files /dev/null and b/web_view_leaflet_map/static/description/res_partner_map.png differ diff --git a/web_view_leaflet_map/static/src/components/map-component/map_view.esm.js b/web_view_leaflet_map/static/src/components/map-component/map_view.esm.js new file mode 100644 index 000000000..ff172882d --- /dev/null +++ b/web_view_leaflet_map/static/src/components/map-component/map_view.esm.js @@ -0,0 +1,329 @@ +import {registry} from "@web/core/registry"; +import {useService} from "@web/core/utils/hooks"; +import {Layout} from "@web/search/layout"; +import {session} from "@web/session"; + +/* global L, console, document, DOMParser */ + +const {Component, useSubEnv, onWillStart, onMounted, onPatched, useRef} = owl; + +export class MapRenderer extends Component { + /** + * Initializes the MapRenderer component, setting up services, references, and configuration. + */ + setup() { + console.log(this.props); + this.orm = useService("orm"); + this.action = useService("action"); + this.mapRef = useRef("mapContainer"); + this.leafletTileUrl = session["leaflet.tile_url"]; + this.leafletCopyright = session["leaflet.copyright"]; + + const archAttrs = this.props.archInfo.arch.attributes; + + this.resModel = this.props.resModel; + this.defaultZoom = parseInt(archAttrs.default_zoom, 10) || 7; + this.maxZoom = parseInt(archAttrs.max_zoom, 10) || 19; + this.zoomSnap = parseInt(archAttrs.zoom_snap, 10) || 1; + + this.fieldLatitude = archAttrs.field_latitude?.value; + this.fieldLongitude = archAttrs.field_longitude?.value; + this.fieldTitle = archAttrs.field_title?.value; + this.fieldAddress = archAttrs.field_address?.value; + this.fieldMarkerIconImage = archAttrs.field_marker_icon_image?.value; + + this.markerIconSizeX = parseInt(archAttrs.marker_icon_size_x?.value, 10) || 64; + this.markerIconSizeY = parseInt(archAttrs.marker_icon_size_y?.value, 10) || 64; + this.markerPopupAnchorX = + parseInt(archAttrs.marker_popup_anchor_x?.value, 10) || 0; + this.markerPopupAnchorY = + parseInt(archAttrs.marker_popup_anchor_y?.value, 10) || -32; + + this.leafletMap = null; + this.leafletFeatureGroup = null; + + onWillStart(async () => { + await this.initDefaultPosition(); + await this.loadRecords(); + }); + + onMounted(() => { + this.initMap(); + this.renderMarkers(); + }); + + onPatched(() => { + console.log("onPatched"); + if (this.leafletMap) { + this.renderMarkers(); + } + }); + } + + /** + * Loads records from the server based on the provided domain and fields. + * @returns {Promise} + */ + async loadRecords() { + const fields = this.getFields(); + + try { + // Cargar registros usando searchRead + const records = await this.orm.searchRead( + this.resModel, + this.props.domain || [], + fields, + { + limit: this.props.limit || 80, + context: this.props.context || {}, + } + ); + this.records = records; + } catch (error) { + console.error("Error loading records:", error); + this.records = []; + } + } + + /** + * Gathers the required fields for the map view. + * @returns {any[]} + */ + getFields() { + const fields = new Set(); + + // Required fields + fields.add("id"); + fields.add("display_name"); + fields.add("date_localization"); + + // Optional fields based on arch attributes + if (this.fieldLatitude) fields.add(this.fieldLatitude); + if (this.fieldLongitude) fields.add(this.fieldLongitude); + if (this.fieldTitle) fields.add(this.fieldTitle); + if (this.fieldAddress) fields.add(this.fieldAddress); + if (this.fieldMarkerIconImage) fields.add(this.fieldMarkerIconImage); + + return Array.from(fields); + } + + /** + * Initializes the default position of the map by calling the server method. + * @returns {Promise} + */ + async initDefaultPosition() { + const result = await this.orm.call( + "res.users", + "get_default_leaflet_position", + [this.props.resModel] + ); + this.defaultLatLng = L.latLng(result.lat, result.lng); + } + + /** + * Initializes the Leaflet map in the container. + */ + initMap() { + const mapDiv = this.mapRef.el; + if (!mapDiv) { + console.error("Map container not found"); + return; + } + + this.leafletMap = L.map(mapDiv, { + zoomSnap: this.zoomSnap, + }).setView(this.defaultLatLng, this.defaultZoom); + + L.tileLayer(this.leafletTileUrl, { + maxZoom: this.maxZoom, + attribution: this.leafletCopyright, + }).addTo(this.leafletMap); + } + + /** + * Renders the markers on the map based on the loaded records. + */ + renderMarkers() { + if (!this.leafletMap) { + console.warn("Map not initialized yet"); + return; + } + + if (this.leafletFeatureGroup) { + this.leafletMap.removeLayer(this.leafletFeatureGroup); + } + + this.leafletFeatureGroup = L.featureGroup().addTo(this.leafletMap); + + for (const record of this.records) { + const marker = this.prepareMarker(record); + if (marker) { + marker.addTo(this.leafletFeatureGroup); + } + } + + this.leafletMap.fitBounds(this.leafletFeatureGroup.getBounds().pad(0.1)); + } + + /** + * Prepares a Leaflet marker for the given record. + * @param {Object} record - The record object containing marker data + * @returns {*} + */ + prepareMarker(record) { + const lat = record[this.fieldLatitude]; + const lng = record[this.fieldLongitude]; + let marker = null; + if (!lat || !lng) { + console.log(`Record ${record.id} has no coordinates`); + return; + } + + const latlng = L.latLng(lat, lng); + if (latlng.lat !== 0 && latlng.lng !== 0) { + const markerOptions = this.prepareMarkerOptions(record); + + marker = L.marker(latlng, markerOptions); + const popup = L.popup().setContent(this.preparePopUpData(record)); + + marker.bindPopup(popup).on("popupopen", () => { + const selector = document.querySelector(".o_map_selector"); + if (selector) { + selector.addEventListener("click", (ev) => { + ev.preventDefault(); + this.onClickLeafletPopup(record); + }); + } + }); + + return marker; + } + } + + /** + * Prepares the Leaflet icon for the marker using the image field. + * @param {Object} record - The record object containing marker data + * @returns {*} + */ + prepareMarkerIcon(record) { + const lastUpdate = record.date_localization || new Date().toISOString(); + const unique = lastUpdate.replace(/[^0-9]/g, ""); + const iconUrl = `/web/image?model=${this.resModel}&id=${record.id}&field=${this.fieldMarkerIconImage}&unique=${unique}`; + + return L.icon({ + iconUrl: iconUrl, + className: "leaflet_marker_icon", + iconSize: [this.markerIconSizeX, this.markerIconSizeY], + popupAnchor: [this.markerPopupAnchorX, this.markerPopupAnchorY], + }); + } + + /** + * Prepares the options for the leaflet marker. + * @param {Object} record - The record object containing marker data + * @returns {{riseOnHover: Boolean, alt: (*|string), title: (*|string)}} + */ + prepareMarkerOptions(record) { + const title = record[this.fieldTitle] || ""; + const result = { + title: title, + alt: title, + riseOnHover: true, + }; + + if (this.fieldMarkerIconImage) { + result.icon = this.prepareMarkerIcon(record); + } + + return result; + } + + /** + * Prepares the HTML content for the leaflet popup. + * @param {Object} record - The record object containing marker data + * @returns {String} + */ + preparePopUpData(record) { + const title = record[this.fieldTitle] || ""; + const address = record[this.fieldAddress] || ""; + + return ` +
+ ${title}
+ ${address ? ` - ${address}` : ""} +
+ `; + } + + /** + * Handles click on the leaflet popup to open the record form view. + * @param {Object} record - The record object containing marker data + */ + onClickLeafletPopup(record) { + this.action.doAction({ + type: "ir.actions.act_window", + res_model: this.resModel, + res_id: record.id, + views: [[false, "form"]], + target: "current", + }); + } +} + +MapRenderer.template = "web_view_leaflet_map.MapRenderer"; +MapRenderer.components = {}; + +/** + * Controller class for the Map view, setting up the environment configuration. + */ +export class MapController extends Component { + setup() { + useSubEnv({ + config: { + ...this.env.config, + }, + }); + } +} + +MapController.template = "web_view_leaflet_map.MapView"; +MapController.components = {Layout, MapRenderer}; + +/** + * Helper function that normalize the architecture input to ensure it is an HTMLElement. + * @param arch + * @returns {HTMLElement|*} + */ +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; +} + +/** + * Definition of the map view for Odoo, including its properties and components. + * @type {{searchMenuTypes: string[], icon: string, Renderer: MapRenderer, multiRecord: boolean, type: string, display_name: string, Controller: MapController, props: (function(*, *): *&{archInfo: {arch: *}, Renderer: MapRenderer})}} + */ +export const mapView = { + type: "leaflet_map", + display_name: "Map", + icon: "fa fa-map-o", + multiRecord: true, + Controller: MapController, + Renderer: MapRenderer, + searchMenuTypes: ["filter", "favorite"], + + props: (genericProps) => { + const archEl = normalizeArch(genericProps.arch); + return { + ...genericProps, + Renderer: MapRenderer, + archInfo: { + arch: archEl, + }, + }; + }, +}; + +registry.category("views").add("leaflet_map", mapView); diff --git a/web_view_leaflet_map/static/src/components/map-component/map_view.xml b/web_view_leaflet_map/static/src/components/map-component/map_view.xml new file mode 100644 index 000000000..d4ad421b4 --- /dev/null +++ b/web_view_leaflet_map/static/src/components/map-component/map_view.xml @@ -0,0 +1,19 @@ + + + + + + +
+ + + + + + +
+
+
+ + + diff --git a/web_view_leaflet_map/static/src/components/map-component/web_view_leaflet_map.css b/web_view_leaflet_map/static/src/components/map-component/web_view_leaflet_map.css new file mode 100644 index 000000000..cc88d6356 --- /dev/null +++ b/web_view_leaflet_map/static/src/components/map-component/web_view_leaflet_map.css @@ -0,0 +1,60 @@ +/* + Custom CSS for the elements introduced by web_view_leaflet_map module +*/ + +.o_leaflet_main_container { + display: flex; + align-content: stretch; + overflow-x: visible; + height: 100%; + width: 100%; +} + +.o_leaflet_map_container { + height: 100%; + width: 100%; +} + +.o_map_selector { + cursor: pointer; +} + +.o_map_selector:hover { + background-color: #eee; +} + +.leaflet_marker_icon { + background-color: white; + border: 1px black solid; + border-radius: 50% !important; +} + +/* + Overload leaflet CSS to work with custom specific things of Odoo CSS +*/ + +/* hide close button to avoid bad navigation experience + Before that patch, if user click on the 'close' button, the home page will be loaded. + (popup can be hidden by clicking out of the popup, on the map) +*/ +.leaflet-popup-close-button { + display: none; +} + +/* Change z-index from 1000 to 999 to avoid to conflict with odoo menu that are set to 1000 + See : https://github.com/odoo/odoo/blob/12.0/addons/web/static/lib/bootstrap/css/bootstrap.css#L2991 +*/ +.leaflet-top, +.leaflet-bottom { + z-index: 999 !important; +} + +/* Odoo is hiding (with display directive) all the aria-hidden elements of Odoo + that prevent to display buttons introduced by the leaflet library. + This directive force to display the elements for the leaflet elements. + See : https://github.com/odoo/odoo/blob/12.0/addons/web/static/src/scss/ui.scss#L13 +*/ +.leaflet-container [aria-hidden="true"], +[aria-hidden="1"] { + display: inline !important; +}