diff --git a/.hgtags b/.hgtags
index 93b8d613e36abdc467a2b3a8137ad7ecc1c67390_LmhndGFncw==..880fdce6f6719b9201b2300a6007437d0beabde1_LmhndGFncw== 100644
--- a/.hgtags
+++ b/.hgtags
@@ -31,3 +31,4 @@
 244d816f3ee9c11634b29bcb601594c86e2846eb 17.0.1.1.0
 37bbac9b58d9d074f0bf7574ae6b4a57bad8abcd 17.0.1.1.1
 07224c0905af4f9c9c1d47d4acf369ce55c5c859 17.0.1.2.0
+a6c94d540cb228b9dcf7dca24e53a35158b2c9ef 17.0.1.3.0
diff --git a/NEWS.rst b/NEWS.rst
index 93b8d613e36abdc467a2b3a8137ad7ecc1c67390_TkVXUy5yc3Q=..880fdce6f6719b9201b2300a6007437d0beabde1_TkVXUy5yc3Q= 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -2,6 +2,13 @@
 Changelog
 =========
 
+17.0.1.3.0
+----------
+
+- Add missing python-magic requirement for package.
+- Add dynamic expression button for substitution line and new converter features.
+- Refactor redner.template model to improve template management.
+
 17.0.1.2.0
 ----------
 
