From f72472e0c654820975223e3a633d398f03c16a07 Mon Sep 17 00:00:00 2001 From: Don Kendall Date: Fri, 19 Jun 2026 12:39:11 -0400 Subject: [PATCH 1/3] [IMP] dms: pre-commit auto fixes --- dms/README.rst | 140 +++++++++++++++--------------- dms/static/description/index.html | 60 +++++++------ dms/test/markdown.md | 1 - 3 files changed, 105 insertions(+), 96 deletions(-) diff --git a/dms/README.rst b/dms/README.rst index 2e770f84a..3aaae5e23 100644 --- a/dms/README.rst +++ b/dms/README.rst @@ -1,3 +1,7 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + ========================== Document Management System ========================== @@ -7,23 +11,23 @@ Document Management System !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:966c4331ff7c75b1ea8cb1d065c878d81250957cd305a5d6422def133e2a7d63 + !! source digest: sha256:451b6d4ca876833e0ac116a7abe59895abb355b15d825ea00d205611a4be8c70 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |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/licence-LGPL--3-blue.png +.. |badge2| image:: https://img.shields.io/badge/license-LGPL--3-blue.png :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html :alt: License: LGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fdms-lightgray.png?logo=github - :target: https://github.com/OCA/dms/tree/18.0/dms + :target: https://github.com/OCA/dms/tree/19.0/dms :alt: OCA/dms .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/dms-18-0/dms-18-0-dms + :target: https://translation.odoo-community.org/projects/dms-19-0/dms-19-0-dms :alt: Translate me on Weblate .. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png - :target: https://runboat.odoo-community.org/builds?repo=OCA/dms&target_branch=18.0 + :target: https://runboat.odoo-community.org/builds?repo=OCA/dms&target_branch=19.0 :alt: Try me on Runboat |badge1| |badge2| |badge3| |badge4| |badge5| @@ -68,9 +72,9 @@ To configure this module, you need to: 2. Create a new document storage. You can choose between three options on ``Save Type``: - - ``Database``: Store the files on the database as a field - - ``Attachment``: Store the files as attachments - - ``File``: Store the files on the file system + - ``Database``: Store the files on the database as a field + - ``Attachment``: Store the files as attachments + - ``File``: Store the files on the file system 2. Create an access group ------------------------- @@ -78,13 +82,13 @@ To configure this module, you need to: 1. Next, create an administrative access group. Go to *Configuration -> Access Groups*. - - Create a new group, name it appropriately, and turn on all three - permissions (Create, Write and Unlink. Read is implied and always - enabled). - - Add any other top-level administrative users to the group if - needed (your user should already be there). - - You can create other groups in here later for fine-grained access - control. + - Create a new group, name it appropriately, and turn on all three + permissions (Create, Write and Unlink. Read is implied and always + enabled). + - Add any other top-level administrative users to the group if needed + (your user should already be there). + - You can create other groups in here later for fine-grained access + control. 3. Create a directory --------------------- @@ -94,18 +98,18 @@ To configure this module, you need to: 2. Create a new directory, mark it as root and select the previously created setting. - - Select the *Groups* tab and add your administrative group created - above. If your directory was already created before the group, you - can also add it in the access groups (*Configuration -> Access - Groups*). + - Select the *Groups* tab and add your administrative group created + above. If your directory was already created before the group, you + can also add it in the access groups (*Configuration -> Access + Groups*). 3. In the directory settings, you can also add other access groups (created above) that will be able to: - - read - - create - - write - - delete + - read + - create + - write + - delete Migration ========= @@ -151,28 +155,28 @@ access to that resource, no matter if logged or not. Known issues / Roadmap ====================== -- Files preview in portal -- Allow to download folder in portal and create zip file with all - content -- Save in cache own_root directories and update in every - create/write/unlink function -- Add a migration procedure for converting an storage to attachment one - for populating existing records with attachments as folders -- Add a link from attachment view in chatter to linked documents -- If Inherit permissions from related record (the - inherit_access_from_parent_record field from storage) is changed when - directories already exist, inconsistencies may occur because groups - defined in the directories and subdirectories will still exist, all - groups in these directories should be removed before changing. -- Since portal users can read ``dms.storage`` records, if your module - extends this model to another storage backend that needs using - secrets, remember to forbid access to the secrets fields by other - means. It would be nice to be able to remove that rule at some point. -- Searchpanel in files: Highlight items (shading) without records when - filtering something (by name for example). -- Accessing the clipboard (for example copy share link of - file/directory) is limited to secure connections. It also happens in - any part of Odoo. +- Files preview in portal +- Allow to download folder in portal and create zip file with all + content +- Save in cache own_root directories and update in every + create/write/unlink function +- Add a migration procedure for converting an storage to attachment one + for populating existing records with attachments as folders +- Add a link from attachment view in chatter to linked documents +- If Inherit permissions from related record (the + inherit_access_from_parent_record field from storage) is changed when + directories already exist, inconsistencies may occur because groups + defined in the directories and subdirectories will still exist, all + groups in these directories should be removed before changing. +- Since portal users can read ``dms.storage`` records, if your module + extends this model to another storage backend that needs using + secrets, remember to forbid access to the secrets fields by other + means. It would be nice to be able to remove that rule at some point. +- Searchpanel in files: Highlight items (shading) without records when + filtering something (by name for example). +- Accessing the clipboard (for example copy share link of + file/directory) is limited to secure connections. It also happens in + any part of Odoo. Bug Tracker =========== @@ -180,7 +184,7 @@ Bug Tracker Bugs are tracked on `GitHub Issues `_. 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 -`feedback `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -196,40 +200,40 @@ Authors Contributors ------------ -- Mathias Markl -- Enric Tobella -- Antoni Romera -- Gelu Boros -- `Tecnativa `__: +- Mathias Markl +- Enric Tobella +- Antoni Romera +- Gelu Boros +- `Tecnativa `__: - - Víctor Martínez - - Pedro M. Baeza - - Jairo Llopis + - Víctor Martínez + - Pedro M. Baeza + - Jairo Llopis -- `Elego `__: +- `Elego `__: - - Yu Weng - - Philip Witte - - Khanh Bui + - Yu Weng + - Philip Witte + - Khanh Bui -- `Subteno `__: +- `Subteno `__: - - Timothée Vannier + - Timothée Vannier -- `Kencove `__: +- `Kencove `__: - - Mohamed Alkobrosli + - Mohamed Alkobrosli Other credits ------------- Some pictures are based on or inspired by: -- `Roundicons `__ -- `Smashicons `__ -- `EmojiOne `__ : Portal DMS icon -- `GitHub Octicons `__ : The main - DMS icon +- `Roundicons `__ +- `Smashicons `__ +- `EmojiOne `__ : Portal DMS icon +- `GitHub Octicons `__ : The main + DMS icon Maintainers ----------- @@ -244,6 +248,6 @@ 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. -This module is part of the `OCA/dms `_ project on GitHub. +This module is part of the `OCA/dms `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/dms/static/description/index.html b/dms/static/description/index.html index e4b0c4fbe..206c97a33 100644 --- a/dms/static/description/index.html +++ b/dms/static/description/index.html @@ -3,7 +3,7 @@ -Document Management System +README.rst -
-

