odoo-addon-dms 17.0.1.2.0__py3-none-any.whl → 18.0.1.0.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. odoo/addons/dms/README.rst +69 -62
  2. odoo/addons/dms/__manifest__.py +1 -5
  3. odoo/addons/dms/data/onboarding_data.xml +0 -1
  4. odoo/addons/dms/i18n/de.po +0 -203
  5. odoo/addons/dms/i18n/dms.pot +51 -55
  6. odoo/addons/dms/i18n/es.po +0 -214
  7. odoo/addons/dms/i18n/fr.po +0 -45
  8. odoo/addons/dms/i18n/he_IL.po +0 -139
  9. odoo/addons/dms/i18n/it.po +0 -219
  10. odoo/addons/dms/i18n/nl.po +0 -6
  11. odoo/addons/dms/i18n/pt.po +0 -215
  12. odoo/addons/dms/i18n/pt_BR.po +0 -201
  13. odoo/addons/dms/models/access_groups.py +9 -8
  14. odoo/addons/dms/models/directory.py +21 -23
  15. odoo/addons/dms/models/dms_category.py +2 -2
  16. odoo/addons/dms/models/dms_file.py +15 -16
  17. odoo/addons/dms/models/dms_security_mixin.py +46 -20
  18. odoo/addons/dms/models/ir_binary.py +2 -2
  19. odoo/addons/dms/models/mixins_thumbnail.py +1 -1
  20. odoo/addons/dms/models/storage.py +3 -3
  21. odoo/addons/dms/readme/CONTRIBUTORS.md +2 -0
  22. odoo/addons/dms/readme/ROADMAP.md +2 -0
  23. odoo/addons/dms/static/description/index.html +13 -6
  24. odoo/addons/dms/static/src/js/fields/path_json/path_owl.esm.js +5 -5
  25. odoo/addons/dms/static/src/js/fields/preview_binary/preview_record.esm.js +2 -4
  26. odoo/addons/dms/static/src/js/views/dms_file_upload.esm.js +2 -5
  27. odoo/addons/dms/static/src/js/views/file_kanban_controller.esm.js +0 -2
  28. odoo/addons/dms/static/src/js/views/file_kanban_record.esm.js +0 -2
  29. odoo/addons/dms/static/src/js/views/file_kanban_renderer.esm.js +0 -2
  30. odoo/addons/dms/static/src/js/views/file_kanban_view.esm.js +0 -2
  31. odoo/addons/dms/static/src/js/views/file_list_controller.esm.js +0 -2
  32. odoo/addons/dms/static/src/js/views/file_list_renderer.esm.js +0 -2
  33. odoo/addons/dms/static/src/js/views/file_list_view.esm.js +0 -2
  34. odoo/addons/dms/static/src/js/views/search_panel.esm.js +1 -5
  35. odoo/addons/dms/static/src/models/attachment.esm.js +0 -2
  36. odoo/addons/dms/static/src/models/attachment_image.esm.js +0 -2
  37. odoo/addons/dms/static/src/models/attachment_viewer_viewable.esm.js +0 -2
  38. odoo/addons/dms/static/tests/tours/dms_portal_tour.esm.js +3 -7
  39. odoo/addons/dms/template/portal.xml +8 -6
  40. odoo/addons/dms/tests/common.py +9 -10
  41. odoo/addons/dms/tests/test_benchmark.py +19 -1
  42. odoo/addons/dms/tests/test_directory.py +8 -5
  43. odoo/addons/dms/tests/test_file_database.py +1 -1
  44. odoo/addons/dms/tests/test_portal.py +16 -17
  45. odoo/addons/dms/views/dms_access_groups_views.xml +13 -13
  46. odoo/addons/dms/views/dms_category.xml +5 -5
  47. odoo/addons/dms/views/dms_directory.xml +101 -110
  48. odoo/addons/dms/views/dms_file.xml +66 -84
  49. odoo/addons/dms/views/dms_tag.xml +14 -33
  50. odoo/addons/dms/views/menu.xml +22 -13
  51. odoo/addons/dms/views/storage.xml +189 -193
  52. odoo/addons/dms/wizards/wizard_dms_file_move_views.xml +1 -1
  53. {odoo_addon_dms-17.0.1.2.0.dist-info → odoo_addon_dms-18.0.1.0.0.2.dist-info}/METADATA +72 -65
  54. {odoo_addon_dms-17.0.1.2.0.dist-info → odoo_addon_dms-18.0.1.0.0.2.dist-info}/RECORD +56 -59
  55. odoo/addons/dms/static/src/scss/directory_kanban.scss +0 -53
  56. odoo/addons/dms/static/src/scss/dms_common.scss +0 -69
  57. odoo/addons/dms/static/src/scss/file_kanban.scss +0 -70
  58. {odoo_addon_dms-17.0.1.2.0.dist-info → odoo_addon_dms-18.0.1.0.0.2.dist-info}/WHEEL +0 -0
  59. {odoo_addon_dms-17.0.1.2.0.dist-info → odoo_addon_dms-18.0.1.0.0.2.dist-info}/top_level.txt +0 -0
