diff --git a/template_content_swapper/README.rst b/template_content_swapper/README.rst index 861e833866..6a62d317e4 100644 --- a/template_content_swapper/README.rst +++ b/template_content_swapper/README.rst @@ -54,11 +54,19 @@ Following fields should be filled in: 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 ===== @@ -88,7 +96,7 @@ Credits Authors ~~~~~~~ -* Quartile Limited +* Quartile Contributors ~~~~~~~~~~~~ diff --git a/template_content_swapper/__manifest__.py b/template_content_swapper/__manifest__.py index eed770b9d6..d6589f086a 100644 --- a/template_content_swapper/__manifest__.py +++ b/template_content_swapper/__manifest__.py @@ -1,9 +1,9 @@ -# Copyright 2024 Quartile Limited +# Copyright 2024 Quartile (https://www.quartile.co) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). { "name": "Template Content Swapper", "version": "16.0.1.1.0", - "author": "Quartile Limited, Odoo Community Association (OCA)", + "author": "Quartile, Odoo Community Association (OCA)", "license": "AGPL-3", "category": "Tools", "website": "https://github.com/OCA/server-ux", diff --git a/template_content_swapper/models/ir_qweb.py b/template_content_swapper/models/ir_qweb.py index 65726586ca..940f4ecb27 100644 --- a/template_content_swapper/models/ir_qweb.py +++ b/template_content_swapper/models/ir_qweb.py @@ -1,17 +1,69 @@ -# Copyright 2024 Quartile Limited +# 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): @@ -19,7 +71,6 @@ def _render(self, template, values=None, **options): if not isinstance(template, str): return result result_str = str(result) - lang_code = "en_US" request = values.get("request") if request: # For views @@ -27,25 +78,41 @@ def _render(self, template, values=None, **options): else: # For reports lang_match = re.search(r'data-oe-lang="([^"]+)"', result_str) - if lang_match: - lang_code = lang_match.group(1) + lang_code = lang_match.group(1) if lang_match else "en_US" view = self.env["ir.ui.view"]._get(template) - content_mappings = ( + mappings = ( self.env["template.content.mapping"] .sudo() - .search( - [ - ("template_id", "=", view.id), - "|", - ("lang", "=", lang_code), - ("lang", "=", False), - ] - ) + .search([("template_id", "=", view.id), ("lang", "in", [lang_code, False])]) ) - if content_mappings: - for mapping in content_mappings: - content_from = mapping.content_from - content_to = mapping.content_to or "" - result_str = result_str.replace(content_from, content_to) - result = Markup(result_str) - return result + 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 index 92672965d8..c5f2f6c3bd 100644 --- a/template_content_swapper/models/template_content_mapping.py +++ b/template_content_swapper/models/template_content_mapping.py @@ -1,4 +1,4 @@ -# Copyright 2024 Quartile Limited +# 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 @@ -15,6 +15,7 @@ def _lang_get(self): 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")], @@ -25,6 +26,11 @@ def _lang_get(self): 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", diff --git a/template_content_swapper/readme/CONFIGURE.rst b/template_content_swapper/readme/CONFIGURE.rst index 5c62e9754c..7475d633b4 100644 --- a/template_content_swapper/readme/CONFIGURE.rst +++ b/template_content_swapper/readme/CONFIGURE.rst @@ -7,7 +7,15 @@ Following fields should be filled in: 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/static/description/index.html b/template_content_swapper/static/description/index.html index 199c708cdf..1a2c21e730 100644 --- a/template_content_swapper/static/description/index.html +++ b/template_content_swapper/static/description/index.html @@ -8,11 +8,10 @@ /* :Author: David Goodger (goodger@python.org) -:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $ +:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $ :Copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. -Despite the name, some widely supported CSS2 features are used. See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to customize this style sheet. @@ -275,7 +274,7 @@ margin-left: 2em ; margin-right: 2em } -pre.code .ln { color: gray; } /* line numbers */ +pre.code .ln { color: grey; } /* line numbers */ pre.code, code { background-color: #eeeeee } pre.code .comment, code .comment { color: #5C6576 } pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } @@ -301,7 +300,7 @@ span.pre { white-space: pre } -span.problematic, pre.problematic { +span.problematic { color: red } span.section-subtitle { @@ -402,11 +401,18 @@

Configuration

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

    @@ -429,7 +435,7 @@

    Credits

    Authors

    @@ -446,9 +452,7 @@

    Contributors

    Maintainers

    This module is maintained by the OCA.

    - -Odoo Community Association - +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.

    diff --git a/template_content_swapper/tests/test_template_content_swapper.py b/template_content_swapper/tests/test_template_content_swapper.py index 4957e4753a..6dc2ae2920 100644 --- a/template_content_swapper/tests/test_template_content_swapper.py +++ b/template_content_swapper/tests/test_template_content_swapper.py @@ -1,4 +1,4 @@ -# Copyright 2024 Quartile Limited +# 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 diff --git a/template_content_swapper/views/template_content_mapping_views.xml b/template_content_swapper/views/template_content_mapping_views.xml index 0936ae60a3..759e6c8e69 100644 --- a/template_content_swapper/views/template_content_mapping_views.xml +++ b/template_content_swapper/views/template_content_mapping_views.xml @@ -11,6 +11,13 @@ name="template_id" attrs="{'readonly':[('report_id','!=',False)]}" /> + +