Document Management System

+
+ + +Odoo Community Association + +
+

Document Management System

-

Beta License: LGPL-3 OCA/dms Translate me on Weblate Try me on Runboat

+

Beta License: LGPL-3 OCA/dms Translate me on Weblate Try me on Runboat

DMS is a module for creating, managing and viewing document files directly within Odoo. This module is only the basis for an entire ecosystem of apps that extend and seamlessly integrate with the document @@ -410,21 +415,21 @@

Document Management System

-

Installation

+

Installation

-

Preview

+

Preview

python-magic library is recommended to be installed for having whole support to get proper file types and file preview.

-

Configuration

+

Configuration

To configure this module, you need to:

-

1. Create a storage

+

1. Create a storage

  1. Go to Documents -> Configuration -> Storages.
  2. Create a new document storage. You can choose between three options @@ -437,15 +442,15 @@

    1. Create a storage

-

2. Create an access group

+

2. Create an access group

  1. Next, create an administrative access group. Go to Configuration -> Access Groups.
    • Create a new group, name it appropriately, and turn on all three permissions (Create, Write and Unlink. Read is implied and always enabled).
    • -
    • Add any other top-level administrative users to the group if -needed (your user should already be there).
    • +
    • Add any other top-level administrative users to the group if needed +(your user should already be there).
    • You can create other groups in here later for fine-grained access control.
    @@ -453,7 +458,7 @@

    2. Create an access group

-

3. Create a directory

+

3. Create a directory

  1. Afterward, go to Documents -> Directories.
  2. Create a new directory, mark it as root and select the previously @@ -476,7 +481,7 @@

    3. Create a directory

-

Migration

+

Migration

If you need to modify the storage Save Type you might want to migrate the file data. To achieve it, you need to:

    @@ -492,19 +497,19 @@

    Migration

    Migration

-

File Wizard Selection

+

File Wizard Selection

There is an action called action_dms_file_wizard_selector to open a wizard to list files in kanban view. This can be used (example dms_attachment_link module) to add a button in kanban view with the action we need.

-

Usage

+

Usage

The best way to manage the documents is to switch to the Documents view. Existing documents can be managed there and new documents can be created.

-

Portal functionality

+

Portal functionality

You can add any portal user to DMS access groups, and then allow that group in directories, so they will see in the portal such directories and their files. Another possibility is to click on “Share” button @@ -513,7 +518,7 @@

Portal functionality

-

Known issues / Roadmap

+

Known issues / Roadmap

  • Files preview in portal
  • Allow to download folder in portal and create zip file with all @@ -540,24 +545,24 @@

    Known issues / Roadmap

-

Bug Tracker

+

Bug Tracker

Bugs are tracked on GitHub Issues. 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 -feedback.

+feedback.

Do not contact contributors directly about support or help with technical issues.

-

Credits

+

Credits

-

Authors

+

Authors

  • MuK IT
  • Tecnativa
-

Contributors

+

