# HG changeset patch
# User Balde Oury <oury.balde@xcg-consulting.fr>
# Date 1735577760 0
#      Mon Dec 30 16:56:00 2024 +0000
# Branch 17.0
# Node ID a6c94d540cb228b9dcf7dca24e53a35158b2c9ef
# Parent  1f1a085357d7baaa2c4d223ddeeec1eb414cea5f
Add dynamic expression button for substitution line and new converter features

- Introduced a button to dynamically set the expression in the substitution line.
- The feature is available for converters of type "Odoo template", "Odoo template + Evaluation", and "Field".
- Added functionality to visualize how to define each converter, displaying a placeholder (example) for each converter type.
- The code has been designed to ease the integration of additional converters in the future.
- Refactored the code to improve performance and stability.

- Add missing python-magic requirement for package

diff --git a/NEWS.rst b/NEWS.rst
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -5,7 +5,9 @@
 17.0.1.3.0
 ----------
 
-Refactor redner.template model to improve template management.
+- 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
--- a/__manifest__.py
+++ b/__manifest__.py
@@ -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
--- 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,19 +15,17 @@
 "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
-#, python-format
 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,20 +63,18 @@
 #. 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
-#, python-format
 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,9 +631,17 @@
 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
-#, 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,12 +689,24 @@
 
 #. 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
-#, python-format
 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
--- a/models/redner_substitution.py
+++ b/models/redner_substitution.py
@@ -25,11 +25,38 @@
 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"
+    _inherit = ["mail.render.mixin"]
     _description = "Redner Substitution"
 
     keyword = fields.Char(string="Variable", help="Template variable name")
@@ -40,28 +67,71 @@
         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")
 
-    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"
     )
 
-    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(".")
 
+    @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/pyproject.toml b/pyproject.toml
--- 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
--- 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
--- /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
--- /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
--- /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/views/ir_actions_report.xml b/views/ir_actions_report.xml
--- a/views/ir_actions_report.xml
+++ b/views/ir_actions_report.xml
@@ -43,28 +43,24 @@
                             <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"
                                 />
-                                <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
--- a/views/mail_template.xml
+++ b/views/mail_template.xml
@@ -39,28 +39,24 @@
                         <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"
                             />
-                            <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/wizard/__init__.py b/wizard/__init__.py
--- 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
--- 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
--- 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
--- 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>