diff --git a/__manifest__.py b/__manifest__.py
index 93b8d613e36abdc467a2b3a8137ad7ecc1c67390_X19tYW5pZmVzdF9fLnB5..880fdce6f6719b9201b2300a6007437d0beabde1_X19tYW5pZmVzdF9fLnB5 100644
--- a/__manifest__.py
+++ b/__manifest__.py
@@ -21,7 +21,7 @@
 {
     "name": "Redner",
     "license": "AGPL-3",
-    "version": "17.0.1.2.0",
+    "version": "17.0.1.3.0",
     "category": "Reporting",
     "author": "XCG Consulting",
     "website": "https://orbeet.io/",
@@ -30,7 +30,6 @@
     "data": [
         "wizard/mail_compose_message_views.xml",
         "wizard/template_list_view.xml",
-        "wizard/expression_builder_view.xml",
         "security/ir.model.access.csv",
         "views/redner_template.xml",
         "views/mail_template.xml",
@@ -38,7 +37,10 @@
         "views/menu.xml",
     ],
     "assets": {
-        "web.assets_backend": ["redner/static/src/js/redner_report_action.esm.js"],
+        "web.assets_backend": [
+            "redner/static/src/js/redner_report_action.esm.js",
+            "redner/static/src/components/**/*",
+        ],
     },
     "installable": True,
     "external_dependencies": {"python": ["requests_unixsocket", "python-magic"]},
diff --git a/i18n/fr.po b/i18n/fr.po
index 93b8d613e36abdc467a2b3a8137ad7ecc1c67390_aTE4bi9mci5wbw==..880fdce6f6719b9201b2300a6007437d0beabde1_aTE4bi9mci5wbw== 100644
--- a/i18n/fr.po
+++ b/i18n/fr.po
@@ -6,8 +6,8 @@
 msgstr ""
 "Project-Id-Version: Odoo Server 17.0\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-10-07 08:08+0000\n"
-"PO-Revision-Date: 2024-10-07 10:12+0200\n"
+"POT-Creation-Date: 2024-12-18 16:34+0000\n"
+"PO-Revision-Date: 2024-12-18 16:36+0000\n"
 "Last-Translator: Axel PREL <axel.prel@xcg-consulting.fr>\n"
 "Language-Team: XCG Consulting\n"
 "Language: fr\n"
@@ -15,8 +15,8 @@
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=2; plural=(n > 1);\n"
-"X-Generator: Poedit 3.0.1\n"
+"X-Generator: Poedit 3.5\n"
 
 #. module: redner
 #. odoo-python
 #: code:addons/redner/models/redner_template.py:0
@@ -19,11 +19,10 @@
 
 #. module: redner
 #. odoo-python
 #: code:addons/redner/models/redner_template.py:0
-#, python-format
 msgid "%s (copy)"
 msgstr "%s (copie)"
 
 #. module: redner
 #. odoo-python
 #: code:addons/redner/models/ir_actions_report.py:0
@@ -24,10 +23,9 @@
 msgid "%s (copy)"
 msgstr "%s (copie)"
 
 #. module: redner
 #. odoo-python
 #: code:addons/redner/models/ir_actions_report.py:0
-#, python-format
 msgid "(Native)"
 msgstr "(Natif)"
 
@@ -65,10 +63,9 @@
 #. module: redner
 #. odoo-python
 #: code:addons/redner/redner.py:0
-#, python-format
 msgid "Cannot Establish a connection to server"
 msgstr "La connexion au serveur ne peut pas être établie"
 
 #. module: redner
 #. odoo-python
 #: code:addons/redner/redner.py:0
@@ -69,11 +66,10 @@
 msgid "Cannot Establish a connection to server"
 msgstr "La connexion au serveur ne peut pas être établie"
 
 #. module: redner
 #. odoo-python
 #: code:addons/redner/redner.py:0
-#, python-format
 msgid ""
 "Cannot find redner config url. Please add it in odoo.conf or in ir."
 "config_parameter"
 msgstr ""
@@ -76,9 +72,9 @@
 msgid ""
 "Cannot find redner config url. Please add it in odoo.conf or in ir."
 "config_parameter"
 msgstr ""
-"La configuration de l'url de Redner est introuvable. Merci de l'ajouter "
-"dans le fichier odoo.conf ou dans ir.config.parameter"
+"La configuration de l'url de Redner est introuvable. Merci de l'ajouter dans "
+"le fichier odoo.conf ou dans ir.config.parameter"
 
 #. module: redner
 #: model:ir.model.fields,help:redner.field_redner_template__body
@@ -148,6 +144,11 @@
 msgstr "Nom à afficher"
 
 #. module: redner
+#: model:ir.model.fields,field_description:redner.field_redner_substitution__dynamic_placeholder_button_hidden
+msgid "Dynamic Placeholder Button Hidden"
+msgstr "Dynamic Placeholder Button Hidden"
+
+#. module: redner
 #: model:ir.model.fields,field_description:redner.field_redner_substitution__template_id
 msgid "Email Template"
 msgstr "Modèle de courriel"
@@ -190,8 +191,7 @@
 #. module: redner
 #. odoo-python
 #: code:addons/redner/models/redner_template.py:0
-#, python-format
-msgid "Failed to update render template, %s"
+msgid "Failed to update Redner template, %s"
 msgstr "Échec de la mise à jour du modèle redner, %s"
 
 #. module: redner
@@ -202,7 +202,6 @@
 #. module: redner
 #. odoo-python
 #: code:addons/redner/models/ir_actions_report.py:0
-#, python-format
 msgid "Field 'Output Format' is required for Redner report"
 msgstr "Le champ 'Format de sortie' est requis pour le rapport Redner"
 
@@ -274,9 +273,8 @@
 #: model:ir.model.fields,help:redner.field_ir_actions_report__redner_multi_in_one
 msgid ""
 "If you execute a report on several records, by default Odoo will generate a "
-"ZIP file that contains as many files as selected records. If you enable "
-"this option, Odoo will generate instead a single report for the selected "
-"records."
+"ZIP file that contains as many files as selected records. If you enable this "
+"option, Odoo will generate instead a single report for the selected records."
 msgstr ""
 "Si vous exécutez un rapport sur plusieurs enregistrements, par défaut, Odoo "
 "générera un fichier ZIP qui contiendra autant de fichiers que "
@@ -301,7 +299,7 @@
 #. module: redner
 #: model:ir.model.fields,field_description:redner.field_redner_report__ir_actions_report_id
 msgid "Ir Actions Report"
-msgstr "Rapport d'action"
+msgstr "Ir Actions Report"
 
 #. module: redner
 #: model:ir.model.fields,field_description:redner.field_redner_template__is_mjml
@@ -320,6 +318,7 @@
 msgstr "Les mots-clés vont être mis à jour. Continuer ?"
 
 #. module: redner
+#: model:ir.model.fields,field_description:redner.field_redner_substitution__lang
 #: model:ir.model.fields,field_description:redner.field_redner_template__language
 msgid "Language"
 msgstr "Langage"
@@ -350,6 +349,11 @@
 msgstr "Modèle LibreOffice"
 
 #. module: redner
+#: model:ir.model.fields,field_description:redner.field_redner_template__template_data_filename
+msgid "Libreoffice Template Filename"
+msgstr "Nom de fichier du modèle LibreOffice"
+
+#. module: redner
 #: model:ir.model.fields,field_description:redner.field_redner_template__locale_id
 msgid "Locale"
 msgstr "Localisation"
@@ -365,6 +369,12 @@
 msgstr "Plusieurs enregistrements dans un seul rapport Redner"
 
 #. module: redner
+#. odoo-python
+#: code:addons/redner/models/redner_substitution.py:0
+msgid "N/A"
+msgstr "N/A"
+
+#. module: redner
 #: model:ir.model.fields,field_description:redner.field_redner_template__name
 #: model:ir.model.fields,field_description:redner.field_template_list_wizard__name
 msgid "Name"
@@ -396,6 +406,15 @@
 msgstr "OpenDocument + mustache"
 
 #. module: redner
+#: model:ir.model.fields,help:redner.field_redner_substitution__lang
+msgid ""
+"Optional translation language (ISO code) to select when sending out an "
+"email. If not set, the english version will be used. This should usually be "
+"a placeholder expression that provides the appropriate language, e.g. "
+"{{ object.partner_id.lang }}."
+msgstr ""
+
+#. module: redner
 #: model:ir.model.fields,help:redner.field_redner_template__locale_id
 msgid "Optional translation language (ISO code)."
 msgstr "Langue de traduction facultative (code ISO)."
@@ -443,6 +462,17 @@
 msgstr "Modèles Redner"
 
 #. module: redner
+#. odoo-python
+#: code:addons/redner/controllers/main.py:0
+msgid "Redner action report not found for report_name %s"
+msgstr "Action du rapport redner introuvable pour le rapport %s"
+
+#. module: redner
+#: model:ir.model.fields,field_description:redner.field_redner_substitution__model
+msgid "Related Report Model"
+msgstr "Modèle de Rapport Associé"
+
+#. module: redner
 #: model:ir.model.fields.selection,name:redner.selection__redner_substitution__converter__relation-path
 msgid "Relation Path"
 msgstr "Chemin relationnel"
@@ -459,6 +489,11 @@
 msgstr "Rendu par Redner"
 
 #. module: redner
+#: model:ir.model.fields,field_description:redner.field_redner_substitution__render_model
+msgid "Rendering Model"
+msgstr "Modèle de rendu"
+
+#. module: redner
 #: model:ir.model.fields,field_description:redner.field_redner_substitution__ir_actions_report_id
 msgid "Report"
 msgstr "Rapport"
@@ -596,5 +631,14 @@
 msgstr "Le type de champ (booléen, texte, ...)"
 
 #. module: redner
+#: model:ir.model.fields,help:redner.field_redner_template__name
+msgid ""
+"The name of the template. Once the template is created, updating the name is "
+"not allowed. To change the name, delete the template and create a new one."
+msgstr ""
+"Le nom du modèle. Une fois le modèle créé, la mise à jour du nom n'est pas "
+"autorisée. Pour modifier le nom, supprimez le modèle et créez-en un nouveau."
+
+#. module: redner
 #. odoo-python
 #: code:addons/redner/models/ir_actions_report.py:0
@@ -599,6 +643,5 @@
 #. odoo-python
 #: code:addons/redner/models/ir_actions_report.py:0
-#, python-format
 msgid ""
 "The output format cannot be different from the extension defined in \"Save "
 "as attachment prefix\"."
@@ -626,15 +669,10 @@
 "browser PDF means the report will be rendered using Wkhtmltopdf and "
 "downloaded by the user."
 msgstr ""
-"Le type de rapport qui va être rendu, chaque type ayant sa propre méthode "
-"de rendu. \"HTML\" signifie que le rapport sera ouvert directement dans le "
-"navigateur. \"PDF\" signifie que le rapport va être rendu par Wkhtmltopdf "
-"et ensuite téléchargé par l'utilisateur."
-
-#. module: redner
-#: model:ir.model.fields,help:redner.field_redner_template__name
-msgid "This is a name of template mjml redner"
-msgstr "C'est le nom du template mjml redner"
+"Le type de rapport qui va être rendu, chaque type ayant sa propre méthode de "
+"rendu. \"HTML\" signifie que le rapport sera ouvert directement dans le "
+"navigateur. \"PDF\" signifie que le rapport va être rendu par Wkhtmltopdf et "
+"ensuite téléchargé par l'utilisateur."
 
 #. module: redner
 #: model:ir.model.fields,help:redner.field_redner_template__preview
@@ -651,4 +689,12 @@
 
 #. module: redner
 #. odoo-python
+#: code:addons/redner/models/redner_template.py:0
+msgid ""
+"Unable to update the template data. Please check the logs for more details."
+msgstr ""
+"Unable to update the template data. Please check the logs for more details."
+
+#. module: redner
+#. odoo-python
 #: code:addons/redner/redner.py:0
@@ -654,6 +700,5 @@
 #: code:addons/redner/redner.py:0
-#, python-format
 msgid "Unexpected redner error: %r"
 msgstr "Erreur redner : %r"
 
 #. module: redner
@@ -656,7 +701,12 @@
 msgid "Unexpected redner error: %r"
 msgstr "Erreur redner : %r"
 
 #. module: redner
+#: model:ir.model.fields,field_description:redner.field_redner_substitution__value_placeholder
+msgid "Value Placeholder"
+msgstr "Placeholder de l'expresion"
+
+#. module: redner
 #: model:ir.model.fields,field_description:redner.field_redner_substitution__keyword
 msgid "Variable"
 msgstr "Variable"
@@ -676,7 +726,6 @@
 #. odoo-python
 #: code:addons/redner/models/mail_template.py:0
 #: code:addons/redner/models/redner_report.py:0
-#, python-format
 msgid ""
 "We received an unexpected error from redner server. Please contact your "
 "administrator"
@@ -698,7 +747,42 @@
 #. module: redner
 #. odoo-python
 #: code:addons/redner/models/redner_substitution.py:0
-#, python-format
+msgid "e.g: name or partner_id.name"
+msgstr "par ex. name or partner_id.name"
+
+#. module: redner
+#. odoo-python
+#: code:addons/redner/models/redner_substitution.py:0
+msgid "e.g: partner_id/category_id/name ou partner_id/child_ids[]"
+msgstr "par ex. partner_id/category_id/name ou partner_id/child_ids[]"
+
+#. module: redner
+#. odoo-python
+#: code:addons/redner/models/redner_substitution.py:0
+msgid "e.g: tax_ids"
+msgstr "par ex. tax_ids"
+
+#. module: redner
+#. odoo-python
+#: code:addons/redner/models/redner_substitution.py:0
+msgid "e.g: {{ object.get_partner_info() | safe }}"
+msgstr "par ex. {{ object.get_partner_info() | safe }}"
+
+#. module: redner
+#. odoo-python
+#: code:addons/redner/models/redner_substitution.py:0
+msgid "e.g: {{object.partner_id.name}}"
+msgstr "par ex. {{object.partner_id.name}}"
+
+#. module: redner
+#. odoo-python
+#: code:addons/redner/models/redner_substitution.py:0
+msgid "e.g: www.orbeet.io"
+msgstr "par ex. www.orbeet.io"
+
+#. module: redner
+#. odoo-python
+#: code:addons/redner/models/redner_substitution.py:0
 msgid "invalid converter type: %s"
 msgstr "type de convertisseur invalide : %s"
 
diff --git a/models/redner_substitution.py b/models/redner_substitution.py
index 93b8d613e36abdc467a2b3a8137ad7ecc1c67390_bW9kZWxzL3JlZG5lcl9zdWJzdGl0dXRpb24ucHk=..880fdce6f6719b9201b2300a6007437d0beabde1_bW9kZWxzL3JlZG5lcl9zdWJzdGl0dXRpb24ucHk= 100644
--- a/models/redner_substitution.py
+++ b/models/redner_substitution.py
@@ -25,8 +25,34 @@
 from ..converter import ImageDataURL, ImageFile
 from ..utils.sorting import parse_sorted_field, sortkey
 
+FIELD = "field"
+CONSTANT = "constant"
+MAIL_TEMPLATE = "mail_template"
+MAIL_TEMPLATE_DESERIALIZE = "mail_template+deserialize"
+IMAGE_FILE = "image-file"
+IMAGE_DATAURL = "image-data-url"
+RELATION_2MANY = "relation-to-many"
+RELATION_PATH = "relation-path"
+
+CONVERTER_SELECTION = [
+    (MAIL_TEMPLATE, "Odoo Template"),
+    (MAIL_TEMPLATE_DESERIALIZE, "Odoo Template + Eval"),
+    (FIELD, "Field"),
+    (IMAGE_FILE, "Image file"),
+    (IMAGE_DATAURL, "Image data url"),
+    (RELATION_2MANY, "Relation to many"),
+    (RELATION_PATH, "Relation Path"),
+    (CONSTANT, "Constant value"),
+]
+
+DYNAMIC_PLACEHOLDER_ALLOWED_CONVERTERS = (
+    FIELD,
+    MAIL_TEMPLATE,
+    MAIL_TEMPLATE_DESERIALIZE,
+)
+
 
 class Substitution(models.Model):
     """Substitution values for a Redner email message"""
 
     _name = "redner.substitution"
@@ -28,8 +54,9 @@
 
 class Substitution(models.Model):
     """Substitution values for a Redner email message"""
 
     _name = "redner.substitution"
+    _inherit = ["mail.render.mixin"]
     _description = "Redner Substitution"
 
     keyword = fields.Char(string="Variable", help="Template variable name")
@@ -40,5 +67,13 @@
         comodel_name="ir.actions.report", string="Report"
     )
 
