# HG changeset patch
# User Vincent Hatakeyama <vincent.hatakeyama@xcg-consulting.fr>
# Date 1740052304 -3600
#      Thu Feb 20 12:51:44 2025 +0100
# Branch 18.0
# Node ID 0de61891edacf2629b4aca660ae72d020f8edcf9
# Parent  e87d2e751c850954a2125261af05822ff430d90c
Add JsonLD_ID converter

diff --git a/NEWS.rst b/NEWS.rst
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -1,6 +1,11 @@
 Changelog
 =========
 
+18.0.4.1.0
+----------
+
+Add JsonLD_ID converter.
+
 18.0.4.0.0
 ----------
 
diff --git a/__init__.py b/__init__.py
--- a/__init__.py
+++ b/__init__.py
@@ -52,4 +52,4 @@
     NotInitialized,
     Validator,
 )
-from .xref import Xref
+from .xref import Xref, JsonLD_ID
diff --git a/__manifest__.py b/__manifest__.py
--- a/__manifest__.py
+++ b/__manifest__.py
@@ -21,7 +21,7 @@
     "name": "Converter",
     "license": "AGPL-3",
     "summary": "Convert odoo records to/from plain data structures.",
-    "version": "18.0.4.0.0",
+    "version": "18.0.4.1.0",
     "category": "Hidden",
     "author": "XCG Consulting",
     "website": "https://orbeet.io/",
diff --git a/xref.py b/xref.py
--- a/xref.py
+++ b/xref.py
@@ -1,7 +1,7 @@
 ##############################################################################
 #
 #    Converter Odoo module
-#    Copyright © 2020 XCG Consulting <https://xcg-consulting.fr>
+#    Copyright © 2020, 2025 XCG Consulting <https://xcg-consulting.fr>
 #
 #    This program is free software: you can redistribute it and/or modify
 #    it under the terms of the GNU Affero General Public License as
@@ -17,14 +17,24 @@
 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
 ##############################################################################
-
+import logging
+import os
 from typing import Any
 
-from odoo import api, models  # type: ignore[import-untyped]
+from odoo import _, api, models  # type: ignore[import-untyped]
 
-from .base import Context, NewinstanceType, PostHookConverter
+from .base import (
+    Context,
+    ContextBuilder,
+    Converter,
+    NewinstanceType,
+    PostHookConverter,
+    build_context,
+)
 from .models.ir_model_data import _XREF_IMD_MODULE
 
+_logger = logging.getLogger(__name__)
+
 
 # TODO dans quel cas ça ne pourrait pas être un instance getter???
 class Xref(PostHookConverter):
@@ -65,3 +75,92 @@
     @property
     def is_instance_getter(self) -> bool:
         return self._is_instance_getter
+
+
+class JsonLD_ID(Xref):
+    """This converter represents a JsonLD ID , an url made of
+    a base part defined as ir.config_parameter, an optional breadcrumb
+    and a unique id part using the standard xmlid.
+    """
+
+    def __init__(
+        self,
+        breadcrumb: str | Converter,
+        module: str | None = _XREF_IMD_MODULE,
+        is_instance_getter: bool = True,
+        unique_id_field: str | None = None,
+        context: ContextBuilder | None = None,
+        has_base_url: bool = True,
+    ):
+        """
+        :param breadcrumb: Part of the url describing the entity,
+        must match the syntax expected by os.path, ie absolute path
+        begins with a slash. With absolute path the base part is
+        ignored. Can also be a converter, if so, the result of the
+        combined converters must be a string.
+        """
+        super().__init__(
+            module=module,
+            is_instance_getter=is_instance_getter,
+        )
+        self.converter: Converter | None = None
+        self._breadcrumb = breadcrumb if isinstance(breadcrumb, str) else None
+        if isinstance(breadcrumb, Converter):
+            self.converter = breadcrumb
+        self._unique_id_field = unique_id_field
+        self._context = context
+        self._has_base_url = has_base_url
+
+    def odoo_to_message(self, instance: models.BaseModel, ctx: Context = None) -> Any:
+        if not instance:
+            return ""
+
+        ctx = build_context(instance, ctx, self._context)
+        if self._unique_id_field is not None:
+            name = getattr(instance, self._unique_id_field)
+        else:
+            _module, name = instance.env["ir.model.data"].object_to_module_and_name(
+                instance, self._module
+            )
+
+        jsonld_id_base_url = (
+            instance.env["ir.config_parameter"]
+            .sudo()
+            .get_param("sync.jsonld_id_base_url")
+        )
+        if self._has_base_url and not jsonld_id_base_url:
+            _logger.error(
+                _("Missing config parameter: 'sync.jsonld_id_base_url' is not defined")
+            )
+            return ""
+
+        if self.converter is not None:
+            self._breadcrumb = self.converter.odoo_to_message(instance, ctx)
+
+        xref = os.path.join(
+            jsonld_id_base_url if self._has_base_url else "",
+            self._breadcrumb if self._breadcrumb is not None else "",
+            name,
+        )
+        instance.env["ir.model.data"].set_xmlid(
+            instance, xref, module=self._module, only_when_missing=True
+        )
+
+        # In case of jsonld_id, replace the xref generated by
+        # `object_to_module_and_name` with jsonld format.
+        if self._has_base_url:
+            # Need sudo as typical user does not have the rights on ir.model.data
+            imd = (
+                instance.env["ir.model.data"]
+                .sudo()
+                .search(
+                    [
+                        ("module", "=", self._module),
+                        ("model", "=", instance._name),
+                        ("res_id", "=", instance.id),
+                    ]
+                )
+            )
+            imd.write({"name": xref})
+
+        return xref