# HG changeset patch
# User Axel Prel <axel.prel@xcg-consulting.fr>
# Date 1741707439 -3600
#      Tue Mar 11 16:37:19 2025 +0100
# Branch 18.0
# Node ID a1e1e12a5fa33fb915395663db00c97ddb3fe694
# Parent  e27b5297cef42eceb72b355b5f12bd2d39da1f33
# EXP-Topic RED-514
redner: new menu for template list and server config
accessible with group conditionnality

diff --git a/NEWS.rst b/NEWS.rst
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -5,6 +5,12 @@
 18.0.1.7.0
 ----------
 
+Add a redner menu in "Apps" containing:
+- the redner template management page (create, list, update, delete templates)
+- a configuration wizard allowing quick server configuration (apikey, account, url)
+The redner template management page is accessible if you are an admin or a redner user
+The configuration page is accessible if you are an admin or a redner_admin
+
 Res config: add redner integration parameters (server_url, account, api_key)
 
 Template locale is by default user locale, not fr_FR
diff --git a/__manifest__.py b/__manifest__.py
--- a/__manifest__.py
+++ b/__manifest__.py
@@ -29,8 +29,11 @@
     # converter: https://orus.io/xcg/odoo-modules/converter
     "depends": ["converter", "mail", "web"],
     "data": [
+        "security/groups.xml",
+        "security/ir.model.access.csv",
         "wizard/mail_compose_message_views.xml",
         "wizard/template_list_view.xml",
+        "wizard/redner_configurator_wizard.xml",
         "security/ir.model.access.csv",
         "views/redner_template.xml",
         "views/mail_template.xml",
@@ -38,6 +41,9 @@
         "views/res_config_settings_view.xml",
         "views/menu.xml",
     ],
