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
9 changes: 6 additions & 3 deletions database_cleanup/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ Database cleanup

|badge1| |badge2| |badge3| |badge4| |badge5|

Clean your Odoo database from remnants of modules, models, columns and
tables left by uninstalled modules (prior to 7.0) or a homebrew database
Clean your Odoo database from remnants of modules, models, columns, tables and
attachment records left by uninstalled modules (prior to 7.0) or a homebrew database
upgrade to a new major version of Odoo.

Caution! This module is potentially harmful and can *easily* destroy the
Expand All @@ -52,7 +52,7 @@ Usage

After installation of this module, go to the Settings menu -> Technical ->
Database cleanup. This menu is only available to members of the *Access Rights*
group. Go through the modules, models, columns and tables
group. Go through the modules, models, columns, tables and attachment
entries under this menu (in that order) and find out if there is orphaned data
in your database. You can either delete entries by line, or sweep all entries
in one big step (if you are *really* confident).
Expand Down Expand Up @@ -88,6 +88,9 @@ Contributors
* Mark Schuit <mark@gig.solutions>
* `360ERP <https://www.360erp.com>`_:
* Andrea Stirpe
* `Cetmix <https://cetmix.com/>`_:
* Ivan Sokolov
* George Smirnov

Maintainers
~~~~~~~~~~~
Expand Down
1 change: 1 addition & 0 deletions database_cleanup/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"views/purge_data.xml",
"views/create_indexes.xml",
"views/purge_properties.xml",
"views/purge_attachments.xml",
"views/menu.xml",
"security/ir.model.access.csv",
],
Expand Down
1 change: 1 addition & 0 deletions database_cleanup/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from . import purge_fields
from . import purge_columns
from . import purge_tables
from . import purge_attachments
from . import purge_data
from . import purge_menus
from . import create_indexes
Expand Down
110 changes: 110 additions & 0 deletions database_cleanup/models/purge_attachments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Copyright 2026 Cetmix
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

import os

from odoo import _, api, fields, models
from odoo.exceptions import AccessError, UserError, ValidationError

REASON_MISSING_FILE = "missing_file"


class CleanupPurgeLineAttachment(models.TransientModel):
_inherit = "cleanup.purge.line"
_name = "cleanup.purge.line.attachment"
_description = "Cleanup Purge Line Attachment"

attachment_id = fields.Many2one("ir.attachment")
reason = fields.Selection(
[
(REASON_MISSING_FILE, "File missing in filestore"),
],
)
error_message = fields.Char(readonly=True)
wizard_id = fields.Many2one("cleanup.purge.wizard.attachment", readonly=True)

def purge(self):
"""Unlink orphaned attachment records upon manual confirmation.

Filters unpurged lines with attachment_id. Unlinks each attachment
individually; failures are logged and skipped so the batch continues.
Only successfully removed attachments get their lines marked purged.

:return: result of write({"purged": True}) on successfully purged lines,
or True if none were purged
"""
if self:
objs = self
else:
objs = self.env["cleanup.purge.line.attachment"].browse(
self._context.get("active_ids")
)
to_unlink = objs.filtered(lambda x: not x.purged and x.attachment_id)
self.logger.info("Purging attachments: %s", to_unlink.mapped("name"))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid logging attachment names at INFO level.

to_unlink.mapped("name") can include user-provided filenames and leak sensitive data into logs. Log only aggregate metadata (e.g., count).

🔒 Proposed safe logging change
-        self.logger.info("Purging attachments: %s", to_unlink.mapped("name"))
+        if to_unlink:
+            self.logger.info("Purging orphaned attachments count=%d", len(to_unlink))

