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.
- odoo/addons/dms/README.rst +132 -78
- odoo/addons/dms/__manifest__.py +34 -16
- odoo/addons/dms/actions/file.xml +1 -3
- odoo/addons/dms/controllers/main.py +0 -34
- odoo/addons/dms/controllers/portal.py +178 -92
- odoo/addons/dms/data/onboarding_data.xml +77 -0
- odoo/addons/dms/demo/access_group.xml +3 -9
- odoo/addons/dms/i18n/de.po +13 -117
- odoo/addons/dms/i18n/dms.pot +137 -174
- odoo/addons/dms/i18n/es.po +15 -58
- odoo/addons/dms/i18n/fr.po +296 -450
- odoo/addons/dms/i18n/he_IL.po +13 -117
- odoo/addons/dms/i18n/it.po +15 -131
- odoo/addons/dms/i18n/nl.po +13 -117
- odoo/addons/dms/i18n/pt.po +13 -121
- odoo/addons/dms/i18n/pt_BR.po +28 -147
- odoo/addons/dms/i18n/ru.po +13 -117
- odoo/addons/dms/models/__init__.py +7 -1
- odoo/addons/dms/models/abstract_dms_mixin.py +1 -1
- odoo/addons/dms/models/access_groups.py +18 -11
- odoo/addons/dms/models/base.py +10 -2
- odoo/addons/dms/models/directory.py +46 -75
- odoo/addons/dms/models/{category.py → dms_category.py} +10 -42
- odoo/addons/dms/models/dms_file.py +125 -147
- odoo/addons/dms/models/dms_security_mixin.py +26 -22
- odoo/addons/dms/models/ir_attachment.py +0 -1
- odoo/addons/dms/models/ir_binary.py +19 -0
- odoo/addons/dms/models/mail_thread.py +2 -4
- odoo/addons/dms/models/mixins_thumbnail.py +13 -8
- odoo/addons/dms/models/onboarding_onboarding.py +16 -0
- odoo/addons/dms/models/onboarding_onboarding_step.py +50 -0
- odoo/addons/dms/models/res_company.py +22 -47
- odoo/addons/dms/models/res_config_settings.py +0 -1
- odoo/addons/dms/models/storage.py +4 -24
- odoo/addons/dms/models/tag.py +1 -1
- odoo/addons/dms/readme/CONFIGURE.md +59 -0
- odoo/addons/dms/readme/CONTRIBUTORS.md +14 -0
- odoo/addons/dms/readme/CREDITS.md +6 -0
- odoo/addons/dms/readme/DESCRIPTION.md +9 -0
- odoo/addons/dms/readme/INSTALL.md +4 -0
- odoo/addons/dms/readme/ROADMAP.md +19 -0
- odoo/addons/dms/readme/USAGE.md +11 -0
- odoo/addons/dms/security/security.xml +1 -0
- odoo/addons/dms/static/description/icon.png +0 -0
- odoo/addons/dms/static/description/icon.svg +4 -1
- odoo/addons/dms/static/description/index.html +133 -88
- odoo/addons/dms/static/description/portal_icon.svg +12 -0
- odoo/addons/dms/static/src/js/fields/{path_owl.esm.js → path_json/path_owl.esm.js} +22 -4
- odoo/addons/dms/static/src/js/fields/{path_owl.xml → path_json/path_owl.xml} +7 -4
- odoo/addons/dms/static/src/js/fields/preview_binary/preview_record.esm.js +47 -0
- odoo/addons/dms/static/src/js/{views/fields/binary → fields/preview_binary}/preview_record.xml +5 -4
- odoo/addons/dms/static/src/js/views/dms_file_upload.esm.js +155 -148
- odoo/addons/dms/static/src/js/views/file_kanban_controller.xml +0 -1
- odoo/addons/dms/static/src/js/views/file_kanban_record.esm.js +26 -27
- odoo/addons/dms/static/src/js/views/file_kanban_renderer.esm.js +1 -2
- odoo/addons/dms/static/src/js/views/file_kanban_renderer.xml +12 -16
- odoo/addons/dms/static/src/js/views/file_kanban_view.esm.js +11 -9
- odoo/addons/dms/static/src/js/views/file_list_controller.esm.js +1 -1
- odoo/addons/dms/static/src/js/views/file_list_renderer.xml +1 -7
- odoo/addons/dms/static/src/js/views/file_list_view.esm.js +11 -9
- odoo/addons/dms/static/src/js/views/search_panel.esm.js +9 -10
- odoo/addons/dms/static/src/models/attachment.esm.js +83 -82
- odoo/addons/dms/static/src/models/attachment_image.esm.js +30 -28
- odoo/addons/dms/static/src/models/attachment_viewer_viewable.esm.js +27 -25
- odoo/addons/dms/static/src/scss/directory_kanban.scss +28 -73
- odoo/addons/dms/static/src/scss/dms_common.scss +69 -0
- odoo/addons/dms/static/src/scss/file_kanban.scss +22 -34
- odoo/addons/dms/static/src/scss/portal.scss +12 -0
- odoo/addons/dms/static/tests/tours/dms_portal_tour.esm.js +46 -0
- odoo/addons/dms/{views/dms_portal_templates.xml → template/portal.xml} +13 -3
- odoo/addons/dms/tests/common.py +20 -4
- odoo/addons/dms/tests/data/mail01.eml +2 -2
- odoo/addons/dms/tests/data/mail02.eml +2 -2
- odoo/addons/dms/tests/test_benchmark.py +16 -21
- odoo/addons/dms/tests/test_directory.py +128 -40
- odoo/addons/dms/tests/test_file.py +112 -30
- odoo/addons/dms/tests/test_file_database.py +50 -24
- odoo/addons/dms/tests/test_portal.py +50 -37
- odoo/addons/dms/tests/test_storage_attachment.py +85 -63
- odoo/addons/dms/tests/test_storage_database.py +89 -35
- odoo/addons/dms/tools/file.py +63 -11
- odoo/addons/dms/views/dms_access_groups_views.xml +2 -17
- odoo/addons/dms/views/{category.xml → dms_category.xml} +4 -9
- odoo/addons/dms/views/{directory.xml → dms_directory.xml} +168 -176
- odoo/addons/dms/views/dms_file.xml +170 -191
- odoo/addons/dms/views/{tag.xml → dms_tag.xml} +50 -53
- odoo/addons/dms/views/menu.xml +52 -21
- odoo/addons/dms/views/res_config_settings.xml +31 -82
- odoo/addons/dms/views/storage.xml +7 -13
- odoo_addon_dms-17.0.1.0.0.2.dist-info/METADATA +257 -0
- {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
- {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
- odoo_addon_dms-17.0.1.0.0.2.dist-info/top_level.txt +1 -0
- odoo/addons/dms/readme/CONFIGURE.rst +0 -40
- odoo/addons/dms/readme/CONTRIBUTORS.rst +0 -16
- odoo/addons/dms/readme/CREDITS.rst +0 -6
- odoo/addons/dms/readme/DESCRIPTION.rst +0 -6
- odoo/addons/dms/readme/INSTALL.rst +0 -9
- odoo/addons/dms/readme/ROADMAP.rst +0 -8
- odoo/addons/dms/readme/USAGE.rst +0 -8
- odoo/addons/dms/static/src/js/components/path/path.esm.js +0 -0
- odoo/addons/dms/static/src/js/dms_portal_tour.js +0 -57
- odoo/addons/dms/static/src/js/fields/path.js +0 -81
- odoo/addons/dms/static/src/js/views/fields/binary/preview_record.esm.js +0 -35
- odoo/addons/dms/static/src/js/views/many_drop_target.js +0 -98
- odoo/addons/dms/static/src/scss/variables.scss +0 -9
- odoo/addons/dms/static/src/xml/views.xml +0 -72
- odoo/addons/dms/template/assets.xml +0 -44
- odoo/addons/dms/template/onboarding.xml +0 -97
- odoo_addon_dms-16.0.1.8.0.3.dist-info/METADATA +0 -203
- 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
|
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="
|
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
|
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
|
-
|
169
|
-
|
170
|
-
|
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
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
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
|
249
|
+
if should_logging:
|
262
250
|
_logger.info(
|
263
251
|
_(
|
264
|
-
"Migrate File %(index)s of %(record_count)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(
|
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
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
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
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
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
|
-
|
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
|
-
|
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 = "{} > {}"
|
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
|
530
|
-
if
|
531
|
-
|
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(
|
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
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
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
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
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
|
-
|
566
|
-
|
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
|
-
|
606
|
-
|
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(
|
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(
|
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
|
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 = "{
|
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
|
-
|
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
|
-
|
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 = "{}.{}"
|
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
|
-
#
|
123
|
-
#
|
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
|
-
"""
|
168
|
-
|
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
|
-
|
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 `
|
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")
|
@@ -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
|
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).
|
13
|
+
return super(MailThread, _self)._process_attachments_for_post(
|
16
14
|
attachments, attachment_ids, message_data
|
17
15
|
)
|