odoo-addon-dms 16.0.1.8.0.3__py3-none-any.whl → 17.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 (111) hide show
  1. odoo/addons/dms/README.rst +132 -78
  2. odoo/addons/dms/__manifest__.py +34 -16
  3. odoo/addons/dms/actions/file.xml +1 -3
  4. odoo/addons/dms/controllers/main.py +0 -34
  5. odoo/addons/dms/controllers/portal.py +178 -92
  6. odoo/addons/dms/data/onboarding_data.xml +77 -0
  7. odoo/addons/dms/demo/access_group.xml +3 -9
  8. odoo/addons/dms/i18n/de.po +13 -117
  9. odoo/addons/dms/i18n/dms.pot +137 -174
  10. odoo/addons/dms/i18n/es.po +15 -58
  11. odoo/addons/dms/i18n/fr.po +296 -450
  12. odoo/addons/dms/i18n/he_IL.po +13 -117
  13. odoo/addons/dms/i18n/it.po +15 -131
  14. odoo/addons/dms/i18n/nl.po +13 -117
  15. odoo/addons/dms/i18n/pt.po +13 -121
  16. odoo/addons/dms/i18n/pt_BR.po +28 -147
  17. odoo/addons/dms/i18n/ru.po +13 -117
  18. odoo/addons/dms/models/__init__.py +7 -1
  19. odoo/addons/dms/models/abstract_dms_mixin.py +1 -1
  20. odoo/addons/dms/models/access_groups.py +18 -11
  21. odoo/addons/dms/models/base.py +10 -2
  22. odoo/addons/dms/models/directory.py +46 -75
  23. odoo/addons/dms/models/{category.py → dms_category.py} +10 -42
  24. odoo/addons/dms/models/dms_file.py +125 -147
  25. odoo/addons/dms/models/dms_security_mixin.py +26 -22
  26. odoo/addons/dms/models/ir_attachment.py +0 -1
  27. odoo/addons/dms/models/ir_binary.py +19 -0
  28. odoo/addons/dms/models/mail_thread.py +2 -4
  29. odoo/addons/dms/models/mixins_thumbnail.py +13 -8
  30. odoo/addons/dms/models/onboarding_onboarding.py +16 -0
  31. odoo/addons/dms/models/onboarding_onboarding_step.py +50 -0
  32. odoo/addons/dms/models/res_company.py +22 -47
  33. odoo/addons/dms/models/res_config_settings.py +0 -1
  34. odoo/addons/dms/models/storage.py +4 -24
  35. odoo/addons/dms/models/tag.py +1 -1
  36. odoo/addons/dms/readme/CONFIGURE.md +59 -0
  37. odoo/addons/dms/readme/CONTRIBUTORS.md +14 -0
  38. odoo/addons/dms/readme/CREDITS.md +6 -0
  39. odoo/addons/dms/readme/DESCRIPTION.md +9 -0
  40. odoo/addons/dms/readme/INSTALL.md +4 -0
  41. odoo/addons/dms/readme/ROADMAP.md +19 -0
  42. odoo/addons/dms/readme/USAGE.md +11 -0
  43. odoo/addons/dms/security/security.xml +1 -0
  44. odoo/addons/dms/static/description/icon.png +0 -0
  45. odoo/addons/dms/static/description/icon.svg +4 -1
  46. odoo/addons/dms/static/description/index.html +133 -88
  47. odoo/addons/dms/static/description/portal_icon.svg +12 -0
  48. odoo/addons/dms/static/src/js/fields/{path_owl.esm.js → path_json/path_owl.esm.js} +22 -4
  49. odoo/addons/dms/static/src/js/fields/{path_owl.xml → path_json/path_owl.xml} +7 -4
  50. odoo/addons/dms/static/src/js/fields/preview_binary/preview_record.esm.js +47 -0
  51. odoo/addons/dms/static/src/js/{views/fields/binary → fields/preview_binary}/preview_record.xml +5 -4
  52. odoo/addons/dms/static/src/js/views/dms_file_upload.esm.js +155 -148
  53. odoo/addons/dms/static/src/js/views/file_kanban_controller.xml +0 -1
  54. odoo/addons/dms/static/src/js/views/file_kanban_record.esm.js +26 -27
  55. odoo/addons/dms/static/src/js/views/file_kanban_renderer.esm.js +1 -2
  56. odoo/addons/dms/static/src/js/views/file_kanban_renderer.xml +12 -16
  57. odoo/addons/dms/static/src/js/views/file_kanban_view.esm.js +11 -9
  58. odoo/addons/dms/static/src/js/views/file_list_controller.esm.js +1 -1
  59. odoo/addons/dms/static/src/js/views/file_list_renderer.xml +1 -7
  60. odoo/addons/dms/static/src/js/views/file_list_view.esm.js +11 -9
  61. odoo/addons/dms/static/src/js/views/search_panel.esm.js +9 -10
  62. odoo/addons/dms/static/src/models/attachment.esm.js +83 -82
  63. odoo/addons/dms/static/src/models/attachment_image.esm.js +30 -28
  64. odoo/addons/dms/static/src/models/attachment_viewer_viewable.esm.js +27 -25
  65. odoo/addons/dms/static/src/scss/directory_kanban.scss +28 -73
  66. odoo/addons/dms/static/src/scss/dms_common.scss +69 -0
  67. odoo/addons/dms/static/src/scss/file_kanban.scss +22 -34
  68. odoo/addons/dms/static/src/scss/portal.scss +12 -0
  69. odoo/addons/dms/static/tests/tours/dms_portal_tour.esm.js +46 -0
  70. odoo/addons/dms/{views/dms_portal_templates.xml → template/portal.xml} +13 -3
  71. odoo/addons/dms/tests/common.py +20 -4
  72. odoo/addons/dms/tests/data/mail01.eml +2 -2
  73. odoo/addons/dms/tests/data/mail02.eml +2 -2
  74. odoo/addons/dms/tests/test_benchmark.py +16 -21
  75. odoo/addons/dms/tests/test_directory.py +128 -40
  76. odoo/addons/dms/tests/test_file.py +112 -30
  77. odoo/addons/dms/tests/test_file_database.py +50 -24
  78. odoo/addons/dms/tests/test_portal.py +50 -37
  79. odoo/addons/dms/tests/test_storage_attachment.py +85 -63
  80. odoo/addons/dms/tests/test_storage_database.py +89 -35
  81. odoo/addons/dms/tools/file.py +63 -11
  82. odoo/addons/dms/views/dms_access_groups_views.xml +2 -17
  83. odoo/addons/dms/views/{category.xml → dms_category.xml} +4 -9
  84. odoo/addons/dms/views/{directory.xml → dms_directory.xml} +168 -176
  85. odoo/addons/dms/views/dms_file.xml +170 -191
  86. odoo/addons/dms/views/{tag.xml → dms_tag.xml} +50 -53
  87. odoo/addons/dms/views/menu.xml +52 -21
  88. odoo/addons/dms/views/res_config_settings.xml +31 -82
  89. odoo/addons/dms/views/storage.xml +7 -13
  90. odoo_addon_dms-17.0.1.0.0.2.dist-info/METADATA +257 -0
  91. {odoo_addon_dms-16.0.1.8.0.3.dist-info → odoo_addon_dms-17.0.1.0.0.2.dist-info}/RECORD +93 -93
  92. {odoo_addon_dms-16.0.1.8.0.3.dist-info → odoo_addon_dms-17.0.1.0.0.2.dist-info}/WHEEL +1 -1
  93. odoo_addon_dms-17.0.1.0.0.2.dist-info/top_level.txt +1 -0
  94. odoo/addons/dms/readme/CONFIGURE.rst +0 -40
  95. odoo/addons/dms/readme/CONTRIBUTORS.rst +0 -16
  96. odoo/addons/dms/readme/CREDITS.rst +0 -6
  97. odoo/addons/dms/readme/DESCRIPTION.rst +0 -6
  98. odoo/addons/dms/readme/INSTALL.rst +0 -9
  99. odoo/addons/dms/readme/ROADMAP.rst +0 -8
  100. odoo/addons/dms/readme/USAGE.rst +0 -8
  101. odoo/addons/dms/static/src/js/components/path/path.esm.js +0 -0
  102. odoo/addons/dms/static/src/js/dms_portal_tour.js +0 -57
  103. odoo/addons/dms/static/src/js/fields/path.js +0 -81
  104. odoo/addons/dms/static/src/js/views/fields/binary/preview_record.esm.js +0 -35
  105. odoo/addons/dms/static/src/js/views/many_drop_target.js +0 -98
  106. odoo/addons/dms/static/src/scss/variables.scss +0 -9
  107. odoo/addons/dms/static/src/xml/views.xml +0 -72
  108. odoo/addons/dms/template/assets.xml +0 -44
  109. odoo/addons/dms/template/onboarding.xml +0 -97
  110. odoo_addon_dms-16.0.1.8.0.3.dist-info/METADATA +0 -203
  111. odoo_addon_dms-16.0.1.8.0.3.dist-info/top_level.txt +0 -1