As per coding guidelines, logging sensitive data (like user identifiers) is a compliance/privacy risk.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
self.logger.info("Purging attachments: %s", to_unlink.mapped("name"))
if to_unlink:
self.logger.info("Purging orphaned attachments count=%d", len(to_unlink))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@database_cleanup/models/purge_attachments.py` at line 32, The current
logger.info call in purge_attachments.py logs user-provided filenames via
to_unlink.mapped("name"); change it to log only aggregate metadata (e.g., the
number of attachments) by replacing the logger.info invocation that references
to_unlink.mapped("name") with a message like "Purging attachments: count=%d"
using len(to_unlink) or to_unlink.count(), ensuring no per-attachment names or
other user data are emitted; update any surrounding comments to reflect the
privacy-safe logging policy and keep the reference to to_unlink and logger.info
so the correct call site is modified.

purged_line_ids = []
for line in to_unlink:
attach = line.attachment_id
try:
attach.unlink()
purged_line_ids.append(line.id)
except (UserError, ValidationError, AccessError) as exc:
self.logger.warning(
"Attachment #%s cannot be deleted: %s",
attach.id,
str(exc),
)
line.error_message = str(exc)
if not purged_line_ids:
return True
return (
self.env["cleanup.purge.line.attachment"]
.browse(purged_line_ids)
.write({"purged": True})
)


class CleanupPurgeWizardAttachment(models.TransientModel):
_inherit = "cleanup.purge.wizard"
_name = "cleanup.purge.wizard.attachment"
_description = "Purge attachments"

@api.model
def find(self):
"""Collect ir.attachment records whose backing files are missing on disk.

Requires file storage. Searches binary attachments with store_fname,
checks each file exists via os.path.isfile(_full_path(store_fname)).

:raises UserError: if storage != "file" or no orphaned entries found
"""
if self.env["ir.attachment"]._storage() != "file":
raise UserError(
_(
"Attachment storage is not 'file'. "
"Purge of orphaned attachments only works with file storage."
)
)
res = []
attachments = self.env["ir.attachment"].search(
[
("store_fname", "!=", False),
("type", "=", "binary"),
]
)
for attach in attachments:
full_path = self.env["ir.attachment"]._full_path(attach.store_fname)
if not os.path.isfile(full_path):
res.append(
fields.Command.create(
{
"attachment_id": attach.id,
"name": attach.store_fname or attach.name or str(attach.id),
"reason": REASON_MISSING_FILE,
}
)
)
Comment thread
Aldeigja marked this conversation as resolved.
if not res:
raise UserError(_("No orphaned attachment entries found"))
return res

purge_line_ids = fields.One2many("cleanup.purge.line.attachment", "wizard_id")
3 changes: 3 additions & 0 deletions database_cleanup/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@
* Mark Schuit <mark@gig.solutions>
* `360ERP <https://www.360erp.com>`_:
* Andrea Stirpe
* `Cetmix <https://cetmix.com/>`_:
* Ivan Sokolov
* George Smirnov
4 changes: 2 additions & 2 deletions database_cleanup/readme/DESCRIPTION.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Clean your Odoo database from remnants of modules, models, columns and
tables left by uninstalled modules (prior to 7.0) or a homebrew database
Clean your Odoo database from remnants of modules, models, columns, tables and
attachment records left by uninstalled modules (prior to 7.0) or a homebrew database
upgrade to a new major version of Odoo.

