# HG changeset patch # User Axel Prel <axel.prel@xcg-consulting.fr> # Date 1741707439 -3600 # Tue Mar 11 16:37:19 2025 +0100 # Branch 18.0 # Node ID a1e1e12a5fa33fb915395663db00c97ddb3fe694 # Parent e27b5297cef42eceb72b355b5f12bd2d39da1f33 # EXP-Topic RED-514 redner: new menu for template list and server config accessible with group conditionnality diff --git a/NEWS.rst b/NEWS.rst --- a/NEWS.rst +++ b/NEWS.rst @@ -5,6 +5,12 @@ 18.0.1.7.0 ---------- +Add a redner menu in "Apps" containing: +- the redner template management page (create, list, update, delete templates) +- a configuration wizard allowing quick server configuration (apikey, account, url) +The redner template management page is accessible if you are an admin or a redner user +The configuration page is accessible if you are an admin or a redner_admin + Res config: add redner integration parameters (server_url, account, api_key) Template locale is by default user locale, not fr_FR diff --git a/__manifest__.py b/__manifest__.py --- a/__manifest__.py +++ b/__manifest__.py @@ -29,8 +29,11 @@ # converter: https://orus.io/xcg/odoo-modules/converter "depends": ["converter", "mail", "web"], "data": [ + "security/groups.xml", + "security/ir.model.access.csv", "wizard/mail_compose_message_views.xml", "wizard/template_list_view.xml", + "wizard/redner_configurator_wizard.xml", "security/ir.model.access.csv", "views/redner_template.xml", "views/mail_template.xml", @@ -38,6 +41,9 @@ "views/res_config_settings_view.xml", "views/menu.xml", ], + "demo": [ + "demo/users.xml", + ], "assets": { "web.assets_backend": [ "redner/static/src/js/redner_report_action.esm.js", diff --git a/demo/users.xml b/demo/users.xml new file mode 100644 --- /dev/null +++ b/demo/users.xml @@ -0,0 +1,31 @@ +<odoo> + <data noupdate="1"> + <!-- Redner Admin User --> + <record id="redner_admin_demo" model="res.users"> + <field name="name">Redner Admin</field> + <field name="login">radmin</field> + <field name="email">redner_admin@example.com</field> + <field name="password">radmin</field> + <field name="company_id" ref="base.main_company" /> + <field name="company_ids" eval="[(6, 0, [ref('base.main_company')])]" /> + <field + name="groups_id" + eval="[(6, 0, [ref('redner.group_redner_admin')])]" + /> + </record> + + <!-- Redner User --> + <record id="redner_user_demo" model="res.users"> + <field name="name">Redner User</field> + <field name="login">ruser</field> + <field name="email">redner_user@example.com</field> + <field name="password">ruser</field> + <field name="company_id" ref="base.main_company" /> + <field name="company_ids" eval="[(6, 0, [ref('base.main_company')])]" /> + <field + name="groups_id" + eval="[(6, 0, [ref('redner.group_redner_user')])]" + /> + </record> + </data> +</odoo> diff --git a/security/groups.xml b/security/groups.xml new file mode 100644 --- /dev/null +++ b/security/groups.xml @@ -0,0 +1,23 @@ +<odoo> + <data noupdate="0"> + <!-- Category --> + <record id="module_category_redner" model="ir.module.category"> + <field name="name">Redner</field> + <field name="sequence">10</field> + </record> + + <!-- User Group --> + <record id="group_redner_user" model="res.groups"> + <field name="name">Redner User</field> + <field name="category_id" ref="module_category_redner" /> + <field name="implied_ids" eval="[(4, ref('base.group_user'))]" /> + </record> + + <!-- Admin Group --> + <record id="group_redner_admin" model="res.groups"> + <field name="name">Redner Administrator</field> + <field name="category_id" ref="module_category_redner" /> + <field name="implied_ids" eval="[(4, ref('base.group_user'))]" /> + </record> + </data> +</odoo> diff --git a/security/ir.model.access.csv b/security/ir.model.access.csv --- a/security/ir.model.access.csv +++ b/security/ir.model.access.csv @@ -1,5 +1,25 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_redner_report,access_redner_report,model_redner_report,base.group_no_one,1,1,1,1 -access_redner_template,access_redner_template,model_redner_template,base.group_no_one,1,1,1,1 -access_redner_substitution,access_redner_substitution,model_redner_substitution,base.group_no_one,1,1,1,1 -access_redner_template_list,access_redner_template_list,model_template_list_wizard,base.group_no_one,1,1,1,1 +access_redner_report_system,access_redner_report_system,model_redner_report,base.group_system,1,1,1,1 +access_redner_report_user,access_redner_report_user,model_redner_report,base.group_user,1,0,0,0 +access_redner_report_redner_user,access_redner_report_redner_user,model_redner_report,redner.group_redner_user,1,1,1,1 +access_redner_report_redner_admin,access_redner_report_redner_admin,model_redner_report,redner.group_redner_admin,1,0,0,0 + +access_redner_template_system,access_redner_template_system,model_redner_template,base.group_system,1,1,1,1 +access_redner_template_user,access_redner_template_user,model_redner_template,base.group_user,1,0,0,0 +access_redner_template_redner_user,access_redner_template_redner_user,model_redner_template,redner.group_redner_user,1,1,1,1 +access_redner_template_redner_admin,access_redner_template_redner_admin,model_redner_template,redner.group_redner_admin,1,0,0,0 + +access_redner_substitution_system,access_redner_substitution_system,model_redner_substitution,base.group_system,1,1,1,1 +access_redner_substitution_user,access_redner_substitution_user,model_redner_substitution,base.group_user,1,0,0,0 +access_redner_substitution_redner_user,access_redner_substitution_redner_user,model_redner_substitution,redner.group_redner_user,1,1,1,1 +access_redner_substitution_redner_admin,access_redner_substitution_redner_admin,model_redner_substitution,redner.group_redner_admin,1,0,0,0 + +access_redner_template_list_system,access_redner_template_list_system,model_template_list_wizard,base.group_system,1,1,1,1 +access_redner_template_list_user,access_redner_template_list_user,model_template_list_wizard,base.group_user,1,0,0,0 +access_redner_template_list_redner_user,access_redner_template_list_redner_user,model_template_list_wizard,redner.group_redner_user,1,1,1,1 +access_redner_template_list_redner_admin,access_redner_template_list_redner_admin,model_template_list_wizard,redner.group_redner_admin,1,0,0,0 + +access_redner_configurator_wizard_system,access_redner_configurator_wizard_system,model_redner_configurator_wizard,base.group_system,1,1,1,1 +access_redner_configurator_wizard_user,access_redner_configurator_wizard_user,model_redner_configurator_wizard,base.group_user,0,0,0,0 +access_redner_configurator_wizard_redner_user,access_redner_configurator_wizard_redner_user,model_redner_configurator_wizard,redner.group_redner_user,0,0,0,0 +access_redner_configurator_wizard_redner_admin,access_redner_configurator_wizard_redner_admin,model_redner_configurator_wizard,redner.group_redner_admin,1,1,1,1 diff --git a/views/menu.xml b/views/menu.xml --- a/views/menu.xml +++ b/views/menu.xml @@ -10,4 +10,32 @@ action="redner_template_action" sequence="8" /> + + <!-- Redner main menu item --> + <menuitem + id="redner_main_menu" + name="Redner" + parent="base.menu_management" + sequence="15" + groups="base.group_system,redner.group_redner_user,redner.group_redner_admin" + /> + <!-- Templates submenu --> + <menuitem + id="redner_template_main_menu" + name="Templates" + parent="redner_main_menu" + action="redner_template_action" + groups="base.group_system,redner.group_redner_user" + sequence="151" + /> + + <!-- Configuration submenu --> + <menuitem + id="redner_config_main_menu" + name="Configuration" + parent="redner_main_menu" + action="redner_config_action" + groups="base.group_system,redner.group_redner_admin" + sequence="152" + /> </odoo> diff --git a/wizard/__init__.py b/wizard/__init__.py --- a/wizard/__init__.py +++ b/wizard/__init__.py @@ -1,4 +1,5 @@ from . import ( mail_compose_message, + redner_configurator_wizard, template_list, ) diff --git a/wizard/redner_configurator_wizard.py b/wizard/redner_configurator_wizard.py new file mode 100644 --- /dev/null +++ b/wizard/redner_configurator_wizard.py @@ -0,0 +1,62 @@ +############################################################################## +# +# Redner Odoo module +# Copyright © 2016, 2023-2025 XCG Consulting <https://xcg-consulting.fr> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +############################################################################## + +from odoo import api, fields, models +from odoo.exceptions import AccessError + + +class RednerConfiguratorWizard(models.TransientModel): + _name = "redner.configurator.wizard" + _description = "Redner Configuration Wizard" + + server_url = fields.Char(string="Server URL") + account = fields.Char(string="Account") + api_key = fields.Char(string="API Key") + + @api.model + def default_get(self, fields_list): + """Load default values from ir.config_parameter""" + res = super().default_get(fields_list) + Param = self.env["ir.config_parameter"].sudo() + + res.update( + { + "server_url": Param.get_param("redner.server_url", default=""), + "account": Param.get_param("redner.account", default=""), + "api_key": Param.get_param("redner.api_key", default=""), + } + ) + return res + + def action_apply(self): + """Save values back to ir.config_parameter""" + if not self.env.user.has_group( + "redner.group_redner_admin" + ) and not self.env.user.has_group("base.group_system"): + raise AccessError( + "You do not have permission to modify Redner configuration." + ) + + Param = self.env["ir.config_parameter"].sudo() + Param.set_param("redner.server_url", self.server_url or "") + Param.set_param("redner.account", self.account or "") + Param.set_param("redner.api_key", self.api_key or "") + + return {"type": "ir.actions.act_window_close"} diff --git a/wizard/redner_configurator_wizard.xml b/wizard/redner_configurator_wizard.xml new file mode 100644 --- /dev/null +++ b/wizard/redner_configurator_wizard.xml @@ -0,0 +1,32 @@ +<odoo> + <record id="view_redner_configurator_wizard" model="ir.ui.view"> + <field name="name">redner.configurator.wizard.form</field> + <field name="model">redner.configurator.wizard</field> + <field name="arch" type="xml"> + <form string="Redner Configuration"> + <group> + <field name="server_url" /> + <field name="account" /> + <field name="api_key" /> + </group> + <footer> + <button + string="Apply" + type="object" + name="action_apply" + class="oe_highlight" + /> + <button string="Cancel" class="oe_link" special="cancel" /> + </footer> + </form> + </field> + </record> + + <record id="redner_config_action" model="ir.actions.act_window"> + <field name="name">Redner Configuration</field> + <field name="res_model">redner.configurator.wizard</field> + <field name="view_mode">form</field> + <field name="view_id" ref="view_redner_configurator_wizard" /> + <field name="target">new</field> + </record> +</odoo> # HG changeset patch # User Axel Prel <axel.prel@xcg-consulting.fr> # Date 1741797223 -3600 # Wed Mar 12 17:33:43 2025 +0100 # Branch 18.0 # Node ID c3797452927ded2d4242abfff7d1a286e3e8d8da # Parent a1e1e12a5fa33fb915395663db00c97ddb3fe694 # EXP-Topic RED-514 simplify ir_model_access names diff --git a/security/ir.model.access.csv b/security/ir.model.access.csv --- a/security/ir.model.access.csv +++ b/security/ir.model.access.csv @@ -1,25 +1,25 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_redner_report_system,access_redner_report_system,model_redner_report,base.group_system,1,1,1,1 -access_redner_report_user,access_redner_report_user,model_redner_report,base.group_user,1,0,0,0 -access_redner_report_redner_user,access_redner_report_redner_user,model_redner_report,redner.group_redner_user,1,1,1,1 -access_redner_report_redner_admin,access_redner_report_redner_admin,model_redner_report,redner.group_redner_admin,1,0,0,0 +report_system,report_system,model_redner_report,base.group_system,1,1,1,1 +report_user,report_user,model_redner_report,base.group_user,1,0,0,0 +report_redner_user,report_redner_user,model_redner_report,redner.group_redner_user,1,1,1,1 +report_redner_admin,report_redner_admin,model_redner_report,redner.group_redner_admin,1,0,0,0 -access_redner_template_system,access_redner_template_system,model_redner_template,base.group_system,1,1,1,1 -access_redner_template_user,access_redner_template_user,model_redner_template,base.group_user,1,0,0,0 -access_redner_template_redner_user,access_redner_template_redner_user,model_redner_template,redner.group_redner_user,1,1,1,1 -access_redner_template_redner_admin,access_redner_template_redner_admin,model_redner_template,redner.group_redner_admin,1,0,0,0 +template_system,template_system,model_redner_template,base.group_system,1,1,1,1 +template_user,template_user,model_redner_template,base.group_user,1,0,0,0 +template_redner_user,template_redner_user,model_redner_template,redner.group_redner_user,1,1,1,1 +template_redner_admin,template_redner_admin,model_redner_template,redner.group_redner_admin,1,0,0,0 -access_redner_substitution_system,access_redner_substitution_system,model_redner_substitution,base.group_system,1,1,1,1 -access_redner_substitution_user,access_redner_substitution_user,model_redner_substitution,base.group_user,1,0,0,0 -access_redner_substitution_redner_user,access_redner_substitution_redner_user,model_redner_substitution,redner.group_redner_user,1,1,1,1 -access_redner_substitution_redner_admin,access_redner_substitution_redner_admin,model_redner_substitution,redner.group_redner_admin,1,0,0,0 +substitution_system,substitution_system,model_redner_substitution,base.group_system,1,1,1,1 +substitution_user,substitution_user,model_redner_substitution,base.group_user,1,0,0,0 +substitution_redner_user,substitution_redner_user,model_redner_substitution,redner.group_redner_user,1,1,1,1 +substitution_redner_admin,substitution_redner_admin,model_redner_substitution,redner.group_redner_admin,1,0,0,0 -access_redner_template_list_system,access_redner_template_list_system,model_template_list_wizard,base.group_system,1,1,1,1 -access_redner_template_list_user,access_redner_template_list_user,model_template_list_wizard,base.group_user,1,0,0,0 -access_redner_template_list_redner_user,access_redner_template_list_redner_user,model_template_list_wizard,redner.group_redner_user,1,1,1,1 -access_redner_template_list_redner_admin,access_redner_template_list_redner_admin,model_template_list_wizard,redner.group_redner_admin,1,0,0,0 +template_list_system,template_list_system,model_template_list_wizard,base.group_system,1,1,1,1 +template_list_user,template_list_user,model_template_list_wizard,base.group_user,1,0,0,0 +template_list_redner_user,template_list_redner_user,model_template_list_wizard,redner.group_redner_user,1,1,1,1 +template_list_redner_admin,template_list_redner_admin,model_template_list_wizard,redner.group_redner_admin,1,0,0,0 -access_redner_configurator_wizard_system,access_redner_configurator_wizard_system,model_redner_configurator_wizard,base.group_system,1,1,1,1 -access_redner_configurator_wizard_user,access_redner_configurator_wizard_user,model_redner_configurator_wizard,base.group_user,0,0,0,0 -access_redner_configurator_wizard_redner_user,access_redner_configurator_wizard_redner_user,model_redner_configurator_wizard,redner.group_redner_user,0,0,0,0 -access_redner_configurator_wizard_redner_admin,access_redner_configurator_wizard_redner_admin,model_redner_configurator_wizard,redner.group_redner_admin,1,1,1,1 +configurator_system,configurator_system,model_redner_configurator_wizard,base.group_system,1,1,1,1 +configurator_user,configurator_user,model_redner_configurator_wizard,base.group_user,0,0,0,0 +configurator_redner_user,configurator_redner_user,model_redner_configurator_wizard,redner.group_redner_user,0,0,0,0 +configurator_redner_admin,configurator_redner_admin,model_redner_configurator_wizard,redner.group_redner_admin,1,1,1,1 # HG changeset patch # User Axel Prel <axel.prel@xcg-consulting.fr> # Date 1742206535 -3600 # Mon Mar 17 11:15:35 2025 +0100 # Branch 18.0 # Node ID 98cccb0803deb1908a5c5a0bbc11721418b6472d # Parent c3797452927ded2d4242abfff7d1a286e3e8d8da # EXP-Topic RED-549 remove converter.py and use converter module image converters diff --git a/converter.py b/converter.py deleted file mode 100644 --- a/converter.py +++ /dev/null @@ -1,67 +0,0 @@ -############################################################################## -# -# Redner Odoo module -# Copyright © 2016, 2025 XCG Consulting <https://xcg-consulting.fr> -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. -# -############################################################################## - -import base64 -from collections.abc import Mapping -from typing import Any - -from odoo import models # type: ignore[import-untyped] -from odoo.addons.converter import Converter -from odoo.tools.mimetypes import guess_mimetype # type: ignore[import-untyped] - - -def image(value: bytes): - # get MIME type associated with the decoded_data. - image_base64 = base64.b64decode(value) - mimetype = guess_mimetype(image_base64) - return {"body": value.decode("ascii"), "mime-type": mimetype} - - -class ImageFile(Converter): - def __init__(self, fieldname): - self.fieldname = fieldname - - def odoo_to_message( - self, instance: models.Model, ctx: Mapping | None = None - ) -> Any: - value = getattr(instance, self.fieldname) - - if not value: - return {} - - return image(value) - - -class ImageDataURL(Converter): - def __init__(self, fieldname): - self.fieldname = fieldname - - def odoo_to_message( - self, instance: models.Model, ctx: Mapping | None = None - ) -> Any: - value = getattr(instance, self.fieldname) - - if not value: - return "" - - content = base64.b64decode(value) - mimetype = guess_mimetype(content) - - return "data:{};base64,{}".format(mimetype, value.decode("ascii")) diff --git a/models/mail_template.py b/models/mail_template.py --- a/models/mail_template.py +++ b/models/mail_template.py @@ -22,10 +22,9 @@ import logging from odoo import _, fields, models # type: ignore[import-untyped] +from odoo.addons import converter from odoo.exceptions import ValidationError # type: ignore[import-untyped] -from ..converter import image - _logger = logging.getLogger(__name__) @@ -124,5 +123,5 @@ def render_variable_hook(self, variables): """Override to add additional variables in mail "render template" func""" - variables.update({"image": lambda value: image(value)}) + variables.update({"image": lambda value: converter.image(value)}) return super().render_variable_hook(variables) diff --git a/models/redner_substitution.py b/models/redner_substitution.py --- a/models/redner_substitution.py +++ b/models/redner_substitution.py @@ -22,7 +22,6 @@ from odoo.addons import converter from odoo.exceptions import ValidationError # type: ignore[import-untyped] -from ..converter import ImageDataURL, ImageFile from ..utils.sorting import parse_sorted_field, sortkey FIELD = "field" @@ -161,11 +160,11 @@ path, name = sub.value.rsplit(".", 1) else: path, name = None, sub.value - conv = ImageFile(name) + conv = converter.ImageFile(name) if path: conv = converter.relation(path.replace(".", "/"), conv) elif sub.converter == "image-data-url": - conv = ImageDataURL(sub.value) + conv = converter.ImageDataURL(sub.value) elif sub.converter == "relation-to-many": # Unpack the result of finding a field with its sort order into # variable names. # HG changeset patch # User Axel Prel <axel.prel@xcg-consulting.fr> # Date 1742206780 -3600 # Mon Mar 17 11:19:40 2025 +0100 # Branch 18.0 # Node ID 8b39746b574656c0b3028eb58ece5d00a44f3c77 # Parent 98cccb0803deb1908a5c5a0bbc11721418b6472d # EXP-Topic RED-549 substitutions: use constants diff --git a/models/redner_substitution.py b/models/redner_substitution.py --- a/models/redner_substitution.py +++ b/models/redner_substitution.py @@ -141,13 +141,13 @@ def build_converter(self): d = {} for sub in self: - if sub.converter == "mail_template": + if sub.converter == MAIL_TEMPLATE: conv = converter.MailTemplate(sub.value, False) - elif sub.converter == "mail_template+deserialize": + elif sub.converter == MAIL_TEMPLATE_DESERIALIZE: conv = converter.MailTemplate(sub.value, True) - elif sub.converter == "constant": + elif sub.converter == CONSTANT: conv = converter.Constant(sub.value) - elif sub.converter == "field": + elif sub.converter == FIELD: if "." in sub.value: path, name = sub.value.rsplit(".", 1) else: @@ -155,7 +155,7 @@ conv = converter.Field(name) if path: conv = converter.relation(path.replace(".", "/"), conv) - elif sub.converter == "image-file": + elif sub.converter == IMAGE_FILE: if "." in sub.value: path, name = sub.value.rsplit(".", 1) else: @@ -163,9 +163,9 @@ conv = converter.ImageFile(name) if path: conv = converter.relation(path.replace(".", "/"), conv) - elif sub.converter == "image-data-url": + elif sub.converter == IMAGE_DATAURL: conv = converter.ImageDataURL(sub.value) - elif sub.converter == "relation-to-many": + elif sub.converter == RELATION_2MANY: # Unpack the result of finding a field with its sort order into # variable names. value, sorted = parse_sorted_field(sub.value) @@ -175,7 +175,7 @@ sortkey=sortkey(sorted) if sorted else None, converter=sub.get_children().build_converter(), ) - elif sub.converter == "relation-path": + elif sub.converter == RELATION_PATH: conv = converter.relation( sub.value, sub.get_children().build_converter() ) # HG changeset patch # User Axel Prel <axel.prel@xcg-consulting.fr> # Date 1742206458 -3600 # Mon Mar 17 11:14:18 2025 +0100 # Branch 18.0 # Node ID b9af52c79fd6b6617b41bc54c2071ed91739d921 # Parent 8b39746b574656c0b3028eb58ece5d00a44f3c77 # EXP-Topic RED-549 add qr code converter in substitutions diff --git a/models/redner_substitution.py b/models/redner_substitution.py --- a/models/redner_substitution.py +++ b/models/redner_substitution.py @@ -30,6 +30,8 @@ MAIL_TEMPLATE_DESERIALIZE = "mail_template+deserialize" IMAGE_FILE = "image-file" IMAGE_DATAURL = "image-data-url" +QR_CODE_FILE = "qr-code-file" +QR_CODE_DATAURL = "qr-code-data-url" RELATION_2MANY = "relation-to-many" RELATION_PATH = "relation-path" @@ -39,6 +41,8 @@ (FIELD, "Field"), (IMAGE_FILE, "Image file"), (IMAGE_DATAURL, "Image data url"), + (QR_CODE_FILE, "QR Code file"), + (QR_CODE_DATAURL, "QR Code data url"), (RELATION_2MANY, "Relation to many"), (RELATION_PATH, "Relation Path"), (CONSTANT, "Constant value"), @@ -165,6 +169,10 @@ conv = converter.relation(path.replace(".", "/"), conv) elif sub.converter == IMAGE_DATAURL: conv = converter.ImageDataURL(sub.value) + elif sub.converter == QR_CODE_FILE: + conv = converter.QRCodeFile(sub.value) + elif sub.converter == QR_CODE_DATAURL: + conv = converter.QRCodeDataURL(sub.value) elif sub.converter == RELATION_2MANY: # Unpack the result of finding a field with its sort order into # variable names. # HG changeset patch # User Axel Prel <axel.prel@xcg-consulting.fr> # Date 1742292689 -3600 # Tue Mar 18 11:11:29 2025 +0100 # Branch 18.0 # Node ID c7ee9e306253feb1cd74ab456955898d45f0187a # Parent b9af52c79fd6b6617b41bc54c2071ed91739d921 # EXP-Topic RED-549 make ruff happier diff --git a/models/redner_substitution.py b/models/redner_substitution.py --- a/models/redner_substitution.py +++ b/models/redner_substitution.py @@ -48,6 +48,7 @@ (CONSTANT, "Constant value"), ] + DYNAMIC_PLACEHOLDER_ALLOWED_CONVERTERS = ( FIELD, MAIL_TEMPLATE, @@ -142,6 +143,38 @@ ] ) + def _build_field(self, sub): + if "." in sub.value: + path, name = sub.value.rsplit(".", 1) + else: + path, name = None, sub.value + conv = converter.Field(name) + if path: + conv = converter.relation(path.replace(".", "/"), conv) + return conv + + def _build_image(self, sub): + if "." in sub.value: + path, name = sub.value.rsplit(".", 1) + else: + path, name = None, sub.value + conv = converter.ImageFile(name) + if path: + conv = converter.relation(path.replace(".", "/"), conv) + return conv + + def _build_rel_2_many(self, sub): + # Unpack the result of finding a field with its sort order into + # variable names. + value, sorted = parse_sorted_field(sub.value) + conv = converter.RelationToMany( + value, + None, + sortkey=sortkey(sorted) if sorted else None, + converter=sub.get_children().build_converter(), + ) + return conv + def build_converter(self): d = {} for sub in self: @@ -152,21 +185,9 @@ elif sub.converter == CONSTANT: conv = converter.Constant(sub.value) elif sub.converter == FIELD: - if "." in sub.value: - path, name = sub.value.rsplit(".", 1) - else: - path, name = None, sub.value - conv = converter.Field(name) - if path: - conv = converter.relation(path.replace(".", "/"), conv) + conv = self._build_field(sub) elif sub.converter == IMAGE_FILE: - if "." in sub.value: - path, name = sub.value.rsplit(".", 1) - else: - path, name = None, sub.value - conv = converter.ImageFile(name) - if path: - conv = converter.relation(path.replace(".", "/"), conv) + conv = self._build_image(sub) elif sub.converter == IMAGE_DATAURL: conv = converter.ImageDataURL(sub.value) elif sub.converter == QR_CODE_FILE: @@ -174,15 +195,7 @@ elif sub.converter == QR_CODE_DATAURL: conv = converter.QRCodeDataURL(sub.value) elif sub.converter == RELATION_2MANY: - # Unpack the result of finding a field with its sort order into - # variable names. - value, sorted = parse_sorted_field(sub.value) - conv = converter.RelationToMany( - value, - None, - sortkey=sortkey(sorted) if sorted else None, - converter=sub.get_children().build_converter(), - ) + conv = self._build_rel_2_many(sub) elif sub.converter == RELATION_PATH: conv = converter.relation( sub.value, sub.get_children().build_converter() # HG changeset patch # User Axel Prel <axel.prel@xcg-consulting.fr> # Date 1742292765 -3600 # Tue Mar 18 11:12:45 2025 +0100 # Branch 18.0 # Node ID 92dd238dff9f0b0f1ad88d597dcda09c4be1082f # Parent c7ee9e306253feb1cd74ab456955898d45f0187a # EXP-Topic RED-549 update NEWS diff --git a/NEWS.rst b/NEWS.rst --- a/NEWS.rst +++ b/NEWS.rst @@ -5,6 +5,11 @@ 18.0.1.7.0 ---------- +- add the QR Code converter to the substitution menu + +- remove the converter.py file containing the image converter + this converter has been move to the converter module + Add a redner menu in "Apps" containing: - the redner template management page (create, list, update, delete templates) - a configuration wizard allowing quick server configuration (apikey, account, url) # HG changeset patch # User Axel Prel <axel.prel@xcg-consulting.fr> # Date 1742465481 -3600 # Thu Mar 20 11:11:21 2025 +0100 # Branch 18.0 # Node ID 5bd4867eae5d4403d7eac177490920c33c9403cd # Parent 92dd238dff9f0b0f1ad88d597dcda09c4be1082f # EXP-Topic RED-549 set correct converter dependency in pyproject diff --git a/pyproject.toml b/pyproject.toml --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ "Framework :: Odoo :: 18.0", "License :: OSI Approved :: GNU Affero General Public License v3", ] -dependencies = ["odoo==18.0.*", "odoo-addon-converter >=18.0.4,<18.0.7"] +dependencies = ["odoo==18.0.*", "odoo-addon-converter >=18.0.6.1,<18.0.7"] [project.optional-dependencies] unixsocket = ["requests_unixsocket"]