From 0a650040c1d49d203507d079e25c1e4b429745ae Mon Sep 17 00:00:00 2001
From: Holger Brunn
Date: Wed, 6 May 2026 17:28:55 +0200
Subject: [PATCH 01/10] [IMP] base_view_inheritance_extension: better error
messages
---
.../models/ir_ui_view.py | 19 +++++++++++++++++++
.../test_base_view_inheritance_extension.py | 8 ++++++++
2 files changed, 27 insertions(+)
diff --git a/base_view_inheritance_extension/models/ir_ui_view.py b/base_view_inheritance_extension/models/ir_ui_view.py
index fa16930cd49..6207d642014 100644
--- a/base_view_inheritance_extension/models/ir_ui_view.py
+++ b/base_view_inheritance_extension/models/ir_ui_view.py
@@ -187,6 +187,7 @@ def _inheritance_handler_attributes_update(self, source, specs):
"""
node = self.locate_node(source, specs)
+ self._assert_node(node, specs)
for spec in specs:
attr_name = spec.get("name")
# Parse ast from both node and spec
@@ -212,6 +213,7 @@ def _inheritance_handler_attributes_text_add(self, source, specs):
$node>"""
node = self.locate_node(source, specs)
+ self._assert_node(node, specs)
for attribute_node in specs:
attribute_name = attribute_node.get("name")
old_value = node.get(attribute_name) or ""
@@ -230,6 +232,7 @@ def _inheritance_handler_attributes_domain_add(self, source, specs):
$node>"""
node = self.locate_node(source, specs)
+ self._assert_node(node, specs)
for attribute_node in specs:
attribute_name = attribute_node.get("name")
condition = attribute_node.get("condition")
@@ -275,3 +278,19 @@ def _str2var_domain_text(self, domain_str):
domain_str = pattern.sub(r"\1", domain_str)
pattern = re.compile(r"'([^']+)_is_a_var_to_replace'")
return pattern.sub(r"\1", domain_str)
+
+ @api.model
+ def _assert_node(self, node, specs):
+ """Raise a ValueError detailing the failing specification if node is None"""
+ if node is None:
+ self._raise_view_error(
+ self.env._(
+ "Element '%s' cannot be located in parent view",
+ etree.tostring(
+ etree.Element(
+ specs.tag,
+ {a: v for a, v in specs.attrib.items() if a != "position"},
+ )
+ ).decode("utf8"),
+ )
+ )
diff --git a/base_view_inheritance_extension/tests/test_base_view_inheritance_extension.py b/base_view_inheritance_extension/tests/test_base_view_inheritance_extension.py
index 5052859e97e..79e1d93acff 100644
--- a/base_view_inheritance_extension/tests/test_base_view_inheritance_extension.py
+++ b/base_view_inheritance_extension/tests/test_base_view_inheritance_extension.py
@@ -267,3 +267,11 @@ def test_wraptext(self):
inherited_view.write(
{"arch": ''}
)
+
+ def test_locate_failure_error_message(self):
+ """Be sure there is a helpful error message when locating nodes fails"""
+ source = etree.fromstring("")
+ specs = etree.fromstring('')
+ with self.assertRaisesRegex(ValueError, "nonexistent.*parent view") as caught:
+ self.env["ir.ui.view"].apply_inheritance_specs(source, specs)
+ self.assertTrue(caught.exception.context)
From 7abe0e36c7c757256c0817cf94466b9b58b3f516 Mon Sep 17 00:00:00 2001
From: Moises Lopez - Vauxoo
Date: Thu, 15 Oct 2026 19:43:45 +0000
Subject: [PATCH 02/10] [IMP] attachment_unindex_content: read index_content
from the file
The module answers `_index` with False so nothing is stored in
`index_content`, and the field is then empty for every reader. Code that
needs the value gets a boolean, for instance the website controller that
looks a theme color up in the CSS bundle through
`asset.css().index_content`:
TypeError: expected string or bytes-like object, got 'bool'
Keep the column empty, but when the field is fetched and the database has
no value, index the file at that moment with the next `_index` of the MRO
(the one the module disables) and only put the result in the cache. Nothing
is written to the database, searching the field gives the same as before,
and a value written by hand is kept.
The tests that checked that the field reads False now check the stored
column.
---
attachment_unindex_content/README.rst | 9 +++
.../models/ir_attachment.py | 28 +++++++
.../readme/DESCRIPTION.md | 8 ++
.../static/description/index.html | 8 ++
.../tests/test_attachment_index.py | 77 ++++++++++++++++++-
5 files changed, 126 insertions(+), 4 deletions(-)
diff --git a/attachment_unindex_content/README.rst b/attachment_unindex_content/README.rst
index 4e1ef159110..d4016a51941 100644
--- a/attachment_unindex_content/README.rst
+++ b/attachment_unindex_content/README.rst
@@ -65,6 +65,15 @@ it at ``ir.attachment`` base.
Also, after the installation, the ``index_content`` field on attachments
already recorded in database will be cleared.
+Nothing is stored, but the field is still readable: when the database
+has no content for an attachment, reading ``index_content`` indexes the
+file at that moment and puts the result in the cache only. Code that
+reads the field, as the website does to find a color in the CSS bundle,
+keeps working with no copy of the content in the database. The cost is
+reading the file of every attachment whose ``index_content`` is
+requested. Searching on the field still finds nothing, because the
+column stays empty.
+
.. |SQL Query result showing indexed content| image:: https://user-images.githubusercontent.com/442938/67894113-45d27a80-fb2e-11e9-9a22-ba43d8b444c5.png
:target: https://user-images.githubusercontent.com/442938/67894113-45d27a80-fb2e-11e9-9a22-ba43d8b444c5.png
diff --git a/attachment_unindex_content/models/ir_attachment.py b/attachment_unindex_content/models/ir_attachment.py
index 772e38ce20e..54b37196d8d 100644
--- a/attachment_unindex_content/models/ir_attachment.py
+++ b/attachment_unindex_content/models/ir_attachment.py
@@ -10,3 +10,31 @@ class IrAttachment(models.Model):
@api.model
def _index(self, *args, **kwargs):
return False
+
+ def _fetch_query(self, query, fields):
+ """Give ``index_content`` the content of the file when the database has none.
+
+ The content is not stored, so the column is empty, but whoever reads the field
+ still needs its value: core reads it to look a color up in the CSS bundle of the
+ website. Read it from the file, as the indexation would have stored it, and only
+ put it in the cache: nothing is written to the database.
+ """
+ fetched = super()._fetch_query(query, fields)
+ field = self._fields["index_content"]
+ if fetched and field in fields:
+ cache = field._get_cache(self.env)
+ for attachment in fetched:
+ if cache.get(attachment.id):
+ continue
+ content = self._index_content_from_file(attachment)
+ if content:
+ field._update_cache(attachment, content)
+ return fetched
+
+ def _index_content_from_file(self, attachment):
+ """Return what the indexation would have stored for ``attachment``."""
+ raw = attachment.raw
+ if not raw:
+ return False
+ # Skip this class: it is the one that disables the indexation
+ return super()._index(raw, attachment.mimetype, checksum=attachment.checksum)
diff --git a/attachment_unindex_content/readme/DESCRIPTION.md b/attachment_unindex_content/readme/DESCRIPTION.md
index b9a3937441c..36ceee2c777 100644
--- a/attachment_unindex_content/readme/DESCRIPTION.md
+++ b/attachment_unindex_content/readme/DESCRIPTION.md
@@ -29,3 +29,11 @@ it at `ir.attachment` base.
Also, after the installation, the `index_content` field on attachments
already recorded in database will be cleared.
+
+Nothing is stored, but the field is still readable: when the database has
+no content for an attachment, reading `index_content` indexes the file at
+that moment and puts the result in the cache only. Code that reads the
+field, as the website does to find a color in the CSS bundle, keeps working
+with no copy of the content in the database. The cost is reading the file
+of every attachment whose `index_content` is requested. Searching on the
+field still finds nothing, because the column stays empty.
diff --git a/attachment_unindex_content/static/description/index.html b/attachment_unindex_content/static/description/index.html
index 0a576fc1bff..537778a682e 100644
--- a/attachment_unindex_content/static/description/index.html
+++ b/attachment_unindex_content/static/description/index.html
@@ -402,6 +402,14 @@ Attachment Unindex Content
it at ir.attachment base.
Also, after the installation, the index_content field on attachments
already recorded in database will be cleared.
+Nothing is stored, but the field is still readable: when the database
+has no content for an attachment, reading index_content indexes the
+file at that moment and puts the result in the cache only. Code that
+reads the field, as the website does to find a color in the CSS bundle,
+keeps working with no copy of the content in the database. The cost is
+reading the file of every attachment whose index_content is
+requested. Searching on the field still finds nothing, because the
+column stays empty.
Table of contents
diff --git a/attachment_unindex_content/tests/test_attachment_index.py b/attachment_unindex_content/tests/test_attachment_index.py
index 4d1e1383864..55d45ea2b42 100644
--- a/attachment_unindex_content/tests/test_attachment_index.py
+++ b/attachment_unindex_content/tests/test_attachment_index.py
@@ -24,6 +24,14 @@ def setUpClass(cls):
cls.test_text_content = b"This is a test file content for indexing verification"
cls.test_text_b64 = base64.b64encode(cls.test_text_content)
+ def _stored_index_content(self, attachment):
+ """Return the ``index_content`` stored, not the one the field reads."""
+ attachment.flush_recordset()
+ self.env.cr.execute(
+ "SELECT index_content FROM ir_attachment WHERE id = %s", (attachment.id,)
+ )
+ return self.env.cr.fetchone()[0]
+
def test_attachment_index_content_disabled(self):
"""Test that index_content is not populated when creating attachment.
@@ -39,7 +47,7 @@ def test_attachment_index_content_disabled(self):
)
self.assertFalse(
- attachment.index_content,
+ self._stored_index_content(attachment),
"index_content should be False (empty) when creating attachment "
"because the module disables content indexation",
)
@@ -118,7 +126,7 @@ def test_multiple_attachments_no_indexing(self):
# Verify that none of the attachments have index_content populated
for attachment in attachments:
self.assertFalse(
- attachment.index_content,
+ self._stored_index_content(attachment),
f"Attachment {attachment.name} should not have indexed content",
)
@@ -151,7 +159,7 @@ def test_different_file_types_no_indexing(self):
)
self.assertFalse(
- attachment.index_content,
+ self._stored_index_content(attachment),
f"Attachment {name} with mimetype {mimetype} should not be indexed",
)
@@ -174,4 +182,65 @@ def test_attachment_search_without_index(self):
"Should find attachment by name even without content indexing",
)
self.assertEqual(found.id, attachment.id)
- self.assertFalse(found.index_content, "Found attachment should not be indexed")
+ self.assertFalse(
+ self._stored_index_content(found), "Found attachment should not be indexed"
+ )
+
+ def test_index_content_is_read_from_the_file(self):
+ """The field reads the content of the file, which is not stored."""
+ attachment = self.Attachment.create(
+ {
+ "name": "readable.txt",
+ "datas": self.test_text_b64,
+ "mimetype": "text/plain",
+ }
+ )
+ attachment.invalidate_recordset()
+ self.assertEqual(
+ attachment.index_content, self.test_text_content.decode(), "Read the file"
+ )
+ self.assertFalse(
+ self._stored_index_content(attachment), "Nothing is stored in the database"
+ )
+
+ def test_index_content_is_read_from_the_file_for_several_records(self):
+ attachments = self.Attachment.create(
+ [
+ {
+ "name": f"readable_{i}.txt",
+ "raw": f"content number {i}".encode(),
+ "mimetype": "text/plain",
+ }
+ for i in range(3)
+ ]
+ )
+ attachments.invalidate_recordset()
+ self.assertEqual(
+ attachments.mapped("index_content"),
+ [f"content number {i}" for i in range(3)],
+ )
+ self.assertEqual(
+ [row["index_content"] for row in attachments.read(["index_content"])],
+ [f"content number {i}" for i in range(3)],
+ )
+
+ def test_index_content_of_an_attachment_without_file_is_empty(self):
+ attachment = self.Attachment.create(
+ {
+ "name": "link",
+ "type": "url",
+ "url": "https://example.com",
+ "mimetype": "text/plain",
+ }
+ )
+ attachment.invalidate_recordset()
+ self.assertFalse(attachment.index_content)
+
+ def test_index_content_written_by_hand_is_kept(self):
+ attachment = self.Attachment.create(
+ {"name": "ocr.txt", "raw": b"from the file", "mimetype": "text/plain"}
+ )
+ attachment.index_content = "from the ocr"
+ attachment.flush_recordset()
+ attachment.invalidate_recordset()
+ self.assertEqual(attachment.index_content, "from the ocr")
From cd19306f0d362d99cae29349b8d6ffe62f5f8692 Mon Sep 17 00:00:00 2001
From: oca-ci
Date: Fri, 23 Oct 2026 05:13:43 +0000
Subject: [PATCH 03/10] [UPD] Update base_view_inheritance_extension.pot
---
.../i18n/base_view_inheritance_extension.pot | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/base_view_inheritance_extension/i18n/base_view_inheritance_extension.pot b/base_view_inheritance_extension/i18n/base_view_inheritance_extension.pot
index ecb0b4f08d9..f65b98bbbf8 100644
--- a/base_view_inheritance_extension/i18n/base_view_inheritance_extension.pot
+++ b/base_view_inheritance_extension/i18n/base_view_inheritance_extension.pot
@@ -18,6 +18,12 @@ msgstr ""
msgid "Display Name"
msgstr ""
+#. module: base_view_inheritance_extension
+#. odoo-python
+#: code:addons/base_view_inheritance_extension/models/ir_ui_view.py:0
+msgid "Element '%s' cannot be located in parent view"
+msgstr ""
+
#. module: base_view_inheritance_extension
#: model:ir.model.fields,field_description:base_view_inheritance_extension.field_ir_ui_view__id
msgid "ID"
From 316602e021f89da0d3dc33ba9e887ac3e9619fd8 Mon Sep 17 00:00:00 2001
From: OCA-git-bot
Date: Fri, 23 Oct 2026 05:17:52 +0000
Subject: [PATCH 04/10] [BOT] post-merge updates
---
README.md | 2 +-
base_view_inheritance_extension/README.rst | 2 +-
base_view_inheritance_extension/__manifest__.py | 2 +-
base_view_inheritance_extension/static/description/index.html | 4 ++--
4 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/README.md b/README.md
index 65f1eb90fc9..728d4417244 100644
--- a/README.md
+++ b/README.md
@@ -32,7 +32,7 @@ addon | version | maintainers | summary
[base_search_fuzzy](base_search_fuzzy/) | 19.0.1.0.0 | | Fuzzy search with the PostgreSQL trigram extension
[base_technical_user](base_technical_user/) | 19.0.1.0.0 | | Add a technical user parameter on the company
[base_time_window](base_time_window/) | 19.0.1.0.0 | | Base model to handle time windows
-[base_view_inheritance_extension](base_view_inheritance_extension/) | 19.0.1.0.0 |
| Adds more operators for view inheritance
+[base_view_inheritance_extension](base_view_inheritance_extension/) | 19.0.1.0.1 |
| Adds more operators for view inheritance
[base_write_diff](base_write_diff/) | 19.0.1.0.0 | | Prevents updates on fields whose values won't change anyway
[bus_alt_connection](bus_alt_connection/) | 19.0.1.0.0 | | Needed when using PgBouncer as a connection pooler
[database_cleanup](database_cleanup/) | 19.0.1.0.6 | | Database cleanup
diff --git a/base_view_inheritance_extension/README.rst b/base_view_inheritance_extension/README.rst
index 3bab25802d3..1a175b134c7 100644
--- a/base_view_inheritance_extension/README.rst
+++ b/base_view_inheritance_extension/README.rst
@@ -11,7 +11,7 @@ Extended view inheritance
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
- !! source digest: sha256:9089555b5aea01b23ff4a0350d6fd69589d63f1beb4e272ed5a96eebf05b2a81
+ !! source digest: sha256:9ed71530bde28eea51dc4bcbb711aed870efc9b5c3ef861adca68e3f129b3a62
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Mature-brightgreen.png
diff --git a/base_view_inheritance_extension/__manifest__.py b/base_view_inheritance_extension/__manifest__.py
index 5219ab7b857..167e019aa9a 100644
--- a/base_view_inheritance_extension/__manifest__.py
+++ b/base_view_inheritance_extension/__manifest__.py
@@ -3,7 +3,7 @@
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
{
"name": "Extended view inheritance",
- "version": "19.0.1.0.0",
+ "version": "19.0.1.0.1",
"development_status": "Mature",
"author": "Therp BV,Odoo Community Association (OCA)",
"license": "LGPL-3",
diff --git a/base_view_inheritance_extension/static/description/index.html b/base_view_inheritance_extension/static/description/index.html
index 611aa8047c1..3ed0d5d02b1 100644
--- a/base_view_inheritance_extension/static/description/index.html
+++ b/base_view_inheritance_extension/static/description/index.html
@@ -3,7 +3,7 @@
-README.rst
+Extended view inheritance