+    "demo": [
+        "demo/users.xml",
+    ],
     "assets": {
         "web.assets_backend": [
             "redner/static/src/js/redner_report_action.esm.js",
diff --git a/demo/users.xml b/demo/users.xml
new file mode 100644
--- /dev/null
+++ b/demo/users.xml
@@ -0,0 +1,31 @@
+<odoo>
+    <data noupdate="1">
+        <!-- Redner Admin User -->
+        <record id="redner_admin_demo" model="res.users">
+            <field name="name">Redner Admin</field>
+            <field name="login">radmin</field>
+            <field name="email">redner_admin@example.com</field>
+            <field name="password">radmin</field>
+            <field name="company_id" ref="base.main_company" />
+            <field name="company_ids" eval="[(6, 0, [ref('base.main_company')])]" />
+            <field
+                name="groups_id"
+                eval="[(6, 0, [ref('redner.group_redner_admin')])]"
+            />
+        </record>
+
+        <!-- Redner User -->
+        <record id="redner_user_demo" model="res.users">
+            <field name="name">Redner User</field>
+            <field name="login">ruser</field>
+            <field name="email">redner_user@example.com</field>
+            <field name="password">ruser</field>
+            <field name="company_id" ref="base.main_company" />
+            <field name="company_ids" eval="[(6, 0, [ref('base.main_company')])]" />
+            <field
+                name="groups_id"
+                eval="[(6, 0, [ref('redner.group_redner_user')])]"
+            />
+        </record>
+    </data>
+</odoo>
diff --git a/security/groups.xml b/security/groups.xml
new file mode 100644
--- /dev/null
+++ b/security/groups.xml
@@ -0,0 +1,23 @@
+<odoo>
+    <data noupdate="0">
+        <!-- Category -->
+        <record id="module_category_redner" model="ir.module.category">
+            <field name="name">Redner</field>
+            <field name="sequence">10</field>
+        </record>
+
+        <!-- User Group -->
+        <record id="group_redner_user" model="res.groups">
+            <field name="name">Redner User</field>
+            <field name="category_id" ref="module_category_redner" />
+            <field name="implied_ids" eval="[(4, ref('base.group_user'))]" />
+        </record>
+
+        <!-- Admin Group -->
+        <record id="group_redner_admin" model="res.groups">
+            <field name="name">Redner Administrator</field>
+            <field name="category_id" ref="module_category_redner" />
+            <field name="implied_ids" eval="[(4, ref('base.group_user'))]" />
+        </record>
+    </data>
+</odoo>
diff --git a/security/ir.model.access.csv b/security/ir.model.access.csv
--- a/security/ir.model.access.csv
+++ b/security/ir.model.access.csv
@@ -1,5 +1,25 @@
 id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
-access_redner_report,access_redner_report,model_redner_report,base.group_no_one,1,1,1,1
-access_redner_template,access_redner_template,model_redner_template,base.group_no_one,1,1,1,1
-access_redner_substitution,access_redner_substitution,model_redner_substitution,base.group_no_one,1,1,1,1
-access_redner_template_list,access_redner_template_list,model_template_list_wizard,base.group_no_one,1,1,1,1
+access_redner_report_system,access_redner_report_system,model_redner_report,base.group_system,1,1,1,1
+access_redner_report_user,access_redner_report_user,model_redner_report,base.group_user,1,0,0,0
+access_redner_report_redner_user,access_redner_report_redner_user,model_redner_report,redner.group_redner_user,1,1,1,1
+access_redner_report_redner_admin,access_redner_report_redner_admin,model_redner_report,redner.group_redner_admin,1,0,0,0
+
+access_redner_template_system,access_redner_template_system,model_redner_template,base.group_system,1,1,1,1
+access_redner_template_user,access_redner_template_user,model_redner_template,base.group_user,1,0,0,0
+access_redner_template_redner_user,access_redner_template_redner_user,model_redner_template,redner.group_redner_user,1,1,1,1
+access_redner_template_redner_admin,access_redner_template_redner_admin,model_redner_template,redner.group_redner_admin,1,0,0,0
+
+access_redner_substitution_system,access_redner_substitution_system,model_redner_substitution,base.group_system,1,1,1,1
+access_redner_substitution_user,access_redner_substitution_user,model_redner_substitution,base.group_user,1,0,0,0
+access_redner_substitution_redner_user,access_redner_substitution_redner_user,model_redner_substitution,redner.group_redner_user,1,1,1,1
+access_redner_substitution_redner_admin,access_redner_substitution_redner_admin,model_redner_substitution,redner.group_redner_admin,1,0,0,0
+
+access_redner_template_list_system,access_redner_template_list_system,model_template_list_wizard,base.group_system,1,1,1,1
+access_redner_template_list_user,access_redner_template_list_user,model_template_list_wizard,base.group_user,1,0,0,0
+access_redner_template_list_redner_user,access_redner_template_list_redner_user,model_template_list_wizard,redner.group_redner_user,1,1,1,1
+access_redner_template_list_redner_admin,access_redner_template_list_redner_admin,model_template_list_wizard,redner.group_redner_admin,1,0,0,0
+
+access_redner_configurator_wizard_system,access_redner_configurator_wizard_system,model_redner_configurator_wizard,base.group_system,1,1,1,1
+access_redner_configurator_wizard_user,access_redner_configurator_wizard_user,model_redner_configurator_wizard,base.group_user,0,0,0,0
+access_redner_configurator_wizard_redner_user,access_redner_configurator_wizard_redner_user,model_redner_configurator_wizard,redner.group_redner_user,0,0,0,0
+access_redner_configurator_wizard_redner_admin,access_redner_configurator_wizard_redner_admin,model_redner_configurator_wizard,redner.group_redner_admin,1,1,1,1
diff --git a/views/menu.xml b/views/menu.xml
--- a/views/menu.xml
+++ b/views/menu.xml
@@ -10,4 +10,32 @@
         action="redner_template_action"
         sequence="8"
     />
+
+    <!-- Redner main menu item -->
+    <menuitem
+        id="redner_main_menu"
+        name="Redner"
+        parent="base.menu_management"
+        sequence="15"
+        groups="base.group_system,redner.group_redner_user,redner.group_redner_admin"
+    />
+    <!-- Templates submenu -->
+    <menuitem
+        id="redner_template_main_menu"
+        name="Templates"
+        parent="redner_main_menu"
+        action="redner_template_action"
+        groups="base.group_system,redner.group_redner_user"
+        sequence="151"
+    />
+
+    <!-- Configuration submenu -->
+    <menuitem
+        id="redner_config_main_menu"
+        name="Configuration"
+        parent="redner_main_menu"
+        action="redner_config_action"
+        groups="base.group_system,redner.group_redner_admin"
+        sequence="152"
+    />
 </odoo>
diff --git a/wizard/__init__.py b/wizard/__init__.py
--- a/wizard/__init__.py
+++ b/wizard/__init__.py
@@ -1,4 +1,5 @@
 from . import (
     mail_compose_message,
+    redner_configurator_wizard,
     template_list,
 )
diff --git a/wizard/redner_configurator_wizard.py b/wizard/redner_configurator_wizard.py
new file mode 100644
--- /dev/null
+++ b/wizard/redner_configurator_wizard.py
@@ -0,0 +1,62 @@
+##############################################################################
+#
+#    Redner Odoo module
+#    Copyright © 2016, 2023-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
+#    published by the Free Software Foundation, either version 3 of the
+#    License, or (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU Affero General Public License for more details.
+#
+#    You should have received a copy of the GNU Affero General Public License
+#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+from odoo import api, fields, models
+from odoo.exceptions import AccessError
+
+
+class RednerConfiguratorWizard(models.TransientModel):
+    _name = "redner.configurator.wizard"
+    _description = "Redner Configuration Wizard"
+
+    server_url = fields.Char(string="Server URL")
+    account = fields.Char(string="Account")
+    api_key = fields.Char(string="API Key")
+
+    @api.model
+    def default_get(self, fields_list):
+        """Load default values from ir.config_parameter"""
+        res = super().default_get(fields_list)
+        Param = self.env["ir.config_parameter"].sudo()
+
+        res.update(
+            {
+                "server_url": Param.get_param("redner.server_url", default=""),
+                "account": Param.get_param("redner.account", default=""),
+                "api_key": Param.get_param("redner.api_key", default=""),
+            }
+        )
+        return res
+
+    def action_apply(self):
+        """Save values back to ir.config_parameter"""
+        if not self.env.user.has_group(
+            "redner.group_redner_admin"
+        ) and not self.env.user.has_group("base.group_system"):
+            raise AccessError(
+                "You do not have permission to modify Redner configuration."
+            )
+
+        Param = self.env["ir.config_parameter"].sudo()
+        Param.set_param("redner.server_url", self.server_url or "")
+        Param.set_param("redner.account", self.account or "")
+        Param.set_param("redner.api_key", self.api_key or "")
+
+        return {"type": "ir.actions.act_window_close"}
diff --git a/wizard/redner_configurator_wizard.xml b/wizard/redner_configurator_wizard.xml
new file mode 100644
--- /dev/null
+++ b/wizard/redner_configurator_wizard.xml
@@ -0,0 +1,32 @@
+<odoo>
+    <record id="view_redner_configurator_wizard" model="ir.ui.view">
+        <field name="name">redner.configurator.wizard.form</field>
+        <field name="model">redner.configurator.wizard</field>
+        <field name="arch" type="xml">
+            <form string="Redner Configuration">
+                <group>
+                    <field name="server_url" />
+                    <field name="account" />
+                    <field name="api_key" />
+                </group>
+                <footer>
+                    <button
+                        string="Apply"
+                        type="object"
+                        name="action_apply"
+                        class="oe_highlight"
+                    />
+                    <button string="Cancel" class="oe_link" special="cancel" />
+                </footer>
+            </form>
+        </field>
+    </record>
+
+    <record id="redner_config_action" model="ir.actions.act_window">
+        <field name="name">Redner Configuration</field>
+        <field name="res_model">redner.configurator.wizard</field>
+        <field name="view_mode">form</field>
+        <field name="view_id" ref="view_redner_configurator_wizard" />
+        <field name="target">new</field>
+    </record>
+</odoo>
# HG changeset patch
# User Axel Prel <axel.prel@xcg-consulting.fr>
# Date 1741797223 -3600
#      Wed Mar 12 17:33:43 2025 +0100
# Branch 18.0
# Node ID c3797452927ded2d4242abfff7d1a286e3e8d8da
# Parent  a1e1e12a5fa33fb915395663db00c97ddb3fe694
# EXP-Topic RED-514
simplify ir_model_access names

diff --git a/security/ir.model.access.csv b/security/ir.model.access.csv
--- a/security/ir.model.access.csv
+++ b/security/ir.model.access.csv
@@ -1,25 +1,25 @@
 id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
-access_redner_report_system,access_redner_report_system,model_redner_report,base.group_system,1,1,1,1
-access_redner_report_user,access_redner_report_user,model_redner_report,base.group_user,1,0,0,0
-access_redner_report_redner_user,access_redner_report_redner_user,model_redner_report,redner.group_redner_user,1,1,1,1
-access_redner_report_redner_admin,access_redner_report_redner_admin,model_redner_report,redner.group_redner_admin,1,0,0,0
+report_system,report_system,model_redner_report,base.group_system,1,1,1,1
+report_user,report_user,model_redner_report,base.group_user,1,0,0,0
+report_redner_user,report_redner_user,model_redner_report,redner.group_redner_user,1,1,1,1
+report_redner_admin,report_redner_admin,model_redner_report,redner.group_redner_admin,1,0,0,0
 
-access_redner_template_system,access_redner_template_system,model_redner_template,base.group_system,1,1,1,1
-access_redner_template_user,access_redner_template_user,model_redner_template,base.group_user,1,0,0,0
-access_redner_template_redner_user,access_redner_template_redner_user,model_redner_template,redner.group_redner_user,1,1,1,1
-access_redner_template_redner_admin,access_redner_template_redner_admin,model_redner_template,redner.group_redner_admin,1,0,0,0
+template_system,template_system,model_redner_template,base.group_system,1,1,1,1
+template_user,template_user,model_redner_template,base.group_user,1,0,0,0
+template_redner_user,template_redner_user,model_redner_template,redner.group_redner_user,1,1,1,1
+template_redner_admin,template_redner_admin,model_redner_template,redner.group_redner_admin,1,0,0,0
 
-access_redner_substitution_system,access_redner_substitution_system,model_redner_substitution,base.group_system,1,1,1,1
-access_redner_substitution_user,access_redner_substitution_user,model_redner_substitution,base.group_user,1,0,0,0
-access_redner_substitution_redner_user,access_redner_substitution_redner_user,model_redner_substitution,redner.group_redner_user,1,1,1,1
-access_redner_substitution_redner_admin,access_redner_substitution_redner_admin,model_redner_substitution,redner.group_redner_admin,1,0,0,0
+substitution_system,substitution_system,model_redner_substitution,base.group_system,1,1,1,1
+substitution_user,substitution_user,model_redner_substitution,base.group_user,1,0,0,0
+substitution_redner_user,substitution_redner_user,model_redner_substitution,redner.group_redner_user,1,1,1,1
+substitution_redner_admin,substitution_redner_admin,model_redner_substitution,redner.group_redner_admin,1,0,0,0
 
-access_redner_template_list_system,access_redner_template_list_system,model_template_list_wizard,base.group_system,1,1,1,1
-access_redner_template_list_user,access_redner_template_list_user,model_template_list_wizard,base.group_user,1,0,0,0
-access_redner_template_list_redner_user,access_redner_template_list_redner_user,model_template_list_wizard,redner.group_redner_user,1,1,1,1
-access_redner_template_list_redner_admin,access_redner_template_list_redner_admin,model_template_list_wizard,redner.group_redner_admin,1,0,0,0
+template_list_system,template_list_system,model_template_list_wizard,base.group_system,1,1,1,1
+template_list_user,template_list_user,model_template_list_wizard,base.group_user,1,0,0,0
+template_list_redner_user,template_list_redner_user,model_template_list_wizard,redner.group_redner_user,1,1,1,1
+template_list_redner_admin,template_list_redner_admin,model_template_list_wizard,redner.group_redner_admin,1,0,0,0
 
-access_redner_configurator_wizard_system,access_redner_configurator_wizard_system,model_redner_configurator_wizard,base.group_system,1,1,1,1
-access_redner_configurator_wizard_user,access_redner_configurator_wizard_user,model_redner_configurator_wizard,base.group_user,0,0,0,0
-access_redner_configurator_wizard_redner_user,access_redner_configurator_wizard_redner_user,model_redner_configurator_wizard,redner.group_redner_user,0,0,0,0
-access_redner_configurator_wizard_redner_admin,access_redner_configurator_wizard_redner_admin,model_redner_configurator_wizard,redner.group_redner_admin,1,1,1,1
+configurator_system,configurator_system,model_redner_configurator_wizard,base.group_system,1,1,1,1
+configurator_user,configurator_user,model_redner_configurator_wizard,base.group_user,0,0,0,0
+configurator_redner_user,configurator_redner_user,model_redner_configurator_wizard,redner.group_redner_user,0,0,0,0
+configurator_redner_admin,configurator_redner_admin,model_redner_configurator_wizard,redner.group_redner_admin,1,1,1,1
# HG changeset patch
# User Axel Prel <axel.prel@xcg-consulting.fr>
# Date 1742206535 -3600
#      Mon Mar 17 11:15:35 2025 +0100
# Branch 18.0
# Node ID 98cccb0803deb1908a5c5a0bbc11721418b6472d
# Parent  c3797452927ded2d4242abfff7d1a286e3e8d8da
# EXP-Topic RED-549
remove converter.py and use converter module image converters

diff --git a/converter.py b/converter.py
deleted file mode 100644
--- a/converter.py
+++ /dev/null
@@ -1,67 +0,0 @@
-##############################################################################
-#
-#    Redner Odoo module
-#    Copyright © 2016, 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
-#    published by the Free Software Foundation, either version 3 of the
-#    License, or (at your option) any later version.
-#
-#    This program is distributed in the hope that it will be useful,
-#    but WITHOUT ANY WARRANTY; without even the implied warranty of
-#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-#    GNU Affero General Public License for more details.
-#
-#    You should have received a copy of the GNU Affero General Public License
-#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
-#
-##############################################################################
-
-import base64
-from collections.abc import Mapping
-from typing import Any
-
-from odoo import models  # type: ignore[import-untyped]
-from odoo.addons.converter import Converter
-from odoo.tools.mimetypes import guess_mimetype  # type: ignore[import-untyped]
-
-
-def image(value: bytes):
-    # get MIME type associated with the decoded_data.
-    image_base64 = base64.b64decode(value)
-    mimetype = guess_mimetype(image_base64)
-    return {"body": value.decode("ascii"), "mime-type": mimetype}
-
-
-class ImageFile(Converter):
-    def __init__(self, fieldname):
-        self.fieldname = fieldname
-
-    def odoo_to_message(
-        self, instance: models.Model, ctx: Mapping | None = None
-    ) -> Any:
-        value = getattr(instance, self.fieldname)
-
-        if not value:
-            return {}
-
-        return image(value)
-
-
-class ImageDataURL(Converter):
-    def __init__(self, fieldname):
-        self.fieldname = fieldname
-
-    def odoo_to_message(
-        self, instance: models.Model, ctx: Mapping | None = None
-    ) -> Any:
-        value = getattr(instance, self.fieldname)
-
-        if not value:
-            return ""
-
-        content = base64.b64decode(value)
-        mimetype = guess_mimetype(content)
-
-        return "data:{};base64,{}".format(mimetype, value.decode("ascii"))
diff --git a/models/mail_template.py b/models/mail_template.py
--- a/models/mail_template.py
+++ b/models/mail_template.py
@@ -22,10 +22,9 @@
 import logging
 
 from odoo import _, fields, models  # type: ignore[import-untyped]
+from odoo.addons import converter
 from odoo.exceptions import ValidationError  # type: ignore[import-untyped]
 
-from ..converter import image
-
 _logger = logging.getLogger(__name__)
 
 
@@ -124,5 +123,5 @@
 
     def render_variable_hook(self, variables):
         """Override to add additional variables in mail "render template" func"""
-        variables.update({"image": lambda value: image(value)})
+        variables.update({"image": lambda value: converter.image(value)})
         return super().render_variable_hook(variables)
diff --git a/models/redner_substitution.py b/models/redner_substitution.py
--- a/models/redner_substitution.py
+++ b/models/redner_substitution.py
@@ -22,7 +22,6 @@
 from odoo.addons import converter
 from odoo.exceptions import ValidationError  # type: ignore[import-untyped]
 
-from ..converter import ImageDataURL, ImageFile
 from ..utils.sorting import parse_sorted_field, sortkey
 
 FIELD = "field"
@@ -161,11 +160,11 @@
                     path, name = sub.value.rsplit(".", 1)
                 else:
                     path, name = None, sub.value
-                conv = ImageFile(name)
+                conv = converter.ImageFile(name)
                 if path:
                     conv = converter.relation(path.replace(".", "/"), conv)
             elif sub.converter == "image-data-url":
-                conv = ImageDataURL(sub.value)
+                conv = converter.ImageDataURL(sub.value)
             elif sub.converter == "relation-to-many":
                 # Unpack the result of finding a field with its sort order into
                 # variable names.
# HG changeset patch
# User Axel Prel <axel.prel@xcg-consulting.fr>
# Date 1742206780 -3600
#      Mon Mar 17 11:19:40 2025 +0100
# Branch 18.0
# Node ID 8b39746b574656c0b3028eb58ece5d00a44f3c77
# Parent  98cccb0803deb1908a5c5a0bbc11721418b6472d
# EXP-Topic RED-549
substitutions: use constants

diff --git a/models/redner_substitution.py b/models/redner_substitution.py
--- a/models/redner_substitution.py
+++ b/models/redner_substitution.py
@@ -141,13 +141,13 @@
     def build_converter(self):
         d = {}
         for sub in self:
-            if sub.converter == "mail_template":
+            if sub.converter == MAIL_TEMPLATE:
                 conv = converter.MailTemplate(sub.value, False)
-            elif sub.converter == "mail_template+deserialize":
+            elif sub.converter == MAIL_TEMPLATE_DESERIALIZE:
                 conv = converter.MailTemplate(sub.value, True)
-            elif sub.converter == "constant":
+            elif sub.converter == CONSTANT:
                 conv = converter.Constant(sub.value)
-            elif sub.converter == "field":
+            elif sub.converter == FIELD:
                 if "." in sub.value:
                     path, name = sub.value.rsplit(".", 1)
                 else:
@@ -155,7 +155,7 @@
                 conv = converter.Field(name)
                 if path:
                     conv = converter.relation(path.replace(".", "/"), conv)
-            elif sub.converter == "image-file":
+            elif sub.converter == IMAGE_FILE:
                 if "." in sub.value:
                     path, name = sub.value.rsplit(".", 1)
                 else:
@@ -163,9 +163,9 @@
                 conv = converter.ImageFile(name)
                 if path:
                     conv = converter.relation(path.replace(".", "/"), conv)
-            elif sub.converter == "image-data-url":
+            elif sub.converter == IMAGE_DATAURL:
                 conv = converter.ImageDataURL(sub.value)
-            elif sub.converter == "relation-to-many":
+            elif sub.converter == RELATION_2MANY:
                 # Unpack the result of finding a field with its sort order into
                 # variable names.
                 value, sorted = parse_sorted_field(sub.value)
@@ -175,7 +175,7 @@
                     sortkey=sortkey(sorted) if sorted else None,
                     converter=sub.get_children().build_converter(),
                 )
-            elif sub.converter == "relation-path":
+            elif sub.converter == RELATION_PATH:
                 conv = converter.relation(
                     sub.value, sub.get_children().build_converter()
                 )
# HG changeset patch
# User Axel Prel <axel.prel@xcg-consulting.fr>
# Date 1742206458 -3600
#      Mon Mar 17 11:14:18 2025 +0100
# Branch 18.0
# Node ID b9af52c79fd6b6617b41bc54c2071ed91739d921
# Parent  8b39746b574656c0b3028eb58ece5d00a44f3c77
# EXP-Topic RED-549
add qr code converter in substitutions

diff --git a/models/redner_substitution.py b/models/redner_substitution.py
--- a/models/redner_substitution.py
+++ b/models/redner_substitution.py
@@ -30,6 +30,8 @@
 MAIL_TEMPLATE_DESERIALIZE = "mail_template+deserialize"
 IMAGE_FILE = "image-file"
 IMAGE_DATAURL = "image-data-url"
+QR_CODE_FILE = "qr-code-file"
+QR_CODE_DATAURL = "qr-code-data-url"
 RELATION_2MANY = "relation-to-many"
 RELATION_PATH = "relation-path"
 
@@ -39,6 +41,8 @@
     (FIELD, "Field"),
     (IMAGE_FILE, "Image file"),
     (IMAGE_DATAURL, "Image data url"),
+    (QR_CODE_FILE, "QR Code file"),
+    (QR_CODE_DATAURL, "QR Code data url"),
     (RELATION_2MANY, "Relation to many"),
     (RELATION_PATH, "Relation Path"),
     (CONSTANT, "Constant value"),
@@ -165,6 +169,10 @@
                     conv = converter.relation(path.replace(".", "/"), conv)
             elif sub.converter == IMAGE_DATAURL:
                 conv = converter.ImageDataURL(sub.value)
+            elif sub.converter == QR_CODE_FILE:
+                conv = converter.QRCodeFile(sub.value)
+            elif sub.converter == QR_CODE_DATAURL:
+                conv = converter.QRCodeDataURL(sub.value)
             elif sub.converter == RELATION_2MANY:
                 # Unpack the result of finding a field with its sort order into
                 # variable names.
# HG changeset patch
# User Axel Prel <axel.prel@xcg-consulting.fr>
# Date 1742292689 -3600
#      Tue Mar 18 11:11:29 2025 +0100
# Branch 18.0
# Node ID c7ee9e306253feb1cd74ab456955898d45f0187a
# Parent  b9af52c79fd6b6617b41bc54c2071ed91739d921
# EXP-Topic RED-549
make ruff happier

diff --git a/models/redner_substitution.py b/models/redner_substitution.py
--- a/models/redner_substitution.py
+++ b/models/redner_substitution.py
@@ -48,6 +48,7 @@
     (CONSTANT, "Constant value"),
 ]
 