Caution! This module is potentially harmful and can *easily* destroy the
Expand Down
2 changes: 1 addition & 1 deletion database_cleanup/readme/USAGE.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
After installation of this module, go to the Settings menu -> Technical ->
Database cleanup. This menu is only available to members of the *Access Rights*
group. Go through the modules, models, columns and tables
group. Go through the modules, models, columns, tables and attachment
entries under this menu (in that order) and find out if there is orphaned data
in your database. You can either delete entries by line, or sweep all entries
in one big step (if you are *really* confident).
Expand Down
2 changes: 2 additions & 0 deletions database_cleanup/security/ir.model.access.csv
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ access_cleanup_purge_line_menu,access_cleanup_purge_line_menu,model_cleanup_purg
access_cleanup_purge_wizard_menu,access_cleanup_purge_wizard_menu,model_cleanup_purge_wizard_menu,base.group_user,1,1,1,1
access_cleanup_purge_line_property,access_cleanup_purge_line_property,model_cleanup_purge_line_property,base.group_user,1,1,1,1
access_cleanup_purge_wizard_property,access_cleanup_purge_wizard_property,model_cleanup_purge_wizard_property,base.group_user,1,1,1,1
access_cleanup_purge_line_attachment,access_cleanup_purge_line_attachment,model_cleanup_purge_line_attachment,base.group_user,1,1,1,1
access_cleanup_purge_wizard_attachment,access_cleanup_purge_wizard_attachment,model_cleanup_purge_wizard_attachment,base.group_user,1,1,1,1
9 changes: 6 additions & 3 deletions database_cleanup/static/description/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -375,8 +375,8 @@ <h1>Database cleanup</h1>
!! source digest: sha256:f2204c1d994e5d6dc3dc0ee765ef5b3d735612c05c4a8d467bab9d9bedd99872
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<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-tools/tree/16.0/database_cleanup"><img alt="OCA/server-tools" src="https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/server-tools-16-0/server-tools-16-0-database_cleanup"><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-tools&amp;target_branch=16.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>Clean your Odoo database from remnants of modules, models, columns and
tables left by uninstalled modules (prior to 7.0) or a homebrew database
<p>Clean your Odoo database from remnants of modules, models, columns, tables and
attachment records left by uninstalled modules (prior to 7.0) or a homebrew database
upgrade to a new major version of Odoo.</p>
<p>Caution! This module is potentially harmful and can <em>easily</em> destroy the
integrity of your data. Do not use if you are not entirely comfortable
Expand All @@ -400,7 +400,7 @@ <h1>Database cleanup</h1>
<h2><a class="toc-backref" href="#toc-entry-1">Usage</a></h2>
<p>After installation of this module, go to the Settings menu -&gt; Technical -&gt;
Database cleanup. This menu is only available to members of the <em>Access Rights</em>
group. Go through the modules, models, columns and tables
group. Go through the modules, models, columns, tables and attachment
entries under this menu (in that order) and find out if there is orphaned data
in your database. You can either delete entries by line, or sweep all entries
in one big step (if you are <em>really</em> confident).</p>
Expand Down Expand Up @@ -439,6 +439,9 @@ <h3><a class="toc-backref" href="#toc-entry-5">Contributors</a></h3>
</dd>
</dl>
</li>
<li><a class="reference external" href="https://cetmix.com/">Cetmix</a>:
* Ivan Sokolov
* George Smirnov</li>
</ul>
</div>
<div class="section" id="maintainers">
Expand Down
1 change: 1 addition & 0 deletions database_cleanup/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from . import common
from . import test_create_indexes
from . import test_identifier_adapter
from . import test_purge_attachments
from . import test_purge_columns
from . import test_purge_data
from . import test_purge_fields
Expand Down
153 changes: 153 additions & 0 deletions database_cleanup/tests/test_purge_attachments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# Copyright 2026 Cetmix
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

import base64
import os
from unittest.mock import patch

from odoo.exceptions import UserError
from odoo.fields import Command
from odoo.tests.common import tagged

from .common import Common, environment


# Use post_install to get all models loaded more info: odoo/odoo#13458
@tagged("post_install", "-at_install")
class TestCleanupPurgeLineAttachment(Common):
def setUp(self):
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is Odoo 16.0, you should use setUpClass()

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is Odoo 16.0, you should use setUpClass()

@ivs-cetmix Thank you for the suggestion.

While setUpClass() is preferred for performance , I used setUp() here to ensure strict test isolation.
Tests involve purge_all(), since unittest does not guarantee the order of execution sharing the same records across tests via setUpClass() could be risky. For instance, test_purge_orphaned_attachments could delete records that causes test_find_orphaned_attachments to fail.

