From eaf7d3cad1dc1aaa2c2414b9d06f446683c5dd05 Mon Sep 17 00:00:00 2001
From: Erik Papernyuk <80971885+Erp4759@users.noreply.github.com>
Date: Fri, 3 Jul 2026 16:21:03 +0800
Subject: [PATCH] [FIX] dms: complete Odoo 19 migration fixes
---
dms/models/directory.py | 62 ++++++++++++++++++-------------------
dms/models/dms_file.py | 3 ++
dms/models/storage.py | 3 +-
dms/tests/test_directory.py | 15 ++++++++-
dms/tests/test_file.py | 45 +++++++++++++++++++++++++++
dms/tests/test_portal.py | 43 +++++++++++++++++++++++++
dms/views/dms_directory.xml | 5 ++-
dms/views/dms_file.xml | 5 ++-
8 files changed, 146 insertions(+), 35 deletions(-)
diff --git a/dms/models/directory.py b/dms/models/directory.py
index 33dc931ea..b147e017f 100644
--- a/dms/models/directory.py
+++ b/dms/models/directory.py
@@ -8,7 +8,6 @@
import base64
import logging
import os
-from ast import literal_eval
from collections import defaultdict
from typing import Literal # noqa # pylint: disable=unused-import
@@ -88,7 +87,7 @@ def _default_parent_id(self):
if context.get("active_model") == self._name and context.get("active_id"):
return context["active_id"]
else:
- return False
+ return context.get("dms_parent_id")
group_ids = fields.Many2many(
comodel_name="dms.access.group",
@@ -380,8 +379,8 @@ def _search_panel_directory(self, **kwargs):
@api.model
def _search_starred(self, operator, operand):
if operator in ("=", "in") and operand:
- return [("user_star_ids", "in", [self.env.uid])]
- return [("user_star_ids", "not in", [self.env.uid])]
+ return Domain([("user_star_ids", "in", [self.env.uid])])
+ return Domain([("user_star_ids", "not in", [self.env.uid])])
@api.depends("name", "parent_id.complete_name")
def _compute_complete_name(self):
@@ -490,8 +489,12 @@ def _compute_parent_id(self):
if record.is_root_directory:
record.parent_id = None
else:
- # HACK: Not needed in v14 due to odoo/odoo#64359
- record.parent_id = record.parent_id
+ record.parent_id = (
+ record.parent_id
+ or record._origin.parent_id
+ or self.env.context.get("default_parent_id")
+ or self.env.context.get("dms_parent_id")
+ )
@api.depends("is_root_directory", "parent_id")
def _compute_root_id(self):
@@ -753,37 +756,34 @@ def _search_panel_domain_image(
def action_dms_directories_all_directory(self):
self.ensure_one()
action = self.env["ir.actions.act_window"]._for_xml_id(
- "dms.action_dms_directory"
- )
- domain = Domain.AND(
- [
- literal_eval(action["domain"].strip()),
- [("parent_id", "child_of", self.id)],
- ]
+ "dms.action_dms_directories_all_directory"
)
action["display_name"] = self.name
- action["domain"] = domain
- action["context"] = dict(
- self.env.context,
- default_parent_id=self.id,
- searchpanel_default_parent_id=self.id,
- )
+ action["domain"] = [
+ ("parent_id", "child_of", self.id),
+ ("is_hidden", "=", False),
+ ("id", "!=", self.id),
+ ]
+ action["context"] = {
+ "default_parent_id": self.id,
+ "dms_parent_id": self.id,
+ "searchpanel_default_parent_id": self.id,
+ }
return action
def action_dms_files_all_directory(self):
self.ensure_one()
- action = self.env["ir.actions.act_window"]._for_xml_id("dms.action_dms_file")
- domain = Domain.AND(
- [
- literal_eval(action["domain"].strip()),
- [("directory_id", "child_of", self.id)],
- ]
+ action = self.env["ir.actions.act_window"]._for_xml_id(
+ "dms.action_dms_files_all_directory"
)
action["display_name"] = self.name
- action["domain"] = domain
- action["context"] = dict(
- self.env.context,
- default_directory_id=self.id,
- searchpanel_default_directory_id=self.id,
- )
+ action["domain"] = [
+ ("directory_id", "child_of", self.id),
+ ("is_hidden", "=", False),
+ ]
+ action["context"] = {
+ "default_directory_id": self.id,
+ "dms_directory_id": self.id,
+ "searchpanel_default_directory_id": self.id,
+ }
return action
diff --git a/dms/models/dms_file.py b/dms/models/dms_file.py
index 544201498..1d141fd6f 100644
--- a/dms/models/dms_file.py
+++ b/dms/models/dms_file.py
@@ -54,6 +54,7 @@ class DMSFile(models.Model):
required=True,
index="btree",
tracking=True, # Leave log if "moved" to another directory
+ default=lambda self: self.env.context.get("dms_directory_id"),
)
root_directory_id = fields.Many2one(related="directory_id.root_directory_id")
# Override acording to defined in AbstractDmsMixin
@@ -568,6 +569,8 @@ def _create_model_attachment(self, vals):
directory_id = self.env.context.get("active_id")
elif self.env.context.get("default_directory_id"):
directory_id = self.env.context.get("default_directory_id")
+ elif self.env.context.get("dms_directory_id"):
+ directory_id = self.env.context.get("dms_directory_id")
directory = self.env["dms.directory"].browse(directory_id)
if (
directory.res_model
diff --git a/dms/models/storage.py b/dms/models/storage.py
index 7f29d3bdc..4e5e1d621 100644
--- a/dms/models/storage.py
+++ b/dms/models/storage.py
@@ -7,6 +7,7 @@
from odoo import api, fields, models
from odoo.exceptions import AccessError
+from odoo.fields import Domain
_logger = logging.getLogger(__name__)
@@ -88,7 +89,7 @@ class Storage(models.Model):
def _search_model(self, operator, value):
allowed_items = self.env["ir.model"].sudo().search([("model", operator, value)])
- return [("model_ids", "in", allowed_items.ids)]
+ return Domain([("model_ids", "in", allowed_items.ids)])
@api.onchange("save_type")
def _onchange_save_type(self):
diff --git a/dms/tests/test_directory.py b/dms/tests/test_directory.py
index 3f69b5df7..b08473152 100644
--- a/dms/tests/test_directory.py
+++ b/dms/tests/test_directory.py
@@ -8,7 +8,7 @@
from odoo import Command
from odoo.exceptions import AccessError, UserError
-from odoo.tests import new_test_user
+from odoo.tests import Form, new_test_user
from odoo.tests.common import users
from odoo.tools import mute_logger
@@ -41,6 +41,19 @@ def test_create_directory(self):
msg="The root directory should have one subdirectory",
)
+ def test_default_parent_from_context(self):
+ action = self.directory.action_dms_directories_all_directory()
+ action_context = action["context"].copy()
+ action_context.pop("default_parent_id")
+ directory_form = Form(self.directory_model.with_context(**action_context))
+ self.assertEqual(directory_form.parent_id, self.directory)
+
+ action = self.directory.action_dms_files_all_directory()
+ action_context = action["context"].copy()
+ action_context.pop("default_directory_id")
+ file_form = Form(self.file_model.with_context(**action_context))
+ self.assertEqual(file_form.directory_id, self.directory)
+
@users("dms-manager", "dms-user")
def test_copy_root_directory(self):
copy_root_directory = self.directory.copy()
diff --git a/dms/tests/test_file.py b/dms/tests/test_file.py
index e91526271..bb2cae185 100644
--- a/dms/tests/test_file.py
+++ b/dms/tests/test_file.py
@@ -47,6 +47,21 @@ def setUpClass(cls):
)
cls.directory_group_a.group_ids = [(4, cls.group_a.id)]
cls.file2 = cls.create_file(directory=cls.sub_directory_x)
+ cls.readonly_user = new_test_user(
+ cls.env, login="read-only", groups="dms.group_dms_user"
+ )
+ cls.readonly_group = cls.access_group_model.create(
+ {
+ "name": "Read only",
+ "explicit_user_ids": [(6, 0, [cls.readonly_user.id])],
+ }
+ )
+ cls.readonly_directory = cls.create_directory(storage=cls.storage)
+ cls.readonly_directory.group_ids = [(6, 0, cls.readonly_group.ids)]
+ cls.readonly_subdirectory = cls.create_directory(
+ directory=cls.readonly_directory
+ )
+ cls.readonly_file = cls.create_file(directory=cls.readonly_subdirectory)
@users("user-a")
def test_unaccessible_file(self):
@@ -140,6 +155,36 @@ def test_record_level_access(self):
}
)
+ @users("read-only")
+ @mute_logger("odoo.addons.base.models.ir_rule", "odoo.models")
+ def test_read_only_access(self):
+ """Read-only access groups must not grant mutation permissions."""
+ readonly_file = self.readonly_file.with_user(self.env.user)
+ readonly_file.check_access("read")
+ for operation in ("write", "unlink"):
+ with self.assertRaises(
+ AccessError, msg=f"read-only user {operation} must be denied"
+ ):
+ readonly_file.check_access(operation)
+ with self.assertRaises(AccessError, msg="read-only file write must fail"):
+ readonly_file.write({"name": "forbidden.txt"})
+ with self.assertRaises(AccessError, msg="read-only file create must fail"):
+ self.file_model.with_user(self.env.user).create(
+ {
+ "name": "forbidden.txt",
+ "directory_id": self.readonly_subdirectory.id,
+ "content": self.content_base64(),
+ }
+ )
+ with self.assertRaises(AccessError, msg="read-only directory create must fail"):
+ self.directory_model.with_user(self.env.user).create(
+ {
+ "name": "Forbidden child",
+ "is_root_directory": False,
+ "parent_id": self.readonly_directory.id,
+ }
+ )
+
@users("dms-manager", "dms-user")
@mute_logger("odoo.models.unlink")
def test_content_file(self):
diff --git a/dms/tests/test_portal.py b/dms/tests/test_portal.py
index ee6841b4d..12f397924 100644
--- a/dms/tests/test_portal.py
+++ b/dms/tests/test_portal.py
@@ -1,7 +1,10 @@
# Copyright 2021-2025 Tecnativa - Víctor Martínez
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl)
+import json
+
import odoo.tests
+from odoo import http
from odoo.exceptions import AccessError
from odoo.tests.common import new_test_user, users
from odoo.tools import mute_logger
@@ -86,6 +89,46 @@ def test_access_portal(self):
self.assertEqual(
response.status_code, 200, "Can access directory with correct access_token"
)
+ # A direct binary URL must not bypass DMS access checks.
+ response = self.url_open(
+ f"/web/content?id={self.other_file_partner.id}&field=content"
+ "&model=dms.file&filename_field=name&download=true",
+ timeout=20,
+ )
+ self.assertNotEqual(
+ response.status_code,
+ 200,
+ "Can't download a restricted file through a direct URL",
+ )
+
+ def test_upload_and_download(self):
+ upload_directory = self.create_directory(storage=self.create_storage())
+ self.authenticate("dms-manager", "dms-manager")
+ response = self.url_open(
+ "/web/binary/upload_dms_file",
+ data={
+ "csrf_token": http.Request.csrf_token(self),
+ "directory_id": upload_directory.id,
+ },
+ files={
+ "ufile": (
+ "uploaded.txt",
+ b"Odoo 19 DMS upload",
+ "text/plain",
+ )
+ },
+ )
+ response.raise_for_status()
+ result = json.loads(response.content)
+ self.assertFalse(result[0].get("error"))
+ dms_file = self.file_model.browse(result[0]["id"])
+ self.assertEqual(dms_file.name, "uploaded.txt")
+ download = self.url_open(
+ f"/web/content?id={dms_file.id}&field=content&model=dms.file"
+ "&filename_field=name&download=true"
+ )
+ download.raise_for_status()
+ self.assertEqual(download.content, b"Odoo 19 DMS upload")
def test_tour(self):
for tour in ("dms_portal_mail_tour", "dms_portal_partners_tour"):
diff --git a/dms/views/dms_directory.xml b/dms/views/dms_directory.xml
index 4d048a53f..ddde91de1 100644
--- a/dms/views/dms_directory.xml
+++ b/dms/views/dms_directory.xml
@@ -306,7 +306,10 @@
widget="many2many_tags"
options="{'color_field': 'color'}"
/>
-
+