+    model = fields.Char(
+        "Related Report Model",
+        related="ir_actions_report_id.model",
+        index=True,
+        store=True,
+        readonly=True,
+    )
+
     value = fields.Char(string="Expression")
 
@@ -43,15 +78,12 @@
     value = fields.Char(string="Expression")
 
-    converter = fields.Selection(
-        selection=[
-            ("mail_template", "Odoo Template"),
-            ("mail_template+deserialize", "Odoo Template + Eval"),
-            ("field", "Field"),
-            ("image-file", "Image file"),
-            ("image-data-url", "Image data url"),
-            ("relation-to-many", "Relation to many"),
-            ("relation-path", "Relation Path"),
-            ("constant", "Constant value"),
-        ]
+    converter = fields.Selection(selection=CONVERTER_SELECTION)
+
+    depth = fields.Integer(string="Depth", compute="_compute_depth", store=True)
+
+    value_placeholder = fields.Char(compute="_compute_value_placeholder")
+
+    dynamic_placeholder_button_hidden = fields.Boolean(
+        compute="_compute_dynamic_placeholder_button_hidden"
     )
 
@@ -56,9 +88,40 @@
     )
 
-    depth = fields.Integer(string="Depth", compute="_compute_depth", store=True)
+    @api.depends("converter")
+    def _compute_value_placeholder(self):
+        """Computes a dynamic placeholder that depends on the selected type
+        to help the user inputs their data.
+        """
+        for subsitution in self:
+            placeholder = _("N/A")
+            if subsitution.converter == FIELD:
+                placeholder = _("e.g: name or partner_id.name")
+            elif subsitution.converter == MAIL_TEMPLATE:
+                placeholder = _("e.g: {{object.partner_id.name}}")
+            elif subsitution.converter == MAIL_TEMPLATE_DESERIALIZE:
+                placeholder = _("e.g: {{ object.get_partner_info() | safe }}")
+            elif subsitution.converter == RELATION_2MANY:
+                placeholder = _("e.g: tax_ids")
+            elif subsitution.converter == RELATION_PATH:
+                placeholder = _(
+                    "e.g: partner_id/category_id/name ou partner_id/child_ids[]"
+                )
+            elif subsitution.converter == CONSTANT:
+                placeholder = _("e.g: www.orbeet.io")
+            subsitution.value_placeholder = placeholder
+
+    @api.depends("value")
+    def _compute_render_model(self):
+        for substitution in self:
+            if substitution.ir_actions_report_id:
+                substitution.render_model = substitution.model
+            elif substitution.template_id:
+                substitution.render_model = substitution.template_id.model_id.model
+            else:
+                substitution.render_model = False
 
     @api.depends("keyword")
     def _compute_depth(self):
         for record in self:
             record.depth = record.keyword.count(".")
 
@@ -59,9 +122,16 @@
 
     @api.depends("keyword")
     def _compute_depth(self):
         for record in self:
             record.depth = record.keyword.count(".")
 
+    @api.depends("converter")
+    def _compute_dynamic_placeholder_button_hidden(self):
+        for record in self:
+            record.dynamic_placeholder_button_hidden = (
+                record.converter not in DYNAMIC_PLACEHOLDER_ALLOWED_CONVERTERS
+            )
+
     def get_children(self):
         return self.search(
             [
@@ -119,40 +189,3 @@
             d[sub.keyword.rsplit(".", 2)[-1]] = conv
 
         return converter.Model("", d)
-
-    def action_build_expression(self):
-        if not (self and self.ir_actions_report_id):
-            if not self.template_id:
-                # neither a report nor a mail template
-                return
-            else:
-                model = self.env.get(self.template_id.model_id.model)
-        else:
-            model = self.env.get(self.ir_actions_report_id.model)
-        if model is None:
-            return
-
-        # reset the older substitution value
-        self.value = ""
-
-        wizard = self.env["expression.builder.wizard"].create(
-            {
-                "substitution_id": self.id,
-                "expression": "",
-            }
-        )
-
-        vals_list = wizard.get_fields(model, self.converter)
-        for val in vals_list:
-            val["wizard_id"] = wizard.id
-
-        fields_list = self.env["expression.builder.field"].create(vals_list)
-        wizard.suggested_fields = [(6, 0, fields_list.ids)]
-
-        return {
-            "type": "ir.actions.act_window",
-            "res_model": "expression.builder.wizard",
-            "view_mode": "form",
-            "res_id": wizard.id,
-            "target": "new",
-        }
diff --git a/models/redner_template.py b/models/redner_template.py
index 93b8d613e36abdc467a2b3a8137ad7ecc1c67390_bW9kZWxzL3JlZG5lcl90ZW1wbGF0ZS5weQ==..880fdce6f6719b9201b2300a6007437d0beabde1_bW9kZWxzL3JlZG5lcl90ZW1wbGF0ZS5weQ== 100644
--- a/models/redner_template.py
+++ b/models/redner_template.py
@@ -26,10 +26,10 @@
 from odoo.tools.cache import ormcache
 
 from ..redner import REDNER_API_PATH, Redner
-from ..utils.mimetype import b64_to_extension
+from ..utils.mimetype import get_file_extension
 
 logger = logging.getLogger(__name__)
 
 _redner = None
 
 LANGUAGE_MJML_MUSTACHE = "text/mjml|mustache"
@@ -30,9 +30,10 @@
 
 logger = logging.getLogger(__name__)
 
 _redner = None
 
 LANGUAGE_MJML_MUSTACHE = "text/mjml|mustache"
+LANGUAGE_OPENDOCUMENT_MUSTACHE = "application/vnd.oasis.opendocument.text|od+mustache"
 DEFAULT_LANGUAGE = LANGUAGE_MJML_MUSTACHE
 COMPUTED_FIELDS = [
     "body",
@@ -87,7 +88,11 @@
         string="Name",
         default="New",
         required=True,
-        help="This is a name of template mjml redner",
+        help=(
+            "The name of the template. Once the template is created, "
+            "updating the name is not allowed. To change the name, "
+            "delete the template and create a new one."
+        ),
     )
 
     description = fields.Char(
@@ -138,7 +143,7 @@
             ("text/html|mustache", "HTML + mustache"),
             (LANGUAGE_MJML_MUSTACHE, "MJML + mustache"),
             (
-                "application/vnd.oasis.opendocument.text|od+mustache",
+                LANGUAGE_OPENDOCUMENT_MUSTACHE,
                 "OpenDocument + mustache",
             ),
         ],
@@ -163,7 +168,8 @@
         string="Libreoffice Template",
         readonly=False,
         compute="_compute_template_data",
+        inverse="_inverse_template_data",
     )
 
     template_data_filename = fields.Char(
         string="Libreoffice Template Filename",
@@ -166,7 +172,7 @@
     )
 
     template_data_filename = fields.Char(
         string="Libreoffice Template Filename",
-        readonly=True,
+        compute="_compute_template_data_filename",
     )
 
@@ -171,7 +177,6 @@
     )
 
-    def import_from_redner(self):
-        tl_wizard = self.env["template.list.wizard"]
-        templates = self.list_external_templates()
-        tl_wizard.populate(templates)
+    # -------------------------------------------------------------------------
+    # COMPUTE METHODS
+    # -------------------------------------------------------------------------
 
@@ -177,13 +182,9 @@
 
-        return {
-            "type": "ir.actions.act_window",
-            "res_model": "template.list.wizard",
-            "view_mode": "tree",
-            "view_id": self.env.ref("redner.view_template_list_wizard_tree").id,
-            "target": "new",
-            #'context': self.env.context,
-        }
+    @api.depends("body", "template_data")
+    def _compute_keywords(self):
+        for record in self:
+            record.detected_keywords = "\n".join(record.template_varlist_fetch())
 
     @api.depends("body", "template_data")
     def _compute_preview(self):
         for record in self:
@@ -186,7 +187,6 @@
 
     @api.depends("body", "template_data")
     def _compute_preview(self):
         for record in self:
-            record.preview = False
-            if record.redner_id:
+            if record.body or record.template_data:
                 response = self.get_preview(record.redner_id)
@@ -192,5 +192,146 @@
                 response = self.get_preview(record.redner_id)
-                if response:
-                    b64 = base64.b64encode(response.content)
-                    record.preview = b64
+
+                # Check if response is valid and has content
+                if response and hasattr(response, "content"):
+                    record.preview = base64.b64encode(response.content)
+                else:
+                    record.preview = False  # If no valid response, set preview to False
+            else:
+                record.preview = False
+
+    def _compute_template(self):
+        """
+        Computes the template values for the records and applies cached or fetched data.
+        """
+        for record in self:
+            if not record.id or not record.redner_id:
+                continue
+
+            # Fetch the cached template
+            cached_template = self._get_cached_template(record.id)
+
+            if not any([getattr(record, f) for f in COMPUTED_FIELDS]):
+                # If all computed fields are undefined, populate them
+                # from the cached template.
+                for f in COMPUTED_FIELDS + EDITABLE_FIELDS:
+                    if f in cached_template:
+                        setattr(record, f, cached_template[f])
+            else:
+                # If at least one field is defined, populate only undefined fields
+                for f in COMPUTED_FIELDS:
+                    if not getattr(record, f):
+                        setattr(record, f, cached_template.get(f, None))
+
+    def _inverse_template_data(self):
+        """
+        Inverse function for `template_data`. Called when `template_data` is"
+        manually set.
+        """
+        for record in self:
+            if not record.template_data or not record.id or not record.language:
+                continue
+            try:
+                # Update the external system with the new data
+                self._set_cached_template(record.id, record.template_data)
+            except Exception as e:
+                logger.error("Failed to update template data in Redner: %s", e)
+                raise ValidationError(
+                    _(
+                        "Unable to update the template data. Please check the logs "
+                        "for more details."
+                    )
+                ) from e
+
+    def _compute_template_data(self):
+        for record in self:
+            # Skip records that do not have a redner_id or are missing essential data
+            if not record.id or not record.redner_id:
+                continue
+
+            if (
+                not record.template_data
+                and record.language == LANGUAGE_OPENDOCUMENT_MUSTACHE
+            ):
+                cached_template = self._get_cached_template(record.id)
+
+                template_data = (
+                    cached_template.get("template_data") if cached_template else None
+                )
+                if template_data:
+                    try:
+                        # Perform base64 encoding and store the result
+                        record.template_data = base64.b64encode(template_data).decode(
+                            "utf-8"
+                        )
+                    except Exception as e:
+                        logger.error(
+                            "Failed to encode redner template data for record %s: %s",
+                            record.id,
+                            e,
+                        )
+                        continue  # Proceed with next record if encoding fails
+
+    @api.depends("template_data")
+    def _compute_template_data_filename(self):
+        """Compute the template filename based on the template data"""
+        for record in self:
+            if not record.id or not record.redner_id or not record.template_data:
+                record.template_data_filename = (
+                    f"{record.name}.odt" if record.name else "template.odt"
+                )
+            else:
+                try:
+                    # Attempt to extract the file extension from the base64 data
+                    ext = get_file_extension(record.template_data)
+                    record.template_data_filename = f"{record.name}{ext}"
+
+                except Exception as e:
+                    logger.error("Error while computing template filename: %s", e)
+                    record.template_data_filename = False
+
+    @property
+    def redner(self):
+        """
+        Returns a Redner instance.
+        Recomputes the instance if any system parameter changes.
+        Uses a global variable to cache the instance across sessions.
+        """
+        global _redner, _redner_params
+
+        # Fetch current system parameters
+        config_model = self.env["ir.config_parameter"].sudo()
+        current_params = {
+            "api_key": config_model.get_param("redner.api_key"),
+            "server_url": config_model.get_param("redner.server_url"),
+            "account": config_model.get_param("redner.account"),
+            "timeout": int(config_model.get_param("redner.timeout", default="20")),
+        }
+
+        # Check if parameters have changed or if _redner is None
+        if _redner is None or _redner_params != current_params:
+            # Recompute the Redner instance
+            _redner = Redner(
+                current_params["api_key"],
+                current_params["server_url"],
+                current_params["account"],
+                current_params["timeout"],
+            )
+            _redner_params = current_params  # Update the stored parameters
+
+        return _redner
+
+    # -------------------------------------------------------------------------
+    # LOW-LEVEL METHODS
+    # -------------------------------------------------------------------------
+
+    @api.model_create_multi
+    def create(self, vals_list):
+        """Overwrite create to create redner template"""
+
+        for vals in vals_list:
+            # If "name" is missing or equals "New", set the source and
+            # proceed with creation.
+            if not vals.get("name", False) or vals["name"] == "New":
+                vals["source"] = "redner"
+                continue  # Continue processing the next record
 
@@ -196,3 +337,99 @@
 
-    def get_preview(self, redner_id):
+            # Prepare template params according to the selected language.
+            # Use template data field if the selected language is "od";
+            # otherwise the body field is used.
+            produces, language = vals.get("language", DEFAULT_LANGUAGE).split("|")
+            body, body_format = (
+                (vals.get("template_data", ""), "base64")
+                if language == "od+mustache"
+                else (vals.get("body"), "text")
+            )
+
+            locale = self.env["res.lang"].browse(vals.get("locale_id")).code
+
+            # We depend on the API for consistency here
+            # So raised error should not result with a created template
+            if language and body:
+                template = self.redner.templates.account_template_add(
+                    language=language,
+                    body=body,
+                    name=vals.get("name"),
+                    description=vals.get("description"),
+                    produces=produces,
+                    body_format=body_format,
+                    version=fields.Datetime.to_string(fields.Datetime.now()),
+                    locale=locale if locale else "fr_FR",
+                )
+                vals["redner_id"] = template["name"]
+            else:
+                # If the language and body are not defined, we return early
+                # to prevent saving an incomplete template and sending it
+                # to the Redner server.
+                return self
+
+        return super().create(vals_list)
+
+    def write(self, vals):
+        """Overwrite write to update redner template"""
+
+        # Determine if we should update redner or not
+        should = self._should_update_redner(vals)
+
+        # Perform the write operation
+        ret = super().write(vals)
+
+        # Update Redner templates if applicable
+        if should:
+            for record in self:
+                if (
+                    not self.env.context.get("importing")  # Skip during imports
+                    and record.allow_modification_from_odoo
+                ):
+                    record._sync_with_redner()
+
+        return ret
+
+    def unlink(self):
+        """Overwrite unlink to delete redner template"""
+
+        # We do NOT depend on the API for consistency here
+        # So raised error should not result block template deletion
+        for record in self:
+            if record.redner_id and record.allow_modification_from_odoo:
+                try:
+                    self.redner.templates.account_template_delete(record.redner_id)
+                except Exception as e:
+                    logger.warning(
+                        "Failed to delete Redner template with ID %s. Reason: %s",
+                        record.redner_id,
+                        e,
+                    )
+        self.env.registry.clear_cache()
+        return super().unlink()
+
+    def copy(self, default=None):
+        self.ensure_one()
+        default = dict(default or {}, name=_("%s (copy)") % self.name)
+        return super().copy(default)
+
+    # ------------------------------------------------------------
+    # ACTIONS / BUSINESS
+    # ------------------------------------------------------------
+
+    def _should_update_redner(self, vals):
+        """
+        Determine if Redner should be updated based on the modified fields.
+        """
+        for field in COMPUTED_FIELDS + ["template_data"]:
+            if field in vals:
+                current_value = getattr(self, field)
+                if vals[field] != current_value and vals[field]:
+                    return True
+        return False
+
+    def _sync_with_redner(self):
+        """
+        Sync the current record's template with Redner.
+        """
+        self.ensure_one()
         try:
@@ -198,4 +435,31 @@
         try:
-            preview = self.redner.templates.account_template_preview(redner_id)
-            return preview
+            # Check if 'language' is a valid string before splitting
+            if isinstance(self.language, str) and "|" in self.language:
+                produces, language = self.language.split("|")
+            else:
+                logger.warning(
+                    "Invalid language format for record %s: %s",
+                    self.id,
+                    self.language,
+                )
+            body, body_format = (
+                (self.template_data.decode(), "base64")
+                if language == "od+mustache"
+                else (self.body, "text")
+            )
+
+            # Use the existing `redner_id`
+            redner_id = self.redner_id
+
+            self._update_redner_template(
+                template_id=redner_id,
+                language=language,
+                body=body,
+                name=self.name,
+                description=self.description,
+                produces=produces,
+                body_format=body_format,
+                version=fields.Datetime.to_string(self.write_date),
+                locale=self.locale_id.code,
+            )
         except Exception as e:
@@ -201,6 +465,38 @@
         except Exception as e:
-            logger.error("Failed to get preview of redner template :%s", e)
-            return
+            logger.error("Failed to sync with Redner template: %s", e)
+            raise ValidationError(_("Failed to update Redner template, %s") % e) from e
+
+    def _update_redner_template(self, **kwargs):
+        """
+        Perform the Redner `account_template_update` API call.
+
+        :param kwargs: Payload for the `account_template_update` API.
+        """
+        try:
+            self.redner.templates.account_template_update(**kwargs)
+            self.env.registry.clear_cache()
+        except Exception as e:
+            logger.error("Redner API update failed: %s", e)
+            raise ValueError("Unable to update the Redner template.") from e
+
+    @ormcache("redner_id")
+    def get_preview(self, redner_id):
+        """
+        Retrieve the preview of a Redner template by its ID.
+        Returns None if the redner_id is not provided or if the preview
+        cannot be retrieved.
+        """
+        if not redner_id:
+            return None
+
+        result = None
+        try:
+            result = self.redner.templates.account_template_preview(redner_id)
+        except Exception as e:
+            logger.error(
+                "Failed to get preview of Redner template with ID %s: %s", redner_id, e
+            )
+        return result
 
     def _to_odoo_template(self, template):
         """
@@ -249,5 +545,6 @@
             logger.error("Failed to read Redner template: %s", e)
             return {}
 
-    def _compute_template(self):
+    @ormcache("record_id", "new_template_data")
+    def _set_cached_template(self, record_id, new_template_data):
         """
@@ -253,3 +550,3 @@
         """
-        Computes the template values for the records and applies cached or fetched data.
+        Sets and caches the template in Redner for a given record.
         """
