Skip to content

[19.0][MIG] dms_field: Migration to 19.0 - #486

Open
mmircoli-nexapp wants to merge 78 commits into
OCA:19.0from
mmircoli-nexapp:19.0-mig-dms_field
Open

mmircoli-nexapp wants to merge 78 commits into
OCA:19.0from
mmircoli-nexapp:19.0-mig-dms_field

Conversation

@mmircoli-nexapp

Copy link
Copy Markdown
Contributor

Migration of dms_field to 19.0

Changes:

Jaime Arroyo and others added 30 commits June 22, 2026 16:49
... so to speak.

@Tecnativa TT29847
Co-authored-by: luismalta <luis.malta@kmee.com.br>
Currently translated at 100.0% (75 of 75 strings)

Translation: dms-15.0/dms-15.0-dms_field
Translate-URL: https://translation.odoo-community.org/projects/dms-15-0/dms-15-0-dms_field/it/
Currently translated at 100.0% (75 of 75 strings)

Translation: dms-15.0/dms-15.0-dms_field
Translate-URL: https://translation.odoo-community.org/projects/dms-15-0/dms-15-0-dms_field/it/
The root directory can't be opened using the contextual action menu (`open`)

Impacted versions:

    12.0 and above

Steps to reproduce:

    Go to "DMS directories" in the partner form view
    Create a new root directory
    Right-click and choose `Open`

Current behavior:

    Odoo raises an error `TypeError: record is null`

Expected behavior:

    Open the directory like if we click on the `Open` button in the right section (Document preview)
- Create dms.field.template to set model, groups and directory structure.
- Remove the wizard to create a directory from a record.
- Directory structure is created when record is created (partner for example).
- Change the information on the right side.
- Have directory/file tree open when loading.

TT46804

TT46804
…cut several files and paste them into another directory)

TT47482
…n as default state

Before this change, when lauching the reload, the child directories
of the reloaded one are opened too.

By doing at this step and not setting the default state, the reloads are
done just for the implied nodes.

Furthermore, the code is improved to just reload the directories that
have being changed.
Currently translated at 100.0% (75 of 75 strings)

Translation: dms-15.0/dms-15.0-dms_field
Translate-URL: https://translation.odoo-community.org/projects/dms-15-0/dms-15-0-dms_field/es/
With hr_dms_field
The steps to reproduce the problem are:

1. Open Employee
2. Go to Documents Page
3. Drop a file on a directory
4. Go to another Employee
5. Try to drop a file

The file can not be dropped.
In the migration, the view type is changed from dms_tree to
dms_list. That's because in this point https://github.com/odoo/odoo/blob/16.0/addons/web/static/src/views/fields/field.js#L282
the 'tree' string is replaced by 'list' what is causing problems
trying to set the view type as it was before.
A parent directory can be set in templates, if set, the directory that is created
linked to a record will be a "child" of that directory.

TT48650
…ors when creating directories from a template.

Example of use case:
- Create a partner.
- Create a template linked to partners.
- Create a directory through the template.
- An access group linked to the partner is created.
- Delete the partner directory.
- Creates a new directory through the template.
- No error will occur and the previous access group will be re-used.
…ring

Now the process will be as follows:
- The auto-generated access group will be created.
- The user will be defined in explicit_user_ids of the group
created in the previous step.

In the directory created (from the corresponding record) the auto-generated
group (for the user to have access) + the access groups that the template
had will be used.
OCA-git-bot and others added 21 commits June 22, 2026 16:49
Use case:
- Modify the user Marc Demo and define some permissions in Employees
- Go to the employee Abigail Peterson and create her folder
- Modify the Access group > Everyone for Employee DMS and remove the
corresponding group to leave only Mitchell Admin defined as an explicit user
- Log in with Marc Demo and try to modify some data for the employee
Abigail Peterson (define a tag, for example).

TT60793
Currently translated at 91.9% (57 of 62 strings)