+
 DYNAMIC_PLACEHOLDER_ALLOWED_CONVERTERS = (
     FIELD,
     MAIL_TEMPLATE,
@@ -142,6 +143,38 @@
             ]
         )
 
+    def _build_field(self, sub):
+        if "." in sub.value:
+            path, name = sub.value.rsplit(".", 1)
+        else:
+            path, name = None, sub.value
+        conv = converter.Field(name)
+        if path:
+            conv = converter.relation(path.replace(".", "/"), conv)
+        return conv
+
+    def _build_image(self, sub):
+        if "." in sub.value:
+            path, name = sub.value.rsplit(".", 1)
+        else:
+            path, name = None, sub.value
+        conv = converter.ImageFile(name)
+        if path:
+            conv = converter.relation(path.replace(".", "/"), conv)
+        return conv
+
+    def _build_rel_2_many(self, sub):
+        # Unpack the result of finding a field with its sort order into
+        # variable names.
+        value, sorted = parse_sorted_field(sub.value)
+        conv = converter.RelationToMany(
+            value,
+            None,
+            sortkey=sortkey(sorted) if sorted else None,
+            converter=sub.get_children().build_converter(),
+        )
+        return conv
+
     def build_converter(self):
         d = {}
         for sub in self:
@@ -152,21 +185,9 @@
             elif sub.converter == CONSTANT:
                 conv = converter.Constant(sub.value)
             elif sub.converter == FIELD:
-                if "." in sub.value:
-                    path, name = sub.value.rsplit(".", 1)
-                else:
-                    path, name = None, sub.value
-                conv = converter.Field(name)
-                if path:
-                    conv = converter.relation(path.replace(".", "/"), conv)
+                conv = self._build_field(sub)
             elif sub.converter == IMAGE_FILE:
-                if "." in sub.value:
-                    path, name = sub.value.rsplit(".", 1)
-                else:
-                    path, name = None, sub.value
-                conv = converter.ImageFile(name)
-                if path:
-                    conv = converter.relation(path.replace(".", "/"), conv)
+                conv = self._build_image(sub)
             elif sub.converter == IMAGE_DATAURL:
                 conv = converter.ImageDataURL(sub.value)
             elif sub.converter == QR_CODE_FILE:
@@ -174,15 +195,7 @@
             elif sub.converter == QR_CODE_DATAURL:
                 conv = converter.QRCodeDataURL(sub.value)
             elif sub.converter == RELATION_2MANY:
-                # Unpack the result of finding a field with its sort order into
-                # variable names.
-                value, sorted = parse_sorted_field(sub.value)
-                conv = converter.RelationToMany(
-                    value,
-                    None,
-                    sortkey=sortkey(sorted) if sorted else None,
-                    converter=sub.get_children().build_converter(),
-                )
+                conv = self._build_rel_2_many(sub)
             elif sub.converter == RELATION_PATH:
                 conv = converter.relation(
                     sub.value, sub.get_children().build_converter()
# HG changeset patch
# User Axel Prel <axel.prel@xcg-consulting.fr>
# Date 1742292765 -3600
#      Tue Mar 18 11:12:45 2025 +0100
# Branch 18.0
# Node ID 92dd238dff9f0b0f1ad88d597dcda09c4be1082f
# Parent  c7ee9e306253feb1cd74ab456955898d45f0187a
# EXP-Topic RED-549
update NEWS

diff --git a/NEWS.rst b/NEWS.rst
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -5,6 +5,11 @@
 18.0.1.7.0
 ----------
 
+- add the QR Code converter to the substitution menu
+
+- remove the converter.py file containing the image converter
+  this converter has been move to the converter module
+
 Add a redner menu in "Apps" containing:
 - the redner template management page (create, list, update, delete templates)
 - a configuration wizard allowing quick server configuration (apikey, account, url)
# HG changeset patch
# User Axel Prel <axel.prel@xcg-consulting.fr>
# Date 1742465481 -3600
#      Thu Mar 20 11:11:21 2025 +0100
# Branch 18.0
# Node ID 5bd4867eae5d4403d7eac177490920c33c9403cd
# Parent  92dd238dff9f0b0f1ad88d597dcda09c4be1082f
# EXP-Topic RED-549
set correct converter dependency in pyproject

diff --git a/pyproject.toml b/pyproject.toml
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -16,7 +16,7 @@
   "Framework :: Odoo :: 18.0",
   "License :: OSI Approved :: GNU Affero General Public License v3",
 ]
-dependencies = ["odoo==18.0.*", "odoo-addon-converter >=18.0.4,<18.0.7"]
+dependencies = ["odoo==18.0.*", "odoo-addon-converter >=18.0.6.1,<18.0.7"]
 
 [project.optional-dependencies]
 unixsocket = ["requests_unixsocket"]