diff --git a/template_content_swapper/README.rst b/template_content_swapper/README.rst new file mode 100644 index 0000000000..ed0115a6a3 --- /dev/null +++ b/template_content_swapper/README.rst @@ -0,0 +1,147 @@ +======================== +Template Content Swapper +======================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:08a67b31885d4a29f1ca6707cc1cff1f731e59ebe74c78a3183cbef924deb2eb + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--ux-lightgray.png?logo=github + :target: https://github.com/OCA/server-ux/tree/17.0/template_content_swapper + :alt: OCA/server-ux +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/server-ux-17-0/server-ux-17-0-template_content_swapper + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/server-ux&target_branch=17.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module offers a generic functionality for replacing QWeb view +elements. Typically, element replacements are conducted through a +template using an XPATH replacement by creating a new module. With this +module, users don't need to do this; they simply need to create +template.content.mapping records for the templates they wish to modify. + +Examples: + +- Replace 'Salesperson' label with 'Sales Representative' in the + quotation print. +- Replace 'Add to Cart' button with 'Add to Basket' in the eCommerce + product page. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +Go to *Settings > Technical > User Interface > Template Content +Mappings* to create/maintain records. + +Following fields should be filled in: + +- **Report** (optional): Report record that includes the string you'd + like to replace. Setting a report record will automatically update + the template field. +- **Template** (required): The main QWeb template (ir.ui.view record) + that includes the string you'd like to replace. +- **Domain** (optional): Domain used to restrict the records this + configuration applies to. This option is only available for report + configurations. Example: [('partner_id', '=', 1)] +- **Language** (optional): Target language for string replacement. If + left blank, the replacement will be applied to all languages. +- **Content From** (required): An existing string to be replaced. +- **Content To** (optional): A new string to replace the existing + string. + +As a limitation, domain-based configurations that change content outside +the article section (for example, header or footer content) only work +when printing a single record. When multiple records are printed in one +batch, those domain conditions are not applied to the header/footer and +only affect the article content. + +Usage +===== + +To use this module, first configure the template content mappings. + +|image| + +Then, go to the UI where your configured template is utilized. + +|image1| + +|image2| + +.. |image| image:: https://raw.githubusercontent.com/OCA/server-ux/17.0/template_content_swapper/static/img/mapping.png +.. |image1| image:: https://raw.githubusercontent.com/OCA/server-ux/17.0/template_content_swapper/static/img/login_before.png +.. |image2| image:: https://raw.githubusercontent.com/OCA/server-ux/17.0/template_content_swapper/static/img/login_after.png + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Quartile + +Contributors +------------ + +- `Quartile `__: + + - Aung Ko Ko Lin + - Yoshi Tashiro + - Tatsuki Kanda + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-yostashiro| image:: https://github.com/yostashiro.png?size=40px + :target: https://github.com/yostashiro + :alt: yostashiro +.. |maintainer-AungKoKoLin1997| image:: https://github.com/AungKoKoLin1997.png?size=40px + :target: https://github.com/AungKoKoLin1997 + :alt: AungKoKoLin1997 + +Current `maintainers `__: + +|maintainer-yostashiro| |maintainer-AungKoKoLin1997| + +This module is part of the `OCA/server-ux `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/template_content_swapper/__init__.py b/template_content_swapper/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/template_content_swapper/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/template_content_swapper/__manifest__.py b/template_content_swapper/__manifest__.py new file mode 100644 index 0000000000..56b4a0973b --- /dev/null +++ b/template_content_swapper/__manifest__.py @@ -0,0 +1,17 @@ +# Copyright 2024 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Template Content Swapper", + "version": "17.0.1.0.0", + "author": "Quartile, Odoo Community Association (OCA)", + "license": "AGPL-3", + "category": "Tools", + "website": "https://github.com/OCA/server-ux", + "depends": ["base"], + "data": [ + "security/ir.model.access.csv", + "views/template_content_mapping_views.xml", + ], + "maintainers": ["yostashiro", "AungKoKoLin1997"], + "installable": True, +} diff --git a/template_content_swapper/i18n/it.po b/template_content_swapper/i18n/it.po new file mode 100644 index 0000000000..62659acc20 --- /dev/null +++ b/template_content_swapper/i18n/it.po @@ -0,0 +1,145 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * template_content_swapper +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2025-05-16 08:24+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.10.4\n" + +#. module: template_content_swapper +#: model:ir.model.fields,field_description:template_content_swapper.field_template_content_mapping__active_lang_count +msgid "Active Lang Count" +msgstr "Numero lingue attive" + +#. module: template_content_swapper +#: model:ir.model.fields,field_description:template_content_swapper.field_template_content_mapping__content_from +msgid "Content From" +msgstr "Contenuto file" + +#. module: template_content_swapper +#: model:ir.model.fields,field_description:template_content_swapper.field_template_content_mapping__content_to +msgid "Content To" +msgstr "Contenuto a" + +#. module: template_content_swapper +#: model:ir.model.fields,field_description:template_content_swapper.field_template_content_mapping__create_uid +msgid "Created by" +msgstr "Creato da" + +#. module: template_content_swapper +#: model:ir.model.fields,field_description:template_content_swapper.field_template_content_mapping__create_date +msgid "Created on" +msgstr "Creato il" + +#. module: template_content_swapper +#: model:ir.model.fields,field_description:template_content_swapper.field_template_content_mapping__display_name +msgid "Display Name" +msgstr "Nome visualizzato" + +#. module: template_content_swapper +#: model_terms:ir.ui.view,arch_db:template_content_swapper.view_template_content_mapping_search +msgid "Frontend" +msgstr "Frontend" + +#. module: template_content_swapper +#: model_terms:ir.ui.view,arch_db:template_content_swapper.view_template_content_mapping_search +msgid "Group By" +msgstr "Raggruppa per" + +#. module: template_content_swapper +#: model:ir.model.fields,field_description:template_content_swapper.field_template_content_mapping__id +msgid "ID" +msgstr "ID" + +#. module: template_content_swapper +#: model:ir.model.fields,help:template_content_swapper.field_template_content_mapping__lang +msgid "" +"If no language is selected, the mapping will be applied to all languages." +msgstr "" +"Se non è selezionata una lingua, la mappatura verrà applicata a tutte le " +"lingue." + +#. module: template_content_swapper +#: model:ir.model.fields,field_description:template_content_swapper.field_template_content_mapping__lang +#: model_terms:ir.ui.view,arch_db:template_content_swapper.view_template_content_mapping_search +msgid "Language" +msgstr "Lingua" + +#. module: template_content_swapper +#: model:ir.model.fields,field_description:template_content_swapper.field_template_content_mapping____last_update +msgid "Last Modified on" +msgstr "Ultima modifica il" + +#. module: template_content_swapper +#: model:ir.model.fields,field_description:template_content_swapper.field_template_content_mapping__write_uid +msgid "Last Updated by" +msgstr "Ultimo aggiornamento di" + +#. module: template_content_swapper +#: model:ir.model.fields,field_description:template_content_swapper.field_template_content_mapping__write_date +msgid "Last Updated on" +msgstr "Ultimo aggiornamento il" + +#. module: template_content_swapper +#: model:ir.model.fields,field_description:template_content_swapper.field_template_content_mapping__name +msgid "Name" +msgstr "Nome" + +#. module: template_content_swapper +#: model:ir.model,name:template_content_swapper.model_ir_qweb +msgid "Qweb" +msgstr "Qweb" + +#. module: template_content_swapper +#: model:ir.model.fields,field_description:template_content_swapper.field_template_content_mapping__report_id +#: model_terms:ir.ui.view,arch_db:template_content_swapper.view_template_content_mapping_search +msgid "Report" +msgstr "Resoconto" + +#. module: template_content_swapper +#: model:ir.model.fields,help:template_content_swapper.field_template_content_mapping__template_id +msgid "Select the main template of the report / frontend page to be modified." +msgstr "" +"Selezionare il modello principale del resoconto / pagina frontend da " +"modificare." + +#. module: template_content_swapper +#: model:ir.model.fields,help:template_content_swapper.field_template_content_mapping__content_from +msgid "Set the content (string) to be replaced. e.g. 'Salesperson'." +msgstr "Impostare il contenuto (stringa) da sostituire. Es. 'Venditore'." + +#. module: template_content_swapper +#: model:ir.model.fields,help:template_content_swapper.field_template_content_mapping__content_to +msgid "Set your new content (string). e.g. 'Sales Representative'." +msgstr "Impostare il nuovo contenuto (stringa). Es. 'Rappresentante'." + +#. module: template_content_swapper +#: model:ir.model.fields,field_description:template_content_swapper.field_template_content_mapping__template_id +#: model_terms:ir.ui.view,arch_db:template_content_swapper.view_template_content_mapping_search +msgid "Template" +msgstr "Modello" + +#. module: template_content_swapper +#: model:ir.model,name:template_content_swapper.model_template_content_mapping +msgid "Template Content Mapping" +msgstr "Modello mappatura contenuto" + +#. module: template_content_swapper +#: model:ir.actions.act_window,name:template_content_swapper.action_template_content_mapping +#: model:ir.ui.menu,name:template_content_swapper.menu_template_content_mapping +#: model_terms:ir.ui.view,arch_db:template_content_swapper.view_template_content_mapping_search +msgid "Template Content Mappings" +msgstr "Modello mappature contenuto" + +#~ msgid "View" +#~ msgstr "Vista" diff --git a/template_content_swapper/i18n/template_content_swapper.pot b/template_content_swapper/i18n/template_content_swapper.pot new file mode 100644 index 0000000000..426d196ea7 --- /dev/null +++ b/template_content_swapper/i18n/template_content_swapper.pot @@ -0,0 +1,129 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * template_content_swapper +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: template_content_swapper +#: model:ir.model.fields,field_description:template_content_swapper.field_template_content_mapping__content_from +msgid "Content From" +msgstr "" + +#. module: template_content_swapper +#: model:ir.model.fields,field_description:template_content_swapper.field_template_content_mapping__content_to +msgid "Content To" +msgstr "" + +#. module: template_content_swapper +#: model:ir.model.fields,field_description:template_content_swapper.field_template_content_mapping__create_uid +msgid "Created by" +msgstr "" + +#. module: template_content_swapper +#: model:ir.model.fields,field_description:template_content_swapper.field_template_content_mapping__create_date +msgid "Created on" +msgstr "" + +#. module: template_content_swapper +#: model:ir.model.fields,field_description:template_content_swapper.field_template_content_mapping__display_name +msgid "Display Name" +msgstr "" + +#. module: template_content_swapper +#: model_terms:ir.ui.view,arch_db:template_content_swapper.view_template_content_mapping_search +msgid "Frontend" +msgstr "" + +#. module: template_content_swapper +#: model_terms:ir.ui.view,arch_db:template_content_swapper.view_template_content_mapping_search +msgid "Group By" +msgstr "" + +#. module: template_content_swapper +#: model:ir.model.fields,field_description:template_content_swapper.field_template_content_mapping__id +msgid "ID" +msgstr "" + +#. module: template_content_swapper +#: model:ir.model.fields,help:template_content_swapper.field_template_content_mapping__lang +msgid "" +"If no language is selected, the mapping will be applied to all languages." +msgstr "" + +#. module: template_content_swapper +#: model:ir.model.fields,field_description:template_content_swapper.field_template_content_mapping__lang +#: model_terms:ir.ui.view,arch_db:template_content_swapper.view_template_content_mapping_search +msgid "Language" +msgstr "" + +#. module: template_content_swapper +#: model:ir.model.fields,field_description:template_content_swapper.field_template_content_mapping__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: template_content_swapper +#: model:ir.model.fields,field_description:template_content_swapper.field_template_content_mapping__write_date +msgid "Last Updated on" +msgstr "" + +#. module: template_content_swapper +#: model:ir.model.fields,field_description:template_content_swapper.field_template_content_mapping__name +msgid "Name" +msgstr "" + +#. module: template_content_swapper +#: model:ir.actions.server,name:template_content_swapper.action_open_template_mapping_dynamic +msgid "Open Template Mapping" +msgstr "" + +#. module: template_content_swapper +#: model:ir.model,name:template_content_swapper.model_ir_qweb +msgid "Qweb" +msgstr "" + +#. module: template_content_swapper +#: model:ir.model.fields,field_description:template_content_swapper.field_template_content_mapping__report_id +#: model_terms:ir.ui.view,arch_db:template_content_swapper.view_template_content_mapping_search +msgid "Report" +msgstr "" + +#. module: template_content_swapper +#: model:ir.model.fields,help:template_content_swapper.field_template_content_mapping__template_id +msgid "Select the main template of the report / frontend page to be modified." +msgstr "" + +#. module: template_content_swapper +#: model:ir.model.fields,help:template_content_swapper.field_template_content_mapping__content_from +msgid "Set the content (string) to be replaced. e.g. 'Salesperson'." +msgstr "" + +#. module: template_content_swapper +#: model:ir.model.fields,help:template_content_swapper.field_template_content_mapping__content_to +msgid "Set your new content (string). e.g. 'Sales Representative'." +msgstr "" + +#. module: template_content_swapper +#: model:ir.model.fields,field_description:template_content_swapper.field_template_content_mapping__template_id +#: model_terms:ir.ui.view,arch_db:template_content_swapper.view_template_content_mapping_search +msgid "Template" +msgstr "" + +#. module: template_content_swapper +#: model:ir.model,name:template_content_swapper.model_template_content_mapping +msgid "Template Content Mapping" +msgstr "" + +#. module: template_content_swapper +#: model:ir.ui.menu,name:template_content_swapper.menu_template_content_mapping +#: model_terms:ir.ui.view,arch_db:template_content_swapper.view_template_content_mapping_search +msgid "Template Content Mappings" +msgstr "" diff --git a/template_content_swapper/models/__init__.py b/template_content_swapper/models/__init__.py new file mode 100644 index 0000000000..07dffcd23a --- /dev/null +++ b/template_content_swapper/models/__init__.py @@ -0,0 +1,2 @@ +from . import ir_qweb +from . import template_content_mapping diff --git a/template_content_swapper/models/ir_qweb.py b/template_content_swapper/models/ir_qweb.py new file mode 100644 index 0000000000..be5ac7f8ce --- /dev/null +++ b/template_content_swapper/models/ir_qweb.py @@ -0,0 +1,119 @@ +# Copyright 2024 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import logging +import re + +from lxml import html +from markupsafe import Markup + +from odoo import api, models +from odoo.tools.profiler import QwebTracker +from odoo.tools.safe_eval import safe_eval + +_logger = logging.getLogger(__name__) + +ARTICLE_XPATH = '//div[contains(@class, "article") and @data-oe-model and @data-oe-id]' + + +class IrQWeb(models.AbstractModel): + _inherit = "ir.qweb" + + def _apply_mappings(self, html_str, mappings, model_name=None, res_id=None): + """Apply mappings to HTML string, optionally filtering by record domain.""" + for m in mappings: + if m.domain: + if m.report_model and m.report_model != model_name: + continue + if not self._record_matches_domain(model_name, res_id, m.domain): + continue + html_str = html_str.replace(m.content_from, m.content_to or "") + return html_str + + def _record_matches_domain(self, model_name, res_id, domain_str): + """Check if record (model_name, res_id) matches the given domain.""" + try: + dom = safe_eval(domain_str) + except Exception: + _logger.warning( + "Invalid domain on template.content.mapping for %s,%s: %s", + model_name, + res_id, + domain_str, + ) + return False + return bool(self.env[model_name].search_count([("id", "=", res_id)] + dom)) + + def _apply_mappings_on_articles(self, articles, domain_mappings): + """Apply domain mappings per article block for multi-record renders.""" + for article in articles: + article_html = html.tostring(article, encoding="unicode") + new_html = self._apply_mappings( + article_html, + domain_mappings, + article.get("data-oe-model"), + int(article.get("data-oe-id")), + ) + if new_html != article_html: + try: + article.getparent().replace(article, html.fromstring(new_html)) + except Exception: + _logger.exception( + "Failed to replace article HTML for %s,%s", + article.get("data-oe-model"), + article.get("data-oe-id"), + ) + + @QwebTracker.wrap_render + @api.model + def _render(self, template, values=None, **options): + result = super()._render(template, values=values, **options) + values = values or {} + if not isinstance(template, str): + return result + result_str = str(result) + request = values.get("request") + if request: + # For views + lang_code = request.env.lang + else: + # For reports + lang_match = re.search(r'data-oe-lang="([^"]+)"', result_str) + lang_code = lang_match.group(1) if lang_match else "en_US" + view = self.env["ir.ui.view"]._get(template) + mappings = ( + self.env["template.content.mapping"] + .sudo() + .search([("template_id", "=", view.id), ("lang", "in", [lang_code, False])]) + ) + if not mappings: + return result + global_mappings = [m for m in mappings if not m.domain] + domain_mappings = [m for m in mappings if m.domain] + result_str = self._apply_mappings(result_str, global_mappings) + if not domain_mappings: + return Markup(result_str) + try: + root = html.fromstring(result_str) + except Exception: + _logger.warning( + "Failed to parse HTML for template %s, skipping domain-based mappings.", + template, + ) + return Markup(result_str) + articles = root.xpath(ARTICLE_XPATH) + if not articles: + return Markup(result_str) + if len(articles) == 1: + # Single record → domain mappings can be applied globally + article = articles[0] + result_str = self._apply_mappings( + result_str, + domain_mappings, + article.get("data-oe-model"), + int(article.get("data-oe-id")), + ) + return Markup(result_str) + self._apply_mappings_on_articles(articles, domain_mappings) + final_html = html.tostring(root, encoding="unicode") + return Markup(final_html) diff --git a/template_content_swapper/models/template_content_mapping.py b/template_content_swapper/models/template_content_mapping.py new file mode 100644 index 0000000000..2f84be73f5 --- /dev/null +++ b/template_content_swapper/models/template_content_mapping.py @@ -0,0 +1,72 @@ +# Copyright 2024 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class TemplateContentMapping(models.Model): + _name = "template.content.mapping" + _description = "Template Content Mapping" + _order = "template_id, content_from" + + @api.model + def _lang_get(self): + return self.env["res.lang"].get_installed() + + name = fields.Char(compute="_compute_name", store=True, readonly=True) + report_id = fields.Many2one("ir.actions.report") + report_model = fields.Char(related="report_id.model") + template_id = fields.Many2one( + "ir.ui.view", + domain=[("type", "=", "qweb")], + required=True, + compute="_compute_template_id", + store=True, + readonly=False, + precompute=True, + help="Select the main template of the report / frontend page to be modified.", + ) + domain = fields.Char( + help="Optional domain on the report records. The mapping is applied " + "only if the record in the report matches this domain. " + "Example: [('partner_id', '=', 1)]", + ) + lang = fields.Selection( + _lang_get, + string="Language", + default=lambda self: self.env.lang, + help="If no language is selected, the mapping will be applied to all " + "languages.", + ) + content_from = fields.Char( + required=True, + help="Set the content (string) to be replaced. e.g. 'Salesperson'.", + ) + content_to = fields.Char( + help="Set your new content (string). e.g. 'Sales Representative'.", + ) + + @api.depends("content_from", "content_to") + def _compute_name(self): + for record in self: + record.name = False + if record.content_from: + record.name = f"{record.content_from} -> {record.content_to or ''}" + + @api.depends("report_id") + def _compute_template_id(self): + for rec in self: + rec.template_id = False + if rec.report_id: + report_name = rec.report_id.report_name + rec.template_id = self.env["ir.ui.view"]._get(report_name).sudo() + + def open_template_mapping(self): + multi_lang = len(self.env["res.lang"].get_installed()) > 1 + return { + "type": "ir.actions.act_window", + "name": "Template Content Mappings", + "res_model": "template.content.mapping", + "view_mode": "list", + "context": {"multi_lang": multi_lang}, + } diff --git a/template_content_swapper/pyproject.toml b/template_content_swapper/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/template_content_swapper/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/template_content_swapper/readme/CONFIGURE.md b/template_content_swapper/readme/CONFIGURE.md new file mode 100644 index 0000000000..7475d633b4 --- /dev/null +++ b/template_content_swapper/readme/CONFIGURE.md @@ -0,0 +1,21 @@ +Go to *Settings > Technical > User Interface > Template Content Mappings* to +create/maintain records. + +Following fields should be filled in: + +* **Report** (optional): Report record that includes the string you'd like to replace. + Setting a report record will automatically update the template field. +* **Template** (required): The main QWeb template (ir.ui.view record) that includes the + string you'd like to replace. +* **Domain** (optional): Domain used to restrict the records this configuration + applies to. This option is only available for report configurations. Example: + [('partner_id', '=', 1)] +* **Language** (optional): Target language for string replacement. If left blank, the + replacement will be applied to all languages. +* **Content From** (required): An existing string to be replaced. +* **Content To** (optional): A new string to replace the existing string. + +As a limitation, domain-based configurations that change content outside the article +section (for example, header or footer content) only work when printing a single +record. When multiple records are printed in one batch, those domain conditions are +not applied to the header/footer and only affect the article content. diff --git a/template_content_swapper/readme/CONTRIBUTORS.md b/template_content_swapper/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..d79dc41e42 --- /dev/null +++ b/template_content_swapper/readme/CONTRIBUTORS.md @@ -0,0 +1,4 @@ +- [Quartile](https://www.quartile.co): + - Aung Ko Ko Lin + - Yoshi Tashiro + - Tatsuki Kanda diff --git a/template_content_swapper/readme/DESCRIPTION.md b/template_content_swapper/readme/DESCRIPTION.md new file mode 100644 index 0000000000..b1b8c0122e --- /dev/null +++ b/template_content_swapper/readme/DESCRIPTION.md @@ -0,0 +1,12 @@ +This module offers a generic functionality for replacing QWeb view +elements. Typically, element replacements are conducted through a +template using an XPATH replacement by creating a new module. With this +module, users don't need to do this; they simply need to create +template.content.mapping records for the templates they wish to modify. + +Examples: + +- Replace 'Salesperson' label with 'Sales Representative' in the + quotation print. +- Replace 'Add to Cart' button with 'Add to Basket' in the eCommerce + product page. diff --git a/template_content_swapper/readme/USAGE.md b/template_content_swapper/readme/USAGE.md new file mode 100644 index 0000000000..2c2be2c346 --- /dev/null +++ b/template_content_swapper/readme/USAGE.md @@ -0,0 +1,9 @@ +To use this module, first configure the template content mappings. + +![image](../static/img/mapping.png) + +Then, go to the UI where your configured template is utilized. + +![image](../static/img/login_before.png) + +![image](../static/img/login_after.png) diff --git a/template_content_swapper/security/ir.model.access.csv b/template_content_swapper/security/ir.model.access.csv new file mode 100644 index 0000000000..762d73da27 --- /dev/null +++ b/template_content_swapper/security/ir.model.access.csv @@ -0,0 +1,5 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_template_content_mapping_public,template_content_mapping_public,model_template_content_mapping,base.group_public,1,0,0,0 +access_template_content_mapping_portal,template_content_mapping_portal,model_template_content_mapping,base.group_portal,1,0,0,0 +access_template_content_mapping_user,template_content_mapping_user,model_template_content_mapping,base.group_user,1,0,0,0 +access_template_content_mapping_system,template_content_mapping_system,model_template_content_mapping,base.group_system,1,1,1,1 diff --git a/template_content_swapper/static/description/icon.png b/template_content_swapper/static/description/icon.png new file mode 100644 index 0000000000..3a0328b516 Binary files /dev/null and b/template_content_swapper/static/description/icon.png differ diff --git a/template_content_swapper/static/description/index.html b/template_content_swapper/static/description/index.html new file mode 100644 index 0000000000..dba2406a75 --- /dev/null +++ b/template_content_swapper/static/description/index.html @@ -0,0 +1,477 @@ + + + + + +Template Content Swapper + + + +
+

