Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion template_content_swapper/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
=====

Expand Down Expand Up @@ -88,7 +96,7 @@ Credits
Authors
~~~~~~~

* Quartile Limited
* Quartile

Contributors
~~~~~~~~~~~~
Expand Down
4 changes: 2 additions & 2 deletions template_content_swapper/__manifest__.py
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
107 changes: 87 additions & 20 deletions template_content_swapper/models/ir_qweb.py
Original file line number Diff line number Diff line change
@@ -1,51 +1,118 @@
# 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):
result = super()._render(template, values=values, **options)
if not isinstance(template, str):
return result
result_str = str(result)
lang_code = "en_US"
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)
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)
8 changes: 7 additions & 1 deletion template_content_swapper/models/template_content_mapping.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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")],
Expand All @@ -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",
Expand Down
8 changes: 8 additions & 0 deletions template_content_swapper/readme/CONFIGURE.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
20 changes: 12 additions & 8 deletions template_content_swapper/static/description/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@

/*
:Author: David Goodger ([email protected])
: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.
Expand Down Expand Up @@ -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 }
Expand All @@ -301,7 +300,7 @@
span.pre {
white-space: pre }

span.problematic, pre.problematic {
span.problematic {
color: red }

span.section-subtitle {
Expand Down Expand Up @@ -402,11 +401,18 @@ <h1><a class="toc-backref" href="#toc-entry-1">Configuration</a></h1>
Setting a report record will automatically update the template field.</li>
<li><strong>Template</strong> (required): The main QWeb template (ir.ui.view record) that includes the
string you’d like to replace.</li>
<li><strong>Domain</strong> (optional): Domain used to restrict the records this configuration
applies to. This option is only available for report configurations. Example:
[(‘partner_id’, ‘=’, 1)]</li>
<li><strong>Language</strong> (optional): Target language for string replacement. If left blank, the
replacement will be applied to all languages.</li>
<li><strong>Content From</strong> (required): An existing string to be replaced.</li>
<li><strong>Content To</strong> (optional): A new string to replace the existing string.</li>
</ul>
<p>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.</p>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#toc-entry-2">Usage</a></h1>
Expand All @@ -429,7 +435,7 @@ <h1><a class="toc-backref" href="#toc-entry-4">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#toc-entry-5">Authors</a></h2>
<ul class="simple">
<li>Quartile Limited</li>
<li>Quartile</li>
</ul>
</div>
<div class="section" id="contributors">
Expand All @@ -446,9 +452,7 @@ <h2><a class="toc-backref" href="#toc-entry-6">Contributors</a></h2>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#toc-entry-7">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org">
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
</a>
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
<p>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.</p>
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@
name="template_id"
attrs="{'readonly':[('report_id','!=',False)]}"
/>
<field name="report_model" invisible="1" />
<field
name="domain"
attrs="{'readonly':[('report_id','=',False)]}"
widget="domain"
options="{'model': 'report_model'}"
/>
<field
name="lang"
attrs="{'invisible':[('active_lang_count', '&lt;=', 1)]}"
Expand Down