"""Create two ir.attachment records; delete backing file of one (orphan).

:var orphan: ir.attachment with backing file removed via os.unlink
:var valid: ir.attachment with file intact
:return: None
"""
super().setUp()
with environment() as env:
IrAttachment = env["ir.attachment"]
datas = base64.b64encode(b"test_orphan").decode("ascii")
orphan = IrAttachment.create(
{
"name": "test_orphan_attachment.txt",
"type": "binary",
"datas": datas,
}
)
datas_valid = base64.b64encode(b"test_valid").decode("ascii")
valid = IrAttachment.create(
{
"name": "test_valid_attachment.txt",
"type": "binary",
"datas": datas_valid,
}
)
# Delete backing file to create orphan
full_path = IrAttachment._full_path(orphan.store_fname)
os.unlink(full_path)
Comment on lines +45 to +46
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Verify whether test/config code explicitly enforces filestore-backed attachments.
rg -n -C2 --type=py "ir_attachment\.location|store_fname|_full_path\("

Repository: cetmix/oca-server-tools

Length of output: 2461


🏁 Script executed:

cd database_cleanup && find . -name "test_purge_attachments.py" -type f

Repository: cetmix/oca-server-tools

Length of output: 100


🏁 Script executed:

cat -n database_cleanup/tests/test_purge_attachments.py

Repository: cetmix/oca-server-tools

Length of output: 3489


🏁 Script executed:

cat -n database_cleanup/tests/common.py

Repository: cetmix/oca-server-tools

Length of output: 1028


🏁 Script executed:

fd -name "conftest.py|__init__.py" database_cleanup/tests/

Repository: cetmix/oca-server-tools

Length of output: 297


🏁 Script executed:

rg -n "storage|ir\.attachment.*location|ir\.attachment.*store_fname" database_cleanup/tests/ --type=py

Repository: cetmix/oca-server-tools

Length of output: 49


🏁 Script executed:

rg -A10 "def _full_path" database_cleanup/ --type=py

Repository: cetmix/oca-server-tools

Length of output: 49


🏁 Script executed:

rg -B5 -A10 "store_fname" database_cleanup/models/ --type=py

Repository: cetmix/oca-server-tools

Length of output: 3493


Add defensive assertions to fixture setup before deleting backing file.

Lines 42-43 assume orphan.store_fname is set and the file exists without validation. If attachment storage is not filestore-backed or file creation fails, the test setup fails with unclear error messages. Add explicit assertions to fail fast with clear context.

Proposed fix
+            self.assertTrue(
+                orphan.store_fname,
+                "Expected filestore-backed attachment (store_fname is empty)",
+            )
             full_path = IrAttachment._full_path(orphan.store_fname)
