# 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>