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
51 changes: 28 additions & 23 deletions template_content_swapper/README.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
.. image:: https://odoo-community.org/readme-banner-image
:target: https://odoo-community.org/get-involved?utm_source=readme
:alt: Odoo Community Association

========================
Template Content Swapper
========================
Expand All @@ -17,7 +13,7 @@ Template Content Swapper
.. |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/license-AGPL--3-blue.png
.. |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
Expand All @@ -40,10 +36,10 @@ 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.
- 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**

Expand All @@ -58,16 +54,25 @@ 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.
- **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.
- **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
=====
Expand Down Expand Up @@ -107,11 +112,11 @@ Authors
Contributors
------------

- `Quartile <https://www.quartile.co>`__:
- `Quartile <https://www.quartile.co>`__:

- Aung Ko Ko Lin
- Yoshi Tashiro
- Tatsuki Kanda
- Aung Ko Ko Lin
- Yoshi Tashiro
- Tatsuki Kanda

Maintainers
-----------
Expand Down
105 changes: 86 additions & 19 deletions template_content_swapper/models/ir_qweb.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,69 @@
# 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):
Expand All @@ -20,33 +72,48 @@ 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
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)
6 changes: 6 additions & 0 deletions template_content_swapper/models/template_content_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
30 changes: 18 additions & 12 deletions template_content_swapper/readme/CONFIGURE.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
Go to *Settings \> Technical \> User Interface \> Template Content
Mappings* to create/maintain records.
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.
- **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.
* **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.
40 changes: 21 additions & 19 deletions template_content_swapper/static/description/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>README.rst</title>
<title>Template Content Swapper</title>
<style type="text/css">

/*
Expand Down Expand Up @@ -360,21 +360,16 @@
</style>
</head>
<body>
<div class="document">
<div class="document" id="template-content-swapper">
<h1 class="title">Template Content Swapper</h1>


<a class="reference external image-reference" href="https://odoo-community.org/get-involved?utm_source=readme">
<img alt="Odoo Community Association" src="https://odoo-community.org/readme-banner-image" />
</a>
<div class="section" id="template-content-swapper">
<h1>Template Content Swapper</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:08a67b31885d4a29f1ca6707cc1cff1f731e59ebe74c78a3183cbef924deb2eb
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/license-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/server-ux/tree/18.0/template_content_swapper"><img alt="OCA/server-ux" src="https://img.shields.io/badge/github-OCA%2Fserver--ux-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/server-ux-18-0/server-ux-18-0-template_content_swapper"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/server-ux&amp;target_branch=18.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/server-ux/tree/18.0/template_content_swapper"><img alt="OCA/server-ux" src="https://img.shields.io/badge/github-OCA%2Fserver--ux-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/server-ux-18-0/server-ux-18-0-template_content_swapper"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/server-ux&amp;target_branch=18.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>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
Expand Down Expand Up @@ -402,49 +397,57 @@ <h1>Template Content Swapper</h1>
</ul>
</div>
<div class="section" id="configuration">
<h2><a class="toc-backref" href="#toc-entry-1">Configuration</a></h2>
<h1><a class="toc-backref" href="#toc-entry-1">Configuration</a></h1>
<p>Go to <em>Settings &gt; Technical &gt; User Interface &gt; Template Content
Mappings</em> to create/maintain records.</p>
<p>Following fields should be filled in:</p>
<ul class="simple">
<li><strong>Report</strong> (optional): Report record that includes the string you’d
like to replace. Setting a report record will automatically update the
template field.</li>
like to replace. 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">
<h2><a class="toc-backref" href="#toc-entry-2">Usage</a></h2>
<h1><a class="toc-backref" href="#toc-entry-2">Usage</a></h1>
<p>To use this module, first configure the template content mappings.</p>
<p><img alt="image" src="https://raw.githubusercontent.com/OCA/server-ux/18.0/template_content_swapper/static/img/mapping.png" /></p>
<p>Then, go to the UI where your configured template is utilized.</p>
<p><img alt="image1" src="https://raw.githubusercontent.com/OCA/server-ux/18.0/template_content_swapper/static/img/login_before.png" /></p>
<p><img alt="image2" src="https://raw.githubusercontent.com/OCA/server-ux/18.0/template_content_swapper/static/img/login_after.png" /></p>
</div>
<div class="section" id="bug-tracker">
<h2><a class="toc-backref" href="#toc-entry-3">Bug Tracker</a></h2>
<h1><a class="toc-backref" href="#toc-entry-3">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/server-ux/issues">GitHub Issues</a>.
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
<a class="reference external" href="https://github.com/OCA/server-ux/issues/new?body=module:%20template_content_swapper%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h2><a class="toc-backref" href="#toc-entry-4">Credits</a></h2>
<h1><a class="toc-backref" href="#toc-entry-4">Credits</a></h1>
<div class="section" id="authors">
<h3><a class="toc-backref" href="#toc-entry-5">Authors</a></h3>
<h2><a class="toc-backref" href="#toc-entry-5">Authors</a></h2>
<ul class="simple">
<li>Quartile</li>
</ul>
</div>
<div class="section" id="contributors">
<h3><a class="toc-backref" href="#toc-entry-6">Contributors</a></h3>
<h2><a class="toc-backref" href="#toc-entry-6">Contributors</a></h2>
<ul class="simple">
<li><a class="reference external" href="https://www.quartile.co">Quartile</a>:<ul>
<li>Aung Ko Ko Lin</li>
Expand All @@ -455,7 +458,7 @@ <h3><a class="toc-backref" href="#toc-entry-6">Contributors</a></h3>
</ul>
</div>
<div class="section" id="maintainers">
<h3><a class="toc-backref" href="#toc-entry-7">Maintainers</a></h3>
<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" />
Expand All @@ -470,6 +473,5 @@ <h3><a class="toc-backref" href="#toc-entry-7">Maintainers</a></h3>
</div>
</div>
</div>
</div>
</body>
</html>
Loading