@@ -2299,204 +2299,3 @@ msgstr "exe,msi"
2299
2299
  #: model_terms:ir.ui.view,arch_db:dms.view_dms_directory_form
2300
2300
  msgid "mail.catchall.domain"
2301
2301
  msgstr ""
2302
-
2303
- #~ msgid ""
2304
- #~ "<i class=\"fa fa-archive\"/>\n"
2305
- #~ " Archive"
2306
- #~ msgstr ""
2307
- #~ "<i class=\"fa fa-archive\"/>\n"
2308
- #~ " Arquivar"
2309
-
2310
- #~ msgid ""
2311
- #~ "<i class=\"fa fa-archive\"/>\n"
2312
- #~ " Unarchive"
2313
- #~ msgstr ""
2314
- #~ "<i class=\"fa fa-archive\"/>\n"
2315
- #~ " Desarquivar"
2316
-
2317
- #~ msgid ""
2318
- #~ "<i class=\"fa fa-download\"/>\n"
2319
- #~ " Download"
2320
- #~ msgstr ""
2321
- #~ "<i class=\"fa fa-download\"/>\n"
2322
- #~ " Baixar"
2323
-
2324
- #~ msgid ""
2325
- #~ "<i class=\"fa fa-external-link\"/>\n"
2326
- #~ " Open"
2327
- #~ msgstr ""
2328
- #~ "<i class=\"fa fa-external-link\"/>\n"
2329
- #~ " Abrir"
2330
-
2331
- #~ msgid ""
2332
- #~ "<i class=\"fa fa-external-link\"/>\n"
2333
- #~ " Open"
2334
- #~ msgstr ""
2335
- #~ "<i class=\"fa fa-external-link\"/>\n"
2336
- #~ " Abrir"
2337
-
2338
- #~ msgid ""
2339
- #~ "<i class=\"fa fa-file-o\"/>\n"
2340
- #~ " Files"
2341
- #~ msgstr ""
2342
- #~ "<i class=\"fa fa-external-link\"/>\n"
2343
- #~ " Abrir"
2344
-
2345
- #~ msgid ""
2346
- #~ "<i class=\"fa fa-folder-o\"/>\n"
2347
- #~ " Directories"
2348
- #~ msgstr ""
2349
- #~ "<i class=\"fa fa-folder-o\"/>\n"
2350
- #~ " Diretórios"
2351
-
2352
- #~ msgid ""
2353
- #~ "<i class=\"fa fa-lock\"/>\n"
2354
- #~ " Lock"
2355
- #~ msgstr ""
2356
- #~ "<i class=\"fa fa-lock\"/>\n"
2357
- #~ " Bloquear"
2358
-
2359
- #~ msgid ""
2360
- #~ "<i class=\"fa fa-pencil-square-o\"/>\n"
2361
- #~ " Edit"
2362
- #~ msgstr ""
2363
- #~ "<i class=\"fa fa-pencil-square-o\"/>\n"
2364
- #~ " Editar"
2365
-
2366
- #~ msgid ""
2367
- #~ "<i class=\"fa fa-pencil-square-o\"/>\n"
2368
- #~ " Edit"
2369
- #~ msgstr ""
2370
- #~ "<i class=\"fa fa-pencil-square-o\"/>\n"
2371
- #~ " Editar"
2372
-
2373
- #~ msgid ""
2374
- #~ "<i class=\"fa fa-trash-o\"/>\n"
2375
- #~ " Delete"
2376
- #~ msgstr ""
2377
- #~ "<i class=\"fa fa-trash-o\"/>\n"
2378
- #~ " Excluir"
2379
-
2380
- #~ msgid ""
2381
- #~ "<i class=\"fa fa-trash-o\"/>\n"
2382
- #~ " Delete"
2383
- #~ msgstr ""
2384
- #~ "<i class=\"fa fa-trash-o\"/>\n"
2385
- #~ " Excluir"
2386
-
2387
- #~ msgid ""
2388
- #~ "<i class=\"fa fa-unlock-alt\"/>\n"
2389
- #~ " Unlock"
2390
- #~ msgstr ""
2391
- #~ "<i class=\"fa fa-unlock-alt\"/>\n"
2392
- #~ " Destravar"
2393
-
2394
- #~ msgid ""
2395
- #~ "<span class=\"o_form_label\">\n"
2396
- #~ " Storages\n"
2397
- #~ " </span>"
2398
- #~ msgstr ""
2399
- #~ "<span class=\"o_form_label\">\n"
2400
- #~ " Armazenamentos\n"
2401
- #~ " </span>"
2402
-
2403
- #~ msgid ""
2404
- #~ "<span class=\"o_form_label\">File\n"
2405
- #~ " Extensions\n"
2406
- #~ " </span>"
2407
- #~ msgstr ""
2408
- #~ "<span class=\"o_form_label\">File\n"
2409
- #~ " Extensões\n"
2410
- #~ " </span>"
2411
-
2412
- #~ msgid "<span class=\"o_form_label\">File Size</span>"
2413
- #~ msgstr "<span class=\"o_form_label\">Tamanho do arquivo</span>"
2414
-
2415
- #, python-format
2416
- #~ msgid "A file with the same name already exists"
2417
- #~ msgstr "Já existe um arquivo com o mesmo nome"
2418
-
2419
- #, python-format
2420
- #~ msgid "A file with the same name already exists."
2421
- #~ msgstr "Já existe um arquivo com o mesmo nome."
2422
-
2423
- #~ msgid "Alias domain"
2424
- #~ msgstr "Domínio alternativo"
2425
-
2426
- #~ msgid ""
2427
- #~ "Define the maximum upload size of a\n"
2428
- #~ " file in MB"
2429
- #~ msgstr ""
2430
- #~ "Defina o tamanho máximo de carregamento de um\n"
2431
- #~ " arquivo em MB"
2432
-
2433
- #~ msgid "Dropdown menu"
2434
- #~ msgstr "Menu Suspenso"
2435
-
2436
- #~ msgid ""
2437
- #~ "Indicate if directories and files access work only with related model "
2438
- #~ "access (for example, if some directories are related with any sale, only "
2439
- #~ "users with read access to these sale can acess)"
2440
- #~ msgstr ""
2441
- #~ "Indica se o acesso a diretórios e arquivos funciona apenas com acesso de "
2442
- #~ "modelo relacionado (por exemplo, se alguns diretórios estiverem "
2443
- #~ "relacionados a alguma venda, apenas usuários com acesso de leitura a "
2444
- #~ "essas vendas poderão acessar)"
2445
-
2446
- #, python-format
2447
- #~ msgid "It is not possible to change parent to other storage."
2448
- #~ msgstr "Não é possível alterar o pai para outro armazenamento."
2449
-
2450
- #~ msgid "Last Modified on"
2451
- #~ msgstr "Última Modificação em"
2452
-
2453
- #, python-format
2454
- #~ msgid "Loading"
2455
- #~ msgstr "Carregando"
2456
-
2457
- #~ msgid "Main Attachment"
2458
- #~ msgstr "Anexo Principal"
2459
-
2460
- #~ msgid "Only admin user"
2461
- #~ msgstr "Somente usuário administrador"
2462
-
2463
- #~ msgid "Operations"
2464
- #~ msgstr "Operações"
2465
-
2466
- #~ msgid "Owner"
2467
- #~ msgstr "Proprietário"
2468
-
2469
- #~ msgid "The configuration is done!"
2470
- #~ msgstr "A configuração está feita!"
2471
-
2472
- #~ msgid ""
2473
- #~ "The owner of records created upon receiving emails on this alias. If this "
2474
- #~ "field is not set the system will attempt to find the right owner based on "
2475
- #~ "the sender (From) address, or will use the Administrator account if no "
2476
- #~ "system user is found for that address."
2477
- #~ msgstr ""
2478
- #~ "O proprietário dos registros criados ao receber e-mails neste alias. Se "
2479
- #~ "este campo não estiver definido, o sistema tentará encontrar o "
2480
- #~ "proprietário correto com base no endereço do remetente (De) ou usará a "
2481
- #~ "conta de Administrador se nenhum usuário do sistema for encontrado para "
2482
- #~ "esse endereço."
2483
-
2484
- #~ msgid ""
2485
- #~ "The save type is used to determine how a file is saved by the\n"
2486
- #~ " system. If you change this setting, you can migrate existing "
2487
- #~ "files\n"
2488
- #~ " manually by triggering the action."
2489
- #~ msgstr ""
2490
- #~ "O tipo de salvamento é usado para determinar como um arquivo é salvo "
2491
- #~ "pelo\n"
2492
- #~ " sistema. Se você alterar essa configuração, poderá migrar os "
2493
- #~ "arquivos existentes\n"
2494
- #~ " manualmente acionando a ação."
2495
-
2496
- #, python-format
2497
- #~ msgid "Viewer"
2498
- #~ msgstr "Visualizador"
2499
-
2500
- #, python-format
2501
- #~ msgid "Error has not been raised"
2502
- #~ msgstr "O erro não foi levantado"
@@ -14,7 +14,7 @@ class DmsAccessGroups(models.Model):
14
14
  _parent_name = "parent_group_id"