Template Content Swapper

+ + +

Beta License: AGPL-3 OCA/server-ux Translate me on Weblate Try me on Runboat

+

This module offers a generic functionality for replacing QWeb view +elements. Typically, element replacements are conducted through a +template using an XPATH replacement by creating a new module. With this +module, users don’t need to do this; they simply need to create +template.content.mapping records for the templates they wish to modify.

+

Examples:

+
    +
  • Replace ‘Salesperson’ label with ‘Sales Representative’ in the +quotation print.
  • +
  • Replace ‘Add to Cart’ button with ‘Add to Basket’ in the eCommerce +product page.
  • +
+

Table of contents

+ +
+

Configuration

+

Go to Settings > Technical > User Interface > Template Content +Mappings to create/maintain records.

+

Following fields should be filled in:

+
    +
  • Report (optional): Report record that includes the string you’d +like to replace. Setting a report record will automatically update +the template field.
  • +
  • Template (required): The main QWeb template (ir.ui.view record) +that includes the string you’d like to replace.
  • +
  • Domain (optional): Domain used to restrict the records this +configuration applies to. This option is only available for report +configurations. Example: [(‘partner_id’, ‘=’, 1)]
  • +
  • Language (optional): Target language for string replacement. If +left blank, the replacement will be applied to all languages.
  • +
  • Content From (required): An existing string to be replaced.
  • +
  • Content To (optional): A new string to replace the existing +string.
  • +