Translation: dms-18.0/dms-18.0-dms_field
Translate-URL: https://translation.odoo-community.org/projects/dms-18-0/dms-18-0-dms_field/es/
   Added domain related to storage_id which allows to create
directories for same records with required template across
multiple storages.
If, for example, l10n_es_aeat_mod190 is installed and the tests are run, the following log is generated:

WARNING devel py.warnings: /opt/odoo/auto/addons/dms_field/models/ir_ui_view.py:21:
FutureWarning: The behavior of this method will change in future versions. Use specific 'len(elem)' or 'elem is not None' test instead

File "/opt/odoo/auto/addons/dms_field/models/ir_ui_view.py", line 21, in _postprocess_tag_dms_list
    if parent_name := parent and parent.get("name"):
Currently translated at 95.0% (58 of 61 strings)

Translation: dms-18.0/dms-18.0-dms_field
Translate-URL: https://translation.odoo-community.org/projects/dms-18-0/dms-18-0-dms_field/ar/
Currently translated at 100.0% (61 of 61 strings)

Translation: dms-18.0/dms-18.0-dms_field
Translate-URL: https://translation.odoo-community.org/projects/dms-18-0/dms-18-0-dms_field/sl/
@mmircoli-nexapp mmircoli-nexapp mentioned this pull request Jun 22, 2026
1 of 7 tasks
@victoralmau

Copy link
Copy Markdown
Member

6 PRs created automatically at once? None of them pass the tests? I'm sorry, but I'm not going to waste my time reviewing something that not even the person who created it (perhaps automatically by AI) has done.
Just my 2 cents.

@foamsoap1

Copy link
Copy Markdown

Thanks for the migration. Running this PR head (2302002) on top of the current 19.0 branch (81a6b1e, Odoo 19.0-20260810) the module installs but three things break; the following patch makes the 14 tests green (--with-demo):

  1. tests/test_dms_field.py: res.groups no longer has a users field in Odoo 19 (user_ids), and odoo.fields.first was removed — replaced by a [:1] slice.
  2. models/dms_directory.py: _search_parents still composes raw SQL through _where_calc, which no longer exists on Odoo 19 models (AttributeError: 'dms.directory' object has no attribute '_where_calc' in test_js_tree / test_parents). The patch expresses the same semantics through the ORM: search the matching (ir.rule-filtered) records, then keep those whose parent is absent or not part of that set; count/offset/limit preserved.
patch (apply with patch -p1 from the addons root)
diff -ruN dms_field/models/dms_directory.py b/dms_field/models/dms_directory.py
--- a/dms_field/models/dms_directory.py	2026-06-22 18:07:49
+++ b/dms_field/models/dms_directory.py	2026-09-17 16:06:03
@@ -120,50 +120,31 @@
     def _search_parents(
         self, domain=False, offset=0, limit=None, order=None, count=False
     ):
+        """Top-level elements of the hierarchy for a domain.
+
+        Odoo 19 removed ``_where_calc``/raw query composition used by the
+        18.0 implementation, so the same semantics are expressed through the
+        ORM: search the matching (and accessible, ir.rules applied) records,
+        then keep those whose parent is absent or not part of that set.
+        """
         if not domain:
             domain = []
         self._check_parent_field()
         self.check_access("read")