Contributors

-

Other credits

+

Other credits

Some pictures are based on or inspired by:

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -605,10 +610,11 @@

Maintainers

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.

-

This module is part of the OCA/dms project on GitHub.

+

This module is part of the OCA/dms project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
diff --git a/dms/test/markdown.md b/dms/test/markdown.md index 721d2b1a4..fab330593 100644 --- a/dms/test/markdown.md +++ b/dms/test/markdown.md @@ -58,7 +58,6 @@ for i in range(10): Now a nested list: 1. First, get these ingredients: - - carrots - celery - lentils From bdc88aab43e3abbb602c6161580e6782f122e459 Mon Sep 17 00:00:00 2001 From: Don Kendall Date: Fri, 19 Jun 2026 14:17:22 -0400 Subject: [PATCH 2/3] [MIG] dms: Migration to 19.0 --- checklog-odoo.cfg | 1 + dms/__manifest__.py | 4 +- dms/controllers/main.py | 14 +++-- dms/controllers/portal.py | 18 +++--- dms/demo/res_users.xml | 2 +- dms/models/access_groups.py | 31 +++++----- dms/models/directory.py | 56 +++++++++++-------- dms/models/dms_category.py | 13 +++-- dms/models/dms_file.py | 32 +++++++---- dms/models/dms_security_mixin.py | 46 +++++++++------ dms/models/storage.py | 10 ++-- dms/models/tag.py | 9 +-- dms/security/security.xml | 10 +++- .../preview_binary/preview_record.esm.js | 2 +- .../src/js/views/dms_file_upload.esm.js | 2 +- .../js/views/file_kanban_controller.esm.js | 12 ---- .../src/js/views/file_kanban_controller.xml | 25 --------- .../src/js/views/file_kanban_record.esm.js | 2 +- .../src/js/views/file_kanban_renderer.xml | 14 ++--- .../src/js/views/file_list_renderer.xml | 6 +- dms/static/tests/tours/dms_portal_tour.esm.js | 13 +++-- dms/tests/common.py | 13 +++++ dms/tests/test_directory.py | 2 +- dms/tests/test_file.py | 18 ++++-- dms/tests/test_file_database.py | 15 ++++- dms/tests/test_portal.py | 52 +++++++++++++++-- dms/tests/test_storage_attachment.py | 4 +- dms/views/dms_directory.xml | 2 +- dms/views/dms_file.xml | 6 +- dms/views/dms_tag.xml | 2 +- dms/views/res_config_settings.xml | 1 - dms/views/storage.xml | 2 +- 32 files changed, 265 insertions(+), 174 deletions(-) delete mode 100644 dms/static/src/js/views/file_kanban_controller.esm.js delete mode 100644 dms/static/src/js/views/file_kanban_controller.xml diff --git a/checklog-odoo.cfg b/checklog-odoo.cfg index 0b55b7bf6..486495855 100644 --- a/checklog-odoo.cfg +++ b/checklog-odoo.cfg @@ -1,3 +1,4 @@ [checklog-odoo] ignore= WARNING.* 0 failed, 0 error\(s\).* + WARNING .* Killing chrome descendants-or-self .* diff --git a/dms/__manifest__.py b/dms/__manifest__.py index 595254d23..0f69a1936 100644 --- a/dms/__manifest__.py +++ b/dms/__manifest__.py @@ -4,8 +4,8 @@ { "name": "Document Management System", - "summary": """Document Management System for Odoo""", - "version": "18.0.1.0.0", + "summary": "Document Management System for Odoo", + "version": "19.0.1.0.0", "category": "Document Management", "license": "LGPL-3", "website": "https://github.com/OCA/dms", diff --git a/dms/controllers/main.py b/dms/controllers/main.py index cf2fb612d..38e982214 100644 --- a/dms/controllers/main.py +++ b/dms/controllers/main.py @@ -4,7 +4,7 @@ import json import unicodedata -from odoo import _, http +from odoo import http from odoo.exceptions import AccessError from odoo.http import request @@ -12,7 +12,7 @@ class OnboardingController(http.Controller): - @http.route("/config/dms.forbidden_extensions", type="json", auth="user") + @http.route("/config/dms.forbidden_extensions", type="jsonrpc", auth="user") def forbidden_extensions(self, **_kwargs): params = request.env["ir.config_parameter"].sudo() return { @@ -49,9 +49,15 @@ def upload_dms_file(self, ufile, directory_id, callback=None): } ) except AccessError: - args.append({"error": _("You are not allowed to upload a file here.")}) + args.append( + { + "error": request.env._( + "You are not allowed to upload a file here." + ) + } + ) except Exception: - args.append({"error": _("Something horrible happened")}) + args.append({"error": request.env._("Something horrible happened")}) else: args.append( { diff --git a/dms/controllers/portal.py b/dms/controllers/portal.py index e1eaa7d64..9e8caef52 100644 --- a/dms/controllers/portal.py +++ b/dms/controllers/portal.py @@ -4,9 +4,9 @@ import base64 from typing import Optional # noqa # pylint: disable=unused-import -from odoo import _, http +from odoo import http +from odoo.fields import Domain from odoo.http import content_disposition, request -from odoo.osv.expression import OR from odoo.addons.portal.controllers.portal import CustomerPortal from odoo.addons.web.controllers.utils import ensure_db @@ -64,12 +64,12 @@ def portal_my_dms( sortby, ) = self._searchbar_data(filterby, sortby) # domain - domain = [ - ("id", "in", request.env["dms.directory"]._get_own_root_directories()), - ] + domain = Domain( + [("id", "in", request.env["dms.directory"]._get_own_root_directories())] + ) # search if search and search_in == "name": - domain += OR([[], [("name", "ilike", search)]]) + domain = Domain.AND([domain, Domain.OR([[], [("name", "ilike", search)]])]) # content according to pager and archive selected items = request.env["dms.directory"].search(domain, order=sort_order) request.session["my_dms_folder_history"] = items.ids @@ -235,14 +235,16 @@ def _searchbar_data(self, filterby, sortby): sortby :rtype: tuple[str, dict, dict, str, str] """ - searchbar_sortings = {"name": {"label": _("Name"), "order": "name asc"}} + searchbar_sortings = { + "name": {"label": request.env._("Name"), "order": "name asc"} + } # default sortby if not sortby: sortby = "name" sort_order = searchbar_sortings[sortby]["order"] # search searchbar_inputs = { - "name": {"input": "name", "label": _("Name")}, + "name": {"input": "name", "label": request.env._("Name")}, } if not filterby: filterby = "name" diff --git a/dms/demo/res_users.xml b/dms/demo/res_users.xml index c586a6007..ca02b6a9a 100644 --- a/dms/demo/res_users.xml +++ b/dms/demo/res_users.xml @@ -7,6 +7,6 @@ --> - + diff --git a/dms/models/access_groups.py b/dms/models/access_groups.py index 71113d28b..899c93026 100644 --- a/dms/models/access_groups.py +++ b/dms/models/access_groups.py @@ -3,7 +3,7 @@ # Copyright 2024 Timothée Vannier - Subteno (https://www.subteno.com). # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -from odoo import _, api, fields, models +from odoo import api, fields, models from odoo.exceptions import ValidationError @@ -47,7 +47,7 @@ class DmsAccessGroups(models.Model): string="Directories", column1="gid", column2="aid", - auto_join=True, + bypass_search_access=True, readonly=True, ) complete_directory_ids = fields.Many2many( @@ -56,7 +56,7 @@ class DmsAccessGroups(models.Model): column1="gid", column2="aid", string="Complete directories", - auto_join=True, + bypass_search_access=True, readonly=True, ) count_users = fields.Integer(compute="_compute_users", store=True) @@ -94,7 +94,7 @@ class DmsAccessGroups(models.Model): column2="uid", string="Group Users", compute="_compute_users", - auto_join=True, + bypass_search_access=True, store=True, recursive=True, ) @@ -104,9 +104,10 @@ def _compute_count_directories(self): for record in self: record.count_directories = len(record.directory_ids) - _sql_constraints = [ - ("name_uniq", "unique (name)", "The name of the group must be unique!") - ] + _name_uniq = models.Constraint( + "unique (name)", + "The name of the group must be unique!", + ) @api.depends( "parent_group_id.perm_inclusive_create", @@ -143,13 +144,13 @@ def default_get(self, fields_list): "parent_group_id", "parent_group_id.users", "group_ids", - "group_ids.users", + "group_ids.user_ids", "explicit_user_ids", ) def _compute_users(self): for record in self: users = ( - record.group_ids.users + record.group_ids.user_ids | record.explicit_user_ids | record.parent_group_id.users ) @@ -158,7 +159,7 @@ def _compute_users(self): def copy_data(self, default=None): vals_list = super().copy_data(default) for group, vals in zip(self, vals_list, strict=False): - vals["name"] = _("%s (copy)") % group.name + vals["name"] = self.env._("%s (copy)", group.name) return vals_list @api.constrains("parent_path") @@ -169,9 +170,9 @@ def _check_parent_recursiveness(self): for one in self.filtered("parent_group_id"): if str(one.id) in one.parent_path.split("/"): raise ValidationError( - _("Parent group '%(parent)s' is child of '%(current)s'.") - % { - "parent": one.parent_group_id.display_name, - "current": one.display_name, - } + self.env._( + "Parent group '%(parent)s' is child of '%(current)s'.", + parent=one.parent_group_id.display_name, + current=one.display_name, + ) ) diff --git a/dms/models/directory.py b/dms/models/directory.py index 34127d2d2..2e1330e02 100644 --- a/dms/models/directory.py +++ b/dms/models/directory.py @@ -12,9 +12,9 @@ from collections import defaultdict from typing import Literal # noqa # pylint: disable=unused-import -from odoo import _, api, fields, models, tools +from odoo import api, fields, models, tools from odoo.exceptions import UserError, ValidationError -from odoo.osv.expression import AND, OR +from odoo.fields import Domain from odoo.tools import consteq, human_size from ..tools.file import check_name, unique_name @@ -60,7 +60,7 @@ class DmsDirectory(models.Model): comodel_name="dms.storage", string="Storage", ondelete="restrict", - auto_join=True, + bypass_search_access=True, store=True, ) parent_id = fields.Many2one( @@ -116,7 +116,7 @@ def _default_parent_id(self): comodel_name="dms.directory", inverse_name="parent_id", string="Subdirectories", - auto_join=False, + bypass_search_access=False, copy=True, ) @@ -153,7 +153,7 @@ def _default_parent_id(self): comodel_name="dms.file", inverse_name="directory_id", string="Files", - auto_join=False, + bypass_search_access=False, copy=True, ) @@ -221,7 +221,7 @@ def _get_domain_by_access_groups(self, operation): if operation == "create": # When creating, I need create access in parent directory, or # self-create permission if it's a root directory - result = OR( + result = Domain.OR( [ [("is_root_directory", "=", False)] + result, [("is_root_directory", "=", True)] + self_filter, @@ -379,7 +379,7 @@ def _search_panel_directory(self, **kwargs): # Search @api.model def _search_starred(self, operator, operand): - if operator == "=" and operand: + if operator in ("=", "in") and operand: return [("user_star_ids", "in", [self.env.uid])] return [("user_star_ids", "not in", [self.env.uid])] @@ -412,14 +412,16 @@ def _compute_count_directories(self): for record in self: directories = len(record.child_directory_ids) record.count_directories = directories - record.count_directories_title = _("%s Subdirectories") % directories + record.count_directories_title = self.env._( + "%s Subdirectories", directories + ) @api.depends("file_ids") def _compute_count_files(self): for record in self: files = len(record.file_ids) record.count_files = files - record.count_files_title = _("%s Files") % files + record.count_files_title = self.env._("%s Files", files) @api.depends("child_directory_ids", "file_ids") def _compute_count_elements(self): @@ -528,7 +530,9 @@ def _onchange_model_id(self): @api.constrains("parent_id") def _check_directory_recursion(self): if self._has_cycle(): - raise ValidationError(_("Error! You cannot create recursive directories.")) + raise ValidationError( + self.env._("Error! You cannot create recursive directories.") + ) return True @api.constrains("storage_id", "model_id") @@ -538,34 +542,40 @@ def _check_storage_id_attachment_model_id(self): ): if not record.model_id: raise ValidationError( - _("A directory has to have model in attachment storage.") + self.env._("A directory has to have model in attachment storage.") ) if not record.is_root_directory and not record.res_id: raise ValidationError( - _("This directory needs to be associated to a record.") + self.env._("This directory needs to be associated to a record.") ) @api.constrains("is_root_directory", "storage_id") def _check_directory_storage(self): for record in self: if record.is_root_directory and not record.storage_id: - raise ValidationError(_("A root directory has to have a storage.")) + raise ValidationError( + self.env._("A root directory has to have a storage.") + ) @api.constrains("is_root_directory", "parent_id") def _check_directory_parent(self): for record in self: if record.is_root_directory and record.parent_id: raise ValidationError( - _("A directory can't be a root and have a parent directory.") + self.env._( + "A directory can't be a root and have a parent directory." + ) ) if not record.is_root_directory and not record.parent_id: - raise ValidationError(_("A directory has to have a parent directory.")) + raise ValidationError( + self.env._("A directory has to have a parent directory.") + ) @api.constrains("name") def _check_name(self): for record in self: if self.env.context.get("check_name", True) and not check_name(record.name): - raise ValidationError(_("The directory name is invalid.")) + raise ValidationError(self.env._("The directory name is invalid.")) if record.is_root_directory: children = record.sudo().storage_id.root_directory_ids else: @@ -576,7 +586,7 @@ def _check_name(self): and child != record ): raise ValidationError( - _("A directory with the same name already exists.") + self.env._("A directory with the same name already exists.") ) # Create, Update, Delete @@ -626,7 +636,7 @@ def message_new(self, msg_dict, custom_values=None): return parent_directory names = parent_directory.child_directory_ids.mapped("name") slug = self.env["ir.http"]._slug - subject = slug(msg_dict.get("subject", _("Alias-Mail-Extraction"))) + subject = slug(msg_dict.get("subject", self.env._("Alias-Mail-Extraction"))) defaults = dict( {"name": unique_name(subject, names, escape_suffix=True)}, **custom_values ) @@ -678,13 +688,15 @@ def write(self, vals): if new_parent_id: if old_storage_id != self.browse(new_parent_id).storage_id.id: raise UserError( - _( + self.env._( "It is not possible to change to a parent " "with other storage." ) ) elif old_storage_id != new_storage_id: - raise UserError(_("It is not possible to change the storage.")) + raise UserError( + self.env._("It is not possible to change the storage.") + ) # Groups part if any(key in vals for key in ["group_ids", "inherit_group_ids"]): res = super().write(vals) @@ -743,7 +755,7 @@ def action_dms_directories_all_directory(self): action = self.env["ir.actions.act_window"]._for_xml_id( "dms.action_dms_directory" ) - domain = AND( + domain = Domain.AND( [ literal_eval(action["domain"].strip()), [("parent_id", "child_of", self.id)], @@ -761,7 +773,7 @@ def action_dms_directories_all_directory(self): 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 = AND( + domain = Domain.AND( [ literal_eval(action["domain"].strip()), [("directory_id", "child_of", self.id)], diff --git a/dms/models/dms_category.py b/dms/models/dms_category.py index 5900f4d93..575b01f3f 100644 --- a/dms/models/dms_category.py +++ b/dms/models/dms_category.py @@ -5,7 +5,7 @@ import logging -from odoo import _, api, fields, models +from odoo import api, fields, models from odoo.exceptions import ValidationError _logger = logging.getLogger(__name__) @@ -63,9 +63,10 @@ class DMSCategory(models.Model): count_directories = fields.Integer(compute="_compute_count_directories") count_files = fields.Integer(compute="_compute_count_files") - _sql_constraints = [ - ("name_uniq", "unique (name)", "Category name already exists!"), - ] + _name_uniq = models.Constraint( + "unique (name)", + "Category name already exists!", + ) @api.depends("name", "parent_id.complete_name") def _compute_complete_name(self): @@ -100,5 +101,7 @@ def _compute_count_files(self): @api.constrains("parent_id") def _check_category_recursion(self): if self._has_cycle(): - raise ValidationError(_("Error! You cannot create recursive categories.")) + raise ValidationError( + self.env._("Error! You cannot create recursive categories.") + ) return True diff --git a/dms/models/dms_file.py b/dms/models/dms_file.py index 302b0bb79..544201498 100644 --- a/dms/models/dms_file.py +++ b/dms/models/dms_file.py @@ -12,9 +12,9 @@ from PIL import Image -from odoo import _, api, fields, models, tools +from odoo import api, fields, models, tools from odoo.exceptions import UserError, ValidationError -from odoo.osv import expression +from odoo.fields import Domain from odoo.tools import consteq, human_size from odoo.tools.mimetypes import guess_mimetype @@ -50,7 +50,7 @@ class DMSFile(models.Model): domain="[('permission_create', '=', True)]", context={"dms_directory_show_path": True}, ondelete="restrict", - auto_join=True, + bypass_search_access=True, required=True, index="btree", tracking=True, # Leave log if "moved" to another directory @@ -250,7 +250,7 @@ def action_migrate(self, should_logging=True): for dms_file in self: if should_logging: _logger.info( - _( + self.env._( "Migrate File %(index)s of %(record_count)s [ %(" "dms_file_migration)s ]", index=index, @@ -275,7 +275,9 @@ def action_wizard_dms_file_move(self): items = self.browse(self.env.context.get("active_ids")) root_directories = items.mapped("root_directory_id") if len(root_directories) > 1: - raise UserError(_("Only files in the same root directory can be moved.")) + raise UserError( + self.env._("Only files in the same root directory can be moved.") + ) result = self.env["ir.actions.act_window"]._for_xml_id( "dms.wizard_dms_file_move_act_window" ) @@ -300,7 +302,7 @@ def _search_panel_domain(self, field, operator, directory_id, comodel_domain=Fal if not comodel_domain: comodel_domain = [] files_ids = self.search([("directory_id", operator, directory_id)]).ids - return expression.AND([comodel_domain, [(field, "in", files_ids)]]) + return Domain.AND([comodel_domain, [(field, "in", files_ids)]]) @api.model def search_panel_select_range(self, field_name, **kwargs): @@ -505,20 +507,24 @@ def _check_storage_id_attachment_res_model(self): record.res_model and record.res_id ): raise ValidationError( - _("A file must have model and resource ID in attachment storage.") + self.env._( + "A file must have model and resource ID in attachment storage." + ) ) @api.constrains("name") def _check_name(self): for record in self: if not file.check_name(record.name): - raise ValidationError(_("The file name is invalid.")) + raise ValidationError(self.env._("The file name is invalid.")) files = record.sudo().directory_id.file_ids if files.filtered( lambda file, record=record: file.name == record.name and file != record ): raise ValidationError( - _("A file with the same name already exists in this directory.") + self.env._( + "A file with the same name already exists in this directory." + ) ) @api.constrains("extension") @@ -527,7 +533,9 @@ def _check_extension(self): lambda rec: rec.extension and rec.extension in self._get_forbidden_extensions() ): - raise ValidationError(_("The file has a forbidden file extension.")) + raise ValidationError( + self.env._("The file has a forbidden file extension.") + ) @api.constrains("size") def _check_size(self): @@ -535,7 +543,9 @@ def _check_size(self): lambda rec: rec.size > self._get_binary_max_size() * 1024 * 1024 ): raise ValidationError( - _("The maximum upload size is %s MB.") % self._get_binary_max_size() + self.env._( + "The maximum upload size is %s MB.", self._get_binary_max_size() + ) ) # Create, Update, Delete diff --git a/dms/models/dms_security_mixin.py b/dms/models/dms_security_mixin.py index 87f65a449..13e02f2e9 100644 --- a/dms/models/dms_security_mixin.py +++ b/dms/models/dms_security_mixin.py @@ -8,12 +8,7 @@ from odoo import api, fields, models from odoo.exceptions import AccessError -from odoo.osv.expression import ( - FALSE_DOMAIN, - NEGATIVE_TERM_OPERATORS, - OR, - TRUE_DOMAIN, -) +from odoo.fields import Domain from odoo.tools import SQL _logger = getLogger(__name__) @@ -60,6 +55,8 @@ class DmsSecurityMixin(models.AbstractModel): @api.model def _get_ref_selection(self): + # All registered models are an intentional choice here. + # pylint: disable=no-search-all models = self.env["ir.model"].sudo().search([]) return [(model.model, model.name) for model in models] @@ -116,14 +113,14 @@ def _get_domain_by_inheritance(self, operation): ] domains = [] # Get all used related records - related_groups = self.sudo().read_group( + related_groups = self.sudo()._read_group( domain=inherited_access_domain + [("res_model", "!=", False)], - fields=["res_id:array_agg"], groupby=["res_model"], + aggregates=["res_id:array_agg"], ) - for group in related_groups: + for res_model, res_id_array in related_groups: try: - model = self.env[group["res_model"]] + model = self.env[res_model] except KeyError: # The model might not be registered. # This is normal if you are upgrading the database. @@ -131,7 +128,7 @@ def _get_domain_by_inheritance(self, operation): # These records will be accessible by DB users only. domains.append( [ - ("res_model", "=", group["res_model"]), + ("res_model", "=", res_model), (True, "=", self.env.user.has_group("base.group_user")), ] ) @@ -143,7 +140,7 @@ def _get_domain_by_inheritance(self, operation): continue domains.append([("res_model", "=", model._name), ("res_id", "=", False)]) # Check record access in batch too - res_ids = [i for i in group["res_id"] if i] # Hack to remove None res_id + res_ids = [i for i in res_id_array if i] # Hack to remove None res_id # Apply exists to skip records that do not exist. (e.g. a res.partner # deleted by database). model_records = model.browse(res_ids).exists() @@ -153,8 +150,7 @@ def _get_domain_by_inheritance(self, operation): domains.append( [("res_model", "=", model._name), ("res_id", "in", related_ok.ids)] ) - result = inherited_access_domain + OR(domains) - return result + return Domain.AND([Domain(inherited_access_domain), Domain.OR(domains)]) @api.model def _get_access_groups_query(self, operation): @@ -211,19 +207,19 @@ def _get_permission_domain(self, operator, value, operation): value = bool(value) # Tricky one, to know if you want to search # positive or negative access - positive = (operator not in NEGATIVE_TERM_OPERATORS) == bool(value) + positive = (operator not in Domain.NEGATIVE_OPERATORS) == bool(value) if _self.env.su: # You're SUPERUSER_ID - return TRUE_DOMAIN if positive else FALSE_DOMAIN + return Domain.TRUE if positive else Domain.FALSE - result = OR( + result = Domain.OR( [ _self._get_domain_by_access_groups(operation), _self._get_domain_by_inheritance(operation), ] ) if not positive: - result.insert(0, "!") + result = ~Domain(result) return result @api.model @@ -286,6 +282,20 @@ def _check_access_dms_record(self, operation: str) -> tuple | None: if any(x_id not in items.ids for x_id in self.ids): raise Rule._make_access_error(operation, (self - items)) + @api.model + def _search(self, domain, *args, **kwargs): + """Inject the DMS access-group + inheritance read filter into the search.""" + if not self.env.su and not self.env.context.get("dms_skip_access_filter"): + self = self.with_context(dms_skip_access_filter=True) + dms_domain = Domain.OR( + [ + self._get_domain_by_access_groups("read"), + self._get_domain_by_inheritance("read"), + ] + ) + domain = Domain.AND([Domain(domain), dms_domain]) + return super()._search(domain, *args, **kwargs) + @api.model_create_multi def create(self, vals_list): # Create as sudo to avoid testing creation permissions before DMS security diff --git a/dms/models/storage.py b/dms/models/storage.py index cbc74cf01..7f29d3bdc 100644 --- a/dms/models/storage.py +++ b/dms/models/storage.py @@ -5,7 +5,7 @@ import logging -from odoo import _, api, fields, models +from odoo import api, fields, models from odoo.exceptions import AccessError _logger = logging.getLogger(__name__) @@ -44,7 +44,7 @@ class Storage(models.Model): comodel_name="dms.directory", inverse_name="storage_id", string="Root Directories", - auto_join=False, + bypass_search_access=False, readonly=False, copy=False, ) @@ -52,7 +52,7 @@ class Storage(models.Model): comodel_name="dms.directory", inverse_name="storage_id", string="Directories", - auto_join=False, + bypass_search_access=False, readonly=True, copy=False, ) @@ -60,7 +60,7 @@ class Storage(models.Model): comodel_name="dms.file", inverse_name="storage_id", string="Files", - auto_join=False, + bypass_search_access=False, readonly=True, copy=False, ) @@ -100,7 +100,7 @@ def _onchange_save_type(self): def action_storage_migrate(self): if self.save_type != "attachment": if not self.env.user.has_group("dms.group_dms_manager"): - raise AccessError(_("Only managers can execute this action.")) + raise AccessError(self.env._("Only managers can execute this action.")) files = self.env["dms.file"].with_context(active_test=False).sudo() for record in self: diff --git a/dms/models/tag.py b/dms/models/tag.py index 25dfb9a57..dac0dc799 100644 --- a/dms/models/tag.py +++ b/dms/models/tag.py @@ -16,7 +16,7 @@ class Tag(models.Model): name = fields.Char(required=True, translate=True) active = fields.Boolean( default=True, - help="The active field allows you " "to hide the tag without removing it.", + help="The active field allows you to hide the tag without removing it.", ) category_id = fields.Many2one( comodel_name="dms.category", @@ -44,9 +44,10 @@ class Tag(models.Model): count_directories = fields.Integer(compute="_compute_count_directories") count_files = fields.Integer(compute="_compute_count_files") - _sql_constraints = [ - ("name_uniq", "unique (name, category_id)", "Tag name already exists!"), - ] + _name_uniq = models.Constraint( + "unique (name, category_id)", + "Tag name already exists!", + ) @api.depends("directory_ids") def _compute_count_directories(self): diff --git a/dms/security/security.xml b/dms/security/security.xml index 463f914c3..5f6bf0db8 100644 --- a/dms/security/security.xml +++ b/dms/security/security.xml @@ -12,17 +12,21 @@ Documents + + Documents + + User - + Manager - + diff --git a/dms/static/src/js/fields/preview_binary/preview_record.esm.js b/dms/static/src/js/fields/preview_binary/preview_record.esm.js index ca55cdd5b..65fd2bbaf 100644 --- a/dms/static/src/js/fields/preview_binary/preview_record.esm.js +++ b/dms/static/src/js/fields/preview_binary/preview_record.esm.js @@ -18,7 +18,7 @@ export class PreviewRecordField extends BinaryField { onFilePreview() { const self = this; - const attachment = this.store.Attachment.insert({ + const attachment = this.store["ir.attachment"].insert({ id: self.props.record.resId, filename: self.props.record.data.display_name || "", name: self.props.record.data.display_name || "", diff --git a/dms/static/src/js/views/dms_file_upload.esm.js b/dms/static/src/js/views/dms_file_upload.esm.js index 99320208c..e2024b79c 100644 --- a/dms/static/src/js/views/dms_file_upload.esm.js +++ b/dms/static/src/js/views/dms_file_upload.esm.js @@ -33,7 +33,7 @@ export function createFileDropZoneExtension() { el.removeEventListener("drop", drop); }; }, - // eslint-disable-next-line no-undef + () => [document.querySelector(".o_content")] ); }, diff --git a/dms/static/src/js/views/file_kanban_controller.esm.js b/dms/static/src/js/views/file_kanban_controller.esm.js deleted file mode 100644 index ae3d05ee2..000000000 --- a/dms/static/src/js/views/file_kanban_controller.esm.js +++ /dev/null @@ -1,12 +0,0 @@ -// /** ******************************************************************************** -// Copyright 2020 Creu Blanca -// Copyright 2017-2019 MuK IT GmbH -// License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -// **********************************************************************************/ - -import {KanbanController} from "@web/views/kanban/kanban_controller"; -export class FileKanbanController extends KanbanController { - setup() { - super.setup(); - } -} diff --git a/dms/static/src/js/views/file_kanban_controller.xml b/dms/static/src/js/views/file_kanban_controller.xml deleted file mode 100644 index da14578ff..000000000 --- a/dms/static/src/js/views/file_kanban_controller.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - -
- - -
-
-
diff --git a/dms/static/src/js/views/file_kanban_record.esm.js b/dms/static/src/js/views/file_kanban_record.esm.js index 335fb08dd..002ec429e 100644 --- a/dms/static/src/js/views/file_kanban_record.esm.js +++ b/dms/static/src/js/views/file_kanban_record.esm.js @@ -44,7 +44,7 @@ export class FileKanbanRecord extends KanbanRecord { mimetype = self.props.record.data.mimetype; } - const attachment = this.store.Attachment.insert({ + const attachment = this.store["ir.attachment"].insert({ id: self.props.record.data.id, filename: self.props.record.data.name, name: self.props.record.data.name, diff --git a/dms/static/src/js/views/file_kanban_renderer.xml b/dms/static/src/js/views/file_kanban_renderer.xml index a1bb3c4fa..8ba228c1f 100644 --- a/dms/static/src/js/views/file_kanban_renderer.xml +++ b/dms/static/src/js/views/file_kanban_renderer.xml @@ -16,12 +16,12 @@ - - + + diff --git a/dms/static/src/js/views/file_list_renderer.xml b/dms/static/src/js/views/file_list_renderer.xml index 0b1b83418..6a88307b9 100644 --- a/dms/static/src/js/views/file_list_renderer.xml +++ b/dms/static/src/js/views/file_list_renderer.xml @@ -13,7 +13,7 @@ t-inherit="web.ListView.Buttons" t-inherit-mode="primary" > - +