@@ -1,6 +1,7 @@
1
1
  # Copyright 2020 Antoni Romera
2
2
  # Copyright 2017-2019 MuK IT GmbH
3
3
  # Copyright 2021 Tecnativa - Víctor Martínez
4
+ # Copyright 2024 Subteno - Timothée Vannier (https://www.subteno.com).
4
5
  # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
5
6
 
6
7
  import base64
@@ -22,7 +23,7 @@ from ..tools import file
22
23
  _logger = logging.getLogger(__name__)
23
24
 
24
25
 
25
- class File(models.Model):
26
+ class DMSFile(models.Model):
26
27
  _name = "dms.file"
27
28
  _description = "File"
28
29
 
@@ -37,10 +38,7 @@ class File(models.Model):
37
38
 
38
39
  _order = "name asc"
39
40
 
40
- # ----------------------------------------------------------
41
41
  # Database
42
- # ----------------------------------------------------------
43
-
44
42
  active = fields.Boolean(
45
43
  string="Archived",
46
44
  default=True,
@@ -50,7 +48,7 @@ class File(models.Model):
50
48
  comodel_name="dms.directory",
51
49
  string="Directory",
52
50
  domain="[('permission_create', '=', True)]",
53
- context="{'dms_directory_show_path': True}",
51
+ context={"dms_directory_show_path": True},
54
52
  ondelete="restrict",
55
53
  auto_join=True,
56
54
  required=True,
@@ -62,7 +60,6 @@ class File(models.Model):
62
60
  storage_id = fields.Many2one(
63
61
  related="directory_id.storage_id",
64
62
  readonly=True,
65
- store=True,
66
63
  prefetch=False,
67
64
  )
68
65
 
@@ -114,12 +111,11 @@ class File(models.Model):
114
111
 
115
112
  checksum = fields.Char(string="Checksum/SHA1", readonly=True, index="btree")
116
113
 
117
- content_binary = fields.Binary(attachment=False, prefetch=False, invisible=True)
114
+ content_binary = fields.Binary(attachment=False, prefetch=False)
118
115
 
119
116
  save_type = fields.Char(
120
117
  compute="_compute_save_type",
121
118
  string="Current Save Type",
122
- invisible=True,
123
119
  prefetch=False,
124
120
  )
125
121
 
@@ -133,8 +129,7 @@ class File(models.Model):
133
129
  require_migration = fields.Boolean(
134
130
  compute="_compute_migration", store=True, compute_sudo=True
135
131
  )
136
-
137
- content_file = fields.Binary(attachment=True, prefetch=False, invisible=True)
132
+ content_file = fields.Binary(attachment=True, prefetch=False)
138
133
 
139
134
  # Extend inherited field(s)
140
135
  image_1920 = fields.Image(compute="_compute_image_1920", store=True, readonly=False)
@@ -165,30 +160,30 @@ class File(models.Model):
165
160
  return res
166
161
 
167
162
  def check_access_token(self, access_token=False):
168
- res = False
169
- if access_token:
170
- if self.access_token and consteq(self.access_token, access_token):
163
+ if not access_token:
164
+ return False
165
+
166
+ if self.access_token and consteq(self.access_token, access_token):
167
+ return True
168
+
169
+ items = (
170
+ self.env["dms.directory"]
171
+ .sudo()
172
+ .search([("access_token", "=", access_token)])
173
+ )
174
+ if items:
175
+ item = items[0]
176
+ if self.directory_id.id == item.id:
171
177
  return True
172
- else:
173
- items = (
174
- self.env["dms.directory"]
175
- .sudo()
176
- .search([("access_token", "=", access_token)])
177
- )
178
- if items:
179
- item = items[0]
180
- if self.directory_id.id == item.id:
181
- return True
182
- else:
183
- directory_item = self.directory_id
184
- while directory_item.parent_id:
185
- if directory_item.id == self.directory_id.id:
186
- return True
187
- directory_item = directory_item.parent_id
188
- # Fix last level
189
- if directory_item.id == self.directory_id.id:
190
- return True
191
- return res
178
+ directory_item = self.directory_id
179
+ while directory_item.parent_id:
180
+ if directory_item.id == self.directory_id.id:
181
+ return True
182
+ directory_item = directory_item.parent_id
183
+ # Fix last level
184
+ if directory_item.id == self.directory_id.id:
185
+ return True
186
+ return False
192
187
 
193
188
  res_model = fields.Char(
194
189
  string="Linked attachments model", related="directory_id.res_model"
@@ -200,17 +195,13 @@ class File(models.Model):
200
195
  comodel_name="ir.attachment",
201
196
  string="Attachment File",
202
197
  prefetch=False,
203
- invisible=True,
204
198
  ondelete="cascade",
205
199
  )
206
200
 
207
201
  def get_human_size(self):
208
202
  return human_size(self.size)
209
203
 
210
- # ----------------------------------------------------------
211
204
  # Helper
212
- # ----------------------------------------------------------
213
-
214
205
  @api.model
215
206
  def _get_checksum(self, binary):
216
207
  return hashlib.sha1(binary or b"").hexdigest()
@@ -250,27 +241,28 @@ class File(models.Model):
250
241
  def _get_icon_placeholder_name(self):
251
242
  return self.extension and "file_%s.svg" % self.extension or ""
252
243
 
253
- # ----------------------------------------------------------
254
244
  # Actions
255
- # ----------------------------------------------------------
256
-
257
- def action_migrate(self, logging=True):
245
+ def action_migrate(self, should_logging=True):
258
246
  record_count = len(self)
259
247
  index = 1
260
248
  for dms_file in self:
261
- if logging:
249
+ if should_logging:
262
250
  _logger.info(
263
251
  _(
264
- "Migrate File %(index)s of %(record_count)s [ %(dms_file_migration)s ]"
252
+ "Migrate File %(index)s of %(record_count)s [ %("
253
+ "dms_file_migration)s ]",
254
+ index=index,
255
+ record_count=record_count,
256
+ dms_file_migration=dms_file.migration,
265
257
  )
266
- % {
267
- "index": index,
268
- "record_count": record_count,
269
- "dms_file_migration": dms_file.migration,
270
- }
271
258
  )
272
259
  index += 1
273
- dms_file.write({"content": dms_file.with_context(**{}).content})
260
+ dms_file.write(
261
+ {
262
+ "content": dms_file.with_context(**{}).content,
263
+ "storage_id": dms_file.directory_id.storage_id.id,
264
+ }
265
+ )
274
266
 
275
267
  def action_save_onboarding_file_step(self):
276
268
  self.env.user.company_id.set_onboarding_step_done(
@@ -288,10 +280,7 @@ class File(models.Model):
288
280
  result["context"] = dict(self.env.context)
289
281
  return result
290
282
 
291
- # ----------------------------------------------------------
292
283
  # SearchPanel
293
- # ----------------------------------------------------------
294
-
295
284
  @api.model
296
285
  def _search_panel_directory(self, **kwargs):
297
286
  search_domain = (kwargs.get("search_domain", []),)
@@ -315,56 +304,61 @@ class File(models.Model):
315
304
  def search_panel_select_range(self, field_name, **kwargs):
316
305
  """This method is overwritten to make it 'similar' to v13.
317
306
  The goal is that the directory searchpanel shows all directories
318
- (even if some folders have no files)."""
319
- if field_name == "directory_id":
320
- domain = [["is_hidden", "=", False]]
321
- # If we pass by context something, we filter more about it we filter
322
- # the directories of the files or we show all of them
323
- if self.env.context.get("active_model", False) == "dms.directory":
324
- active_id = self.env.context.get("active_id")
325
- # para saber que directorios, buscamos las posibles carpetas que nos interesan
326
- files = self.env["dms.file"].search(
327
- [["directory_id", "child_of", active_id]]
328
- )
329
- all_directories = files.mapped("directory_id")
330
- all_directories += files.mapped("directory_id.parent_id")
331
- domain.append(["id", "in", all_directories.ids])
332
- # Get all possible directories
333
- comodel_records = (
334
- self.env["dms.directory"]
335
- .with_context(directory_short_name=True)
336
- .search_read(domain, ["display_name", "parent_id"])
307
+ (even if some folders have no files).
308
+ """
309
+ if field_name != "directory_id":
310
+ context = {}
311
+ if field_name == "category_id":
312
+ context["category_short_name"] = True
313
+ return super(
314
+ DMSFile, self.with_context(**context)
315
+ ).search_panel_select_range(field_name, **kwargs)
316
+
317
+ domain = [("is_hidden", "=", False)]
318
+ # If we pass by context something, we filter more about it we filter
319
+ # the directories of the files, or we show all of them
320
+ if self.env.context.get("active_model") == "dms.directory":
321
+ active_id = self.env.context.get("active_id")
322
+ files = self.env["dms.file"].search(
323
+ [("directory_id", "child_of", active_id)]
337
324
  )
338
- all_record_ids = [rec["id"] for rec in comodel_records]
339
- field_range = {}
340
- enable_counters = kwargs.get("enable_counters")
341
- for record in comodel_records:
342
- record_id = record["id"]
343
- parent = record["parent_id"]
344
- record_values = {
345
- "id": record_id,
346
- "display_name": record["display_name"],
347
- # If the parent directory is not in all the records we should not
348
- # set parent_id because the user does not have access to parent.
349
- "parent_id": (
350
- parent[0] if parent and parent[0] in all_record_ids else False
351
- ),
352
- }
353
- if enable_counters:
354
- record_values["__count"] = 0
355
- field_range[record_id] = record_values
356
- if enable_counters:
357
- res = super().search_panel_select_range(field_name, **kwargs)
358
- for item in res["values"]:
359
- if item["id"] in field_range:
360
- field_range[item["id"]]["__count"] = item["__count"]
361
- return {"parent_field": "parent_id", "values": list(field_range.values())}
362
- context = {}
363
- if field_name == "category_id":
364
- context["category_short_name"] = True
365
- return super(File, self.with_context(**context)).search_panel_select_range(
366
- field_name, **kwargs
325
+ all_directory_ids = []
326
+ for file_record in files:
327
+ directory = file_record.directory_id
328
+ while directory:
329
+ all_directory_ids.append(directory.id)
330
+ directory = directory.parent_id
331
+ domain.append(("id", "in", all_directory_ids))
332
+ # Get all possible directories
333
+ comodel_records = (
334
+ self.env["dms.directory"]
335
+ .with_context(directory_short_name=True)
336
+ .search_read(domain, ["display_name", "parent_id"])
367
337
  )
338
+ all_record_ids = [rec["id"] for rec in comodel_records]
339
+ field_range = {}
340
+ enable_counters = kwargs.get("enable_counters")
341
+ for record in comodel_records:
342
+ record_id = record["id"]
343
+ parent = record["parent_id"]
344
+ record_values = {
345
+ "id": record_id,
346
+ "display_name": record["display_name"],
347
+ # If the parent directory is not in all the records we should not
348
+ # set parent_id because the user does not have access to parent.
349
+ "parent_id": (
350
+ parent[0] if parent and parent[0] in all_record_ids else False
351
+ ),
352
+ }
353
+ if enable_counters:
354
+ record_values["__count"] = 0
355
+ field_range[record_id] = record_values
356
+ if enable_counters:
357
+ res = super().search_panel_select_range(field_name, **kwargs)
358
+ for item in res["values"]:
359
+ if item["id"] in field_range:
360
+ field_range[item["id"]]["__count"] = item["__count"]
361
+ return {"parent_field": "parent_id", "values": list(field_range.values())}
368
362
 
369
363
  @api.model
370
364
  def search_panel_select_multi_range(self, field_name, **kwargs):
@@ -394,18 +388,15 @@ class File(models.Model):
394
388
  "file_ids", operator, directory_id, comodel_domain
395
389
  )
396
390
  return super(
397
- File, self.with_context(directory_short_name=True)
391
+ DMSFile, self.with_context(directory_short_name=True)
398
392
  ).search_panel_select_multi_range(
399
393
  field_name, comodel_domain=directory_comodel_domain, **kwargs
400
394
  )
401
395
  return super(
402
- File, self.with_context(directory_short_name=True)
396
+ DMSFile, self.with_context(directory_short_name=True)
403
397
  ).search_panel_select_multi_range(field_name, **kwargs)
404
398
 
405
- # ----------------------------------------------------------
406
399
  # Read
407
- # ----------------------------------------------------------
408
-
409
400
  @api.depends("name", "directory_id", "directory_id.parent_path")
410
401
  def _compute_path(self):
411
402
  model = self.env["dms.directory"]
@@ -432,7 +423,7 @@ class File(models.Model):
432
423
  current_dir = current_dir.parent_id
433
424
  record.update(
434
425
  {
435
- "path_names": "/".join(path_names),
426
+ "path_names": "/".join(path_names) if all(path_names) else "",
436
427
  "path_json": json.dumps(path_json),
437
428
  }
438
429
  )
@@ -494,23 +485,17 @@ class File(models.Model):
494
485
  else:
495
486
  storage_label = selection.get(storage_type)
496
487
  file_label = selection.get(record.save_type)
497
- record.migration = "{} > {}".format(file_label, storage_label)
488
+ record.migration = f"{file_label} > {storage_label}"
498
489
  record.require_migration = True
499
490
 
500
- # ----------------------------------------------------------
501
491
  # View
502
- # ----------------------------------------------------------
503
-
504
492
  @api.onchange("category_id")
505
493
  def _change_category(self):
506
494
  self.tag_ids = self.tag_ids.filtered(
507
495
  lambda rec: not rec.category_id or rec.category_id == self.category_id
508
496
  )
509
497
 
510
- # ----------------------------------------------------------
511
498
  # Constrains
512
- # ----------------------------------------------------------
513
-
514
499
  @api.constrains("storage_id", "res_model", "res_id")
515
500
  def _check_storage_id_attachment_res_model(self):
516
501
  for record in self:
@@ -526,35 +511,32 @@ class File(models.Model):
526
511
  for record in self:
527
512
  if not file.check_name(record.name):
528
513
  raise ValidationError(_("The file name is invalid."))
529
- files = record.sudo().directory_id.file_ids.name_get()
530
- if list(
531
- filter(
532
- lambda file: file[1] == record.name and file[0] != record.id, files
533
- )
514
+ files = record.sudo().directory_id.file_ids
515
+ if files.filtered(
516
+ lambda file, record=record: file.name == record.name and file != record
534
517
  ):
535
- raise ValidationError(_("A file with the same name already exists."))
518
+ raise ValidationError(
519
+ _("A file with the same name already exists in this directory.")
520
+ )
536
521
 
537
522
  @api.constrains("extension")
538
523
  def _check_extension(self):
539
- for record in self:
540
- if (
541
- record.extension
542
- and record.extension in self._get_forbidden_extensions()
543
- ):
544
- raise ValidationError(_("The file has a forbidden file extension."))
524
+ if self.filtered(
525
+ lambda rec: rec.extension
526
+ and rec.extension in self._get_forbidden_extensions()
527
+ ):
528
+ raise ValidationError(_("The file has a forbidden file extension."))
545
529
 
546
530
  @api.constrains("size")
547
531
  def _check_size(self):
548
- for record in self:
549
- if record.size and record.size > self._get_binary_max_size() * 1024 * 1024:
550
- raise ValidationError(
551
- _("The maximum upload size is %s MB.") % self._get_binary_max_size()
552
- )
532
+ if self.filtered(
533
+ lambda rec: rec.size > self._get_binary_max_size() * 1024 * 1024
534
+ ):
535
+ raise ValidationError(
536
+ _("The maximum upload size is %s MB.") % self._get_binary_max_size()
537
+ )
553
538
 
554
- # ----------------------------------------------------------
555
539
  # Create, Update, Delete
556
- # ----------------------------------------------------------
557
-
558
540
  def _inverse_content(self):
559
541
  updates = defaultdict(set)
560
542
  for record in self:
@@ -562,12 +544,12 @@ class File(models.Model):
562
544
  binary = base64.b64decode(record.content or "")
563
545
  values = record._update_content_vals(values, binary)
564
546
  updates[tools.frozendict(values)].add(record.id)
565
- with self.env.norecompute():
566
- for vals, ids in updates.items():
567
- self.browse(ids).write(dict(vals))
547
+ for vals, ids in updates.items():
548
+ self.browse(ids).write(dict(vals))
568
549
 
569
550
  def _create_model_attachment(self, vals):
570
551
  res_vals = vals.copy()
552
+ directory_id = False
571
553
  if "directory_id" in res_vals:
572
554
  directory_id = res_vals["directory_id"]
573
555
  elif self.env.context.get("active_id"):
@@ -601,14 +583,14 @@ class File(models.Model):
601
583
  def copy(self, default=None):
602
584
  self.ensure_one()
603
585
  default = dict(default or [])
586
+ names = self.sudo().directory_id.file_ids.mapped("name")
604
587
  if "directory_id" in default:
605
- model = self.env["dms.directory"]
606
- directory = model.browse(default["directory_id"])
588
+ directory = self.env["dms.directory"].browse(
589
+ default.get("directory_id", False)
590
+ )
607
591
  names = directory.sudo().file_ids.mapped("name")
608
- else:
609
- names = self.sudo().directory_id.file_ids.mapped("name")
610
592
  default.update({"name": file.unique_name(self.name, names, self.extension)})
611
- return super(File, self).copy(default)
593
+ return super().copy(default)
612
594
 
613
595
  @api.model_create_multi
614
596
  def create(self, vals_list):
@@ -617,12 +599,10 @@ class File(models.Model):
617
599
  if "attachment_id" not in vals:
618
600
  vals = self._create_model_attachment(vals)
619
601
  new_vals_list.append(vals)
620
- return super(File, self).create(new_vals_list)
602
+ return super().create(new_vals_list)
621
603
 
622
604
  # ----------------------------------------------------------
623
605
  # Locking fields and functions
624
- # ----------------------------------------------------------
625
-
626
606
  locked_by = fields.Many2one(comodel_name="res.users")
627
607
 
628
608
  is_locked = fields.Boolean(compute="_compute_locked", string="Locked")
@@ -639,10 +619,7 @@ class File(models.Model):
639
619
  def unlock(self):
640
620
  self.write({"locked_by": None})
641
621
 
642
- # ----------------------------------------------------------
643
622
  # Read, View
644
- # ----------------------------------------------------------
645
-
646
623
  @api.depends("locked_by")
647
624
  def _compute_locked(self):
648
625
  for record in self:
@@ -664,6 +641,7 @@ class File(models.Model):
664
641
  "mimetype": attachment.mimetype,
665
642
  }
666
643
 
644
+ @api.model
667
645
  def get_dms_files_from_attachments(self, attachment_ids=None):
668
646
  """Get the dms files from uploaded attachments.
669
647
  :return: An Array of dms files.
@@ -1,12 +1,18 @@
1
1
  # Copyright 2020 Creu Blanca
2
2
  # Copyright 2021 Tecnativa - Víctor Martínez
3
+ # Copyright 2024 Subteno - Timothée Vannier (https://www.subteno.com).
3
4
  # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
4
5
 
5
6
 
6
7
  from logging import getLogger
7
8
 
8
9
  from odoo import api, fields, models
9
- from odoo.osv.expression import FALSE_DOMAIN, NEGATIVE_TERM_OPERATORS, OR, TRUE_DOMAIN
10
+ from odoo.osv.expression import (
11
+ FALSE_DOMAIN,
12
+ NEGATIVE_TERM_OPERATORS,
13
+ OR,
14
+ TRUE_DOMAIN,
15
+ )
10
16
 
11
17
  _logger = getLogger(__name__)
12
18
 
@@ -60,14 +66,15 @@ class DmsSecurityMixin(models.AbstractModel):
60
66
  for record in self:
61
67
  record.record_ref = False
62
68
  if record.res_model and record.res_id:
63
- record.record_ref = "{},{}".format(record.res_model, record.res_id)
69
+ record.record_ref = f"{record.res_model},{record.res_id}"
64
70
 
65
71
  def _compute_permissions(self):
66
- """Get permissions for the current record.
67
-
68
- ⚠ Not very performant; only display field on form views.
69
72
  """
70
- # Superuser unrestricted 🦸
73
+ Get permissions for the current record.
74
+ """
75
+
76
+ # Update according to presence when applying ir.rule
77
+ self.invalidate_recordset()
71
78
  if self.env.su:
72
79
  self.update(
73
80
  {
@@ -78,7 +85,7 @@ class DmsSecurityMixin(models.AbstractModel):
78
85
  }
79
86
  )
80
87
  return
81
- # Update according to presence when applying ir.rule
88
+
82
89
  creatable = self._filter_access_rules("create")
83
90
  readable = self._filter_access_rules("read")
84
91
  unlinkable = self._filter_access_rules("unlink")
@@ -100,10 +107,7 @@ class DmsSecurityMixin(models.AbstractModel):
100
107
  return []
101
108
  inherited_access_field = "storage_id_inherit_access_from_parent_record"
102
109
  if self._name != "dms.directory":
103
- inherited_access_field = "{}.{}".format(
104
- self._directory_field,
105
- inherited_access_field,
106
- )
110
+ inherited_access_field = f"{self._directory_field}.{inherited_access_field}"
107
111
  inherited_access_domain = [
108
112
  ("storage_id_save_type", "=", "attachment"),
109
113
  (inherited_access_field, "=", True),
@@ -119,8 +123,9 @@ class DmsSecurityMixin(models.AbstractModel):
119
123
  try:
120
124
  model = self.env[group["res_model"]]
121
125
  except KeyError:
122
- # Model not registered. This is normal if you are upgrading the
123
- # database. Otherwise, you probably have garbage DMS data.
126
+ # The model might not be registered.
127
+ # This is normal if you are upgrading the database.
128
+ # Otherwise, you probably have garbage DMS data.
124
129
  # These records will be accessible by DB users only.
125
130
  domains.append(
126
131
  [
@@ -153,7 +158,7 @@ class DmsSecurityMixin(models.AbstractModel):
153
158
  "unlink": "AND dag.perm_inclusive_unlink",
154
159
  "write": "AND dag.perm_inclusive_write",
155
160
  }[operation]
156
- select = """
161
+ select = f"""
157
162
  SELECT
158
163
  dir_group_rel.aid
159
164
  FROM
@@ -163,11 +168,9 @@ class DmsSecurityMixin(models.AbstractModel):
163
168
  INNER JOIN dms_access_group_users_rel AS users
164
169
  ON users.gid = dag.id
165
170
  WHERE
166
- users.uid = %s {}
167
- """.format(
168
- operation_check
169
- )
170
- return (select, (self.env.uid,))
171
+ users.uid = %s {operation_check}
172
+ """
173
+ return select, (self.env.uid,)
171
174
 
172
175
  @api.model
173
176
  def _get_domain_by_access_groups(self, operation):
@@ -202,13 +205,14 @@ class DmsSecurityMixin(models.AbstractModel):
202
205
  if _self.env.su:
203
206
  # You're SUPERUSER_ID
204
207
  return TRUE_DOMAIN if positive else FALSE_DOMAIN
205
- # Obtain and combine domains
208
+
206
209
  result = OR(
207
210
  [
208
211
  _self._get_domain_by_access_groups(operation),
209
212
  _self._get_domain_by_inheritance(operation),
210
213
  ]
211
214
  )
215
+
212
216
  if not positive:
213
217
  result.insert(0, "!")
214
218
  return result
@@ -233,7 +237,7 @@ class DmsSecurityMixin(models.AbstractModel):
233
237
  # Only kept to not break inheritance; see next comment
234
238
  result = super()._filter_access_rules_python(operation)
235
239
  # HACK Always fall back to applying rules by SQL.
236
- # Upstream `_filter_acccess_rules_python()` doesn't use computed fields
240
+ # Upstream `_filter_access_rules_python()` doesn't use computed fields
237
241
  # search methods. Thus, it will take the `[('permission_{operation}',
238
242
  # '=', user.id)]` rule literally. Obviously that will always fail
239
243
  # because `self[f"permission_{operation}"]` will always be a `bool`,
@@ -249,7 +253,7 @@ class DmsSecurityMixin(models.AbstractModel):
249
253
  # Need to flush now, so all groups are stored in DB and the SELECT used
250
254
  # to check access works
251
255
  res.flush_recordset()
252
- # Go back to original sudo state and check we really had creation permission
256
+ # Go back to the original sudo state and check we really had creation permission
253
257
  res = res.sudo(self.env.su)
254
258
  res.check_access_rights("create")
255
259
  res.check_access_rule("create")
@@ -4,7 +4,6 @@ from odoo import api, models
4
4
 
5
5
 
6
6
  class IrAttachment(models.Model):
7
-
8
7
  _inherit = "ir.attachment"
9
8
 
10
9
  def _get_dms_directories(self, res_model, res_id):
@@ -0,0 +1,19 @@
1
+ # Copyright 2024 Subteno - Timothée Vannier (https://www.subteno.com)
2
+ # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
3
+
4
+
5
+ from odoo import models
6
+
7
+
8
+ class IrBinary(models.AbstractModel):
9
+ _inherit = "ir.binary"
10
+
11
+ def _find_record_check_access(self, record, access_token):
12
+ if record._name in ("dms.file", "dms.directory"):
13
+ if record.sudo().check_access_token(access_token):
14
+ # sudo because the user might not usually have access to the record but
15
+ # now the token is valid.
16
+ # Used to display the icon in the portal.
17
+ return record.sudo()
18
+
19
+ return super()._find_record_check_access(record, access_token)
@@ -7,11 +7,9 @@ from odoo import models
7
7
  class MailThread(models.AbstractModel):
8
8
  _inherit = "mail.thread"
9
9
 
10
- def _message_post_process_attachments(
11
- self, attachments, attachment_ids, message_data
12
- ):
10
+ def _process_attachments_for_post(self, attachments, attachment_ids, message_data):
13
11
  """Indicate to DMS that we're attaching a message to a record."""
14
12
  _self = self.with_context(attaching_to_record=True)
15
- return super(MailThread, _self)._message_post_process_attachments(
13
+ return super(MailThread, _self)._process_attachments_for_post(
16
14
  attachments, attachment_ids, message_data
17
15
  )