-        if Domain(domain) == Domain.FALSE:
-            return []
-        query = self._where_calc(domain)
-        self._apply_ir_rules(query, "read")
-        from_clause, from_params = query.from_clause
-        where_clause, where_clause_arguments = query.where_clause
-        parent_where = where_clause and (f" WHERE {where_clause}") or ""
-        parent_query = f'SELECT "{self._table}".id FROM ' + from_clause + parent_where
-        no_parent_clause = f'"{self._table}"."{self._parent_name}" IS NULL'
-        no_access_clause = (
-            f'"{self._table}"."{self._parent_name}" NOT IN ({parent_query})'
+        if Domain(domain).is_false():
+            return 0 if count else []
+        records = self.search(domain, order=order)
+        matching_ids = set(records.ids)
+        parent_field = self._parent_name
+        parents = records.filtered(
+            lambda rec: not rec[parent_field]
+            or rec[parent_field].id not in matching_ids
         )
-        parent_clause = f"({no_parent_clause} OR {no_access_clause})"
-        order_by = f" ORDER BY {self._order_to_sql(order, self._where_calc([])).code}"
-        where_clause_params = where_clause_arguments
-        where_str = (
-            where_clause
-            and (f" WHERE {where_clause} AND {parent_clause}")
-            or (f" WHERE {parent_clause}")
-        )
         if count:
-            # pylint: disable=sql-injection
-            query_str = "SELECT count(1) FROM " + from_clause + where_str
-            self.env.cr.execute(query_str, where_clause_params)
-            return self.env.cr.fetchone()[0]
-        limit_str = limit and " limit %s" or ""
-        offset_str = offset and " offset %s" or ""
-        query_str = (
-            f'SELECT "{self._table}".id FROM '
-            + from_clause
-            + where_str
-            + order_by
-            + limit_str
-            + offset_str
-        )
-        complete_where_clause_params = where_clause_params + where_clause_arguments
-        if limit:
-            complete_where_clause_params.append(limit)
+            return len(parents)
+        ids = parents.ids
         if offset:
-            complete_where_clause_params.append(offset)
-        # pylint: disable=sql-injection
-        self.env.cr.execute(query_str, complete_where_clause_params)
-        return list({x[0] for x in self.env.cr.fetchall()})
+            ids = ids[offset:]
+        if limit:
+            ids = ids[:limit]
+        return ids
diff -ruN dms_field/tests/test_dms_field.py b/dms_field/tests/test_dms_field.py
--- a/dms_field/tests/test_dms_field.py	2026-06-22 18:07:49
+++ b/dms_field/tests/test_dms_field.py	2026-09-17 16:05:27
@@ -3,6 +3,11 @@
 # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
 
 from odoo import fields
+
+
+def _first(records):
+    """Odoo 19 removed ``fields.first``; keep the same semantics."""
+    return records[:1]
 from odoo.exceptions import UserError, ValidationError
 from odoo.tests import new_test_user
 from odoo.tools import mute_logger
@@ -17,7 +22,7 @@
         cls.env = cls.env(context=dict(cls.env.context, test_dms_field=True))
         cls.user_a = new_test_user(cls.env, login="test-user-a")
         cls.group = cls.env["res.groups"].create(
-            {"name": "Test group", "users": [(4, cls.user_a.id)]}
+            {"name": "Test group", "user_ids": [(4, cls.user_a.id)]}
         )
         cls.user_b = new_test_user(cls.env, login="test-user-b")
         cls.template = cls.env.ref("dms_field.field_template_partner")
@@ -162,7 +167,7 @@
 
     def test_creation_process_01_with_parent(self):
         self.assertFalse(self.partner.dms_directory_ids)
-        self.template.parent_directory_id = fields.first(
+        self.template.parent_directory_id = _first(
             self.template.storage_id.root_directory_ids
         )
         template = self.env["dms.field.template"].with_context(
@@ -197,7 +202,7 @@
         self.assertFalse(partner_2.dms_directory_ids)
 
     def test_creation_process_02_with_parent(self):
-        self.template.parent_directory_id = fields.first(
+        self.template.parent_directory_id = _first(
             self.template.storage_id.root_directory_ids
         )
         partner_1 = self.env["res.partner"].create({"name": "Test partner 1"})

We are using this in production-bound work at رغوة صابون and will switch back to the merged module as soon as this PR lands. Happy to push these as commits to the PR branch if you prefer.

sebastienclaude added a commit to asc2si/hexa19-oca-dms that referenced this pull request Sep 18, 2026
@foamsoap1

Copy link
Copy Markdown

While testing this migration on 19.0 we hit a web-client crash caused by DMSFieldMixin.web_save. It shows up after a successful save of any record that has no directory yet.

Symptom
web_save returns normally, then the form raises:
UncaughtPromiseError > TypeError: Cannot read properties of undefined (reading 'records')
at Record._checkValidity ← Record._setData ← Record._save.

Cause

  • web_save deletes dms_directory_ids from specification when the field is not in vals and not self[f_name], so the response has no such key.
  • In 19.0, Record._setData rebuilds this.data from the server values only.
  • _checkValidity() then reads this.data[fieldName].records for every one2many/many2many in activeFields that is not invisible.
  • As soon as a form displays dms_directory_ids for saved records, this.data.dms_directory_ids is undefined after the save. A typical example is a page with invisible="not id": the page modifier is not combined into the field's own invisible.

Notes:

  • A pure create is not affected, because the client sends dms_directory_ids: [] in vals for new records.
  • The write that follows an automatic save is affected. For example, on the partner form the account bank-accounts widget saves the new record when it mounts, so the user's next Save is a write.
  • dms.field.template inherits the mixin as well, and the DMS list controller reads record.data.dms_directory_ids.records, so the same missing key can surface there.

Suggested fix
Keep the key in the response instead of dropping it: remember the requested sub-spec, and when it was removed, fill it with the directories the current user can actually read. This keeps the original intent, which is to never read directories the user has no access to:

    def web_save(self, vals, specification, next_id=None):
        f_name = "dms_directory_ids"
        field_spec = specification.get(f_name)
        if f_name not in vals and f_name in specification and not self[f_name]:
            del specification[f_name]
        res = super().web_save(vals, specification, next_id)
        if field_spec is not None and f_name not in specification:
            Directory = self.env["dms.directory"].with_context(**(field_spec.get("context") or {}))
            readable = Directory.has_access("read")
            for values in res:
                directories = Directory.browse()
                if readable:
                    directories = Directory.search([
                        ("res_model", "=", self._name),
                        ("res_id", "=", values["id"]),
                        ("storage_id.save_type", "!=", "attachment"),
                    ])
                sub_spec = field_spec.get("fields")
                values[f_name] = directories.web_read(sub_spec) if sub_spec else directories.ids
        return res

Minimal test
On a record without directories:

  1. Call record.with_user(user).web_save({"name": "x"}, {"name": {}, "dms_directory_ids": {"fields": {"name": {}}}}).
  2. Assert "dms_directory_ids" in res[0] and that it equals [].
  3. Optionally, assert that a user outside the directory's access groups gets only the directories a search returns for them.

@nachdjo

nachdjo commented Oct 30, 2026

Copy link
Copy Markdown

Preview in the dms_list tree fails on 19.0 because this.store.Attachment is gone. We've been running this in production since September: it opens the viewer on the dms.file's own content instead of faking an ir.attachment, and downloads types the viewer can't show. Happy to open it as a PR against your branch if that's easier.

In dms_field/static/src/views/dms_list/dms_list_renderer.esm.js:

import {FileModel} from "@web/core/file_viewer/file_model";

export class DmsFileViewerModel extends FileModel {
    get urlRoute() {
        return "/web/content";
    }

    get urlQueryParams() {
        return {
            id: this.id,
            field: "content",
            model: "dms.file",
            filename_field: "name",
            filename: this.name,
        };
    }
}
    onDMSPreviewFile(node) {
        const file = Object.assign(new DmsFileViewerModel(), {
            id: node.data.data.id,
            name: node.data.data.name,
            mimetype: node.data.data.mimetype,
        });
        if (file.isViewable) {
            this.fileViewer.open(file);
            return;
        }
        this.notification.add(
            _t("This file type cannot be previewed here, so it was downloaded instead."),
            {type: "info"}
        );
        return download({data: {}, url: file.downloadUrl});
    }

this.store = useService("mail.store") in setup() is then unused and can go.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.