+

As a limitation, domain-based configurations that change content outside +the article section (for example, header or footer content) only work +when printing a single record. When multiple records are printed in one +batch, those domain conditions are not applied to the header/footer and +only affect the article content.

+
+
+

Usage

+

To use this module, first configure the template content mappings.

+

image

+

Then, go to the UI where your configured template is utilized.

+

image1

+

image2

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Quartile
  • +
+
+
+

Contributors

+
    +
  • Quartile:
      +
    • Aung Ko Ko Lin
    • +
    • Yoshi Tashiro
    • +
    • Tatsuki Kanda
    • +
    +
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainers:

+

yostashiro AungKoKoLin1997

+

This module is part of the OCA/server-ux project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/template_content_swapper/static/img/login_after.png b/template_content_swapper/static/img/login_after.png new file mode 100644 index 0000000000..52ad45dad7 Binary files /dev/null and b/template_content_swapper/static/img/login_after.png differ diff --git a/template_content_swapper/static/img/login_before.png b/template_content_swapper/static/img/login_before.png new file mode 100644 index 0000000000..391fc23c6e Binary files /dev/null and b/template_content_swapper/static/img/login_before.png differ diff --git a/template_content_swapper/static/img/mapping.png b/template_content_swapper/static/img/mapping.png new file mode 100644 index 0000000000..f60a8d13ab Binary files /dev/null and b/template_content_swapper/static/img/mapping.png differ diff --git a/template_content_swapper/tests/__init__.py b/template_content_swapper/tests/__init__.py new file mode 100644 index 0000000000..4d75ab6b16 --- /dev/null +++ b/template_content_swapper/tests/__init__.py @@ -0,0 +1 @@ +from . import test_template_content_swapper diff --git a/template_content_swapper/tests/test_template_content_swapper.py b/template_content_swapper/tests/test_template_content_swapper.py new file mode 100644 index 0000000000..642e8bdf75 --- /dev/null +++ b/template_content_swapper/tests/test_template_content_swapper.py @@ -0,0 +1,52 @@ +# Copyright 2024 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo.tests.common import TransactionCase + + +class TestTemplateStringSwapper(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.view_obj = cls.env["ir.ui.view"] + ja = ( + cls.env["res.lang"] + .with_context(active_test=False) + .search([("code", "=", "ja_JP")]) + ) + cls.env["base.language.install"].create({"lang_ids": ja.ids}).lang_install() + + def test_template_string_swapper(self): + template = "web.external_layout" + view = self.view_obj._get(template).sudo() + values = {"company": self.env.company, "report_type": "pdf", "o": view} + result = self.view_obj._render_template(template, values) + self.assertTrue("Page" in str(result)) + self.env["template.content.mapping"].create( + { + "template_id": view.id, + "content_from": "Page", + "content_to": "Slide", + "lang": "en_US", + } + ) + result = self.view_obj._render_template(template, values) + self.assertFalse("Page" in str(result)) + self.assertTrue("Slide" in str(result)) + # Switch the language to Japanese + view_obj = self.view_obj.with_context(lang="ja_JP") + view = view_obj.browse(view.id) + values = {"company": self.env.company, "report_type": "pdf", "o": view} + result = view_obj._render_template(template, values) + self.assertTrue("ページ" in str(result)) + self.env["template.content.mapping"].create( + { + "template_id": view.id, + "content_from": "ページ", + "content_to": "スライド", + "lang": "ja_JP", + } + ) + result = view_obj._render_template(template, values) + self.assertFalse("ページ" in str(result)) + self.assertTrue("スライド" in str(result)) diff --git a/template_content_swapper/views/template_content_mapping_views.xml b/template_content_swapper/views/template_content_mapping_views.xml new file mode 100644 index 0000000000..715a0e8a78 --- /dev/null +++ b/template_content_swapper/views/template_content_mapping_views.xml @@ -0,0 +1,81 @@ + + + + template.content.mapping.tree + template.content.mapping + + + + + + + + + + + + + + + template.content.mapping.search + template.content.mapping + + + + + + + + + + + + + + + + + + + + Open Template Mapping + + code + action = model.open_template_mapping() + + +