+            self.assertTrue(
+                os.path.exists(full_path),
+                f"Expected attachment file to exist: {full_path}",
+            )
             os.unlink(full_path)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@database_cleanup/tests/test_purge_attachments.py` around lines 42 - 43, The
test currently assumes orphan.store_fname exists and the backing file is present
before calling IrAttachment._full_path(...) and os.unlink(...); add defensive
assertions in the fixture setup: assert that orphan.store_fname is truthy (with
a clear message referencing orphan.store_fname and IrAttachment._full_path) and
assert that os.path.exists(full_path) is True (with a message indicating the
expected file path) before calling os.unlink(full_path) so the test fails fast
with a clear error if storage isn't filestore-backed or file creation failed.

self.orphan_attach_id = orphan.id
self.valid_attach_id = valid.id

Comment on lines +26 to +49
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Mar 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Committed fixtures are not cleaned up, which can leak state across tests.

setUp() writes attachments using environment(), and that helper commits by design. Without teardown cleanup, data can persist between tests and introduce order-dependent behavior.

♻️ Proposed fix
 class TestCleanupPurgeLineAttachment(Common):
     def setUp(self):
         """Create two ir.attachment records; delete backing file of one (orphan).
@@
             os.unlink(full_path)
             self.orphan_attach_id = orphan.id
             self.valid_attach_id = valid.id
+
+    def tearDown(self):
+        ids = [
+            rec_id
+            for rec_id in [
+                getattr(self, "orphan_attach_id", False),
+                getattr(self, "valid_attach_id", False),
+            ]
+            if rec_id
+        ]
+        if ids:
+            with environment() as env:
+                env["ir.attachment"].browse(ids).exists().unlink()
+        super().tearDown()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
with environment() as env:
IrAttachment = env["ir.attachment"]
datas = base64.b64encode(b"test_orphan").decode("ascii")
orphan = IrAttachment.create(
{
"name": "test_orphan_attachment.txt",
"type": "binary",
"datas": datas,
}
)
datas_valid = base64.b64encode(b"test_valid").decode("ascii")
valid = IrAttachment.create(
{
"name": "test_valid_attachment.txt",
"type": "binary",
"datas": datas_valid,
}
)
# Delete backing file to create orphan
full_path = IrAttachment._full_path(orphan.store_fname)
os.unlink(full_path)
self.orphan_attach_id = orphan.id
self.valid_attach_id = valid.id
with environment() as env:
IrAttachment = env["ir.attachment"]
datas = base64.b64encode(b"test_orphan").decode("ascii")
orphan = IrAttachment.create(
{
"name": "test_orphan_attachment.txt",
"type": "binary",
"datas": datas,
}
)
datas_valid = base64.b64encode(b"test_valid").decode("ascii")
valid = IrAttachment.create(
{
"name": "test_valid_attachment.txt",
"type": "binary",
"datas": datas_valid,
}
)
# Delete backing file to create orphan
full_path = IrAttachment._full_path(orphan.store_fname)
os.unlink(full_path)
self.orphan_attach_id = orphan.id
self.valid_attach_id = valid.id
def tearDown(self):
ids = [
rec_id
for rec_id in [
getattr(self, "orphan_attach_id", False),
getattr(self, "valid_attach_id", False),
]
if rec_id
]
if ids:
with environment() as env:
env["ir.attachment"].browse(ids).exists().unlink()
super().tearDown()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@database_cleanup/tests/test_purge_attachments.py` around lines 23 - 46, The
test leaves committed attachment records behind; add a tearDown that opens
environment() and removes the fixtures created in setUp by using
IrAttachment.browse(self.orphan_attach_id) and
IrAttachment.browse(self.valid_attach_id) (or search by name) and
unlinking/deleting their backing files and then unlinking/deleting the records
(or call unlink() on the recordset) so the database and filesystem state are
cleaned; ensure the tearDown uses the same environment() context to perform
deletions/commits and guards against missing files/records.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No test in this addon has a tearDown — the established pattern is to let committed fixtures live, relying on the test itself to exercise the purge. test_purge_orphaned_attachments already removes the orphan via purge_all(). Adding tearDown only here breaks consistency with the rest of the addon. If fixture cleanup is desired, it should be addressed addon-wide.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Aldeigja, understood — that's a reasonable stance. The addon-wide convention of relying on committed fixtures and letting the test logic itself exercise cleanup (rather than a tearDown) is a coherent approach, and adding it selectively here would indeed be inconsistent. I'll note this for future reviews.


✏️ Learnings added
Learnt from: Aldeigja
Repo: cetmix/oca-server-tools PR: 5
File: database_cleanup/tests/test_purge_attachments.py:23-46
Timestamp: 2026-03-18T18:21:31.724Z
Learning: In the `database_cleanup` addon (cetmix/oca-server-tools), the established test convention is to let committed fixtures live without a `tearDown` method. Tests rely on their own logic to exercise cleanup (e.g., `purge_all()` removes the orphan attachment in `test_purge_orphaned_attachments`). Do not flag missing `tearDown` in individual test classes for this addon; if fixture cleanup is desired, it should be addressed addon-wide.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

