diff --git a/NEWS.rst b/NEWS.rst index e27b5297cef42eceb72b355b5f12bd2d39da1f33_TkVXUy5yc3Q=..5bd4867eae5d4403d7eac177490920c33c9403cd_TkVXUy5yc3Q= 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -5,6 +5,17 @@ 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) +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 index e27b5297cef42eceb72b355b5f12bd2d39da1f33_X19tYW5pZmVzdF9fLnB5..5bd4867eae5d4403d7eac177490920c33c9403cd_X19tYW5pZmVzdF9fLnB5 100644 --- a/__manifest__.py +++ b/__manifest__.py @@ -29,5 +29,7 @@ # 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", @@ -32,5 +34,6 @@ "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/converter.py b/converter.py deleted file mode 100644 index e27b5297cef42eceb72b355b5f12bd2d39da1f33_Y29udmVydGVyLnB5..0000000000000000000000000000000000000000 --- 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/demo/users.xml b/demo/users.xml new file mode 100644 index 0000000000000000000000000000000000000000..5bd4867eae5d4403d7eac177490920c33c9403cd_ZGVtby91c2Vycy54bWw= --- /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/models/mail_template.py b/models/mail_template.py index e27b5297cef42eceb72b355b5f12bd2d39da1f33_bW9kZWxzL21haWxfdGVtcGxhdGUucHk=..5bd4867eae5d4403d7eac177490920c33c9403cd_bW9kZWxzL21haWxfdGVtcGxhdGUucHk= 100644 --- a/models/mail_template.py +++ b/models/mail_template.py @@ -22,5 +22,6 @@ 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] @@ -25,7 +26,5 @@ 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 index e27b5297cef42eceb72b355b5f12bd2d39da1f33_bW9kZWxzL3JlZG5lcl9zdWJzdGl0dXRpb24ucHk=..5bd4867eae5d4403d7eac177490920c33c9403cd_bW9kZWxzL3JlZG5lcl9zdWJzdGl0dXRpb24ucHk= 100644 --- 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" @@ -31,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" @@ -40,8 +41,10 @@ (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"), ] @@ -43,8 +46,9 @@ (RELATION_2MANY, "Relation to many"), (RELATION_PATH, "Relation Path"), (CONSTANT, "Constant value"), ] + DYNAMIC_PLACEHOLDER_ALLOWED_CONVERTERS = ( FIELD, MAIL_TEMPLATE, @@ -139,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: @@ -142,5 +178,5 @@ 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) @@ -146,3 +182,3 @@ conv = converter.MailTemplate(sub.value, False) - elif sub.converter == "mail_template+deserialize": + elif sub.converter == MAIL_TEMPLATE_DESERIALIZE: conv = converter.MailTemplate(sub.value, True) @@ -148,3 +184,3 @@ conv = converter.MailTemplate(sub.value, True) - elif sub.converter == "constant": + elif sub.converter == CONSTANT: conv = converter.Constant(sub.value) @@ -150,33 +186,17 @@ 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) - elif sub.converter == "image-file": - if "." in sub.value: - path, name = sub.value.rsplit(".", 1) - else: - path, name = None, sub.value - conv = ImageFile(name) - if path: - conv = converter.relation(path.replace(".", "/"), conv) - elif sub.converter == "image-data-url": - conv = ImageDataURL(sub.value) - elif sub.converter == "relation-to-many": - # 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(), - ) - elif sub.converter == "relation-path": + elif sub.converter == FIELD: + conv = self._build_field(sub) + elif sub.converter == IMAGE_FILE: + conv = self._build_image(sub) + 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: + conv = self._build_rel_2_many(sub) + elif sub.converter == RELATION_PATH: conv = converter.relation( sub.value, sub.get_children().build_converter() ) diff --git a/pyproject.toml b/pyproject.toml index e27b5297cef42eceb72b355b5f12bd2d39da1f33_cHlwcm9qZWN0LnRvbWw=..5bd4867eae5d4403d7eac177490920c33c9403cd_cHlwcm9qZWN0LnRvbWw= 100644 --- 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"] diff --git a/security/groups.xml b/security/groups.xml new file mode 100644 index 0000000000000000000000000000000000000000..5bd4867eae5d4403d7eac177490920c33c9403cd_c2VjdXJpdHkvZ3JvdXBzLnhtbA== --- /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 index e27b5297cef42eceb72b355b5f12bd2d39da1f33_c2VjdXJpdHkvaXIubW9kZWwuYWNjZXNzLmNzdg==..5bd4867eae5d4403d7eac177490920c33c9403cd_c2VjdXJpdHkvaXIubW9kZWwuYWNjZXNzLmNzdg== 100644 --- 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 +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 + +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 + +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 + +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 + +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 diff --git a/views/menu.xml b/views/menu.xml index e27b5297cef42eceb72b355b5f12bd2d39da1f33_dmlld3MvbWVudS54bWw=..5bd4867eae5d4403d7eac177490920c33c9403cd_dmlld3MvbWVudS54bWw= 100644 --- 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 index e27b5297cef42eceb72b355b5f12bd2d39da1f33_d2l6YXJkL19faW5pdF9fLnB5..5bd4867eae5d4403d7eac177490920c33c9403cd_d2l6YXJkL19faW5pdF9fLnB5 100644 --- 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 index 0000000000000000000000000000000000000000..5bd4867eae5d4403d7eac177490920c33c9403cd_d2l6YXJkL3JlZG5lcl9jb25maWd1cmF0b3Jfd2l6YXJkLnB5 --- /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 index 0000000000000000000000000000000000000000..5bd4867eae5d4403d7eac177490920c33c9403cd_d2l6YXJkL3JlZG5lcl9jb25maWd1cmF0b3Jfd2l6YXJkLnhtbA== --- /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>