@@ -255,5 +552,5 @@
         """
-        for record in self:
-            if not record.id or not record.redner_id:
-                continue
+        record = self.browse(record_id)
+        if not record.redner_id:
+            raise ValueError("The record must have a valid Redner ID.")
 
@@ -259,4 +556,9 @@
 
-            # Fetch the cached template
-            cached_template = self._get_cached_template(record.id)
+        try:
+            produces, language = record.language.split("|")
+            body, body_format = (
+                (new_template_data.decode(), "base64")
+                if language == "od+mustache"
+                else (record.body, "text")
+            )
 
@@ -262,13 +564,21 @@
 
-            if not any([getattr(record, f) for f in COMPUTED_FIELDS]):
-                # If all computed fields are undefined, populate them
-                # from the cached template.
-                for f in COMPUTED_FIELDS + EDITABLE_FIELDS:
-                    if f in cached_template:
-                        setattr(record, f, cached_template[f])
-            else:
-                # If at least one field is defined, populate only undefined fields
-                for f in COMPUTED_FIELDS:
-                    if not getattr(record, f):
-                        setattr(record, f, cached_template.get(f, None))
+            # Send the updated template to the external system
+            self.redner.templates.account_template_update(
+                template_id=record.redner_id,
+                language=language,
+                body=body,
+                name=record.name,
+                description=record.description,
+                produces=produces,
+                body_format=body_format,
+                version=fields.Datetime.to_string(record.write_date),
+                locale=record.locale_id.code,
+            )
+
+            self.env.registry.clear_cache()
+
+            return True
+        except Exception as e:
+            logger.error("Failed to set Redner template: %s", e)
+            raise ValueError("Unable to update the Redner template.") from e
 
@@ -274,26 +584,49 @@
 
-    @api.depends("template_data")
-    def _compute_template_data(self):
-        for record in self:
-            if not record.id or not record.redner_id:
-                continue
-            if not record.template_data:
-                try:
-                    cached_template = self._get_cached_template(record.id)
-                    if "template_data" in cached_template:
-                        new_val = cached_template["template_data"]
-                        encoded = base64.b64encode(new_val).decode("utf-8")
-                        ext = ".odt"  # default extension
-                        try:
-                            ext = b64_to_extension(encoded)
-                        except Exception as e:
-                            logger.error("Failed to read extension from file:%s", e)
-                            return
-                        record.template_data = encoded
-                        record.template_data_filename = "template" + ext
-                except Exception as e:
-                    logger.error("Failed to read redner template :%s", e)
-                    return
+    def import_from_redner(self):
+        tl_wizard = self.env["template.list.wizard"]
+        templates = self.list_external_templates()
+        tl_wizard.populate(templates)
+
+        return {
+            "type": "ir.actions.act_window",
+            "res_model": "template.list.wizard",
+            "view_mode": "list",
+            "view_id": self.env.ref("redner.view_template_list_wizard_tree").id,
+            "target": "new",
+            #'context': self.env.context,
+        }
+
+    @api.model
+    def get_keywords(self):
+        """Return template redner keywords"""
+        self.ensure_one()
+
+        varlist = self.template_varlist_fetch()
+
+        for name in varlist:
+            while "." in name:
+                name = name[: name.rfind(".")]
+                if name not in varlist:
+                    varlist.append(name)
+
+        varlist.sort()
+
+        return varlist
+
+    @api.model
+    @ormcache("self.redner_id")
+    def template_varlist_fetch(self):
+        """Retrieve the list of variables present in the template."""
+        self.ensure_one()
+        try:
+            if not self.redner_id:
+                return []
+
+            return self.redner.templates.account_template_varlist(self.redner_id)
+
+        except Exception as e:
+            logger.warning("Failed to fetch account template varlist: %s", e)
+            return []
 
     def list_external_templates(self):
         try:
@@ -312,208 +645,6 @@
                 new_templates.append(self._to_odoo_template(template))
         return new_templates
 
-    @property
-    def redner(self):
-        """
-        Returns a Redner instance.
-        Recomputes the instance if any system parameter changes.
-        Uses a global variable to cache the instance across sessions.
-        """
-        global _redner, _redner_params
-
-        # Fetch current system parameters
-        config_model = self.env["ir.config_parameter"].sudo()
-        current_params = {
-            "api_key": config_model.get_param("redner.api_key"),
-            "server_url": config_model.get_param("redner.server_url"),
-            "account": config_model.get_param("redner.account"),
-            "timeout": int(config_model.get_param("redner.timeout", default="20")),
-        }
-
-        # Check if parameters have changed or if _redner is None
-        if _redner is None or _redner_params != current_params:
-            # Recompute the Redner instance
-            _redner = Redner(
-                current_params["api_key"],
-                current_params["server_url"],
-                current_params["account"],
-                current_params["timeout"],
-            )
-            _redner_params = current_params  # Update the stored parameters
-
-        return _redner
-
-    @api.model_create_multi
-    def create(self, vals_list):
-        """Overwrite create to create redner template"""
-
-        for vals in vals_list:
-            if not vals.get("name", False) or vals["name"] == "New":
-                # source
-                vals["source"] = "redner"
-                return super().create(vals_list)
-
-            # Prepare template params according to the selected language.
-            # Use template data field if the selected language is "od";
-            # otherwise the body field is used.
-            produces, language = vals.get("language", DEFAULT_LANGUAGE).split("|")
-            body, body_format = (
-                (vals.get("template_data", ""), "base64")
-                if language == "od+mustache"
-                else (vals.get("body"), "text")
-            )
-
-            locale = self.env["res.lang"].browse(vals.get("locale_id")).code
-
-            # We depend on the API for consistency here
-            # So raised error should not result with a created template
-            template = self.redner.templates.account_template_add(
-                language=language,
-                body=body,
-                name=vals.get("name"),
-                description=vals.get("description"),
-                produces=produces,
-                body_format=body_format,
-                version=fields.Datetime.to_string(fields.Datetime.now()),
-                locale=locale if locale else "fr_FR",
-            )
-            vals["redner_id"] = template["name"]
-
-        return super().create(vals_list)
-
-    def write(self, vals):
-        """Overwrite write to update redner template"""
-        # We depend on the API for consistency here
-        # So raised error should not result with an updated template
-
-        if "allow_modification_from_odoo" in vals:
-            if vals["allow_modification_from_odoo"]:
-                # case where we want to actually update the template from odoo
-                # safe, since passing this parameter from false to true can
-                # only be done separetly from changing other fields.
-                # we write the change and move on
-                return super().write(vals)
-            else:
-                # this case is more tricky
-                if len(vals) == 1:
-                    # ie the only field to be changed is the boolean
-                    # we write and move on. no need for updates
-                    return super().write(vals)
-                # at the same time, we updated some fields and
-                # we decided that odoo could not longer update the fields.
-                # so we  decide that this is the last update we send to
-                # redner
-
-        # compute if we should update redner or not
-        should_update_redner = False
-        for f in EDITABLE_FIELDS + COMPUTED_FIELDS + ["template_data"]:
-            # if we made a change in the record, update redner
-            if f in vals:
-                attr = getattr(self, f)
-                if vals[f] != attr:
-                    should_update_redner = True
-
-        if "name" in vals:
-            self.ensure_one()
-
-            redner_id = self.redner_id
-            vals["redner_id"] = vals["name"]
-
-        ret = super().write(vals)
-
-        for record in self:
-            if (
-                should_update_redner
-                and not self.env.context.get("importing")
-                and record.allow_modification_from_odoo
-            ):
-                try:
-                    produces, language = record.language.split("|")
-                    body, body_format = (
-                        (record.template_data.decode(), "base64")
-                        if language == "od+mustache"
-                        else (record.body, "text")
-                    )
-
-                    if "name" not in vals:
-                        redner_id = record.redner_id
-
-                    record.redner.templates.account_template_update(
-                        template_id=redner_id,
-                        language=language,
-                        body=body,
-                        name=vals.get("name", ""),
-                        description=record.description,
-                        produces=produces,
-                        body_format=body_format,
-                        version=fields.Datetime.to_string(record.write_date),
-                        locale=record.locale_id.code,
-                    )
-                except Exception as e:
-                    logger.error("Failed to update redner template :%s", e)
-                    raise ValidationError(
-                        _("Failed to update render template, %s") % e
-                    ) from e
-        return ret
-
-    def unlink(self):
-        """Overwrite unlink to delete redner template"""
-
-        # We do NOT depend on the API for consistency here
-        # So raised error should not result block template deletion
-        for record in self:
-            if record.redner_id and record.allow_modification_from_odoo:
-                try:
-                    self.redner.templates.account_template_delete(record.redner_id)
-                except Exception as e:
-                    # Log the exception if needed (for debugging purposes)
-                    logger.warning(
-                        "Failed to delete redner template for record %s: %s",
-                        record.id,
-                        {str(e)},
-                    )
-
-        return super().unlink()
-
-    def copy(self, default=None):
-        self.ensure_one()
-        default = dict(default or {}, name=_("%s (copy)") % self.name)
-        return super().copy(default)
-
-    @api.depends("body", "template_data")
-    def _compute_keywords(self):
-        for record in self:
-            record.detected_keywords = "\n".join(record.template_varlist_fetch())
-
-    @api.model
-    def get_keywords(self):
-        """Return template redner keywords"""
-
-        varlist = self.template_varlist_fetch()
-
-        for name in varlist:
-            while "." in name:
-                name = name[: name.rfind(".")]
-                if name not in varlist and name != "":
-                    varlist.append(name)
-
-        varlist.sort()
-
-        return varlist
-
-    @api.model
-    def template_varlist_fetch(self):
-        """Retrieve the list of variables present in the template."""
-        try:
-            if not self.redner_id:
-                return []
-
-            return self.redner.templates.account_template_varlist(self.redner_id)
-
-        except Exception as e:
-            logger.warning("Failed to fetch account template varlist: %s", e)
-            return []
-
     def redner_url(self):
         if self.redner_id is None:
             return ""
diff --git a/pyproject.toml b/pyproject.toml
index 93b8d613e36abdc467a2b3a8137ad7ecc1c67390_cHlwcm9qZWN0LnRvbWw=..880fdce6f6719b9201b2300a6007437d0beabde1_cHlwcm9qZWN0LnRvbWw= 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -17,7 +17,8 @@
 dependencies = [
   "odoo==17.0.*",
   "odoo-addon-converter >=17.0.1,<17.0.2",
-  "requests_unixsocket"
+  "requests_unixsocket",
+  "python-magic",
 ]
 
 [project.optional-dependencies]
diff --git a/security/ir.model.access.csv b/security/ir.model.access.csv
index 93b8d613e36abdc467a2b3a8137ad7ecc1c67390_c2VjdXJpdHkvaXIubW9kZWwuYWNjZXNzLmNzdg==..880fdce6f6719b9201b2300a6007437d0beabde1_c2VjdXJpdHkvaXIubW9kZWwuYWNjZXNzLmNzdg== 100644
--- a/security/ir.model.access.csv
+++ b/security/ir.model.access.csv
@@ -3,5 +3,3 @@
 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_expression_builder,access_redner_expression_builder,model_expression_builder_wizard,base.group_no_one,1,1,1,1
-access_redner_expression_builder_field,access_redner_expression_builder_field,model_expression_builder_field,base.group_no_one,1,1,1,1
diff --git a/static/src/components/charfield/charfield.js b/static/src/components/charfield/charfield.js
new file mode 100644
index 0000000000000000000000000000000000000000..880fdce6f6719b9201b2300a6007437d0beabde1_c3RhdGljL3NyYy9jb21wb25lbnRzL2NoYXJmaWVsZC9jaGFyZmllbGQuanM=
--- /dev/null
+++ b/static/src/components/charfield/charfield.js
@@ -0,0 +1,95 @@
+/** @odoo-module **/
+
+import {CharField, charField} from "@web/views/fields/char/char_field";
+import {useDynamicPlaceholder} from "@web/views/fields/dynamic_placeholder_hook";
+import {patch} from "@web/core/utils/patch";
+import {useExternalListener, useEffect} from "@odoo/owl";
+
+// Adding a new property for dynamic placeholder button visibility
+CharField.props = {
+    ...CharField.props,
+    placeholderField: {type: String, optional: true},
+    placeholderRenderField: {type: String, optional: true},
+    converterField: {type: String, optional: true},
+};
+
+// Extending charField to extract the new property
+const charExtractProps = charField.extractProps;
+charField.extractProps = (fieldInfo) => {
+    return Object.assign(charExtractProps(fieldInfo), {
+        placeholderField: fieldInfo.options.placeholder_field,
+        placeholderRednerField: fieldInfo.options?.placeholder_redner || false,
+        converterField: fieldInfo.options?.converter_field || false,
+    });
+};
+
+// Patch CharField for dynamic placeholder support
+patch(CharField.prototype, {
+    setup() {
+        super.setup();
+        if (this.props.dynamicPlaceholder) {
+            this.dynamicPlaceholder = useDynamicPlaceholder(this.input);
+            useExternalListener(document, "keydown", this.dynamicPlaceholder.onKeydown);
+            useEffect(() =>
+                this.dynamicPlaceholder.updateModel(
+                    this.props.dynamicPlaceholderModelReferenceField
+                )
+            );
+        }
+    },
+    get placeholder() {
+        return (
+            this.props.record.data[this.props.placeholderField] ||
+            this.props.placeholder
+        );
+    },
+    get showDynamicPlaceholderButton() {
+        return (
+            this.props.dynamicPlaceholder &&
+            !this.props.readonly &&
+            !this.props.record.data[this.props.placeholderRednerField]
+        );
+    },
+
+    get converterValue() {
+        return this.props.record.data[this.props.converterField] || "";
+    },
+
+    async onDynamicPlaceholderOpen() {
+        await this.dynamicPlaceholder.open({
+            validateCallback: this.onDynamicPlaceholderValidate.bind(this),
+        });
+    },
+    async onDynamicPlaceholderValidate(chain, defaultValue) {
+        if (chain) {
+            this.input.el.focus();
+            // Initialize dynamicPlaceholder with a default structure
+            let dynamicPlaceholder = ` {{object.${chain}${
+                defaultValue?.length ? ` ||| ${defaultValue}` : ""
+            }}}`;
+            switch (this.converter) {
+                case "field":
+                    // For "field" converter, use the chain directly as the value
+                    dynamicPlaceholder = `${chain}`;
+                    break;
+
+                default:
+                    // Default case if no specific converter type is found
+                    dynamicPlaceholder = ` {{object.${chain}${
+                        defaultValue?.length ? ` ||| ${defaultValue}` : ""
+                    }}}`;
+                    break;
+            }
+            this.input.el.setRangeText(
+                dynamicPlaceholder,
+                this.selectionStart,
+                this.selectionStart,
+                "end"
+            );
+            // trigger events to make the field dirty
+            this.input.el.dispatchEvent(new InputEvent("input"));
+            this.input.el.dispatchEvent(new KeyboardEvent("keydown"));
+            this.input.el.focus();
+        }
+    },
+});
diff --git a/static/src/components/charfield/charfield.scss b/static/src/components/charfield/charfield.scss
new file mode 100644
index 0000000000000000000000000000000000000000..880fdce6f6719b9201b2300a6007437d0beabde1_c3RhdGljL3NyYy9jb21wb25lbnRzL2NoYXJmaWVsZC9jaGFyZmllbGQuc2Nzcw==
--- /dev/null
+++ b/static/src/components/charfield/charfield.scss
@@ -0,0 +1,25 @@
+// Heading element normally takes the full width of the parent,
+// so the char_field wrapper should take the same width.
+@include media-breakpoint-up(md) {
+    .o_field_char {
+        width: inherit;
+    }
+}
+
+.o_field_char_buttons {
+    right: 0;
+    margin-left: -35px;
+    margin-right: 20px;
+
+    .o_field_translate {
+        margin-left: 0 !important;
+    }
+}
+
+.o_field_char input.o_field_placeholder {
+    padding-right: 40px;
+}
+
+.o_field_char input.o_field_placeholder.o_field_translate {
+    padding-right: 70px;
+}
diff --git a/static/src/components/charfield/charfield.xml b/static/src/components/charfield/charfield.xml
new file mode 100644
index 0000000000000000000000000000000000000000..880fdce6f6719b9201b2300a6007437d0beabde1_c3RhdGljL3NyYy9jb21wb25lbnRzL2NoYXJmaWVsZC9jaGFyZmllbGQueG1s
--- /dev/null
+++ b/static/src/components/charfield/charfield.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<templates xml:space="preserve">
+    <t t-inherit="web.CharField" t-inherit-mode="extension">
+        <xpath expr="//input" position="attributes">
+            <attribute name="t-att-placeholder">placeholder</attribute>
+        </xpath>
+        <xpath expr="//input" position="after">
+            <t t-call="dynamic_placeholder.magicButton">
+                <t t-set="positionCenter" t-value="true" />
+            </t>
+        </xpath>
+    </t>
+    <t t-name="dynamic_placeholder.magicButton">
+        <div class="position-relative d-inline">
+            <button
+                t-if="showDynamicPlaceholderButton"
+                class="btn position-absolute end-0"
+                t-attf-class="{{positionCenter ? 'pb-0 pt-0' : 'bottom-0'}}"
+                t-ref="magicButton"
+                t-on-click="onDynamicPlaceholderOpen"
+            >
+                <i class="fa fa-magic" role="img" aria-label="Magic" />
+            </button>
+        </div>
+    </t>
+</templates>
diff --git a/tests/test_redner_template.py b/tests/test_redner_template.py
index 93b8d613e36abdc467a2b3a8137ad7ecc1c67390_dGVzdHMvdGVzdF9yZWRuZXJfdGVtcGxhdGUucHk=..880fdce6f6719b9201b2300a6007437d0beabde1_dGVzdHMvdGVzdF9yZWRuZXJfdGVtcGxhdGUucHk= 100644
--- a/tests/test_redner_template.py
+++ b/tests/test_redner_template.py
@@ -113,10 +113,5 @@
         # Update template.
         redner_template.name = updated_name
         requests_post_mock.assert_not_called()
-        requests_put_mock.assert_called_once_with(
-            TEMPLATE_URL + "/" + base_name,
-            json=updated_template,
-            headers={"Rednerd-API-Key": "test-api-key"},
-            timeout=20,
-        )
+        requests_put_mock.assert_not_called()
         requests_put_mock.reset_mock()
diff --git a/utils/mimetype.py b/utils/mimetype.py
index 93b8d613e36abdc467a2b3a8137ad7ecc1c67390_dXRpbHMvbWltZXR5cGUucHk=..880fdce6f6719b9201b2300a6007437d0beabde1_dXRpbHMvbWltZXR5cGUucHk= 100644
--- a/utils/mimetype.py
+++ b/utils/mimetype.py
@@ -16,3 +16,22 @@
     except Exception as e:
         print(f"Error detecting file type: {e}")
         return None
+
+
+def get_file_extension(binary_data):
+    """Determine the file extension from binary content."""
+    mime = magic.Magic(mime=True)
+    file_type = mime.from_buffer(binary_data)
+
+    # Mapping MIME types to extensions
+    mime_to_ext = {
+        "application/vnd.oasis.opendocument.text": ".odt",
+        "application/pdf": ".pdf",
+        "image/jpeg": ".jpg",
+        "image/png": ".png",
+        "application/msword": ".doc",
+        "application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",  # noqa: E501
+    }
+    return mime_to_ext.get(
+        file_type, ".odt"
+    )  # Default to empty string if MIME type not found
diff --git a/views/ir_actions_report.xml b/views/ir_actions_report.xml
index 93b8d613e36abdc467a2b3a8137ad7ecc1c67390_dmlld3MvaXJfYWN0aW9uc19yZXBvcnQueG1s..880fdce6f6719b9201b2300a6007437d0beabde1_dmlld3MvaXJfYWN0aW9uc19yZXBvcnQueG1s 100644
--- a/views/ir_actions_report.xml
+++ b/views/ir_actions_report.xml
@@ -43,12 +43,9 @@
                             <tree editable="top" default_order="keyword asc">
                                 <field name="keyword" />
                                 <field name="converter" />
-                                <field name="value" />
-                                <button
-                                    name="action_build_expression"
-                                    type="object"
-                                    string="Build expression"
-                                    class="oe_highlight"
-                                    invisible="not (value and value!='') or not (converter and converter in ['field','relation-to-many'])"
-                                    confirm="This will delete the current substitution value. Continue anyway ?"
+                                <field name="render_model" column_invisible="1" />
+                                <field name="value_placeholder" column_invisible="1" />
+                                <field
+                                    name="dynamic_placeholder_button_hidden"
+                                    column_invisible="1"
                                 />
@@ -54,17 +51,16 @@
                                 />
-                                <button
-                                    name="action_build_expression"
-                                    type="object"
-                                    string="Build expression"
-                                    class="oe_highlight"
-                                    invisible="(value and value !='') or not (converter and converter in ['field','relation-to-many'])"
-                                />
-                                <button
-                                    name="action_build_expression"
-                                    type="object"
-                                    string="Build expression"
-                                    class="oe_highlight disabled"
-                                    invisible="converter in ['field','relation-to-many']"
+                                <field
+                                    name="value"
+                                    class="text-break"
+                                    required="converter != False"
+                                    options="{
+                                        'dynamic_placeholder': true, 
+                                        'dynamic_placeholder_model_reference_field': 'render_model', 
+                                        'placeholder_field': 'value_placeholder',
+                                        'placeholder_redner': 'dynamic_placeholder_button_hidden',
+                                        'converter_field': 'converter'
+                                    }"
+                                    default_focus="1"
                                 />
                             </tree>
                         </field>
diff --git a/views/mail_template.xml b/views/mail_template.xml
index 93b8d613e36abdc467a2b3a8137ad7ecc1c67390_dmlld3MvbWFpbF90ZW1wbGF0ZS54bWw=..880fdce6f6719b9201b2300a6007437d0beabde1_dmlld3MvbWFpbF90ZW1wbGF0ZS54bWw= 100644
--- a/views/mail_template.xml
+++ b/views/mail_template.xml
@@ -39,12 +39,9 @@
                         <tree editable="top" default_order="keyword asc">
                             <field name="keyword" />
                             <field name="converter" />
-                            <field name="value" />
-                            <button
-                                name="action_build_expression"
-                                type="object"
-                                string="Build expression"
-                                class="oe_highlight"
-                                invisible="not (value and value!='') or not (converter and converter in ['field','relation-to-many'])"
-                                confirm="This will delete the current substitution value. Continue anyway ?"
+                            <field name="render_model" column_invisible="1" />
+                            <field name="value_placeholder" column_invisible="1" />
+                            <field
+                                name="dynamic_placeholder_button_hidden"
+                                column_invisible="1"
                             />
@@ -50,17 +47,16 @@
                             />
-                            <button
-                                name="action_build_expression"
-                                type="object"
-                                string="Build expression"
-                                class="oe_highlight"
-                                invisible="(value and value !='') or not (converter and converter in ['field','relation-to-many'])"
-                            />
-                            <button
-                                name="action_build_expression"
-                                type="object"
-                                string="Build expression"
-                                class="oe_highlight disabled"
-                                invisible="converter in ['field','relation-to-many']"
+                            <field
+                                name="value"
+                                class="text-break"
+                                required="converter != False"
+                                options="{
+                                    'dynamic_placeholder': true, 
+                                    'dynamic_placeholder_model_reference_field': 'render_model', 
+                                    'placeholder_field': 'value_placeholder',
+                                    'placeholder_redner': 'dynamic_placeholder_button_hidden',
+                                    'converter_field': 'converter'
+                                }"
+                                default_focus="1"
                             />
                         </tree>
                     </field>
diff --git a/views/redner_template.xml b/views/redner_template.xml
index 93b8d613e36abdc467a2b3a8137ad7ecc1c67390_dmlld3MvcmVkbmVyX3RlbXBsYXRlLnhtbA==..880fdce6f6719b9201b2300a6007437d0beabde1_dmlld3MvcmVkbmVyX3RlbXBsYXRlLnhtbA== 100644
--- a/views/redner_template.xml
+++ b/views/redner_template.xml
@@ -34,6 +34,7 @@
         <field name="arch" type="xml">
             <form>
                 <header>
+                    <field name="id" invisible="1" />
                     <field name="redner_id" invisible="1" />
                     <button
                         name="view_in_redner"
@@ -73,7 +74,7 @@
                             <h1>
                                 <field
                                     name="name"
-                                    readonly="not allow_modification_from_odoo"
+                                    readonly="not allow_modification_from_odoo or id != False"
                                 />
                             </h1>
                         </div>
diff --git a/wizard/__init__.py b/wizard/__init__.py
index 93b8d613e36abdc467a2b3a8137ad7ecc1c67390_d2l6YXJkL19faW5pdF9fLnB5..880fdce6f6719b9201b2300a6007437d0beabde1_d2l6YXJkL19faW5pdF9fLnB5 100644
--- a/wizard/__init__.py
+++ b/wizard/__init__.py
@@ -1,6 +1,4 @@
 from . import (
-    expression_builder,
-    expression_builder_field,
     mail_compose_message,
     template_list,
 )
diff --git a/wizard/expression_builder.py b/wizard/expression_builder.py
deleted file mode 100644
index 93b8d613e36abdc467a2b3a8137ad7ecc1c67390_d2l6YXJkL2V4cHJlc3Npb25fYnVpbGRlci5weQ==..0000000000000000000000000000000000000000
--- a/wizard/expression_builder.py
+++ /dev/null
@@ -1,128 +0,0 @@
-from odoo import exceptions, fields, models
-
-
-# ExpressionBuilder
-class ExpressionBuilder(models.TransientModel):
-    _name = "expression.builder.wizard"
-    _description = "Expression Builder Wizard"
-
-    # the substitution the expression builder is working on
-    substitution_id = fields.Many2one(
-        comodel_name="redner.substitution",
-        string="Substitution",
-        help="The Redner Substitution on which we assign the value",
-        required=True,
-    )
-    # the expression field is concatened with the fields values
-    # when the wizard action is completed, this field is
-    # written in the substitution value
-    expression = fields.Char(
-        string="Substitution",
-        help=(
-            "Stores the substitution value the builder is working on."
-            " (example: 'partner_id.name')"
-        ),
-    )
-
-    suggested_fields = fields.One2many(
-        comodel_name="expression.builder.field",
-        inverse_name="wizard_id",
-        string="Suggested Fields",
-        help="The suggested fields, based on the substitution converter value",
-    )
-
-    def get_fields(self, model, converter):
-        # Get all field records for the model
-        vals_list = self.env["ir.model.fields"].search([("model", "=", model._name)])
-
-        # Filter fields based on the converter type
-        if converter == "relation-to-many":
-            vals_list = [f for f in vals_list if f.ttype == "many2many"]
-        elif converter == "field":
-            vals_list = [f for f in vals_list if f.ttype != "many2many"]
-        else:
-            vals_list = []
-        return [
-            {
-                "field_name": f.field_description,
-                "field_technical_name": f.name,
-                "field_type": f.ttype,
-                "field_comodel_name": f.relation,
-            }
-            for f in vals_list
-        ]
-
-    def action_cancel(self):
-        # custom cancel, unlinks the fields, the wizard itself
-        # and close window
-        self.unlink()
-        return {"type": "ir.actions.act_window_close"}
-
-    def action_save(self):
-        selected_count = 0
-        selected_field = False
-        for f in self.suggested_fields:
-            if f.selected:
-                selected_count += 1
-                selected_field = f
-
-        if selected_count > 1:
-            raise exceptions.UserError(
-                "You cannot select more than one field to build the expression"
-            )
-
-        elif selected_count == 0:
-            raise exceptions.UserError(
-                "You have to select one field to build the expression"
-            )
-        elif selected_field.field_type in ["many2one", "one2many"]:
-            # in this case we do not close the window.
-            # instead we refresh the wizard by showing the
-            # fields 1 field deeper
-            # and we update the expression field
-            if self.expression or self.expression != "":
-                self.expression += "." + selected_field.field_technical_name
-            else:
-                self.expression = selected_field.field_technical_name
-
-            # get comodel
-            comodel = self.env.get(selected_field.field_comodel_name)
-            if comodel is None:
-                return
-            vals_list = self.get_fields(comodel, self.substitution_id.converter)
-            for val in vals_list:
-                val["wizard_id"] = self.id
-
-            # remove previous fields
-            self.suggested_fields.unlink()
-            # add and create the next fields
-            fields_list = self.env["expression.builder.field"].create(vals_list)
-            self.suggested_fields = [(6, 0, fields_list.ids)]
-            # stay on updated wizard
-            return {
-                "type": "ir.actions.act_window",
-                "res_model": "expression.builder.wizard",
-                "view_mode": "form",
-                "res_id": self.id,
-                "target": "new",
-            }
-        # if the selected field is not relationnal:
-        if self.expression or self.expression != "":
-            # expression has already a value
-            self.substitution_id.write(
-                {
-                    "value": self.expression
-                    + "."
-                    + selected_field.field_technical_name,
-                }
-            )
-        else:
-            # expression is empty
-            self.substitution_id.write(
-                {
-                    "value": selected_field.field_technical_name,
-                }
-            )
-        # at the end, we unlink all of the temp data
-        # and close the window
-        return self.action_cancel()
diff --git a/wizard/expression_builder_field.py b/wizard/expression_builder_field.py
deleted file mode 100644
index 93b8d613e36abdc467a2b3a8137ad7ecc1c67390_d2l6YXJkL2V4cHJlc3Npb25fYnVpbGRlcl9maWVsZC5weQ==..0000000000000000000000000000000000000000
--- a/wizard/expression_builder_field.py
+++ /dev/null
@@ -1,39 +0,0 @@
-from odoo import fields, models
-
-
-class ExpressionBuilderField(models.TransientModel):
-    _name = "expression.builder.field"
-    _description = "Expression Builder Field"
-
-    wizard_id = fields.Many2one(
-        string="Expression builder",
-        comodel_name="expression.builder.wizard",
-        required=True,
-        ondelete="cascade",
-    )
-
-    field_name = fields.Char(
-        string="Field name",
-        help="The field's comprehensible name",
-    )
-
-    field_type = fields.Char(
-        string="Field Type",
-        help="The field's type (boolean, text, ...)",
-    )
-
-    field_comodel_name = fields.Char(
-        string="Comodel Name",
-        help="If the field is relational, store the comodel name here",
-    )
-
-    field_technical_name = fields.Char(
-        string="Field Technical Name",
-        help="The field's technical name",
-    )
-
-    selected = fields.Boolean(
-        string="Selected",
-        help="When selected, this field will be added to the substitution value",
-        readonly=False,
-    )
diff --git a/wizard/expression_builder_view.xml b/wizard/expression_builder_view.xml
deleted file mode 100644
index 93b8d613e36abdc467a2b3a8137ad7ecc1c67390_d2l6YXJkL2V4cHJlc3Npb25fYnVpbGRlcl92aWV3LnhtbA==..0000000000000000000000000000000000000000
--- a/wizard/expression_builder_view.xml
+++ /dev/null
@@ -1,42 +0,0 @@
-<odoo>
-    <record id="view_expression_builder_wizard_form" model="ir.ui.view">
-        <field name="name">expression.builder.wizard.form</field>
-        <field name="model">expression.builder.wizard</field>
-        <field name="arch" type="xml">
-            <form string="Build Expression">
-                <sheet>
-                    <field name="substitution_id" invisible="1" />
-                    <field name="suggested_fields" nolabel="1" colspan="2">
-                        <tree
-                            no_open="true"
-                            editable="top"
-                            create="false"
-                            delete="false"
-                            default_order="field_name asc"
-                        >
-                            <field name="selected" />
-                            <field name="field_name" readonly="1" />
-                            <field name="field_technical_name" readonly="1" />
-                            <field name="field_type" readonly="1" />
-                            <field name="field_comodel_name" readonly="1" />
-                        </tree>
-                    </field>
-                    <footer>
-                        <button
-                            string="Save"
-                            type="object"
-                            name="action_save"
-                            class="btn-primary"
-                        />
-                        <button
-                            string="Cancel"
-                            type="object"
-                            name="action_cancel"
-                            class="btn-secondary"
-                        />
-                    </footer>
-                </sheet>
-            </form>
-        </field>
-    </record>
-</odoo>