def test_find_orphaned_attachments(self):
"""Assert wizard find() includes orphan in purge lines, excludes valid.

:var wizard: cleanup.purge.wizard.attachment
:var line_attachment_ids: ids from purge_line_ids.mapped("attachment_id")
:return: None
"""
with environment() as env:
wizard = env["cleanup.purge.wizard.attachment"].create({})
line_attachment_ids = wizard.purge_line_ids.mapped("attachment_id").ids
self.assertIn(self.orphan_attach_id, line_attachment_ids)
self.assertNotIn(self.valid_attach_id, line_attachment_ids)

def test_purge_orphaned_attachments(self):
"""Assert purge_all() removes orphan record, leaves valid intact.

:var wizard: cleanup.purge.wizard.attachment
:var orphan: ir.attachment browsed by self.orphan_attach_id
:var valid: ir.attachment browsed by self.valid_attach_id
:return: None
"""
with environment() as env:
wizard = env["cleanup.purge.wizard.attachment"].create({})
wizard.purge_all()
orphan = env["ir.attachment"].browse(self.orphan_attach_id)
valid = env["ir.attachment"].browse(self.valid_attach_id)
self.assertFalse(orphan.exists())
self.assertTrue(valid.exists())

def test_purge_skips_protected_attachment(self):
"""When unlink raises UserError on one line, purge others and skip.

:var wizard: cleanup.purge.wizard.attachment
:return: None
"""
with environment() as env:
IrAttachment = env["ir.attachment"]
datas_a = base64.b64encode(b"test_protected").decode("ascii")
orphan_protected = IrAttachment.create(
{
"name": "test_protected_orphan.txt",
"type": "binary",
"datas": datas_a,
}
)
datas_b = base64.b64encode(b"test_unprotected").decode("ascii")
orphan_other = IrAttachment.create(
{
"name": "test_unprotected_orphan.txt",
"type": "binary",
"datas": datas_b,
}
)
os.unlink(IrAttachment._full_path(orphan_protected.store_fname))
os.unlink(IrAttachment._full_path(orphan_other.store_fname))

protected_id = orphan_protected.id
other_id = orphan_other.id

IrModel = env.registry["ir.attachment"]
original_unlink = IrModel.unlink

def patched_unlink(self):
if protected_id in self.ids:
# Dynamic message avoids translation lint on test-only UserError.
raise UserError(str(protected_id))
return original_unlink(self)

wizard = env["cleanup.purge.wizard.attachment"].create(
{
"purge_line_ids": [
Command.create(
{
"attachment_id": protected_id,
"name": orphan_protected.store_fname
or str(protected_id),
}
),
Command.create(
{
"attachment_id": other_id,
"name": orphan_other.store_fname or str(other_id),
}
),
],
}
)
protected_line = wizard.purge_line_ids.filtered(
lambda l: l.attachment_id.id == protected_id
)
other_line = wizard.purge_line_ids.filtered(
lambda l: l.attachment_id.id == other_id
)
protected_line_id = protected_line.id
other_line_id = other_line.id

with patch.object(IrModel, "unlink", patched_unlink):
wizard.purge_line_ids.purge()

Line = env["cleanup.purge.line.attachment"]
self.assertFalse(Line.browse(protected_line_id).purged)
self.assertTrue(Line.browse(other_line_id).purged)
self.assertTrue(IrAttachment.browse(protected_id).exists())
self.assertFalse(IrAttachment.browse(other_id).exists())
7 changes: 7 additions & 0 deletions database_cleanup/views/menu.xml
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,11 @@
<field name="action" ref="action_purge_property" />
<field name="parent_id" ref="menu_database_cleanup" />
</record>

<record model="ir.ui.menu" id="menu_purge_attachments">
<field name="name">Purge orphaned attachments</field>
<field name="sequence" eval="85" />
<field name="action" ref="action_purge_attachments" />
<field name="parent_id" ref="menu_database_cleanup" />
</record>
</odoo>
Loading
Loading