15
15
 
16
16
  name = fields.Char(string="Group Name", required=True, translate=True)
17
- parent_path = fields.Char(index="btree", unaccent=False)
17
+ parent_path = fields.Char(index="btree")
18
18
 
19
19
  # Permissions written directly on this group
20
20
  perm_create = fields.Boolean(string="Create Access")
@@ -122,9 +122,9 @@ class DmsAccessGroups(models.Model):
122
122
  for one in self:
123
123
  one.update(
124
124
  {
125
- "perm_inclusive_%s" % perm: (
126
- one["perm_%s" % perm]
127
- or one.parent_group_id["perm_inclusive_%s" % perm]
125
+ f"perm_inclusive_{perm}": (
126
+ one[f"perm_{perm}"]
127
+ or one.parent_group_id[f"perm_inclusive_{perm}"]
128
128
  )
129
129
  for perm in ("create", "unlink", "write")
130
130
  }
@@ -155,10 +155,11 @@ class DmsAccessGroups(models.Model):
155
155
  )
156
156
  record.update({"users": users, "count_users": len(users)})
157
157
 
158
- def copy(self, default=None):
159
- default = dict(default or {})
160
- default["name"] = _("%s (copy)") % self.name
161
- return super().copy(default=default)
158
+ def copy_data(self, default=None):
159
+ vals_list = super().copy_data(default)
160
+ for group, vals in zip(self, vals_list, strict=False):
161
+ vals["name"] = _("%s (copy)") % group.name
162
+ return vals_list
162
163
 
163
164
  @api.constrains("parent_path")
164
165
  def _check_parent_recursiveness(self):
@@ -17,8 +17,6 @@ from odoo.exceptions import UserError, ValidationError
17
17
  from odoo.osv.expression import AND, OR
18
18
  from odoo.tools import consteq, human_size
19
19
 
20
- from odoo.addons.http_routing.models.ir_http import slugify
21
-
22
20
  from ..tools.file import check_name, unique_name
23
21
 
24
22
  _logger = logging.getLogger(__name__)
@@ -46,7 +44,7 @@ class DmsDirectory(models.Model):
46
44
  _parent_name = "parent_id"
47
45
  _directory_field = _parent_name
48
46
 
49
- parent_path = fields.Char(index="btree", unaccent=False)
47
+ parent_path = fields.Char(index="btree")
50
48
  is_root_directory = fields.Boolean(
51
49
  default=False,
52
50
  help="""Indicates if the directory is a root directory.
@@ -216,7 +214,7 @@ class DmsDirectory(models.Model):
216
214
  """Special rules for directories."""
217
215
  self_filter = [
218
216
  ("storage_id_inherit_access_from_parent_record", "=", False),
219
- ("id", "inselect", self._get_access_groups_query(operation)),
217
+ ("id", "in", self._get_access_groups_query(operation)),
220
218
  ]
221
219
  # Upstream only filters by parent directory
222
220
  result = super()._get_domain_by_access_groups(operation)
@@ -237,7 +235,7 @@ class DmsDirectory(models.Model):
237
235
  def _compute_access_url(self):
238
236
  res = super()._compute_access_url()
239
237
  for item in self:
240
- item.access_url = "/my/dms/directory/%s" % (item.id)
238
+ item.access_url = f"/my/dms/directory/{item.id}"
241
239
  return res
242
240
 
243
241
  def check_access_token(self, access_token=False):
@@ -278,7 +276,7 @@ class DmsDirectory(models.Model):
278
276
  and consteq(current_directory.access_token, access_token)
279
277
  )
280
278
  or not access_token
281
- and current_directory.check_access_rights("read")
279
+ and current_directory.check_access("read")
282
280
  ):
283
281
  return directories
284
282
  current_directory = current_directory.parent_id
@@ -389,9 +387,8 @@ class DmsDirectory(models.Model):
389
387
  def _compute_complete_name(self):
390
388
  for category in self:
391
389
  if category.parent_id:
392
- category.complete_name = "{} / {}".format(
393
- category.parent_id.complete_name,
394
- category.name,
390
+ category.complete_name = (
391
+ f"{category.parent_id.complete_name} / {category.name}"
395
392
  )
396
393
  else:
397
394
  category.complete_name = category.name
@@ -530,7 +527,7 @@ class DmsDirectory(models.Model):
530
527
  # Constrains
531
528
  @api.constrains("parent_id")
532
529
  def _check_directory_recursion(self):
533
- if not self._check_recursion():
530
+ if self._has_cycle():
534
531
  raise ValidationError(_("Error! You cannot create recursive directories."))
535
532
  return True
536
533
 
@@ -594,18 +591,18 @@ class DmsDirectory(models.Model):
594
591
  not_starred_records.write({"user_star_ids": [(4, self.env.uid)]})
595
592
  starred_records.write({"user_star_ids": [(3, self.env.uid)]})
596
593
 
597
- def copy(self, default=None):
598
- self.ensure_one()
599
- default = dict(default or [])
600
- if "parent_id" in default:
601
- parent_directory = self.browse(default.get("parent_id"))
602
- names = parent_directory.sudo().child_directory_ids.mapped("name")
603
- elif self.is_root_directory:
604
- names = self.sudo().storage_id.root_directory_ids.mapped("name")
605
- else:
606
- names = self.sudo().parent_id.child_directory_ids.mapped("name")
607
- default.update({"name": unique_name(self.name, names)})
608
- return super().copy(default)
594
+ def copy_data(self, default=None):
595
+ vals_list = super().copy_data(default)
596
+ for directory, vals in zip(self, vals_list, strict=False):
597
+ if vals.get("parent_id"):
598
+ parent_directory = self.browse(vals.get("parent_id"))
599
+ names = parent_directory.sudo().child_directory_ids.mapped("name")
600
+ elif directory.is_root_directory:
601
+ names = self.sudo().storage_id.root_directory_ids.mapped("name")
602
+ else:
603
+ names = self.sudo().parent_id.child_directory_ids.mapped("name")
604
+ vals["name"] = unique_name(directory.name, names)
605
+ return vals_list
609
606
 
610
607
  def _alias_get_creation_values(self):
611
608
  values = super()._alias_get_creation_values()
@@ -628,7 +625,8 @@ class DmsDirectory(models.Model):
628
625
  parent_directory._process_message(msg_dict)
629
626
  return parent_directory
630
627
  names = parent_directory.child_directory_ids.mapped("name")
631
- subject = slugify(msg_dict.get("subject", _("Alias-Mail-Extraction")))
628
+ slug = self.env["ir.http"]._slug
629
+ subject = slug(msg_dict.get("subject", _("Alias-Mail-Extraction")))
632
630
  defaults = dict(
633
631
  {"name": unique_name(subject, names, escape_suffix=True)}, **custom_values
634
632
  )
@@ -40,7 +40,7 @@ class DMSCategory(models.Model):
40
40
  comodel_name="dms.category",
41
41
  inverse_name="parent_id",
42
42
  )
43
- parent_path = fields.Char(index="btree", unaccent=False)
43
+ parent_path = fields.Char(index="btree")
44
44
  tag_ids = fields.One2many(
45
45
  string="Tags", comodel_name="dms.tag", inverse_name="category_id"
46
46
  )
@@ -99,6 +99,6 @@ class DMSCategory(models.Model):
99
99
 
100
100
  @api.constrains("parent_id")
101
101
  def _check_category_recursion(self):
102
- if not self._check_recursion():
102
+ if self._has_cycle():
103
103
  raise ValidationError(_("Error! You cannot create recursive categories."))
104
104
  return True
@@ -150,14 +150,14 @@ class DMSFile(models.Model):
150
150
  ):
151
151
  one.image_1920 = one.content
152
152
 
153
- def check_access_rule(self, operation):
154
- self.mapped("directory_id").check_access_rule(operation)
155
- return super().check_access_rule(operation)
153
+ def check_access(self, operation):
154
+ self.mapped("directory_id").check_access(operation)
155
+ return super().check_access(operation)
156
156
 
157
157
  def _compute_access_url(self):
158
158
  res = super()._compute_access_url()
159
159
  for item in self:
160
- item.access_url = "/my/dms/file/%s/download" % (item.id)
160
+ item.access_url = f"/my/dms/file/{item.id}/download"
161
161
  return res
162
162
 
163
163
  def check_access_token(self, access_token=False):
@@ -240,7 +240,7 @@ class DMSFile(models.Model):
240
240
  return [extension.strip() for extension in extensions.split(",")]
241
241
 
242
242
  def _get_icon_placeholder_name(self):
243
- return self.extension and "file_%s.svg" % self.extension or ""
243
+ return self.extension and f"file_{self.extension}.svg" or ""
244
244
 
245
245
  # Actions
246
246
  def action_migrate(self, should_logging=True):
@@ -581,17 +581,16 @@ class DMSFile(models.Model):
581
581
  del res_vals["content"]
582
582
  return res_vals
583
583
 
584
- def copy(self, default=None):
585
- self.ensure_one()
586
- default = dict(default or [])
587
- names = self.sudo().directory_id.file_ids.mapped("name")
588
- if "directory_id" in default:
589
- directory = self.env["dms.directory"].browse(
590
- default.get("directory_id", False)
591
- )
592
- names = directory.sudo().file_ids.mapped("name")
593
- default.update({"name": file.unique_name(self.name, names, self.extension)})
594
- return super().copy(default)
584
+ def copy_data(self, default=None):
585
+ vals_list = super().copy_data(default)
586
+ for dms_file, vals in zip(self, vals_list, strict=False):
587
+ if vals.get("directory_id"):
588
+ directory = self.env["dms.directory"].browse(vals.get("directory_id"))
589
+ names = directory.sudo().file_ids.mapped("name")
590
+ else:
591
+ names = dms_file.sudo().directory_id.file_ids.mapped("name")
592
+ vals["name"] = file.unique_name(dms_file.name, names, dms_file.extension)
593
+ return vals_list
595
594
 
596
595
  @api.model_create_multi
597
596
  def create(self, vals_list):
@@ -1,5 +1,5 @@
1
1
  # Copyright 2020 Creu Blanca
2
- # Copyright 2021 Tecnativa - Víctor Martínez
2
+ # Copyright 2021-2025 Tecnativa - Víctor Martínez
3
3
  # Copyright 2024 Subteno - Timothée Vannier (https://www.subteno.com).
4
4
  # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
5
5
 
@@ -7,12 +7,14 @@
7
7
  from logging import getLogger
8
8
 
9
9
  from odoo import api, fields, models
10
+ from odoo.exceptions import AccessError
10
11
  from odoo.osv.expression import (
11
12
  FALSE_DOMAIN,
12
13
  NEGATIVE_TERM_OPERATORS,
13
14
  OR,
14
15
  TRUE_DOMAIN,
15
16
  )
17
+ from odoo.tools import SQL
16
18
 
17
19
  _logger = getLogger(__name__)
18
20
 
@@ -86,10 +88,10 @@ class DmsSecurityMixin(models.AbstractModel):
86
88
  )
87
89
  return
88
90
 
89
- creatable = self._filter_access_rules("create")
90
- readable = self._filter_access_rules("read")
91
- unlinkable = self._filter_access_rules("unlink")
92
- writeable = self._filter_access_rules("write")
91
+ creatable = self._filtered_access("create")
92
+ readable = self._filtered_access("read")
93
+ unlinkable = self._filtered_access("unlink")
94
+ writeable = self._filtered_access("write")
93
95
  for one in self:
94
96
  one.update(
95
97
  {
@@ -135,7 +137,9 @@ class DmsSecurityMixin(models.AbstractModel):
135
137
  )
136
138
  continue
137
139
  # Check model access only once per batch
138
- if not model.check_access_rights(operation, raise_exception=False):
140
+ try:
141
+ model.check_access(operation)
142
+ except AccessError:
139
143
  continue
140
144
  domains.append([("res_model", "=", model._name), ("res_id", "=", False)])
141
145
  # Check record access in batch too
@@ -143,7 +147,7 @@ class DmsSecurityMixin(models.AbstractModel):
143
147
  # Apply exists to skip records that do not exist. (e.g. a res.partner
144
148
  # deleted by database).
145
149
  model_records = model.browse(res_ids).exists()
146
- related_ok = model_records._filter_access_rules_python(operation)
150
+ related_ok = model_records._filtered_access(operation)
147
151
  if not related_ok:
148
152
  continue
149
153
  domains.append(
@@ -161,7 +165,7 @@ class DmsSecurityMixin(models.AbstractModel):
161
165
  "unlink": "AND dag.perm_inclusive_unlink",
162
166
  "write": "AND dag.perm_inclusive_write",
163
167
  }[operation]
164
- select = f"""
168
+ select = f"""(
165
169
  SELECT
166
170
  dir_group_rel.aid
167
171
  FROM
@@ -172,22 +176,25 @@ class DmsSecurityMixin(models.AbstractModel):
172
176
  ON users.gid = dag.id
173
177
  WHERE
174
178
  users.uid = %s {operation_check}
175
- """
176
- return select, (self.env.uid,)
179
+ )"""
180
+ sql = SQL(
181
+ select,
182
+ self.env.uid,
183
+ )
184
+ return sql
177
185
 
178
186
  @api.model
179
187
  def _get_domain_by_access_groups(self, operation):
180
188
  """Get domain for records accessible applying DMS access groups."""
181
189
  result = [
182
190
  (
183
- "%s.storage_id_inherit_access_from_parent_record"
184
- % self._directory_field,
191
+ f"{self._directory_field}.storage_id_inherit_access_from_parent_record",
185
192
  "=",
186
193
  False,
187
194
  ),
188
195
  (
189
196
  self._directory_field,
190
- "inselect",
197
+ "in",
191
198
  self._get_access_groups_query(operation),
192
199
  ),
193
200
  ]
@@ -215,7 +222,6 @@ class DmsSecurityMixin(models.AbstractModel):
215
222
  _self._get_domain_by_inheritance(operation),
216
223
  ]
217
224
  )
218
-
219
225
  if not positive:
220
226
  result.insert(0, "!")
221
227
  return result
@@ -236,16 +242,37 @@ class DmsSecurityMixin(models.AbstractModel):
236
242
  def _search_permission_write(self, operator, value):
237
243
  return self._get_permission_domain(operator, value, "write")
238
244
 
239
- def _filter_access_rules_python(self, operation):
245
+ def filtered_domain(self, domain):
246
+ """This method is needed to inhibit the behavior when called from the
247
+ _check_access() method with sudo() https://github.com/odoo/odoo/blob/fc737a147b9aefbd6ae5d111835ce3f4f7b4240a/odoo/models.py#L4465.
248
+ It would cause the error that multiple records are not accessed to be
249
+ displayed.
250
+ The _filtered_access() method is also overwritten to prevent this sudo()
251
+ specific behavior and to be able to access only the appropriate records.
252
+ """
253
+ if self.env.su:
254
+ return self
255
+ return super().filtered_domain(domain)
256
+
257
+ def _filtered_access_no_recursion(self, operation: str):
258
+ """This method is just the same as _filtered_access
259
+ but it can not be called withoud super due to
260
+ recursion error.
261
+ """
262
+ if self and not self.env.su and (result := self._check_access(operation)):
263
+ return self - result[0]
264
+ return self
265
+
266
+ def _filtered_access(self, operation):
240
267
  # Only kept to not break inheritance; see next comment
241
- result = super()._filter_access_rules_python(operation)
268
+ result = super()._filtered_access(operation)
242
269
  # HACK Always fall back to applying rules by SQL.
243
- # Upstream `_filter_access_rules_python()` doesn't use computed fields
270
+ # Upstream `_filtered_access()` doesn't use computed fields
244
271
  # search methods. Thus, it will take the `[('permission_{operation}',
245
272
  # '=', user.id)]` rule literally. Obviously that will always fail
246
273
  # because `self[f"permission_{operation}"]` will always be a `bool`,
247
274
  # while `user.id` will always be an `int`.
248
- result |= self._filter_access_rules(operation)
275
+ result |= self._filtered_access_no_recursion(operation)
249
276
  return result
250
277
 
251
278
  @api.model_create_multi
@@ -258,6 +285,5 @@ class DmsSecurityMixin(models.AbstractModel):
258
285
  res.flush_recordset()
259
286
  # Go back to the original sudo state and check we really had creation permission
260
287
  res = res.sudo(self.env.su)
261
- res.check_access_rights("create")
262
- res.check_access_rule("create")
288
+ res.check_access("create")
263
289
  return res
@@ -8,7 +8,7 @@ from odoo import models
8
8
  class IrBinary(models.AbstractModel):
9
9
  _inherit = "ir.binary"
10
10
 
11
- def _find_record_check_access(self, record, access_token):
11
+ def _find_record_check_access(self, record, access_token, field):
12
12
  if record._name in ("dms.file", "dms.directory"):
13
13
  if record.sudo().check_access_token(access_token):
14
14
  # sudo because the user might not usually have access to the record but
@@ -16,4 +16,4 @@ class IrBinary(models.AbstractModel):
16
16
  # Used to display the icon in the portal.
17
17
  return record.sudo()
18
18
 
19
- return super()._find_record_check_access(record, access_token)
19
+ return super()._find_record_check_access(record, access_token, field)
@@ -35,7 +35,7 @@ class Thumbnail(models.AbstractModel):
35
35
  """Obtain URL to record icon."""
36
36
  local_path = self._get_icon_disk_path()
37
37
  icon_name = os.path.basename(local_path)
38
- return "/dms/static/icons/%s" % icon_name
38
+ return f"/dms/static/icons/{icon_name}"
39
39
 
40
40
  @api.depends("image_128")
41
41
  def _compute_icon_url(self):
@@ -18,9 +18,9 @@ class Storage(models.Model):
18
18
  name = fields.Char(required=True)
19
19
  save_type = fields.Selection(
20
20
  selection=[
21
- ("database", _("Database")),
22
- ("file", _("Filestore")),
23
- ("attachment", _("Attachment")),
21
+ ("database", "Database"),
22
+ ("file", "Filestore"),
23
+ ("attachment", "Attachment"),
24
24
  ],
25
25
  default="database",
26
26
  required=True,
@@ -12,3 +12,5 @@
12
12
  - Khanh Bui \<<khanh.bui@mail.elegosoft.com>\>
13
13
  - [Subteno](https://www.subteno.com):
14
14
  - Timothée Vannier <<tva@subteno.com>>
15
+ - [Kencove](https://www.kencove.com):
16
+ - Mohamed Alkobrosli <<malkobrosly@kencove.com>>
@@ -17,3 +17,5 @@
17
17
  means. It would be nice to be able to remove that rule at some point.
18
18
  - Searchpanel in files: Highlight items (shading) without records when
19
19
  filtering something (by name for example).
20
+ - Accessing the clipboard (for example copy share link of file/directory)
21
+ is limited to secure connections. It also happens in any part of Odoo.