First Commit

This commit is contained in:
Deena 2025-07-23 09:25:42 +05:30
commit 6e358ba908
728 changed files with 94436 additions and 0 deletions

245
dms/README.rst Executable file
View File

@ -0,0 +1,245 @@
==========================
Document Management System
==========================
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:966c4331ff7c75b1ea8cb1d065c878d81250957cd305a5d6422def133e2a7d63
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fdms-lightgray.png?logo=github
:target: https://github.com/OCA/dms/tree/17.0/dms
:alt: OCA/dms
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/dms-17-0/dms-17-0-dms
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/dms&target_branch=17.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
DMS is a module for creating, managing and viewing document files
directly within Odoo. This module is only the basis for an entire
ecosystem of apps that extend and seamlessly integrate with the document
management system.
This module adds portal functionality for directories and files for
allowed users, both portal or internal users. You can get as well a
tokenized link from a directory or a file for sharing it with any
anonymous user.
**Table of contents**
.. contents::
:local:
Installation
============
Preview
-------
``python-magic`` library is recommended to be installed for having whole
support to get proper file types and file preview.
Configuration
=============
Configuration
=============
To configure this module, you need to:
1. Create a storage
-------------------
1. Go to *Documents -> Configuration -> Storages*.
2. Create a new document storage. You can choose between three options
on ``Save Type``:
- ``Database``: Store the files on the database as a field
- ``Attachment``: Store the files as attachments
- ``File``: Store the files on the file system
2. Create an access group
-------------------------
1. Next, create an administrative access group. Go to *Configuration ->
Access Groups*.
- Create a new group, name it appropriately, and turn on all three
permissions (Create, Write and Unlink. Read is implied and always
enabled).
- Add any other top-level administrative users to the group if needed
(your user should already be there).
- You can create other groups in here later for fine-grained access
control.
3. Create a directory
---------------------
1. Afterward, go to *Documents -> Directories*.
2. Create a new directory, mark it as root and select the previously
created setting.
- Select the *Groups* tab and add your administrative group created
above. If your directory was already created before the group, you
can also add it in the access groups (*Configuration -> Access
Groups*).
3. In the directory settings, you can also add other access groups
(created above) that will be able to:
- read
- create
- write
- delete
Migration
=========
If you need to modify the storage ``Save Type`` you might want to
migrate the file data. To achieve it, you need to:
1. Go to *Documents -> Configuration -> Storage* and select the storage
you want to modify
2. Modify the save type
3. Press the button Migrate files if you want to migrate all the files
at once
4. Press the button Manual File Migration to specify files one by one
You can check all the files that still need to be migrated from all
storages and migrate them manually on *Documents -> Configuration ->
Migration*
File Wizard Selection
=====================
There is an action called ``action_dms_file_wizard_selector`` to open a
wizard to list files in kanban view. This can be used (example
dms_attachment_link module) to add a button in kanban view with the
action we need.
Usage
=====
The best way to manage the documents is to switch to the Documents view.
Existing documents can be managed there and new documents can be
created.
Portal functionality
--------------------
You can add any portal user to DMS access groups, and then allow that
group in directories, so they will see in the portal such directories
and their files. Another possibility is to click on "Share" button
inside a directory or a file for obtaining a tokenized link for single
access to that resource, no matter if logged or not.
Known issues / Roadmap
======================
- Files preview in portal
- Allow to download folder in portal and create zip file with all
content
- Save in cache own_root directories and update in every
create/write/unlink function
- Add a migration procedure for converting an storage to attachment one
for populating existing records with attachments as folders
- Add a link from attachment view in chatter to linked documents
- If Inherit permissions from related record (the
inherit_access_from_parent_record field from storage) is changed when
directories already exist, inconsistencies may occur because groups
defined in the directories and subdirectories will still exist, all
groups in these directories should be removed before changing.
- Since portal users can read ``dms.storage`` records, if your module
extends this model to another storage backend that needs using
secrets, remember to forbid access to the secrets fields by other
means. It would be nice to be able to remove that rule at some point.
- Searchpanel in files: Highlight items (shading) without records when
filtering something (by name for example).
- Accessing the clipboard (for example copy share link of
file/directory) is limited to secure connections. It also happens in
any part of Odoo.
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/dms/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/dms/issues/new?body=module:%20dms%0Aversion:%2017.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
-------
* MuK IT
* Tecnativa
Contributors
------------
- Mathias Markl <mathias.markl@mukit.at>
- Enric Tobella <etobella@creublanca.es>
- Antoni Romera
- Gelu Boros <gelu.boros@rgbconsulting.com>
- `Tecnativa <https://www.tecnativa.com>`__:
- Víctor Martínez
- Pedro M. Baeza
- Jairo Llopis
- `Elego <https://www.elegosoft.com>`__:
- Yu Weng <yweng@elegosoft.com>
- Philip Witte <phillip.witte@elegosoft.com>
- Khanh Bui <khanh.bui@mail.elegosoft.com>
- `Subteno <https://www.subteno.com>`__:
- Timothée Vannier <tva@subteno.com>
Other credits
-------------
Some pictures are based on or inspired by:
- `Roundicons <https://www.flaticon.com/authors/roundicons>`__
- `Smashicons <https://www.flaticon.com/authors/smashicons>`__
- `EmojiOne <https://github.com/EmojiTwo/emojitwo>`__ : Portal DMS icon
- `GitHub Octicons <https://github.com/primer/octicons/>`__ : The main
DMS icon
Maintainers
-----------
This module is maintained by the OCA.
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
This module is part of the `OCA/dms <https://github.com/OCA/dms/tree/17.0/dms>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

3
dms/__init__.py Executable file
View File

@ -0,0 +1,3 @@
from . import controllers
from . import models
from . import wizards

78
dms/__manifest__.py Executable file
View File

@ -0,0 +1,78 @@
# Copyright 2017-2019 MuK IT GmbH
# Copyright 2024 Subteno - Timothée Vannier (https://www.subteno.com).
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
{
"name": "Document Management System",
"summary": """Document Management System for Odoo""",
"version": "17.0.1.2.1",
"category": "Document Management",
"license": "LGPL-3",
"website": "https://github.com/OCA/dms",
"author": "MuK IT, Tecnativa, Odoo Community Association (OCA)",
"depends": [
"mail",
"http_routing",
"onboarding",
"portal",
"base",
"web",
],
"data": [
# Security
"security/security.xml",
"security/ir.model.access.csv",
# Actions
"actions/file.xml",
# Templates
"template/portal.xml",
# Data
"data/onboarding_data.xml",
# Views
"views/dms_tag.xml",
"views/dms_category.xml",
"views/dms_file.xml",
"views/dms_directory.xml",
"views/storage.xml",
"views/dms_access_groups_views.xml",
"views/res_config_settings.xml",
"views/menu.xml",
# Wizard
"wizards/wizard_dms_file_move_views.xml",
"wizards/wizard_dms_share_views.xml",
],
"assets": {
"web.assets_backend": [
# Style
"dms/static/src/scss/directory_kanban.scss",
"dms/static/src/scss/file_kanban.scss",
"dms/static/src/scss/dms_common.scss",
# JS
"dms/static/src/models/*.js",
"dms/static/src/js/fields/path_json/path_owl.esm.js",
"dms/static/src/js/fields/preview_binary/preview_record.esm.js",
"dms/static/src/js/views/*.esm.js",
# XML
"dms/static/src/js/fields/path_json/path_owl.xml",
"dms/static/src/js/fields/preview_binary/preview_record.xml",
"dms/static/src/js/views/*.xml",
],
"web.assets_frontend": [
"dms/static/src/scss/portal.scss",
],
"web.assets_tests": [
"dms/static/tests/tours/**/*",
],
},
"demo": [
"demo/res_users.xml",
"demo/access_group.xml",
"demo/category.xml",
"demo/tag.xml",
"demo/storage.xml",
"demo/directory.xml",
"demo/file.xml",
],
"icon": "/dms/static/description/icon.png",
"application": True,
}

Binary file not shown.

14
dms/actions/file.xml Executable file
View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
Copyright 2017-2019 MuK IT GmbH
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
-->
<odoo>
<record id="action_dms_attachment_migrate" model="ir.actions.server">
<field name="name">Migrate</field>
<field name="model_id" ref="model_dms_file" />
<field name="binding_model_id" ref="dms.model_dms_file" />
<field name="state">code</field>
<field name="code">records.action_migrate()</field>
</record>
</odoo>

2
dms/controllers/__init__.py Executable file
View File

@ -0,0 +1,2 @@
from . import main
from . import portal

Binary file not shown.

Binary file not shown.

Binary file not shown.

15
dms/controllers/main.py Executable file
View File

@ -0,0 +1,15 @@
# Copyright 2017-2019 MuK IT GmbH
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from odoo import http
from odoo.http import request
class OnboardingController(http.Controller):
@http.route("/config/dms.forbidden_extensions", type="json", auth="user")
def forbidden_extensions(self, **_kwargs):
params = request.env["ir.config_parameter"].sudo()
return {
"forbidden_extensions": params.get_param(
"dms.forbidden_extensions", default=""
)
}

288
dms/controllers/portal.py Executable file
View File

@ -0,0 +1,288 @@
# Copyright 2020-2021 Tecnativa - Víctor Martínez
# Copyright 2024 Subteno - Timothée VANNIER (https://www.subteno.com).
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
import base64
from typing import Optional # noqa # pylint: disable=unused-import
from odoo import _, http
from odoo.http import content_disposition, request
from odoo.osv.expression import OR
from odoo.addons.portal.controllers.portal import CustomerPortal
from odoo.addons.web.controllers.utils import ensure_db
class CustomerPortal(CustomerPortal):
def _dms_check_access(self, model, res_id, access_token=None):
"""
Check access to the record.
:param str model: model
:param int res_id: res_id
:param Optional[str] access_token: access_token
:return: item
"""
item = request.env[model].browse(res_id)
if access_token:
item = item.sudo()
if not item.check_access_token(access_token):
return False
else:
if not item.permission_read:
return False
return item
def _prepare_home_portal_values(self, counters):
values = super()._prepare_home_portal_values(counters)
if "dms_directory_count" in counters:
ids = request.env["dms.directory"]._get_own_root_directories()
values["dms_directory_count"] = len(ids)
return values
@http.route(["/my/dms"], type="http", auth="user", website=True)
def portal_my_dms(
self, sortby=None, filterby=None, search=None, search_in="name", **kw
):
"""
Display the main page for the DMS module.
:param Optional[str] sortby: The field to sort by
:param Optional[str] filterby: The field to filter by
:param Optional[str] search: The search term
:param Optional[str] search_in: The field to search in
:return: response
:rtype: odoo.http.Response
"""
values = self._prepare_portal_layout_values()
(
filterby,
searchbar_inputs,
searchbar_sortings,
sort_order,
sortby,
) = self._searchbar_data(filterby, sortby)
# domain
domain = [
("id", "in", request.env["dms.directory"]._get_own_root_directories()),
]
# search
if search and search_in == "name":
domain += OR([[], [("name", "ilike", search)]])
# content according to pager and archive selected
items = request.env["dms.directory"].search(domain, order=sort_order)
request.session["my_dms_folder_history"] = items.ids
# values
values.update(
{
"dms_directories": items,
"page_name": "dms_directory",
"default_url": "/my/dms",
"searchbar_sortings": searchbar_sortings,
"searchbar_inputs": searchbar_inputs,
"search_in": search_in,
"sortby": sortby,
"filterby": filterby,
"access_token": None,
}
)
return request.render("dms.portal_my_dms", values)
@http.route(
["/my/dms/directory/<int:dms_directory_id>"],
type="http",
auth="public",
website=True,
)
def portal_my_dms_directory(
self,
dms_directory_id=False,
sortby=None,
filterby=None,
search=None,
search_in="name",
access_token=None,
**kw,
):
"""
Display the content of a directory.
:param Optional[int] dms_directory_id: dms_directory_id
:param Optional[str] sortby: sortby
:param Optional[str] filterby: filterby
:param Optional[str] search: search
:param Optional[str] search_in: search_in
:param Optional[str] access_token: access_token
:return: response
:rtype: odoo.http.Response
"""
ensure_db()
# operations
(
filterby,
searchbar_inputs,
searchbar_sortings,
sort_order,
sortby,
) = self._searchbar_data(filterby, sortby)
dms_directory_items, res = self._get_directories(
access_token, dms_directory_id, search, search_in, sort_order
)
if not res:
return request.redirect("/" if access_token else "/my")
dms_directory_sudo = res
# dms_files_count
dms_file_items = self._get_files(
access_token, dms_directory_id, search, search_in, sort_order
)
dms_parent_categories = dms_directory_sudo.sudo()._get_parent_categories(
access_token
)
# values
values = {
"dms_directories": dms_directory_items,
"page_name": "dms_directory",
"default_url": "/my/dms",
"searchbar_sortings": searchbar_sortings,
"searchbar_inputs": searchbar_inputs,
"search_in": search_in,
"sortby": sortby,
"filterby": filterby,
"access_token": access_token,
"dms_directory": dms_directory_sudo,
"dms_files": dms_file_items,
"dms_parent_categories": dms_parent_categories,
}
return request.render("dms.portal_my_dms", values)
def _get_files(self, access_token, dms_directory_id, search, search_in, sort_br):
"""
Get files from dms_directory_id
:param Optional[str] access_token: access_token
:param int dms_directory_id: dms_directory_id
:param Optional[str] search: search
:param Optional[str] search_in: search_in
:param str sort_br: sort_br
:return: dms_file_items
:rtype: odoo.model.dms_file
"""
if not dms_directory_id:
return request.env["dms.file"]
file_domain = [
("is_hidden", "=", False),
("directory_id", "=", dms_directory_id),
]
# search
if search and search_in == "name":
file_domain.append(("name", "ilike", search))
# items
file_model = request.env["dms.file"]
is_access_token_valid = file_model.check_access_token(access_token)
file_model = file_model.sudo() if is_access_token_valid else file_model
dms_file_items = file_model.search(file_domain, order=sort_br)
request.session["my_dms_file_history"] = dms_file_items.ids
return dms_file_items
def _get_directories(
self, access_token, dms_directory_id, search, search_in, sort_order
):
"""
Get directories from dms_directory_id
:param Optional[str] access_token: access_token
:param int dms_directory_id: dms_directory_id
:param Optional[str] search: search
:param Optional[str] search_in: search_in
:param str sort_order: sort_br
:return: dms_directory_items, res
:rtype: tuple[odoo.model.dms_directory, bool|odoo.model.dms_directory]
"""
# domain
domain = [("is_hidden", "=", False), ("parent_id", "=", dms_directory_id)]
# search
if search and search_in:
domain.append(("name", "ilike", search))
# content according to pager and archive selected
directory_model = request.env["dms.directory"]
directory_to_check = directory_model.browse(dms_directory_id)
is_access_token_valid = directory_to_check.check_access_token(access_token)
directory_model = (
directory_model.sudo() if is_access_token_valid else directory_model
)
dms_directory_items = directory_model.search(domain, order=sort_order)
request.session["my_dms_folder_history"] = dms_directory_items.ids
res = self._dms_check_access("dms.directory", dms_directory_id, access_token)
return dms_directory_items, res
def _searchbar_data(self, filterby, sortby):
"""
Prepare searchbar data for portal.
:param str filterby: filterby
:param str sortby: sortby
:return: filterby, searchbar_inputs, searchbar_sortings, sort_order,
sortby
:rtype: tuple[str, dict, dict, str, str]
"""
searchbar_sortings = {"name": {"label": _("Name"), "order": "name asc"}}
# default sortby
if not sortby:
sortby = "name"
sort_order = searchbar_sortings[sortby]["order"]
# search
searchbar_inputs = {
"name": {"input": "name", "label": _("Name")},
}
if not filterby:
filterby = "name"
return (
filterby,
searchbar_inputs,
searchbar_sortings,
sort_order,
sortby,
)
@http.route(
["/my/dms/file/<int:dms_file_id>/download"],
type="http",
auth="public",
website=True,
)
def portal_my_dms_file_download(self, dms_file_id, access_token=None, **kw):
"""
Download a file.
:param int dms_file_id: dms_file_id
:param Optional[str] access_token: access_token
:return: response
:rtype: odoo.http.Response
"""
ensure_db()
res = self._dms_check_access("dms.file", dms_file_id, access_token)
if not res:
if access_token:
return request.redirect("/")
return request.redirect("/my")
if res.attachment_id and request.env.user.has_group("base.group_portal"):
res = res.sudo()
file_content = base64.b64decode(res.content)
content_type = ("Content-Type", "application/octet-stream")
disposition_content = (
"Content-Disposition",
content_disposition(res.name),
)
return request.make_response(file_content, [content_type, disposition_content])

77
dms/data/onboarding_data.xml Executable file
View File

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
Copyright 2024 Subteno - Timothée Vannier (https://www.subteno.com).
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
-->
<odoo noupdate="1">
<!-- Steps (has to be fist so that onboarding panel can access it) -->
<record id="onboarding_step_document_storage" model="onboarding.onboarding.step">
<field name="title">Storage</field>
<field name="description">Create a new Document Storage.</field>
<field name="button_text">Create Storage</field>
<field name="done_text">Document Storage Created!</field>
<field
name="panel_step_open_action_name"
>action_open_documents_onboarding_storage</field>
<field
name="step_image"
type="base64"
file="base/static/img/onboarding_default.png"
/>
<field name="step_image_filename">onboarding_default.png</field>
<field name="step_image_alt">Onboarding Storage</field>
<field name="sequence">1</field>
</record>
<record id="onboarding_step_create_directory" model="onboarding.onboarding.step">
<field name="title">Directory</field>
<field name="description">Create a new Root Directory.</field>
<field name="button_text">Create Directory</field>
<field name="done_text">Root Directory Created!</field>
<field
name="panel_step_open_action_name"
>action_open_documents_onboarding_directory</field>
<field
name="step_image"
type="base64"
file="base/static/img/onboarding_default.png"
/>
<field name="step_image_filename">onboarding_default.png</field>
<field name="step_image_alt">Onboarding Directory</field>
<field name="sequence">2</field>
</record>
<record id="onboarding_step_upload_file" model="onboarding.onboarding.step">
<field name="title">File</field>
<field name="description">Upload your first File.</field>
<field name="button_text">Upload File</field>
<field name="done_text">First File Uploaded!</field>
<field
name="panel_step_open_action_name"
>action_open_documents_onboarding_file</field>
<field
name="step_image"
type="base64"
file="base/static/img/onboarding_default.png"
/>
<field name="step_image_filename">onboarding_default.png</field>
<field name="step_image_alt">Onboarding File</field>
<field name="sequence">3</field>
</record>
<!-- File Onboarding Panel -->
<record id="onboarding_onboarding_dms_file" model="onboarding.onboarding">
<field name="name">File Onboarding</field>
<field
name="step_ids"
eval="[
Command.link(ref('dms.onboarding_step_document_storage')),
Command.link(ref('dms.onboarding_step_create_directory')),
Command.link(ref('dms.onboarding_step_upload_file'))
]"
/>
<field name="route_name">document_onboarding_file</field>
<field name="panel_close_action_name">action_close_panel_dms_file</field>
</record>
</odoo>

18
dms/demo/access_group.xml Executable file
View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<record id="access_group_01_demo" model="dms.access.group">
<field name="name">Admin</field>
<field name="perm_create">True</field>
<field name="perm_write">True</field>
<field name="perm_unlink">True</field>
<field name="explicit_user_ids" eval="[(6, 0, [ref('base.user_admin')])]" />
</record>
<record id="access_group_02_demo" model="dms.access.group">
<field name="name">Portal</field>
<field name="group_ids" eval="[(6, 0, [ref('base.group_portal')])]" />
</record>
<record id="access_group_03_demo" model="dms.access.group">
<field name="name">User</field>
<field name="explicit_user_ids" eval="[(6, 0, [ref('base.user_demo')])]" />
</record>
</odoo>

31
dms/demo/category.xml Executable file
View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
Copyright 2017-2019 MuK IT GmbH
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
-->
<odoo noupdate="1">
<record id="category_01_demo" model="dms.category">
<field name="name">Internal</field>
</record>
<record id="category_02_demo" model="dms.category">
<field name="name">Human Resource</field>
<field name="parent_id" ref="dms.category_01_demo" />
</record>
<record id="category_03_demo" model="dms.category">
<field name="name">Contracts</field>
<field name="parent_id" ref="dms.category_02_demo" />
</record>
<record id="category_04_demo" model="dms.category">
<field name="name">Traveling</field>
<field name="parent_id" ref="dms.category_02_demo" />
</record>
<record id="category_05_demo" model="dms.category">
<field name="name">External</field>
</record>
<record id="category_06_demo" model="dms.category">
<field name="name">News</field>
<field name="parent_id" ref="dms.category_05_demo" />
</record>
</odoo>

147
dms/demo/directory.xml Executable file
View File

@ -0,0 +1,147 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
Copyright 2017-2019 MuK IT GmbH
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
-->
<odoo noupdate="1">
<record id="directory_01_demo" model="dms.directory">
<field name="name">Documents</field>
<field name="is_root_directory" eval="True" />
<field name="parent_id" eval="False" />
<field name="color" eval="1" />
<field name="storage_id" ref="dms.storage_demo" />
<field name="category_id" ref="dms.category_01_demo" />
<field
name="tag_ids"
eval="[(6, 0, [ref('dms.tag_01_demo'), ref('dms.tag_05_demo')])]"
/>
<field name="group_ids" eval="[(6, 0, [ref('dms.access_group_01_demo')])]" />
</record>
<record id="directory_02_demo" model="dms.directory">
<field name="name">Media</field>
<field name="is_root_directory" eval="True" />
<field name="parent_id" eval="False" />
<field name="color" eval="2" />
<field name="storage_id" ref="dms.storage_demo" />
<field
name="tag_ids"
eval="[(6, 0, [ref('dms.tag_01_demo'), ref('dms.tag_03_demo')])]"
/>
<field name="group_ids" eval="[(6, 0, [ref('dms.access_group_01_demo')])]" />
</record>
<record id="directory_03_demo" model="dms.directory">
<field name="name">Sheets</field>
<field name="is_root_directory" eval="False" />
<field name="color" eval="1" />
<field name="parent_id" ref="dms.directory_01_demo" />
<field
name="tag_ids"
eval="[(6, 0, [ref('dms.tag_01_demo'), ref('dms.tag_04_demo')])]"
/>
</record>
<record id="directory_04_demo" model="dms.directory">
<field name="name">Templates</field>
<field name="is_root_directory" eval="False" />
<field name="color" eval="1" />
<field name="parent_id" ref="dms.directory_01_demo" />
<field name="category_id" ref="dms.category_01_demo" />
<field name="tag_ids" eval="[(6, 0, [ref('dms.tag_07_demo')])]" />
</record>
<record id="directory_05_demo" model="dms.directory">
<field name="name">Photos</field>
<field name="is_root_directory" eval="False" />
<field name="color" eval="2" />
<field name="category_id" ref="dms.category_02_demo" />
<field name="parent_id" ref="dms.directory_02_demo" />
<field
name="group_ids"
eval="[(6, 0, [ref('dms.access_group_01_demo'), ref('dms.access_group_02_demo')])]"
/>
</record>
<record id="directory_06_demo" model="dms.directory">
<field name="name">2017</field>
<field name="is_root_directory" eval="False" />
<field name="color" eval="2" />
<field name="parent_id" ref="dms.directory_05_demo" />
<field
name="tag_ids"
eval="[(6, 0, [ref('dms.tag_02_demo'), ref('dms.tag_03_demo')])]"
/>
</record>
<record id="directory_07_demo" model="dms.directory">
<field name="name">2018</field>
<field name="is_root_directory" eval="False" />
<field name="color" eval="2" />
<field name="parent_id" ref="dms.directory_05_demo" />
<field
name="tag_ids"
eval="[(6, 0, [ref('dms.tag_02_demo'), ref('dms.tag_06_demo')])]"
/>
</record>
<record id="directory_08_demo" model="dms.directory">
<field name="name">Videos</field>
<field name="is_root_directory" eval="False" />
<field name="color" eval="2" />
<field name="parent_id" ref="dms.directory_02_demo" />
</record>
<record id="directory_09_demo" model="dms.directory">
<field name="name">Music</field>
<field name="is_root_directory" eval="False" />
<field name="color" eval="2" />
<field name="parent_id" ref="dms.directory_02_demo" />
</record>
<record id="directory_10_demo" model="dms.directory">
<field name="name">Graphics</field>
<field name="is_root_directory" eval="False" />
<field name="parent_id" ref="dms.directory_02_demo" />
</record>
<record id="directory_11_demo" model="dms.directory">
<field name="name">Mails</field>
<field name="is_root_directory" eval="True" />
<field name="parent_id" eval="False" />
<field name="color" eval="3" />
<field name="storage_id" ref="dms.storage_demo" />
<field
name="tag_ids"
eval="[(6, 0, [ref('dms.tag_04_demo'), ref('dms.tag_05_demo')])]"
/>
<field
name="group_ids"
eval="[(6, 0, [ref('dms.access_group_01_demo'), ref('dms.access_group_02_demo')])]"
/>
</record>
<record id="directory_12_demo" model="dms.directory">
<field name="name">Data</field>
<field name="is_root_directory" eval="False" />
<field name="color" eval="1" />
<field name="parent_id" ref="dms.directory_01_demo" />
<field
name="tag_ids"
eval="[(6, 0, [ref('dms.tag_06_demo'), ref('dms.tag_07_demo')])]"
/>
</record>
<record id="directory_13_demo" model="dms.directory">
<field name="name">Code</field>
<field name="is_root_directory" eval="False" />
<field name="color" eval="1" />
<field name="category_id" ref="dms.category_01_demo" />
<field name="parent_id" ref="dms.directory_01_demo" />
</record>
<record id="directory_14_demo" model="dms.directory">
<field name="name">Slides</field>
<field name="is_root_directory" eval="False" />
<field name="category_id" ref="dms.category_01_demo" />
<field name="parent_id" ref="dms.directory_01_demo" />
</record>
<record id="directory_root_res_partner_demo" model="dms.directory">
<field name="name">Partners</field>
<field name="is_root_directory" eval="True" />
<field name="color" eval="1" />
<field name="storage_id" ref="dms.storage_attachment_demo" />
<field name="category_id" ref="dms.category_01_demo" />
<field name="model_id" ref="base.model_res_partner" />
<field name="res_model">res.partner</field>
</record>
</odoo>

241
dms/demo/file.xml Executable file
View File

@ -0,0 +1,241 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
Copyright 2017-2019 MuK IT GmbH
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
-->
<odoo noupdate="1">
<record id="file_01_demo" model="dms.file">
<field name="name">Sydney.jpg</field>
<field name="color" eval="1" />
<field name="directory_id" ref="dms.directory_06_demo" />
<field name="content" type="base64" file="dms/test/image01.jpg" />
<field
name="tag_ids"
eval="[(6, 0, [ref('dms.tag_01_demo'), ref('dms.tag_05_demo')])]"
/>
</record>
<record id="file_02_demo" model="dms.file">
<field name="name">Logo_01.jpg</field>
<field name="color" eval="1" />
<field name="directory_id" ref="dms.directory_07_demo" />
<field name="content" type="base64" file="dms/test/image02.jpg" />
</record>
<record id="file_03_demo" model="dms.file">
<field name="name">Logo_02.jpg</field>
<field name="color" eval="1" />
<field name="directory_id" ref="dms.directory_07_demo" />
<field name="category_id" ref="dms.category_02_demo" />
<field name="content" type="base64" file="dms/test/image03.jpg" />
</record>
<record id="file_04_demo" model="dms.file">
<field name="name">Logo_03.jpg</field>
<field name="color" eval="1" />
<field name="directory_id" ref="dms.directory_07_demo" />
<field name="content" type="base64" file="dms/test/image04.jpg" />
</record>
<record id="file_05_demo" model="dms.file">
<field name="name">Logo.svg</field>
<field name="color" eval="1" />
<field name="directory_id" ref="dms.directory_10_demo" />
<field name="category_id" ref="dms.category_03_demo" />
<field name="content" type="base64" file="dms/test/vector.svg" />
</record>
<record id="file_06_demo" model="dms.file">
<field name="name">Loop_01.wav</field>
<field name="color" eval="1" />
<field name="directory_id" ref="dms.directory_09_demo" />
<field name="content" type="base64" file="dms/test/audio01.wav" />
<field
name="tag_ids"
eval="[(6, 0, [ref('dms.tag_01_demo'), ref('dms.tag_03_demo')])]"
/>
</record>
<record id="file_07_demo" model="dms.file">
<field name="name">Loop_02.wav</field>
<field name="color" eval="2" />
<field name="directory_id" ref="dms.directory_09_demo" />
<field name="content" type="base64" file="dms/test/audio02.wav" />
<field
name="tag_ids"
eval="[(6, 0, [ref('dms.tag_01_demo'), ref('dms.tag_03_demo')])]"
/>
</record>
<record id="file_08_demo" model="dms.file">
<field name="name">Loop_03.mp3</field>
<field name="color" eval="2" />
<field name="directory_id" ref="dms.directory_09_demo" />
<field name="content" type="base64" file="dms/test/audio03.mp3" />
<field
name="tag_ids"
eval="[(6, 0, [ref('dms.tag_01_demo'), ref('dms.tag_03_demo')])]"
/>
</record>
<record id="file_09_demo" model="dms.file">
<field name="name">Loop_04.mp3</field>
<field name="color" eval="2" />
<field name="directory_id" ref="dms.directory_09_demo" />
<field name="content" type="base64" file="dms/test/audio04.mp3" />
<field
name="tag_ids"
eval="[(6, 0, [ref('dms.tag_01_demo'), ref('dms.tag_03_demo')])]"
/>
</record>
<record id="file_10_demo" model="dms.file">
<field name="name">Video.mp4</field>
<field name="color" eval="3" />
<field name="directory_id" ref="dms.directory_08_demo" />
<field name="content" type="base64" file="dms/test/video.mp4" />
<field
name="tag_ids"
eval="[(6, 0, [ref('dms.tag_02_demo'), ref('dms.tag_03_demo')])]"
/>
</record>
<record id="file_11_demo" model="dms.file">
<field name="name">Mail_01.eml</field>
<field name="color" eval="4" />
<field name="directory_id" ref="dms.directory_11_demo" />
<field name="content" type="base64" file="dms/test/mail01.eml" />
<field name="category_id" ref="dms.category_03_demo" />
</record>
<record id="file_12_demo" model="dms.file">
<field name="name">Mail_02.eml</field>
<field name="color" eval="4" />
<field name="directory_id" ref="dms.directory_11_demo" />
<field name="content" type="base64" file="dms/test/mail02.eml" />
</record>
<record id="file_13_demo" model="dms.file">
<field name="name">Text.txt</field>
<field name="directory_id" ref="dms.directory_12_demo" />
<field name="content" type="base64" file="dms/test/text.txt" />
<field
name="tag_ids"
eval="[(6, 0, [ref('dms.tag_05_demo'), ref('dms.tag_06_demo')])]"
/>
</record>
<record id="file_14_demo" model="dms.file">
<field name="name">ASPECTJ.aj</field>
<field name="directory_id" ref="dms.directory_13_demo" />
<field name="content" type="base64" file="dms/test/code01.aj" />
<field name="category_id" ref="dms.category_01_demo" />
</record>
<record id="file_15_demo" model="dms.file">
<field name="name">Bash.sh</field>
<field name="directory_id" ref="dms.directory_13_demo" />
<field name="content" type="base64" file="dms/test/code02.sh" />
</record>
<record id="file_16_demo" model="dms.file">
<field name="name">C.c</field>
<field name="directory_id" ref="dms.directory_13_demo" />
<field name="content" type="base64" file="dms/test/code03.c" />
</record>
<record id="file_17_demo" model="dms.file">
<field name="name">Cplusplus.cc</field>
<field name="directory_id" ref="dms.directory_13_demo" />
<field name="content" type="base64" file="dms/test/code04.cc" />
</record>
<record id="file_18_demo" model="dms.file">
<field name="name">CSharp.cs</field>
<field name="directory_id" ref="dms.directory_13_demo" />
<field name="content" type="base64" file="dms/test/code05.cs" />
</record>
<record id="file_19_demo" model="dms.file">
<field name="name">COBOL.cbl</field>
<field name="directory_id" ref="dms.directory_13_demo" />
<field name="content" type="base64" file="dms/test/code06.cbl" />
</record>
<record id="file_20_demo" model="dms.file">
<field name="name">CoffeeScript.coffee</field>
<field name="directory_id" ref="dms.directory_13_demo" />
<field name="content" type="base64" file="dms/test/code07.coffee" />
</record>
<record id="file_21_demo" model="dms.file">
<field name="name">Fortran.f</field>
<field name="directory_id" ref="dms.directory_13_demo" />
<field name="content" type="base64" file="dms/test/code08.f" />
</record>
<record id="file_22_demo" model="dms.file">
<field name="name">Go.go</field>
<field name="directory_id" ref="dms.directory_13_demo" />
<field name="content" type="base64" file="dms/test/code09.go" />
</record>
<record id="file_23_demo" model="dms.file">
<field name="name">Groovy.groovy</field>
<field name="directory_id" ref="dms.directory_13_demo" />
<field name="content" type="base64" file="dms/test/code10.groovy" />
</record>
<record id="file_24_demo" model="dms.file">
<field name="name">Java.java</field>
<field name="directory_id" ref="dms.directory_13_demo" />
<field name="content" type="base64" file="dms/test/code11.java" />
</record>
<record id="file_25_demo" model="dms.file">
<field name="name">Scala.sc</field>
<field name="directory_id" ref="dms.directory_13_demo" />
<field name="content" type="base64" file="dms/test/code12.sc" />
</record>
<record id="file_26_demo" model="dms.file">
<field name="name">Sample.md</field>
<field name="directory_id" ref="dms.directory_04_demo" />
<field name="content" type="base64" file="dms/test/markdown.md" />
</record>
<record id="file_27_demo" model="dms.file">
<field name="name">Document_05.pdf</field>
<field name="color" eval="1" />
<field name="directory_id" ref="dms.directory_12_demo" />
<field name="content" type="base64" file="dms/test/document01.pdf" />
</record>
<record id="file_28_demo" model="dms.file">
<field name="name">Slide_01.odp</field>
<field name="directory_id" ref="dms.directory_14_demo" />
<field name="content" type="base64" file="dms/test/slide01.odp" />
</record>
<record id="file_29_demo" model="dms.file">
<field name="name">Slide_02.ppt</field>
<field name="directory_id" ref="dms.directory_14_demo" />
<field name="content" type="base64" file="dms/test/slide02.ppt" />
</record>
<record id="file_30_demo" model="dms.file">
<field name="name">Document_02.doc</field>
<field name="color" eval="5" />
<field name="directory_id" ref="dms.directory_12_demo" />
<field name="content" type="base64" file="dms/test/document02.doc" />
</record>
<record id="file_31_demo" model="dms.file">
<field name="name">Document_03.odt</field>
<field name="color" eval="5" />
<field name="directory_id" ref="dms.directory_12_demo" />
<field name="content" type="base64" file="dms/test/document03.odt" />
</record>
<record id="file_32_demo" model="dms.file">
<field name="name">Sheet_01.xls</field>
<field name="color" eval="6" />
<field name="directory_id" ref="dms.directory_03_demo" />
<field name="content" type="base64" file="dms/test/sheet01.xls" />
</record>
<record id="file_33_demo" model="dms.file">
<field name="name">Sheet_02.csv</field>
<field name="color" eval="6" />
<field name="directory_id" ref="dms.directory_03_demo" />
<field name="content" type="base64" file="dms/test/sheet02.csv" />
</record>
<record id="file_34_demo" model="dms.file">
<field name="name">Sheet_03.ods</field>
<field name="color" eval="6" />
<field name="directory_id" ref="dms.directory_03_demo" />
<field name="content" type="base64" file="dms/test/sheet03.ods" />
</record>
<record id="file_35_demo" model="dms.file">
<field name="name">Document_04.rtf</field>
<field name="color" eval="6" />
<field name="directory_id" ref="dms.directory_03_demo" />
<field name="content" type="base64" file="dms/test/document04.rtf" />
</record>
<record id="file_36_demo" model="dms.file">
<field name="name">Text.rst</field>
<field name="color" eval="3" />
<field name="directory_id" ref="dms.directory_02_demo" />
<field name="content" type="base64" file="dms/test/text.rst" />
</record>
</odoo>

12
dms/demo/res_users.xml Executable file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
Copyright 2017-2019 MuK IT GmbH
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
-->
<odoo noupdate="1">
<record id="base.user_demo" model="res.users">
<field eval="[(4, ref('dms.group_dms_user'))]" name="groups_id" />
</record>
</odoo>

20
dms/demo/storage.xml Executable file
View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
Copyright 2017-2019 MuK IT GmbH
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
-->
<odoo noupdate="1">
<record id="storage_demo" model="dms.storage">
<field name="name">Documents Storage</field>
<field name="save_type">database</field>
</record>
<record id="storage_attachment_demo" model="dms.storage">
<field name="name">Attachment Storage</field>
<field name="save_type">attachment</field>
<field name="inherit_access_from_parent_record" eval="True" />
<field name="include_message_attachments" eval="True" />
<field name="model_ids" eval="[(6, 0, [ref('base.model_res_partner')])]" />
</record>
</odoo>

64
dms/demo/tag.xml Executable file
View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
Copyright 2017-2019 MuK IT GmbH
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
-->
<odoo noupdate="1">
<record id="tag_01_demo" model="dms.tag">
<field name="name">Customer</field>
<field name="color">1</field>
<field name="category_id" ref="category_03_demo" />
</record>
<record id="tag_02_demo" model="dms.tag">
<field name="name">Partner</field>
<field name="color">2</field>
<field name="category_id" ref="category_03_demo" />
</record>
<record id="tag_03_demo" model="dms.tag">
<field name="name">Project</field>
<field name="color">3</field>
<field name="category_id" ref="category_04_demo" />
</record>
<record id="tag_04_demo" model="dms.tag">
<field name="name">Sales</field>
<field name="color">4</field>
<field name="category_id" ref="category_05_demo" />
</record>
<record id="tag_05_demo" model="dms.tag">
<field name="name">Portal</field>
<field name="color">5</field>
<field name="category_id" ref="category_05_demo" />
</record>
<record id="tag_06_demo" model="dms.tag">
<field name="name">Apps</field>
<field name="color">6</field>
<field name="category_id" ref="category_05_demo" />
</record>
<record id="tag_07_demo" model="dms.tag">
<field name="name">Accounting</field>
<field name="color">7</field>
<field name="category_id" ref="category_05_demo" />
</record>
<record id="tag_08_demo" model="dms.tag">
<field name="name">Customer Invoice</field>
<field name="color">8</field>
<field name="category_id" ref="category_05_demo" />
</record>
<record id="tag_09_demo" model="dms.tag">
<field name="name">Vendor Bill</field>
<field name="color">9</field>
<field name="category_id" ref="category_05_demo" />
</record>
<record id="tag_10_demo" model="dms.tag">
<field name="name">Product</field>
<field name="color">10</field>
<field name="category_id" ref="category_06_demo" />
</record>
<record id="tag_11_demo" model="dms.tag">
<field name="name">Contract</field>
<field name="color">11</field>
<field name="category_id" ref="category_01_demo" />
</record>
</odoo>

2515
dms/i18n/de.po Executable file

File diff suppressed because it is too large Load Diff

2239
dms/i18n/dms.pot Executable file

File diff suppressed because it is too large Load Diff

2530
dms/i18n/es.po Executable file

File diff suppressed because it is too large Load Diff

2336
dms/i18n/fa.po Executable file

File diff suppressed because it is too large Load Diff

2391
dms/i18n/fr.po Executable file

File diff suppressed because it is too large Load Diff

2407
dms/i18n/he_IL.po Executable file

File diff suppressed because it is too large Load Diff

2558
dms/i18n/it.po Executable file

File diff suppressed because it is too large Load Diff

2254
dms/i18n/nl.po Executable file

File diff suppressed because it is too large Load Diff

2517
dms/i18n/pt.po Executable file

File diff suppressed because it is too large Load Diff

2502
dms/i18n/pt_BR.po Executable file

File diff suppressed because it is too large Load Diff

2247
dms/i18n/ru.po Executable file

File diff suppressed because it is too large Load Diff

23
dms/models/__init__.py Executable file
View File

@ -0,0 +1,23 @@
# Copyright 2024 Subteno - Timothée Vannier (https://www.subteno.com).
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
from . import access_groups
from . import base
from . import mixins_thumbnail
from . import dms_security_mixin
from . import abstract_dms_mixin
from . import storage
from . import directory
from . import dms_file
from . import onboarding_onboarding
from . import onboarding_onboarding_step
from . import dms_category
from . import tag
from . import res_company
from . import res_config_settings
from . import ir_attachment
from . import ir_binary
from . import mail_thread

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,57 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import api, fields, models
class AbstractDmsMixin(models.AbstractModel):
_name = "abstract.dms.mixin"
_description = "Abstract Dms Mixin"
name = fields.Char(required=True, index="btree")
# Only defined to prevent error in other fields that related it
storage_id = fields.Many2one(
comodel_name="dms.storage", string="Storage", store=True, copy=True
)
is_hidden = fields.Boolean(
string="Storage is Hidden",
related="storage_id.is_hidden",
readonly=True,
store=True,
)
company_id = fields.Many2one(
related="storage_id.company_id",
comodel_name="res.company",
string="Company",
readonly=True,
store=True,
index="btree",
)
storage_id_save_type = fields.Selection(related="storage_id.save_type", store=False)
color = fields.Integer(default=0)
category_id = fields.Many2one(
comodel_name="dms.category",
context={"dms_category_show_path": True},
string="Category",
)
@api.model
def search_panel_select_range(self, field_name, **kwargs):
"""Add context to display short folder name."""
_self = self.with_context(
directory_short_name=True, skip_sanitized_parent_hierarchy=True
)
return super(AbstractDmsMixin, _self).search_panel_select_range(
field_name, **kwargs
)
def _search_panel_sanitized_parent_hierarchy(self, records, parent_name, ids):
if self.env.context.get("skip_sanitized_parent_hierarchy"):
all_ids = [value["id"] for value in records]
# Prevent error if user not access to parent record
for value in records:
if value["parent_id"] and value["parent_id"][0] not in all_ids:
value["parent_id"] = False
return records
return super()._search_panel_sanitized_parent_hierarchy(
records=records, parent_name=parent_name, ids=ids
)

176
dms/models/access_groups.py Executable file
View File

@ -0,0 +1,176 @@
# Copyright 2017-2019 MuK IT GmbH
# Copyright 2020 RGB Consulting
# Copyright 2024 Timothée Vannier - Subteno (https://www.subteno.com).
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class DmsAccessGroups(models.Model):
_name = "dms.access.group"
_description = "Record Access Groups"
_parent_store = True
_parent_name = "parent_group_id"
name = fields.Char(string="Group Name", required=True, translate=True)
parent_path = fields.Char(index="btree", unaccent=False)
# Permissions written directly on this group
perm_create = fields.Boolean(string="Create Access")
perm_write = fields.Boolean(string="Write Access")
perm_unlink = fields.Boolean(string="Unlink Access")
# Permissions computed including parent group
perm_inclusive_create = fields.Boolean(
string="Inherited Create Access",
compute="_compute_inclusive_permissions",
store=True,
recursive=True,
)
perm_inclusive_write = fields.Boolean(
string="Inherited Write Access",
compute="_compute_inclusive_permissions",
store=True,
recursive=True,
)
perm_inclusive_unlink = fields.Boolean(
string="Inherited Unlink Access",
compute="_compute_inclusive_permissions",
store=True,
recursive=True,
)
directory_ids = fields.Many2many(
comodel_name="dms.directory",
relation="dms_directory_groups_rel",
string="Directories",
column1="gid",
column2="aid",
auto_join=True,
readonly=True,
)
complete_directory_ids = fields.Many2many(
comodel_name="dms.directory",
relation="dms_directory_complete_groups_rel",
column1="gid",
column2="aid",
string="Complete directories",
auto_join=True,
readonly=True,
)
count_users = fields.Integer(compute="_compute_users", store=True)
count_directories = fields.Integer(compute="_compute_count_directories")
parent_group_id = fields.Many2one(
comodel_name="dms.access.group",
string="Parent Group",
ondelete="cascade",
index="btree",
)
child_group_ids = fields.One2many(
comodel_name="dms.access.group",
inverse_name="parent_group_id",
string="Child Groups",
)
group_ids = fields.Many2many(
comodel_name="res.groups",
relation="dms_access_group_groups_rel",
column1="gid",
column2="rid",
string="Groups",
)
explicit_user_ids = fields.Many2many(
comodel_name="res.users",
relation="dms_access_group_explicit_users_rel",
column1="gid",
column2="uid",
string="Explicit Users",
)
users = fields.Many2many(
comodel_name="res.users",
relation="dms_access_group_users_rel",
column1="gid",
column2="uid",
string="Group Users",
compute="_compute_users",
auto_join=True,
store=True,
recursive=True,
)
@api.depends("directory_ids")
def _compute_count_directories(self):
for record in self:
record.count_directories = len(record.directory_ids)
_sql_constraints = [
("name_uniq", "unique (name)", "The name of the group must be unique!")
]
@api.depends(
"parent_group_id.perm_inclusive_create",
"parent_group_id.perm_inclusive_unlink",
"parent_group_id.perm_inclusive_write",
"parent_path",
"perm_create",
"perm_unlink",
"perm_write",
)
def _compute_inclusive_permissions(self):
"""Provide full permissions inheriting from parent recursively."""
for one in self:
one.update(
{
"perm_inclusive_%s" % perm: (
one["perm_%s" % perm]
or one.parent_group_id["perm_inclusive_%s" % perm]
)
for perm in ("create", "unlink", "write")
}
)
@api.model
def default_get(self, fields_list):
res = super().default_get(fields_list)
if res.get("explicit_user_ids"):
res["explicit_user_ids"] = res["explicit_user_ids"] + [self.env.uid]
else:
res["explicit_user_ids"] = [(6, 0, [self.env.uid])]
return res
@api.depends(
"parent_group_id",
"parent_group_id.users",
"group_ids",
"group_ids.users",
"explicit_user_ids",
)
def _compute_users(self):
for record in self:
users = (
record.group_ids.users
| record.explicit_user_ids
| record.parent_group_id.users
)
record.update({"users": users, "count_users": len(users)})
def copy(self, default=None):
default = dict(default or {})
default["name"] = _("%s (copy)") % self.name
return super().copy(default=default)
@api.constrains("parent_path")
def _check_parent_recursiveness(self):
"""
Forbid recursive relationships.
"""
for one in self.filtered("parent_group_id"):
if str(one.id) in one.parent_path.split("/"):
raise ValidationError(
_("Parent group '%(parent)s' is child of '%(current)s'.")
% {
"parent": one.parent_group_id.display_name,
"current": one.display_name,
}
)

32
dms/models/base.py Executable file
View File

@ -0,0 +1,32 @@
# Copyright 2021 Tecnativa - Jairo Llopis
# Copyright 2024 Tecnativa - Víctor Martínez
# Copyright 2024 Subteno - Timothée Vannier (https://www.subteno.com).
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
from odoo import models
class Base(models.AbstractModel):
_inherit = "base"
def unlink(self):
"""Cascade DMS related resources removal.
Avoid executing in ir.* models (ir.mode, ir.model.fields, etc), in transient
models and in the models we want to check."""
result = super().unlink()
if (
not self._name.startswith("ir.")
and not self.is_transient()
and self._name not in ("dms.file", "dms.directory")
):
domain = [("res_model", "=", self._name), ("res_id", "in", self.ids)]
# Has to check if existing before unlinking, because even if the search
# returns an empty recordset, it will still call the unlink method on it.
# This can result in an infinite loop and a recursion depth error.
files = self.env["dms.file"].sudo().search(domain)
if files:
files.unlink()
directories = self.env["dms.directory"].sudo().search(domain)
if directories:
directories.unlink()
return result

771
dms/models/directory.py Executable file
View File

@ -0,0 +1,771 @@
# Copyright 2017-2019 MuK IT GmbH.
# Copyright 2020 Creu Blanca
# Copyright 2021 Tecnativa - Víctor Martínez
# Copyright 2024 Subteno - Timothée Vannier (https://www.subteno.com).
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
import ast
import base64
import logging
import os
from ast import literal_eval
from collections import defaultdict
from typing import Literal # noqa # pylint: disable=unused-import
from odoo import _, api, fields, models, tools
from odoo.exceptions import UserError, ValidationError
from odoo.osv.expression import AND, OR
from odoo.tools import consteq, human_size
from odoo.addons.http_routing.models.ir_http import slugify
from ..tools.file import check_name, unique_name
_logger = logging.getLogger(__name__)
_path = os.path.dirname(os.path.dirname(__file__))
class DmsDirectory(models.Model):
_name = "dms.directory"
_description = "Directory"
_inherit = [
"portal.mixin",
"dms.security.mixin",
"dms.mixins.thumbnail",
"mail.thread",
"mail.activity.mixin",
"mail.alias.mixin",
"abstract.dms.mixin",
]
_rec_name = "complete_name"
_order = "complete_name"
_parent_store = True
_parent_name = "parent_id"
_directory_field = _parent_name
parent_path = fields.Char(index="btree", unaccent=False)
is_root_directory = fields.Boolean(
default=False,
help="""Indicates if the directory is a root directory.
A root directory has a settings object, while a directory with a set
parent inherits the settings form its parent.""",
)
# Override acording to defined in AbstractDmsMixin
storage_id = fields.Many2one(
compute="_compute_storage_id",
compute_sudo=True,
readonly=False,
comodel_name="dms.storage",
string="Storage",
ondelete="restrict",
auto_join=True,
store=True,
)
parent_id = fields.Many2one(
comodel_name="dms.directory",
string="Parent Directory",
domain="[('permission_create', '=', True)]",
ondelete="restrict",
# Access to a directory doesn't necessarily mean access its parent, so
# prefetching this field could lead to misleading access errors
prefetch=False,
index="btree",
store=True,
readonly=False,
compute="_compute_parent_id",
copy=True,
default=lambda self: self._default_parent_id(),
)
root_directory_id = fields.Many2one(
"dms.directory", "Root Directory", compute="_compute_root_id", store=True
)
def _default_parent_id(self):
context = self.env.context
if context.get("active_model") == self._name and context.get("active_id"):
return context["active_id"]
else:
return False
group_ids = fields.Many2many(
comodel_name="dms.access.group",
relation="dms_directory_groups_rel",
column1="aid",
column2="gid",
string="Groups",
)
complete_group_ids = fields.Many2many(
comodel_name="dms.access.group",
relation="dms_directory_complete_groups_rel",
column1="aid",
column2="gid",
string="Complete Groups",
compute="_compute_groups",
readonly=True,
store=True,
compute_sudo=True,
recursive=True,
)
complete_name = fields.Char(
compute="_compute_complete_name", store=True, recursive=True
)
child_directory_ids = fields.One2many(
comodel_name="dms.directory",
inverse_name="parent_id",
string="Subdirectories",
auto_join=False,
copy=True,
)
tag_ids = fields.Many2many(
comodel_name="dms.tag",
relation="dms_directory_tag_rel",
domain="""[
'|', ['category_id', '=', False],
['category_id', 'child_of', category_id]]
""",
column1="did",
column2="tid",
string="Tags",
compute="_compute_tags",
readonly=False,
store=True,
)
user_star_ids = fields.Many2many(
comodel_name="res.users",
relation="dms_directory_star_rel",
column1="did",
column2="uid",
string="Stars",
)
starred = fields.Boolean(
compute="_compute_starred",
inverse="_inverse_starred",
search="_search_starred",
)
file_ids = fields.One2many(
comodel_name="dms.file",
inverse_name="directory_id",
string="Files",
auto_join=False,
copy=True,
)
count_directories = fields.Integer(
compute="_compute_count_directories", string="Count Subdirectories Title"
)
count_files = fields.Integer(
compute="_compute_count_files", string="Count Files Title"
)
count_directories_title = fields.Char(
compute="_compute_count_directories", string="Count Subdirectories"
)
count_files_title = fields.Char(
compute="_compute_count_files", string="Count Files"
)
count_elements = fields.Integer(compute="_compute_count_elements")
count_total_directories = fields.Integer(
compute="_compute_count_total_directories", string="Total Subdirectories"
)
count_total_files = fields.Integer(
compute="_compute_count_total_files", string="Total Files"
)
count_total_elements = fields.Integer(
compute="_compute_count_total_elements", string="Total Elements"
)
size = fields.Float(compute="_compute_size")
human_size = fields.Char(
compute="_compute_human_size", string="Size (human readable)"
)
inherit_group_ids = fields.Boolean(string="Inherit Groups", default=True)
alias_process = fields.Selection(
selection=[("files", "Single Files"), ("directory", "Subdirectory")],
required=True,
default="directory",
string="Unpack Emails as",
help="""\
Define how incoming emails are processed:\n
- Single Files: The email gets attached to the directory and
all attachments are created as files.\n
- Subdirectory: A new subdirectory is created for each email
and the mail is attached to this subdirectory. The attachments
are created as files of the subdirectory.
""",
)
@api.model
def _get_domain_by_access_groups(self, operation):
"""Special rules for directories."""
self_filter = [
("storage_id_inherit_access_from_parent_record", "=", False),
("id", "inselect", self._get_access_groups_query(operation)),
]
# Upstream only filters by parent directory
result = super()._get_domain_by_access_groups(operation)
if operation == "create":
# When creating, I need create access in parent directory, or
# self-create permission if it's a root directory
result = OR(
[
[("is_root_directory", "=", False)] + result,
[("is_root_directory", "=", True)] + self_filter,
]
)
else:
# In other operations, I only need self access
result = self_filter
return result
def _compute_access_url(self):
res = super()._compute_access_url()
for item in self:
item.access_url = "/my/dms/directory/%s" % (item.id)
return res
def check_access_token(self, access_token=False):
res = False
if access_token:
items = (
self.env["dms.directory"]
.sudo()
.search([("access_token", "=", access_token)])
)
if items:
item = items[0]
if item.id == self.id:
return True
# sudo because the user might not usually have access to the record but
# now the token is valid.
directory_item = self.sudo()
while directory_item.parent_id:
if directory_item.id == item.id:
return True
directory_item = directory_item.parent_id
# Fix last level
if directory_item.id == item.id:
return True
return res
@api.model
def _get_parent_categories(self, access_token):
self.ensure_one()
directories = []
current_directory = self
while current_directory:
directories.insert(0, current_directory)
if (
(
access_token
and current_directory.access_token
and consteq(current_directory.access_token, access_token)
)
or not access_token
and current_directory.check_access_rights("read")
):
return directories
current_directory = current_directory.parent_id
if access_token:
# Reaching here means we didn't find the directory accessible by this token
return [self]
return directories
def _get_own_root_directories(self):
res = self.env["dms.directory"].search_read(
[("is_hidden", "=", False)], ["parent_id"]
)
all_ids = [value["id"] for value in res]
res_ids = []
for item in res:
if not item["parent_id"] or item["parent_id"][0] not in all_ids:
res_ids.append(item["id"])
return res_ids
allowed_model_ids = fields.Many2many(
related="storage_id.model_ids",
comodel_name="ir.model",
)
model_id = fields.Many2one(
comodel_name="ir.model",
domain="[('id', 'in', allowed_model_ids)]",
compute="_compute_model_id",
inverse="_inverse_model_id",
string="Model",
store=True,
)
storage_id_save_type = fields.Selection(
related="storage_id.save_type",
related_sudo=True,
readonly=True,
store=False,
prefetch=False,
)
storage_id_inherit_access_from_parent_record = fields.Boolean(
related="storage_id.inherit_access_from_parent_record",
related_sudo=True,
store=True,
)
@api.depends("res_model")
def _compute_model_id(self):
for record in self:
if not record.res_model:
record.model_id = False
continue
record.model_id = (
self.env["ir.model"].sudo().search([("model", "=", record.res_model)])
)
def _inverse_model_id(self):
for record in self:
record.res_model = record.model_id.model
def toggle_starred(self):
updates = defaultdict(set)
for record in self:
vals = {"starred": not record.starred}
updates[tools.frozendict(vals)].add(record.id)
for vals, ids in updates.items():
self.browse(ids).write(dict(vals))
self.flush_recordset()
# SearchPanel
@api.model
def search_panel_select_range(self, field_name, **kwargs):
context = {}
if field_name == "parent_id":
context["directory_short_name"] = True
return super(
DmsDirectory, self.with_context(**context)
).search_panel_select_range(field_name, **kwargs)
@api.model
def search_panel_select_multi_range(self, field_name, **kwargs):
return super(
DmsDirectory, self.with_context(category_short_name=True)
).search_panel_select_multi_range(field_name, **kwargs)
# Actions
def action_save_onboarding_directory_step(self):
self.env.user.company_id.set_onboarding_step_done(
"documents_onboarding_directory_state"
)
# SearchPanel
@api.model
def _search_panel_directory(self, **kwargs):
search_domain = (kwargs.get("search_domain", []),)
if search_domain and len(search_domain):
for domain in search_domain[0]:
if domain[0] == "parent_id":
return domain[1], domain[2]
return None, None
# Search
@api.model
def _search_starred(self, operator, operand):
if operator == "=" and operand:
return [("user_star_ids", "in", [self.env.uid])]
return [("user_star_ids", "not in", [self.env.uid])]
@api.depends("name", "parent_id.complete_name")
def _compute_complete_name(self):
for category in self:
if category.parent_id:
category.complete_name = "{} / {}".format(
category.parent_id.complete_name,
category.name,
)
else:
category.complete_name = category.name
@api.depends("parent_id")
def _compute_storage_id(self):
for record in self:
if record.parent_id:
record.storage_id = record.parent_id.storage_id
else:
# HACK: Not needed in v14 due to odoo/odoo#64359
record.storage_id = record.storage_id
@api.depends("user_star_ids")
def _compute_starred(self):
for record in self:
record.starred = self.env.user in record.user_star_ids
@api.depends("child_directory_ids")
def _compute_count_directories(self):
for record in self:
directories = len(record.child_directory_ids)
record.count_directories = directories
record.count_directories_title = _("%s Subdirectories") % directories
@api.depends("file_ids")
def _compute_count_files(self):
for record in self:
files = len(record.file_ids)
record.count_files = files
record.count_files_title = _("%s Files") % files
@api.depends("child_directory_ids", "file_ids")
def _compute_count_elements(self):
for record in self:
record.count_elements = record.count_files + record.count_directories
def _compute_count_total_directories(self):
for record in self:
count = (
self.search_count([("id", "child_of", record.id)]) if record.id else 0
)
record.count_total_directories = count - 1 if count > 0 else 0
def _compute_count_total_files(self):
model = self.env["dms.file"]
for record in self:
# Prevent error in some NewId cases
record.count_total_files = (
model.search_count([("directory_id", "child_of", record.id)])
if record.id
else 0
)
def _compute_count_total_elements(self):
for record in self:
record.count_total_elements = (
record.count_total_files + record.count_total_directories
)
def _compute_size(self):
sudo_model = self.env["dms.file"].sudo()
for record in self:
# Avoid NewId
if not record.id:
record.size = 0
continue
recs = sudo_model.search_read(
domain=[("directory_id", "child_of", record.id)],
fields=["size"],
)
record.size = sum(rec.get("size", 0) for rec in recs)
@api.depends("size")
def _compute_human_size(self):
for item in self:
item.human_size = human_size(item.size) if item.size else False
@api.depends(
"group_ids",
"inherit_group_ids",
"parent_id.complete_group_ids",
"parent_path",
)
def _compute_groups(self):
"""Get all DMS security groups affecting this directory."""
for one in self:
groups = one.group_ids
if one.inherit_group_ids:
groups |= one.parent_id.complete_group_ids
self.complete_group_ids = groups
# View
@api.depends("is_root_directory")
def _compute_parent_id(self):
for record in self:
if record.is_root_directory:
record.parent_id = None
else:
# HACK: Not needed in v14 due to odoo/odoo#64359
record.parent_id = record.parent_id
@api.depends("is_root_directory", "parent_id")
def _compute_root_id(self):
for record in self:
if record.is_root_directory:
record.root_directory_id = record
else:
# recursively check all parent nodes up to the root directory
if not record.parent_id.root_directory_id:
record.parent_id._compute_root_id()
record.root_directory_id = record.parent_id.root_directory_id
@api.depends("category_id")
def _compute_tags(self):
for record in self:
tags = record.tag_ids.filtered(
lambda rec, record=record: not rec.category_id
or rec.category_id == record.category_id
)
record.tag_ids = tags
@api.onchange("storage_id")
def _onchange_storage_id(self):
for record in self:
if (
record.storage_id.save_type == "attachment"
and record.storage_id.inherit_access_from_parent_record
):
record.group_ids = False
@api.onchange("model_id")
def _onchange_model_id(self):
self._inverse_model_id()
# Constrains
@api.constrains("parent_id")
def _check_directory_recursion(self):
if not self._check_recursion():
raise ValidationError(_("Error! You cannot create recursive directories."))
return True
@api.constrains("storage_id", "model_id")
def _check_storage_id_attachment_model_id(self):
for record in self.filtered(
lambda directory: directory.storage_id.save_type == "attachment"
):
if not record.model_id:
raise ValidationError(
_("A directory has to have model in attachment storage.")
)
if not record.is_root_directory and not record.res_id:
raise ValidationError(
_("This directory needs to be associated to a record.")
)
@api.constrains("is_root_directory", "storage_id")
def _check_directory_storage(self):
for record in self:
if record.is_root_directory and not record.storage_id:
raise ValidationError(_("A root directory has to have a storage."))
@api.constrains("is_root_directory", "parent_id")
def _check_directory_parent(self):
for record in self:
if record.is_root_directory and record.parent_id:
raise ValidationError(
_("A directory can't be a root and have a parent directory.")
)
if not record.is_root_directory and not record.parent_id:
raise ValidationError(_("A directory has to have a parent directory."))
@api.constrains("name")
def _check_name(self):
for record in self:
if self.env.context.get("check_name", True) and not check_name(record.name):
raise ValidationError(_("The directory name is invalid."))
if record.is_root_directory:
children = record.sudo().storage_id.root_directory_ids
else:
children = record.sudo().parent_id.child_directory_ids
if children.filtered(
lambda child, record=record: child.name == record.name
and child != record
):
raise ValidationError(
_("A directory with the same name already exists.")
)
# Create, Update, Delete
def _inverse_starred(self):
starred_records = self.env["dms.directory"].sudo()
not_starred_records = self.env["dms.directory"].sudo()
for record in self:
if not record.starred and self.env.user in record.user_star_ids:
starred_records |= record
elif record.starred and self.env.user not in record.user_star_ids:
not_starred_records |= record
not_starred_records.write({"user_star_ids": [(4, self.env.uid)]})
starred_records.write({"user_star_ids": [(3, self.env.uid)]})
def copy(self, default=None):
self.ensure_one()
default = dict(default or [])
if "parent_id" in default:
parent_directory = self.browse(default.get("parent_id"))
names = parent_directory.sudo().child_directory_ids.mapped("name")
elif self.is_root_directory:
names = self.sudo().storage_id.root_directory_ids.mapped("name")
else:
names = self.sudo().parent_id.child_directory_ids.mapped("name")
default.update({"name": unique_name(self.name, names)})
return super().copy(default)
def _alias_get_creation_values(self):
values = super()._alias_get_creation_values()
values["alias_model_id"] = self.env["ir.model"].sudo()._get("dms.directory").id
if self.id:
values["alias_defaults"] = defaults = ast.literal_eval(
self.alias_defaults or "{}"
)
defaults["parent_id"] = self.id
return values
@api.model
def message_new(self, msg_dict, custom_values=None):
custom_values = custom_values if custom_values is not None else {}
parent_directory_id = custom_values.get("parent_id")
parent_directory = self.sudo().browse(parent_directory_id)
if not parent_directory_id or not parent_directory.exists():
raise ValueError("No directory could be found!")
if parent_directory.alias_process == "files":
parent_directory._process_message(msg_dict)
return parent_directory
names = parent_directory.child_directory_ids.mapped("name")
subject = slugify(msg_dict.get("subject", _("Alias-Mail-Extraction")))
defaults = dict(
{"name": unique_name(subject, names, escape_suffix=True)}, **custom_values
)
directory = super().message_new(msg_dict, custom_values=defaults)
directory._process_message(msg_dict)
return directory
def message_update(self, msg_dict, update_vals=None):
self._process_message(msg_dict, extra_values=update_vals)
return super().message_update(msg_dict, update_vals=update_vals)
def _process_message(self, msg_dict, extra_values=False):
names = self.sudo().file_ids.mapped("name")
for attachment in msg_dict["attachments"]:
uname = unique_name(attachment.fname, names, escape_suffix=True)
vals = {
"directory_id": self.id,
"name": uname,
}
try:
vals["content"] = base64.b64encode(attachment.content)
except Exception:
vals["content"] = attachment.content
self.env["dms.file"].sudo().create(vals)
names.append(uname)
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if vals.get("parent_id", False):
parent = self.browse([vals["parent_id"]])
data = next(iter(parent.sudo().read(["storage_id"])), {})
vals["storage_id"] = self._convert_to_write(data).get("storage_id")
# Hack to prevent error related to mail_message parent not exists in some cases
ctx = dict(self.env.context).copy()
ctx.update({"default_parent_id": False})
self.env.registry.clear_cache()
res = super(DmsDirectory, self.with_context(**ctx)).create(vals_list)
return res
def write(self, vals):
if any(k in vals.keys() for k in ["storage_id", "parent_id"]):
for item in self:
new_storage_id = vals.get("storage_id", item.storage_id.id)
new_parent_id = vals.get("parent_id", item.parent_id.id)
old_storage_id = (
item.storage_id or item.root_directory_id.storage_id
).id
if new_parent_id:
if old_storage_id != self.browse(new_parent_id).storage_id.id:
raise UserError(
_(
"It is not possible to change to a parent "
"with other storage."
)
)
elif old_storage_id != new_storage_id:
raise UserError(_("It is not possible to change the storage."))
# Groups part
if any(key in vals for key in ["group_ids", "inherit_group_ids"]):
res = super().write(vals)
domain = [("id", "child_of", self.ids)]
records = self.sudo().search(domain)
records.modified(["group_ids"])
records.flush_recordset()
else:
res = super().write(vals)
return res
def unlink(self):
"""Custom cascade unlink.
Cannot rely on DB backend's cascade because subfolder and subfile unlinks
must check custom permissions implementation.
"""
self.file_ids.unlink()
if self.child_directory_ids:
self.child_directory_ids.unlink()
return super(DmsDirectory, self.exists()).unlink()
@api.model
def _search_panel_domain_image(
self, field_name, domain, set_count=False, limit=False
):
"""We need to overwrite function from directories because odoo only return
records with children (very weird for user perspective).
All records are returned now.
"""
if field_name == "parent_id":
res = {}
for item in self.search_read(
domain=domain, fields=["id", "name", "count_directories"]
):
res[item["id"]] = {
"id": item["id"],
"display_name": item["name"],
"__count": item["count_directories"],
}
return res
return super()._search_panel_domain_image(
field_name=field_name, domain=domain, set_count=set_count, limit=limit
)
def action_dms_directories_all_directory(self):
self.ensure_one()
action = self.env["ir.actions.act_window"]._for_xml_id(
"dms.action_dms_directory"
)
domain = AND(
[
literal_eval(action["domain"].strip()),
[("parent_id", "child_of", self.id)],
]
)
action["display_name"] = self.name
action["domain"] = domain
action["context"] = dict(
self.env.context,
default_parent_id=self.id,
searchpanel_default_parent_id=self.id,
)
return action
def action_dms_files_all_directory(self):
self.ensure_one()
action = self.env["ir.actions.act_window"]._for_xml_id("dms.action_dms_file")
domain = AND(
[
literal_eval(action["domain"].strip()),
[("directory_id", "child_of", self.id)],
]
)
action["display_name"] = self.name
action["domain"] = domain
action["context"] = dict(
self.env.context,
default_directory_id=self.id,
searchpanel_default_directory_id=self.id,
)
return action

104
dms/models/dms_category.py Executable file
View File

@ -0,0 +1,104 @@
# Copyright 2020 Creu Blanca
# Copyright 2017-2019 MuK IT GmbH
# Copyright 2024 Subteno - Timothée Vannier (https://www.subteno.com).
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
import logging
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
_logger = logging.getLogger(__name__)
class DMSCategory(models.Model):
_name = "dms.category"
_description = "Document Category"
_parent_store = True
_parent_name = "parent_id"
_order = "complete_name asc"
_rec_name = "complete_name"
name = fields.Char(required=True, translate=True)
active = fields.Boolean(
default=True,
help="The active field allows you to hide the category without removing it.",
)
complete_name = fields.Char(
compute="_compute_complete_name", store=True, recursive=True
)
parent_id = fields.Many2one(
string="Parent Category",
comodel_name="dms.category",
ondelete="cascade",
index="btree",
)
child_category_ids = fields.One2many(
string="Child Categories",
comodel_name="dms.category",
inverse_name="parent_id",
)
parent_path = fields.Char(index="btree", unaccent=False)
tag_ids = fields.One2many(
string="Tags", comodel_name="dms.tag", inverse_name="category_id"
)
directory_ids = fields.One2many(
string="Directories",
comodel_name="dms.directory",
inverse_name="category_id",
readonly=True,
)
file_ids = fields.One2many(
string="Files",
comodel_name="dms.file",
inverse_name="category_id",
readonly=True,
)
count_categories = fields.Integer(
string="Count Subcategories", compute="_compute_count_categories"
)
count_tags = fields.Integer(compute="_compute_count_tags")
count_directories = fields.Integer(compute="_compute_count_directories")
count_files = fields.Integer(compute="_compute_count_files")
_sql_constraints = [
("name_uniq", "unique (name)", "Category name already exists!"),
]
@api.depends("name", "parent_id.complete_name")
def _compute_complete_name(self):
for category in self:
if category.parent_id:
category.complete_name = (
f"{category.parent_id.complete_name} / {category.name}"
)
else:
category.complete_name = category.name
@api.depends("child_category_ids")
def _compute_count_categories(self):
for record in self:
record.count_categories = len(record.child_category_ids)
@api.depends("tag_ids")
def _compute_count_tags(self):
for record in self:
record.count_tags = len(record.tag_ids)
@api.depends("directory_ids")
def _compute_count_directories(self):
for record in self:
record.count_directories = len(record.directory_ids)
@api.depends("file_ids")
def _compute_count_files(self):
for record in self:
record.count_files = len(record.file_ids)
@api.constrains("parent_id")
def _check_category_recursion(self):
if not self._check_recursion():
raise ValidationError(_("Error! You cannot create recursive categories."))
return True

661
dms/models/dms_file.py Executable file
View File

@ -0,0 +1,661 @@
# Copyright 2020 Antoni Romera
# Copyright 2017-2019 MuK IT GmbH
# Copyright 2021 Tecnativa - Víctor Martínez
# Copyright 2024 Subteno - Timothée Vannier (https://www.subteno.com).
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
import base64
import hashlib
import json
import logging
from collections import defaultdict
from PIL import Image
from odoo import _, api, fields, models, tools
from odoo.exceptions import UserError, ValidationError
from odoo.osv import expression
from odoo.tools import consteq, human_size
from odoo.tools.mimetypes import guess_mimetype
from ..tools import file
_logger = logging.getLogger(__name__)
class DMSFile(models.Model):
_name = "dms.file"
_description = "File"
_inherit = [
"portal.mixin",
"dms.security.mixin",
"dms.mixins.thumbnail",
"mail.thread",
"mail.activity.mixin",
"abstract.dms.mixin",
]
_order = "name asc"
# Database
active = fields.Boolean(
string="Archived",
default=True,
help="If a file is set to archived, it is not displayed, but still exists.",
)
directory_id = fields.Many2one(
comodel_name="dms.directory",
string="Directory",
domain="[('permission_create', '=', True)]",
context={"dms_directory_show_path": True},
ondelete="restrict",
auto_join=True,
required=True,
index="btree",
tracking=True, # Leave log if "moved" to another directory
)
root_directory_id = fields.Many2one(related="directory_id.root_directory_id")
# Override acording to defined in AbstractDmsMixin
storage_id = fields.Many2one(
related="directory_id.storage_id",
readonly=True,
prefetch=False,
)
path_names = fields.Char(
compute="_compute_path",
compute_sudo=True,
readonly=True,
store=False,
)
path_json = fields.Text(
compute="_compute_path",
compute_sudo=True,
readonly=True,
store=False,
)
tag_ids = fields.Many2many(
comodel_name="dms.tag",
relation="dms_file_tag_rel",
column1="fid",
column2="tid",
domain="['|', ('category_id', '=', False),('category_id', '=?', category_id)]",
string="Tags",
)
content = fields.Binary(
compute="_compute_content",
inverse="_inverse_content",
attachment=False,
prefetch=False,
required=True,
store=False,
)
extension = fields.Char(compute="_compute_extension", readonly=True, store=True)
mimetype = fields.Char(
compute="_compute_mimetype", string="Type", readonly=True, store=True
)
size = fields.Float(readonly=True)
human_size = fields.Char(
readonly=True,
string="Size (human readable)",
compute="_compute_human_size",
store=True,
)
checksum = fields.Char(string="Checksum/SHA1", readonly=True, index="btree")
content_binary = fields.Binary(attachment=False, prefetch=False)
save_type = fields.Char(
compute="_compute_save_type",
string="Current Save Type",
prefetch=False,
)
migration = fields.Char(
compute="_compute_migration",
string="Migration Status",
readonly=True,
prefetch=False,
compute_sudo=True,
store=True,
)
require_migration = fields.Boolean(
compute="_compute_migration", store=True, compute_sudo=True
)
content_file = fields.Binary(attachment=True, prefetch=False)
# Extend inherited field(s)
image_1920 = fields.Image(compute="_compute_image_1920", store=True, readonly=False)
@api.depends("mimetype", "content")
def _compute_image_1920(self):
"""Provide thumbnail automatically if possible."""
for one in self.filtered("mimetype"):
# Image.MIME provides a dict of mimetypes supported by Pillow,
# SVG is not present in the dict but is also a supported image format
# lacking a better solution, it's being added manually
# Some component modifies the PIL dictionary by adding PDF as a valid
# image type, so it must be explicitly excluded.
if one.mimetype != "application/pdf" and one.mimetype in (
*Image.MIME.values(),
"image/svg+xml",
):
one.image_1920 = one.content
def check_access_rule(self, operation):
self.mapped("directory_id").check_access_rule(operation)
return super().check_access_rule(operation)
def _compute_access_url(self):
res = super()._compute_access_url()
for item in self:
item.access_url = "/my/dms/file/%s/download" % (item.id)
return res
def check_access_token(self, access_token=False):
if not access_token:
return False
if self.access_token and consteq(self.access_token, access_token):
return True
items = (
self.env["dms.directory"]
.sudo()
.search([("access_token", "=", access_token)])
)
if items:
item = items[0]
if self.directory_id.id == item.id:
return True
directory_item = self.directory_id
while directory_item.parent_id:
if directory_item.id == self.directory_id.id:
return True
directory_item = directory_item.parent_id
# Fix last level
if directory_item.id == self.directory_id.id:
return True
return False
res_model = fields.Char(
string="Linked attachments model", related="directory_id.res_model"
)
res_id = fields.Integer(
string="Linked attachments record ID", related="directory_id.res_id"
)
attachment_id = fields.Many2one(
comodel_name="ir.attachment",
string="Attachment File",
prefetch=False,
ondelete="cascade",
)
def get_human_size(self):
return human_size(self.size)
# Helper
@api.model
def _get_checksum(self, binary):
return hashlib.sha1(binary or b"").hexdigest()
@api.model
def _get_content_inital_vals(self):
return {"content_binary": False, "content_file": False}
def _update_content_vals(self, vals, binary):
new_vals = vals.copy()
new_vals.update(
{
"checksum": self._get_checksum(binary),
"size": binary and len(binary) or 0,
}
)
if self.storage_id.save_type in ["file", "attachment"]:
new_vals["content_file"] = self.content
else:
new_vals["content_binary"] = self.content and binary
return new_vals
@api.model
def _get_binary_max_size(self):
return int(
self.env["ir.config_parameter"]
.sudo()
.get_param("dms.binary_max_size", default=25)
)
@api.model
def _get_forbidden_extensions(self):
get_param = self.env["ir.config_parameter"].sudo().get_param
extensions = get_param("dms.forbidden_extensions", default="")
return [extension.strip() for extension in extensions.split(",")]
def _get_icon_placeholder_name(self):
return self.extension and "file_%s.svg" % self.extension or ""
# Actions
def action_migrate(self, should_logging=True):
record_count = len(self)
index = 1
for dms_file in self:
if should_logging:
_logger.info(
_(
"Migrate File %(index)s of %(record_count)s [ %("
"dms_file_migration)s ]",
index=index,
record_count=record_count,
dms_file_migration=dms_file.migration,
)
)
index += 1
dms_file.write(
{
"content": dms_file.with_context(**{}).content,
"storage_id": dms_file.directory_id.storage_id.id,
}
)
def action_save_onboarding_file_step(self):
self.env.user.company_id.set_onboarding_step_done(
"documents_onboarding_file_state"
)
def action_wizard_dms_file_move(self):
items = self.browse(self.env.context.get("active_ids"))
root_directories = items.mapped("root_directory_id")
if len(root_directories) > 1:
raise UserError(_("Only files in the same root directory can be moved."))
result = self.env["ir.actions.act_window"]._for_xml_id(
"dms.wizard_dms_file_move_act_window"
)
result["context"] = dict(self.env.context)
return result
# SearchPanel
@api.model
def _search_panel_directory(self, **kwargs):
search_domain = (kwargs.get("search_domain", []),)
category_domain = kwargs.get("category_domain", [])
if category_domain and len(category_domain):
return "=", category_domain[0][2]
if search_domain and len(search_domain):
for domain in search_domain[0]:
if domain[0] == "directory_id":
return domain[1], domain[2]
return None, None
@api.model
def _search_panel_domain(self, field, operator, directory_id, comodel_domain=False):
if not comodel_domain:
comodel_domain = []
files_ids = self.search([("directory_id", operator, directory_id)]).ids
return expression.AND([comodel_domain, [(field, "in", files_ids)]])
@api.model
def search_panel_select_range(self, field_name, **kwargs):
"""This method is overwritten to make it 'similar' to v13.
The goal is that the directory searchpanel shows all directories
(even if some folders have no files).
"""
if field_name != "directory_id":
context = {}
if field_name == "category_id":
context["category_short_name"] = True
return super(
DMSFile, self.with_context(**context)
).search_panel_select_range(field_name, **kwargs)
domain = [("is_hidden", "=", False)]
# If we pass by context something, we filter more about it we filter
# the directories of the files, or we show all of them
if self.env.context.get("active_model") == "dms.directory":
active_id = self.env.context.get("active_id")
files = self.env["dms.file"].search(
[("directory_id", "child_of", active_id)]
)
all_directory_ids = []
for file_record in files:
directory = file_record.directory_id
while directory:
all_directory_ids.append(directory.id)
directory = directory.parent_id
domain.append(("id", "in", all_directory_ids))
# Get all possible directories
comodel_records = (
self.env["dms.directory"]
.with_context(directory_short_name=True)
.search_read(domain, ["display_name", "parent_id"])
)
all_record_ids = [rec["id"] for rec in comodel_records]
field_range = {}
enable_counters = kwargs.get("enable_counters")
for record in comodel_records:
record_id = record["id"]
parent = record["parent_id"]
record_values = {
"id": record_id,
"display_name": record["display_name"],
# If the parent directory is not in all the records we should not
# set parent_id because the user does not have access to parent.
"parent_id": (
parent[0] if parent and parent[0] in all_record_ids else False
),
}
if enable_counters:
record_values["__count"] = 0
field_range[record_id] = record_values
if enable_counters:
res = super().search_panel_select_range(field_name, **kwargs)
for item in res["values"]:
if item["id"] in field_range:
field_range[item["id"]]["__count"] = item["__count"]
return {"parent_field": "parent_id", "values": list(field_range.values())}
@api.model
def search_panel_select_multi_range(self, field_name, **kwargs):
operator, directory_id = self._search_panel_directory(**kwargs)
if field_name == "tag_ids":
sql_query = """
SELECT t.name AS name, t.id AS id, c.name AS group_name,
c.id AS group_id, COUNT(r.fid) AS count
FROM dms_tag t
JOIN dms_category c ON t.category_id = c.id
LEFT JOIN dms_file_tag_rel r ON t.id = r.tid
WHERE %(filter_by_file_ids)s IS FALSE OR r.fid = ANY(%(file_ids)s)
GROUP BY c.name, c.id, t.name, t.id
ORDER BY c.name, c.id, t.name, t.id;
"""
file_ids = []
if directory_id:
file_ids = self.search([("directory_id", operator, directory_id)]).ids
self.env.cr.execute(
sql_query,
{"file_ids": file_ids, "filter_by_file_ids": bool(directory_id)},
)
return self.env.cr.dictfetchall()
if directory_id and field_name in ["directory_id", "category_id"]:
comodel_domain = kwargs.pop("comodel_domain", [])
directory_comodel_domain = self._search_panel_domain(
"file_ids", operator, directory_id, comodel_domain
)
return super(
DMSFile, self.with_context(directory_short_name=True)
).search_panel_select_multi_range(
field_name, comodel_domain=directory_comodel_domain, **kwargs
)
return super(
DMSFile, self.with_context(directory_short_name=True)
).search_panel_select_multi_range(field_name, **kwargs)
# Read
@api.depends("name", "directory_id", "directory_id.parent_path")
def _compute_path(self):
model = self.env["dms.directory"]
for record in self:
path_names = [record.display_name]
path_json = [
{
"model": record._name,
"name": record.display_name,
"id": isinstance(record.id, int) and record.id or 0,
}
]
current_dir = record.directory_id
while current_dir:
path_names.insert(0, current_dir.name)
path_json.insert(
0,
{
"model": model._name,
"name": current_dir.name,
"id": current_dir._origin.id,
},
)
current_dir = current_dir.parent_id
record.update(
{
"path_names": "/".join(path_names) if all(path_names) else "",
"path_json": json.dumps(path_json),
}
)
@api.depends("name", "mimetype", "content")
def _compute_extension(self):
for record in self:
record.extension = file.guess_extension(
record.name, record.mimetype, record.content
)
@api.depends("content")
def _compute_mimetype(self):
for record in self:
binary = base64.b64decode(record.content or "")
record.mimetype = guess_mimetype(binary)
@api.depends("size")
def _compute_human_size(self):
for item in self:
item.human_size = human_size(item.size)
@api.depends("content_binary", "content_file", "attachment_id")
def _compute_content(self):
bin_size = self.env.context.get("bin_size", False)
for record in self:
if record.content_file:
context = {"human_size": True} if bin_size else {"base64": True}
record.content = record.with_context(**context).content_file
elif record.content_binary:
record.content = (
record.content_binary
if bin_size
else base64.b64encode(record.content_binary)
)
elif record.attachment_id:
context = {"human_size": True} if bin_size else {"base64": True}
record.content = record.with_context(**context).attachment_id.datas
@api.depends("content_binary", "content_file")
def _compute_save_type(self):
for record in self:
if record.content_file:
record.save_type = "file"
else:
record.save_type = "database"
@api.depends("storage_id", "storage_id.save_type")
def _compute_migration(self):
storage_model = self.env["dms.storage"]
save_field = storage_model._fields["save_type"]
values = save_field._description_selection(self.env)
selection = {value[0]: value[1] for value in values}
for record in self:
storage_type = record.storage_id.save_type
if storage_type == "attachment" or storage_type == record.save_type:
record.migration = selection.get(storage_type)
record.require_migration = False
else:
storage_label = selection.get(storage_type)
file_label = selection.get(record.save_type)
record.migration = f"{file_label} > {storage_label}"
record.require_migration = True
# View
@api.onchange("category_id")
def _change_category(self):
self.tag_ids = self.tag_ids.filtered(
lambda rec: not rec.category_id or rec.category_id == self.category_id
)
# Constrains
@api.constrains("storage_id", "res_model", "res_id")
def _check_storage_id_attachment_res_model(self):
for record in self:
if record.storage_id.save_type == "attachment" and not (
record.res_model and record.res_id
):
raise ValidationError(
_("A file must have model and resource ID in attachment storage.")
)
@api.constrains("name")
def _check_name(self):
for record in self:
if not file.check_name(record.name):
raise ValidationError(_("The file name is invalid."))
files = record.sudo().directory_id.file_ids
if files.filtered(
lambda file, record=record: file.name == record.name and file != record
):
raise ValidationError(
_("A file with the same name already exists in this directory.")
)
@api.constrains("extension")
def _check_extension(self):
if self.filtered(
lambda rec: rec.extension
and rec.extension in self._get_forbidden_extensions()
):
raise ValidationError(_("The file has a forbidden file extension."))
@api.constrains("size")
def _check_size(self):
if self.filtered(
lambda rec: rec.size > self._get_binary_max_size() * 1024 * 1024
):
raise ValidationError(
_("The maximum upload size is %s MB.") % self._get_binary_max_size()
)
# Create, Update, Delete
def _inverse_content(self):
updates = defaultdict(set)
for record in self:
values = self._get_content_inital_vals()
binary = base64.b64decode(record.content or "")
values = record._update_content_vals(values, binary)
updates[tools.frozendict(values)].add(record.id)
for vals, ids in updates.items():
self.browse(ids).write(dict(vals))
def _create_model_attachment(self, vals):
res_vals = vals.copy()
directory_id = False
if "directory_id" in res_vals:
directory_id = res_vals["directory_id"]
elif self.env.context.get("active_id"):
directory_id = self.env.context.get("active_id")
elif self.env.context.get("default_directory_id"):
directory_id = self.env.context.get("default_directory_id")
directory = self.env["dms.directory"].browse(directory_id)
if (
directory.res_model
and directory.res_id
and directory.storage_id_save_type == "attachment"
):
attachment = (
self.env["ir.attachment"]
.with_context(dms_file=True)
.create(
{
"name": vals["name"],
"datas": vals["content"],
"res_model": directory.res_model,
"res_id": directory.res_id,
}
)
)
res_vals["attachment_id"] = attachment.id
res_vals["res_model"] = attachment.res_model
res_vals["res_id"] = attachment.res_id
del res_vals["content"]
return res_vals
def copy(self, default=None):
self.ensure_one()
default = dict(default or [])
names = self.sudo().directory_id.file_ids.mapped("name")
if "directory_id" in default:
directory = self.env["dms.directory"].browse(
default.get("directory_id", False)
)
names = directory.sudo().file_ids.mapped("name")
default.update({"name": file.unique_name(self.name, names, self.extension)})
return super().copy(default)
@api.model_create_multi
def create(self, vals_list):
new_vals_list = []
for vals in vals_list:
if "attachment_id" not in vals:
vals = self._create_model_attachment(vals)
new_vals_list.append(vals)
return super().create(new_vals_list)
# ----------------------------------------------------------
# Locking fields and functions
locked_by = fields.Many2one(comodel_name="res.users")
is_locked = fields.Boolean(compute="_compute_locked", string="Locked")
is_lock_editor = fields.Boolean(compute="_compute_locked", string="Editor")
# ----------------------------------------------------------
# Locking
# ----------------------------------------------------------
def lock(self):
self.write({"locked_by": self.env.uid})
def unlock(self):
self.write({"locked_by": None})
# Read, View
@api.depends("locked_by")
def _compute_locked(self):
for record in self:
if record.locked_by.exists():
record.update(
{
"is_locked": True,
"is_lock_editor": record.locked_by.id == record.env.uid,
}
)
else:
record.update({"is_locked": False, "is_lock_editor": False})
def get_attachment_object(self, attachment):
return {
"name": attachment.name,
"datas": attachment.datas,
"res_model": attachment.res_model,
"mimetype": attachment.mimetype,
}
@api.model
def get_dms_files_from_attachments(self, attachment_ids=None):
"""Get the dms files from uploaded attachments.
:return: An Array of dms files.
"""
if not attachment_ids:
raise UserError(_("No attachment was provided"))
attachments = self.env["ir.attachment"].browse(attachment_ids)
if any(
attachment.res_id or attachment.res_model != "dms.file"
for attachment in attachments
):
raise UserError(_("Invalid attachments!"))
return [self.get_attachment_object(attachment) for attachment in attachments]

263
dms/models/dms_security_mixin.py Executable file
View File

@ -0,0 +1,263 @@
# Copyright 2020 Creu Blanca
# Copyright 2021 Tecnativa - Víctor Martínez
# Copyright 2024 Subteno - Timothée Vannier (https://www.subteno.com).
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from logging import getLogger
from odoo import api, fields, models
from odoo.osv.expression import (
FALSE_DOMAIN,
NEGATIVE_TERM_OPERATORS,
OR,
TRUE_DOMAIN,
)
_logger = getLogger(__name__)
class DmsSecurityMixin(models.AbstractModel):
_name = "dms.security.mixin"
_description = "DMS Security Mixin"
# Submodels must define this field that points to the owner dms.directory
_directory_field = "directory_id"
res_model = fields.Char(
string="Linked attachments model", index="btree", store=True
)
res_id = fields.Integer(
string="Linked attachments record ID", index="btree", store=True
)
record_ref = fields.Reference(
string="Record Referenced",
compute="_compute_record_ref",
selection=lambda self: self._get_ref_selection(),
)
permission_read = fields.Boolean(
compute="_compute_permissions",
search="_search_permission_read",
string="Read Access",
)
permission_create = fields.Boolean(
compute="_compute_permissions",
search="_search_permission_create",
string="Create Access",
)
permission_write = fields.Boolean(
compute="_compute_permissions",
search="_search_permission_write",
string="Write Access",
)
permission_unlink = fields.Boolean(
compute="_compute_permissions",
search="_search_permission_unlink",
string="Delete Access",
)
@api.model
def _get_ref_selection(self):
models = self.env["ir.model"].sudo().search([])
return [(model.model, model.name) for model in models]
@api.depends("res_model", "res_id")
def _compute_record_ref(self):
for record in self:
record.record_ref = False
if record.res_model and record.res_id:
record.record_ref = f"{record.res_model},{record.res_id}"
def _compute_permissions(self):
"""
Get permissions for the current record.
"""
# Update according to presence when applying ir.rule
self.invalidate_recordset()
if self.env.su:
self.update(
{
"permission_create": True,
"permission_read": True,
"permission_unlink": True,
"permission_write": True,
}
)
return
creatable = self._filter_access_rules("create")
readable = self._filter_access_rules("read")
unlinkable = self._filter_access_rules("unlink")
writeable = self._filter_access_rules("write")
for one in self:
one.update(
{
"permission_create": bool(one & creatable),
"permission_read": bool(one & readable),
"permission_unlink": bool(one & unlinkable),
"permission_write": bool(one & writeable),
}
)
@api.model
def _get_domain_by_inheritance(self, operation):
"""Get domain for inherited accessible records."""
if self.env.su:
return []
inherited_access_field = "storage_id_inherit_access_from_parent_record"
if self._name != "dms.directory":
inherited_access_field = f"{self._directory_field}.{inherited_access_field}"
inherited_access_domain = [
("storage_id_save_type", "=", "attachment"),
(inherited_access_field, "=", True),
]
domains = []
# Get all used related records
related_groups = self.sudo().read_group(
domain=inherited_access_domain + [("res_model", "!=", False)],
fields=["res_id:array_agg"],
groupby=["res_model"],
)
for group in related_groups:
try:
model = self.env[group["res_model"]]
except KeyError:
# The model might not be registered.
# This is normal if you are upgrading the database.
# Otherwise, you probably have garbage DMS data.
# These records will be accessible by DB users only.
domains.append(
[
("res_model", "=", group["res_model"]),
(True, "=", self.env.user.has_group("base.group_user")),
]
)
continue
# Check model access only once per batch
if not model.check_access_rights(operation, raise_exception=False):
continue
domains.append([("res_model", "=", model._name), ("res_id", "=", False)])
# Check record access in batch too
res_ids = [i for i in group["res_id"] if i] # Hack to remove None res_id
# Apply exists to skip records that do not exist. (e.g. a res.partner
# deleted by database).
model_records = model.browse(res_ids).exists()
related_ok = model_records._filter_access_rules_python(operation)
if not related_ok:
continue
domains.append(
[("res_model", "=", model._name), ("res_id", "in", related_ok.ids)]
)
result = inherited_access_domain + OR(domains)
return result
@api.model
def _get_access_groups_query(self, operation):
"""Return the query to select access groups."""
operation_check = {
"create": "AND dag.perm_inclusive_create",
"read": "",
"unlink": "AND dag.perm_inclusive_unlink",
"write": "AND dag.perm_inclusive_write",
}[operation]
select = f"""
SELECT
dir_group_rel.aid
FROM
dms_directory_complete_groups_rel AS dir_group_rel
INNER JOIN dms_access_group AS dag
ON dir_group_rel.gid = dag.id
INNER JOIN dms_access_group_users_rel AS users
ON users.gid = dag.id
WHERE
users.uid = %s {operation_check}
"""
return select, (self.env.uid,)
@api.model
def _get_domain_by_access_groups(self, operation):
"""Get domain for records accessible applying DMS access groups."""
result = [
(
"%s.storage_id_inherit_access_from_parent_record"
% self._directory_field,
"=",
False,
),
(
self._directory_field,
"inselect",
self._get_access_groups_query(operation),
),
]
return result
@api.model
def _get_permission_domain(self, operator, value, operation):
"""Abstract logic for searching computed permission fields."""
_self = self
# HACK ir.rule domain is always computed with sudo, so if this check is
# true, we can assume safely that you're checking permissions
if self.env.su and value == self.env.uid:
_self = self.sudo(False)
value = bool(value)
# Tricky one, to know if you want to search
# positive or negative access
positive = (operator not in NEGATIVE_TERM_OPERATORS) == bool(value)
if _self.env.su:
# You're SUPERUSER_ID
return TRUE_DOMAIN if positive else FALSE_DOMAIN
result = OR(
[
_self._get_domain_by_access_groups(operation),
_self._get_domain_by_inheritance(operation),
]
)
if not positive:
result.insert(0, "!")
return result
@api.model
def _search_permission_create(self, operator, value):
return self._get_permission_domain(operator, value, "create")
@api.model
def _search_permission_read(self, operator, value):
return self._get_permission_domain(operator, value, "read")
@api.model
def _search_permission_unlink(self, operator, value):
return self._get_permission_domain(operator, value, "unlink")
@api.model
def _search_permission_write(self, operator, value):
return self._get_permission_domain(operator, value, "write")
def _filter_access_rules_python(self, operation):
# Only kept to not break inheritance; see next comment
result = super()._filter_access_rules_python(operation)
# HACK Always fall back to applying rules by SQL.
# Upstream `_filter_access_rules_python()` doesn't use computed fields
# search methods. Thus, it will take the `[('permission_{operation}',
# '=', user.id)]` rule literally. Obviously that will always fail
# because `self[f"permission_{operation}"]` will always be a `bool`,
# while `user.id` will always be an `int`.
result |= self._filter_access_rules(operation)
return result
@api.model_create_multi
def create(self, vals_list):
# Create as sudo to avoid testing creation permissions before DMS security
# groups are attached (otherwise nobody would be able to create)
res = super(DmsSecurityMixin, self.sudo()).create(vals_list)
# Need to flush now, so all groups are stored in DB and the SELECT used
# to check access works
res.flush_recordset()
# Go back to the original sudo state and check we really had creation permission
res = res.sudo(self.env.su)
res.check_access_rights("create")
res.check_access_rule("create")
return res

101
dms/models/ir_attachment.py Executable file
View File

@ -0,0 +1,101 @@
# Copyright 2021 Tecnativa - Víctor Martínez
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from odoo import api, models
from odoo.tools import ormcache
class IrAttachment(models.Model):
_inherit = "ir.attachment"
def _get_dms_directories(self, res_model, res_id):
domain = [
("res_model", "=", res_model),
("res_id", "=", res_id),
("storage_id.save_type", "=", "attachment"),
]
if self.env.context.get("attaching_to_record"):
domain += [("storage_id.include_message_attachments", "=", True)]
return self.env["dms.directory"].search(domain)
def _dms_directories_create(self):
items = self.sudo()._get_dms_directories(self.res_model, False)
for item in items:
model_item = self.env[self.res_model].browse(self.res_id)
ir_model_item = (
self.env["ir.model"].sudo().search([("model", "=", self.res_model)])
)
self.env["dms.directory"].sudo().with_context(check_name=False).create(
{
"name": model_item.display_name,
"model_id": ir_model_item.id,
"res_model": self.res_model,
"res_id": self.res_id,
"parent_id": item.id,
"storage_id": item.storage_id.id,
}
)
@ormcache("model")
def _dms_operations_from_model(self, model):
# Apply sudo to prevent ir.rule from being applied.
item = self.env["dms.storage"].sudo().search([("model_ids.model", "=", model)])
return bool(item)
def _dms_operations(self):
"""Perform the operation only if there is a storage with linked models.
The directory (dms.directory) linked to the record (if it does not exist)
and the file (dms.file) with the linked attachment would be created.
"""
for attachment in self:
if (
not attachment.res_model
or not attachment.res_id
or (
attachment.res_model
and not self._dms_operations_from_model(attachment.res_model)
)
):
continue
directories = attachment._get_dms_directories(
attachment.res_model, attachment.res_id
)
if not directories:
attachment._dms_directories_create()
# Get dms_directories again (with items previously created)
directories = attachment._get_dms_directories(
attachment.res_model, attachment.res_id
)
# Auto-create_files (if not exists)
for directory in directories:
dms_file_model = self.env["dms.file"].sudo()
dms_file = dms_file_model.search(
[
("attachment_id", "=", attachment.id),
("directory_id", "=", directory.id),
]
)
if not dms_file:
dms_file_model.create(
{
"name": attachment.name,
"directory_id": directory.id,
"attachment_id": attachment.id,
"res_model": attachment.res_model,
"res_id": attachment.res_id,
}
)
@api.model_create_multi
def create(self, vals_list):
records = super().create(vals_list)
if not self.env.context.get("dms_file"):
records._dms_operations()
return records
def write(self, vals):
res = super().write(vals)
if not self.env.context.get("dms_file") and self.env.context.get(
"attaching_to_record"
):
self._dms_operations()
return res

19
dms/models/ir_binary.py Executable file
View File

@ -0,0 +1,19 @@
# Copyright 2024 Subteno - Timothée Vannier (https://www.subteno.com)
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from odoo import models
class IrBinary(models.AbstractModel):
_inherit = "ir.binary"
def _find_record_check_access(self, record, access_token):
if record._name in ("dms.file", "dms.directory"):
if record.sudo().check_access_token(access_token):
# sudo because the user might not usually have access to the record but
# now the token is valid.
# Used to display the icon in the portal.
return record.sudo()
return super()._find_record_check_access(record, access_token)

15
dms/models/mail_thread.py Executable file
View File

@ -0,0 +1,15 @@
# Copyright 2021 Tecnativa - Jairo Llopis
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
from odoo import models
class MailThread(models.AbstractModel):
_inherit = "mail.thread"
def _process_attachments_for_post(self, attachments, attachment_ids, message_data):
"""Indicate to DMS that we're attaching a message to a record."""
_self = self.with_context(attaching_to_record=True)
return super(MailThread, _self)._process_attachments_for_post(
attachments, attachment_ids, message_data
)

49
dms/models/mixins_thumbnail.py Executable file
View File

@ -0,0 +1,49 @@
# Copyright 2017-2019 MuK IT GmbH.
# Copyright 2020 Creu Blanca
# Copyright 2024 Subteno - Timothée Vannier (https://www.subteno.com).
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
import os
from odoo import api, fields, models
from odoo.tools.misc import file_path
class Thumbnail(models.AbstractModel):
_name = "dms.mixins.thumbnail"
_inherit = "image.mixin"
_description = "DMS thumbnail and icon mixin"
icon_url = fields.Char(string="Icon URL", compute="_compute_icon_url")
def _get_icon_disk_path(self):
"""Get the local disk path to record icon."""
name = self._get_icon_placeholder_name()
folders = ["dms", "static", "icons"]
try:
path = file_path(os.path.join(*folders, name))
except FileNotFoundError:
return file_path(os.path.join(*folders, "file_unknown.svg"))
return path or file_path(os.path.join(*folders, "file_unknown.svg"))
def _get_icon_placeholder_name(self):
return "folder.svg"
def _get_icon_url(self):
"""Obtain URL to record icon."""
local_path = self._get_icon_disk_path()
icon_name = os.path.basename(local_path)
return "/dms/static/icons/%s" % icon_name
@api.depends("image_128")
def _compute_icon_url(self):
"""Get icon static file URL."""
for one in self:
# Get URL to thumbnail or to the default icon by file extension
one.icon_url = (
f"/web/image/{one._name}/{one.id}/image_128/128x128?crop=1"
if one.image_128
else f"{one._get_icon_url()}?crop=1"
)

View File

@ -0,0 +1,16 @@
# Copyright 2024 Subteno - Timothée Vannier (https://www.subteno.com).
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from odoo import api, models
class OnboardingOnboarding(models.Model):
_inherit = "onboarding.onboarding"
# ----------------------------------------------------------
# Actions
# ----------------------------------------------------------
@api.model
def action_close_panel_dms_file(self):
self.action_close_panel("dms.onboarding_onboarding_dms_file")

View File

@ -0,0 +1,50 @@
# Copyright 2024 Subteno - Timothée Vannier (https://www.subteno.com).
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from odoo import api, models
class OnboardingOnboardingStep(models.Model):
_inherit = "onboarding.onboarding.step"
# ----------------------------------------------------------
# Actions
# ----------------------------------------------------------
@api.model
def action_open_documents_onboarding_storage(self):
"""
Open the form to create a new storage from the onboarding panel.
"""
return self.env.ref("dms.action_dms_storage_new").read()[0]
@api.model
def action_open_documents_onboarding_directory(self):
"""
Open the form to create a new directory from the onboarding panel.
"""
storage = self.env["dms.storage"].search([], order="create_date desc", limit=1)
action = self.env.ref("dms.action_dms_directory_new").read()[0]
action["context"] = {
**self.env.context,
**{
"default_is_root_directory": True,
"default_storage_id": storage and storage.id,
},
}
return action
@api.model
def action_open_documents_onboarding_file(self):
"""
Open the form to create a new file from the onboarding panel.
"""
directory = self.env["dms.directory"].search(
[], order="create_date desc", limit=1
)
action = self.env.ref("dms.action_dms_file_new").read()[0]
action["context"] = {
**self.env.context,
**{"default_directory_id": directory and directory.id},
}
return action

86
dms/models/res_company.py Executable file
View File

@ -0,0 +1,86 @@
# Copyright 2020 Creu Blanca
# Copyright 2017-2019 MuK IT GmbH
# Copyright 2024 Subteno - Timothée Vannier (https://www.subteno.com).
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
import logging
from odoo import api, fields, models
_logger = logging.getLogger(__name__)
class ResCompany(models.Model):
_inherit = "res.company"
documents_onboarding_state = fields.Selection(
selection=[
("not_done", "Not done"),
("just_done", "Just done"),
("done", "Done"),
("closed", "Closed"),
],
default="not_done",
)
documents_onboarding_storage_state = fields.Selection(
selection=[
("not_done", "Not done"),
("just_done", "Just done"),
("done", "Done"),
("closed", "Closed"),
],
default="not_done",
)
documents_onboarding_directory_state = fields.Selection(
selection=[
("not_done", "Not done"),
("just_done", "Just done"),
("done", "Done"),
("closed", "Closed"),
],
default="not_done",
)
documents_onboarding_file_state = fields.Selection(
selection=[
("not_done", "Not done"),
("just_done", "Just done"),
("done", "Done"),
("closed", "Closed"),
],
default="not_done",
)
# Functions
def get_and_update_documents_onboarding_state(self):
step_states = [
"documents_onboarding_storage_state",
"documents_onboarding_directory_state",
"documents_onboarding_file_state",
]
onboarding_state = "documents_onboarding_state"
old_values = {}
all_done = True
for step_state in step_states:
old_values[step_state] = self[step_state]
if self[step_state] == "just_done":
self[step_state] = "done"
all_done = all_done and self[step_state] == "done"
if all_done:
old_values[onboarding_state] = (
"just_done" if self[onboarding_state] == "not_done" else "done"
)
self[onboarding_state] = "done"
return old_values
# Actions
@api.model
def action_close_documents_onboarding(self):
self.env.user.company_id.documents_onboarding_state = "closed"
def set_onboarding_step_done(self, step):
self.ensure_one()
if self[step] == "not_done":
self[step] = "just_done"

View File

@ -0,0 +1,21 @@
# Copyright 2020 Creu Blanca
# Copyright 2017-2019 MuK IT GmbH
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from odoo import fields, models
class ResConfigSettings(models.TransientModel):
_inherit = "res.config.settings"
documents_binary_max_size = fields.Integer(
string="Size",
help="Defines the maximum upload size in MB. Default (25MB)",
config_parameter="dms.binary_max_size",
)
documents_forbidden_extensions = fields.Char(
string="Extensions",
help="Defines a list of forbidden file extensions. (Example: 'exe,msi')",
config_parameter="dms.forbidden_extensions",
)

133
dms/models/storage.py Executable file
View File

@ -0,0 +1,133 @@
# Copyright 2017-2019 MuK IT GmbH.
# Copyright 2020 Creu Blanca
# Copyright 2021 Tecnativa - Víctor Martínez
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
import logging
from odoo import _, api, fields, models
from odoo.exceptions import AccessError
_logger = logging.getLogger(__name__)
class Storage(models.Model):
_name = "dms.storage"
_description = "Storage"
name = fields.Char(required=True)
save_type = fields.Selection(
selection=[
("database", _("Database")),
("file", _("Filestore")),
("attachment", _("Attachment")),
],
default="database",
required=True,
help="The save type is used to determine how a file is saved by the system. "
"If you change this setting, you can migrate existing files manually by "
"triggering the action.",
)
company_id = fields.Many2one(
comodel_name="res.company",
string="Company",
default=lambda self: self.env.company,
help="If set, directories and files will only be available for "
"the selected company.",
)
is_hidden = fields.Boolean(
string="Storage is Hidden",
default=False,
help="Indicates if directories and files are hidden by default.",
)
root_directory_ids = fields.One2many(
comodel_name="dms.directory",
inverse_name="storage_id",
string="Root Directories",
auto_join=False,
readonly=False,
copy=False,
)
storage_directory_ids = fields.One2many(
comodel_name="dms.directory",
inverse_name="storage_id",
string="Directories",
auto_join=False,
readonly=True,
copy=False,
)
storage_file_ids = fields.One2many(
comodel_name="dms.file",
inverse_name="storage_id",
string="Files",
auto_join=False,
readonly=True,
copy=False,
)
count_storage_directories = fields.Integer(
compute="_compute_count_storage_directories", string="Count Directories"
)
count_storage_files = fields.Integer(
compute="_compute_count_storage_files", string="Count Files"
)
model_ids = fields.Many2many("ir.model", string="Linked Models")
inherit_access_from_parent_record = fields.Boolean(
string="Inherit permissions from related record",
default=False,
help="Indicate if directories and files access work only with "
"related model access (for example, if some directories are related "
"with any sale, only users with read access to these sale can access)",
)
include_message_attachments = fields.Boolean(
string="Create files from message attachments",
default=False,
help="Indicate if directories and files auto-create in mail "
"composition process too",
)
model = fields.Char(search="_search_model", store=False)
def _search_model(self, operator, value):
allowed_items = self.env["ir.model"].sudo().search([("model", operator, value)])
return [("model_ids", "in", allowed_items.ids)]
@api.onchange("save_type")
def _onchange_save_type(self):
for record in self:
if record.save_type == "attachment":
record.inherit_access_from_parent_record = True
# Actions
def action_storage_migrate(self):
if self.save_type != "attachment":
if not self.env.user.has_group("dms.group_dms_manager"):
raise AccessError(_("Only managers can execute this action."))
files = self.env["dms.file"].with_context(active_test=False).sudo()
for record in self:
domain = [
("require_migration", "=", True),
("storage_id", "=", record.id),
]
files.search(domain).action_migrate()
def action_save_onboarding_storage_step(self):
self.env.user.company_id.set_onboarding_step_done(
"documents_onboarding_storage_state"
)
# Read, View
@api.depends("storage_directory_ids")
def _compute_count_storage_directories(self):
for record in self:
record.count_storage_directories = len(record.storage_directory_ids)
@api.depends("storage_file_ids")
def _compute_count_storage_files(self):
for record in self:
record.count_storage_files = len(record.storage_file_ids)
def write(self, values):
res = super().write(values)
if "model_ids" in values:
self.env.registry.clear_cache()
return res

59
dms/models/tag.py Executable file
View File

@ -0,0 +1,59 @@
# Copyright 2020 RGB Consulting
# Copyright 2017-2019 MuK IT GmbH
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
import logging
from odoo import api, fields, models
_logger = logging.getLogger(__name__)
class Tag(models.Model):
_name = "dms.tag"
_description = "Document Tag"
name = fields.Char(required=True, translate=True)
active = fields.Boolean(
default=True,
help="The active field allows you " "to hide the tag without removing it.",
)
category_id = fields.Many2one(
comodel_name="dms.category",
context={"dms_category_show_path": True},
string="Category",
ondelete="set null",
)
color = fields.Integer(string="Color Index", default=10)
directory_ids = fields.Many2many(
comodel_name="dms.directory",
relation="dms_directory_tag_rel",
column1="tid",
column2="did",
string="Directories",
readonly=True,
)
file_ids = fields.Many2many(
comodel_name="dms.file",
relation="dms_file_tag_rel",
column1="tid",
column2="fid",
string="Files",
readonly=True,
)
count_directories = fields.Integer(compute="_compute_count_directories")
count_files = fields.Integer(compute="_compute_count_files")
_sql_constraints = [
("name_uniq", "unique (name, category_id)", "Tag name already exists!"),
]
@api.depends("directory_ids")
def _compute_count_directories(self):
for rec in self:
rec.count_directories = len(rec.directory_ids)
@api.depends("file_ids")
def _compute_count_files(self):
for rec in self:
rec.count_files = len(rec.file_ids)

3
dms/pyproject.toml Executable file
View File

@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"

59
dms/readme/CONFIGURE.md Executable file
View File

@ -0,0 +1,59 @@
# Configuration
To configure this module, you need to:
## 1. Create a storage
1. Go to *Documents -\> Configuration -\> Storages*.
2. Create a new document storage. You can choose between three options on `Save Type`:
- `Database`: Store the files on the database as a field
- `Attachment`: Store the files as attachments
- `File`: Store the files on the file system
## 2. Create an access group
1. Next, create an administrative access group. Go to *Configuration -\> Access Groups*.
- Create a new group, name it appropriately, and turn on all three
permissions (Create, Write and Unlink. Read is implied and always
enabled).
- Add any other top-level administrative users to the group if
needed (your user should already be there).
- You can create other groups in here later for fine-grained access
control.
## 3. Create a directory
1. Afterward, go to *Documents -\> Directories*.
2. Create a new directory, mark it as root and select the previously created setting.
- Select the *Groups* tab and add your administrative group created
above.
If your directory was already created before the group, you can also add it in the
access groups (*Configuration -\> Access Groups*).
3. In the directory settings, you can also add other access groups (created above) that will be able to:
- read
- create
- write
- delete
# Migration
If you need to modify the storage `Save Type` you might want to migrate
the file data. To achieve it, you need to:
1. Go to *Documents -\> Configuration -\> Storage* and select the
storage you want to modify
2. Modify the save type
3. Press the button Migrate files if you want to migrate all the files
at once
4. Press the button Manual File Migration to specify files one by one
You can check all the files that still need to be migrated from all
storages and migrate them manually on *Documents -\> Configuration -\>
Migration*
# File Wizard Selection
There is an action called `action_dms_file_wizard_selector` to open a
wizard to list files in kanban view. This can be used (example
dms_attachment_link module) to add a button in kanban view with the
action we need.

14
dms/readme/CONTRIBUTORS.md Executable file
View File

@ -0,0 +1,14 @@
- Mathias Markl \<<mathias.markl@mukit.at>\>
- Enric Tobella \<<etobella@creublanca.es>\>
- Antoni Romera
- Gelu Boros \<<gelu.boros@rgbconsulting.com>\>
- [Tecnativa](https://www.tecnativa.com):
- Víctor Martínez
- Pedro M. Baeza
- Jairo Llopis
- [Elego](https://www.elegosoft.com):
- Yu Weng \<<yweng@elegosoft.com>\>
- Philip Witte \<<phillip.witte@elegosoft.com>\>
- Khanh Bui \<<khanh.bui@mail.elegosoft.com>\>
- [Subteno](https://www.subteno.com):
- Timothée Vannier <<tva@subteno.com>>

6
dms/readme/CREDITS.md Executable file
View File

@ -0,0 +1,6 @@
Some pictures are based on or inspired by:
- [Roundicons](https://www.flaticon.com/authors/roundicons)
- [Smashicons](https://www.flaticon.com/authors/smashicons)
- [EmojiOne](https://github.com/EmojiTwo/emojitwo) : Portal DMS icon
- [GitHub Octicons](https://github.com/primer/octicons/) : The main DMS icon

9
dms/readme/DESCRIPTION.md Executable file
View File

@ -0,0 +1,9 @@
DMS is a module for creating, managing and viewing document files
directly within Odoo. This module is only the basis for an entire
ecosystem of apps that extend and seamlessly integrate with the document
management system.
This module adds portal functionality for directories and files for
allowed users, both portal or internal users. You can get as well a
tokenized link from a directory or a file for sharing it with any
anonymous user.

4
dms/readme/INSTALL.md Executable file
View File

@ -0,0 +1,4 @@
## Preview
`python-magic` library is recommended to be installed for having whole
support to get proper file types and file preview.

21
dms/readme/ROADMAP.md Executable file
View File

@ -0,0 +1,21 @@
- Files preview in portal
- Allow to download folder in portal and create zip file with all
content
- Save in cache own_root directories and update in every
create/write/unlink function
- Add a migration procedure for converting an storage to attachment one
for populating existing records with attachments as folders
- Add a link from attachment view in chatter to linked documents
- If Inherit permissions from related record (the
inherit_access_from_parent_record field from storage) is changed when
directories already exist, inconsistencies may occur because groups
defined in the directories and subdirectories will still exist, all
groups in these directories should be removed before changing.
- Since portal users can read `dms.storage` records, if your module
extends this model to another storage backend that needs using
secrets, remember to forbid access to the secrets fields by other
means. It would be nice to be able to remove that rule at some point.
- Searchpanel in files: Highlight items (shading) without records when
filtering something (by name for example).
- Accessing the clipboard (for example copy share link of file/directory)
is limited to secure connections. It also happens in any part of Odoo.

11
dms/readme/USAGE.md Executable file
View File

@ -0,0 +1,11 @@
The best way to manage the documents is to switch to the Documents view.
Existing documents can be managed there and new documents can be
created.
## Portal functionality
You can add any portal user to DMS access groups, and then allow that
group in directories, so they will see in the portal such directories
and their files. Another possibility is to click on "Share" button
inside a directory or a file for obtaining a tokenized link for single
access to that resource, no matter if logged or not.

View File

@ -0,0 +1,27 @@
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
access_dms_tag_user,dms_tag_user,model_dms_tag,group_dms_user,1,1,1,1
access_dms_category_user,dms_category_user,model_dms_category,group_dms_user,1,1,1,1
access_dms_storage_base_user,dms_storage_base_user,model_dms_storage,base.group_user,1,0,0,0
access_dms_storage_portal,dms_storage_portal,model_dms_storage,base.group_portal,1,0,0,0
access_dms_storage_user,dms_storage_user,model_dms_storage,group_dms_user,1,0,0,0
access_dms_storage_manager,dms_storage_manager,model_dms_storage,group_dms_manager,1,1,1,1
access_dms_directory_public,dms_directory_public,model_dms_directory,base.group_public,1,0,0,0
access_dms_directory_portal,dms_directory_portal,model_dms_directory,base.group_portal,1,0,0,0
access_dms_directory_base_user,dms_directory_base_user,model_dms_directory,base.group_user,1,0,0,0
access_dms_directory_user,dms_directory_user,model_dms_directory,group_dms_user,1,1,1,1
access_dms_file_public,dms_file_public,model_dms_file,base.group_public,1,0,0,0
access_dms_file_portal,dms_file_portal,model_dms_file,base.group_portal,1,0,0,0
access_dms_file_base_user,dms_file_base_user,model_dms_file,base.group_user,1,0,0,0
access_dms_file_user,dms_file_user,model_dms_file,group_dms_user,1,1,1,1
access_dms_access_group_public,access_dms_access_group_public,model_dms_access_group,base.group_public,1,0,0,0
access_dms_access_group_portal,access_dms_access_group_portal,model_dms_access_group,base.group_portal,1,0,0,0
access_security_access_groups_user,access_security_access_groups_user,model_dms_access_group,base.group_user,1,0,0,0
access_security_access_groups_dms_user,access_security_access_groups_dms_user,model_dms_access_group,group_dms_user,1,1,1,1
access_wizard_dms_file_move,access_wizard_dms_file_move,model_wizard_dms_file_move,group_dms_user,1,1,1,1
access_wizard_dms_share,access_wizard_dms_share,model_wizard_dms_share,group_dms_manager,1,1,1,0
1 id name model_id/id group_id/id perm_read perm_write perm_create perm_unlink
2 access_dms_tag_user dms_tag_user model_dms_tag group_dms_user 1 1 1 1
3 access_dms_category_user dms_category_user model_dms_category group_dms_user 1 1 1 1
4 access_dms_storage_base_user dms_storage_base_user model_dms_storage base.group_user 1 0 0 0
5 access_dms_storage_portal dms_storage_portal model_dms_storage base.group_portal 1 0 0 0
6 access_dms_storage_user dms_storage_user model_dms_storage group_dms_user 1 0 0 0
7 access_dms_storage_manager dms_storage_manager model_dms_storage group_dms_manager 1 1 1 1
8 access_dms_directory_public dms_directory_public model_dms_directory base.group_public 1 0 0 0
9 access_dms_directory_portal dms_directory_portal model_dms_directory base.group_portal 1 0 0 0
10 access_dms_directory_base_user dms_directory_base_user model_dms_directory base.group_user 1 0 0 0
11 access_dms_directory_user dms_directory_user model_dms_directory group_dms_user 1 1 1 1
12 access_dms_file_public dms_file_public model_dms_file base.group_public 1 0 0 0
13 access_dms_file_portal dms_file_portal model_dms_file base.group_portal 1 0 0 0
14 access_dms_file_base_user dms_file_base_user model_dms_file base.group_user 1 0 0 0
15 access_dms_file_user dms_file_user model_dms_file group_dms_user 1 1 1 1
16 access_dms_access_group_public access_dms_access_group_public model_dms_access_group base.group_public 1 0 0 0
17 access_dms_access_group_portal access_dms_access_group_portal model_dms_access_group base.group_portal 1 0 0 0
18 access_security_access_groups_user access_security_access_groups_user model_dms_access_group base.group_user 1 0 0 0
19 access_security_access_groups_dms_user access_security_access_groups_dms_user model_dms_access_group group_dms_user 1 1 1 1
20 access_wizard_dms_file_move access_wizard_dms_file_move model_wizard_dms_file_move group_dms_user 1 1 1 1
21 access_wizard_dms_share access_wizard_dms_share model_wizard_dms_share group_dms_manager 1 1 1 0

191
dms/security/security.xml Executable file
View File

@ -0,0 +1,191 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
Copyright 2017-2019 MuK IT GmbH
Copyright 2020 Creu Blanca
Copyright 2021 Tecnativa - Víctor Martínez
Copyright 2024 Subteno - Timothée Vannier (https://www.subteno.com).
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
-->
<odoo>
<record id="category_dms_security" model="ir.module.category">
<field name="name">Documents</field>
</record>
<record id="group_dms_user" model="res.groups">
<field name="name">User</field>
<field name="category_id" ref="category_dms_security" />
<field name="implied_ids" eval="[(4, ref('base.group_user'))]" />
</record>
<record id="group_dms_manager" model="res.groups">
<field name="name">Manager</field>
<field name="implied_ids" eval="[(4, ref('group_dms_user'))]" />
<field name="category_id" ref="category_dms_security" />
<field
name="users"
eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"
/>
</record>
<record id="rule_multi_company_storage" model="ir.rule">
<field name="name">DMS Storage multi-company</field>
<field name="model_id" ref="model_dms_storage" />
<field name="global" eval="True" />
<field
name="domain_force"
>['|',('company_id','=',False),('company_id','in',company_ids)]</field>
</record>
<record id="rule_multi_company_directory" model="ir.rule">
<field name="name">DMS Directory multi-company</field>
<field name="model_id" ref="model_dms_directory" />
<field name="global" eval="True" />
<field
name="domain_force"
>['|',('company_id','=',False),('company_id','in',company_ids)]</field>
</record>
<record id="rule_multi_company_file" model="ir.rule">
<field name="name">File multi-company</field>
<field name="model_id" ref="model_dms_file" />
<field name="global" eval="True" />
<field
name="domain_force"
>['|',('company_id','=',False),('company_id','in',company_ids)]</field>
</record>
<record id="rule_file_locked" model="ir.rule">
<field name="name">Locked files are only modified by locker user.</field>
<field name="model_id" ref="model_dms_file" />
<field name="groups" eval="[(4, ref('base.group_user'))]" />
<field name="global" eval="True" />
<field name="perm_read" eval="0" />
<field name="perm_create" eval="1" />
<field name="perm_write" eval="1" />
<field name="perm_unlink" eval="1" />
<field
name="domain_force"
>['|', ('locked_by', '=', False), ('locked_by', '=', user.id)]</field>
</record>
<record id="rule_security_groups_user" model="ir.rule">
<field name="name">DMS users can only edit and delete their own groups.</field>
<field name="model_id" ref="model_dms_access_group" />
<field name="groups" eval="[(4, ref('group_dms_user'))]" />
<field name="perm_read" eval="0" />
<field name="perm_create" eval="0" />
<field name="perm_write" eval="1" />
<field name="perm_unlink" eval="1" />
<field name="domain_force">[('create_uid','=',user.id)]</field>
</record>
<record id="rule_security_groups_manager" model="ir.rule">
<field name="name">DMS Managers can edit and delete all groups.</field>
<field name="model_id" ref="model_dms_access_group" />
<field name="groups" eval="[(4, ref('group_dms_manager'))]" />
<field name="perm_read" eval="0" />
<field name="perm_create" eval="0" />
<field name="perm_write" eval="1" />
<field name="perm_unlink" eval="1" />
<field name="domain_force">[(1 ,'=', 1)]</field>
</record>
<!-- Forbid lower groups access to hidden storage -->
<record id="rule_forbid_hidden_storage" model="ir.rule">
<field name="name">Basic users cannot access hidden storage</field>
<field name="model_id" ref="model_dms_storage" />
<field
name="groups"
eval="[(4, ref('base.group_portal')), (4, ref('group_dms_user'))]"
/>
<field name="perm_read" eval="1" />
<field name="perm_create" eval="1" />
<field name="perm_write" eval="1" />
<field name="perm_unlink" eval="1" />
<field name="domain_force">[('is_hidden', '=', False)]</field>
</record>
<record id="rule_allow_hidden_storage" model="ir.rule">
<field name="name">Managers can access hidden storage</field>
<field name="model_id" ref="model_dms_storage" />
<field name="groups" eval="[(4, ref('group_dms_manager'))]" />
<field name="perm_read" eval="1" />
<field name="perm_create" eval="1" />
<field name="perm_write" eval="1" />
<field name="perm_unlink" eval="1" />
<field name="domain_force">[('is_hidden', '=', True)]</field>
</record>
<!-- These rules leverage computed permission management -->
<record id="rule_directory_computed_create" model="ir.rule">
<field name="name">Apply computed create permissions.</field>
<field name="model_id" ref="model_dms_directory" />
<field name="global" eval="True" />
<field name="perm_read" eval="0" />
<field name="perm_create" eval="1" />
<field name="perm_write" eval="0" />
<field name="perm_unlink" eval="0" />
<field name="domain_force">[('permission_create', '=', user.id)]</field>
</record>
<record id="rule_directory_computed_read" model="ir.rule">
<field name="name">Apply computed read permissions.</field>
<field name="model_id" ref="model_dms_directory" />
<field name="global" eval="True" />
<field name="perm_read" eval="1" />
<field name="perm_create" eval="0" />
<field name="perm_write" eval="0" />
<field name="perm_unlink" eval="0" />
<field name="domain_force">[('permission_read', '=', user.id)]</field>
</record>
<record id="rule_directory_computed_unlink" model="ir.rule">
<field name="name">Apply computed unlink permissions.</field>
<field name="model_id" ref="model_dms_directory" />
<field name="global" eval="True" />
<field name="perm_read" eval="0" />
<field name="perm_create" eval="0" />
<field name="perm_write" eval="0" />
<field name="perm_unlink" eval="1" />
<field name="domain_force">[('permission_unlink', '=', user.id)]</field>
</record>
<record id="rule_directory_computed_write" model="ir.rule">
<field name="name">Apply computed write permissions.</field>
<field name="model_id" ref="model_dms_directory" />
<field name="global" eval="True" />
<field name="perm_read" eval="0" />
<field name="perm_create" eval="0" />
<field name="perm_write" eval="1" />
<field name="perm_unlink" eval="0" />
<field name="domain_force">[('permission_write', '=', user.id)]</field>
</record>
<record id="rule_file_computed_create" model="ir.rule">
<field name="name">Apply computed create permissions.</field>
<field name="model_id" ref="model_dms_file" />
<field name="global" eval="True" />
<field name="perm_read" eval="0" />
<field name="perm_create" eval="1" />
<field name="perm_write" eval="0" />
<field name="perm_unlink" eval="0" />
<field name="domain_force">[('permission_create', '=', user.id)]</field>
</record>
<record id="rule_file_computed_read" model="ir.rule">
<field name="name">Apply computed read permissions.</field>
<field name="model_id" ref="model_dms_file" />
<field name="global" eval="True" />
<field name="perm_read" eval="1" />
<field name="perm_create" eval="0" />
<field name="perm_write" eval="0" />
<field name="perm_unlink" eval="0" />
<field name="domain_force">[('permission_read', '=', user.id)]</field>
</record>
<record id="rule_file_computed_unlink" model="ir.rule">
<field name="name">Apply computed unlink permissions.</field>
<field name="model_id" ref="model_dms_file" />
<field name="global" eval="True" />
<field name="perm_read" eval="0" />
<field name="perm_create" eval="0" />
<field name="perm_write" eval="0" />
<field name="perm_unlink" eval="1" />
<field name="domain_force">[('permission_unlink', '=', user.id)]</field>
</record>
<record id="rule_file_computed_write" model="ir.rule">
<field name="name">Apply computed write permissions.</field>
<field name="model_id" ref="model_dms_file" />
<field name="global" eval="True" />
<field name="perm_read" eval="0" />
<field name="perm_create" eval="0" />
<field name="perm_write" eval="1" />
<field name="perm_unlink" eval="0" />
<field name="domain_force">[('permission_write', '=', user.id)]</field>
</record>
</odoo>

BIN
dms/static/description/icon.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,4 @@
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.6594 14.0094C12.443 12.2246 14.8624 11.2213 17.3856 11.22H36.4169C39.4075 11.22 42.235 12.6338 44.0294 15.0263L48.9231 21.5513C49.0498 21.7201 49.2139 21.8571 49.4027 21.9515C49.5915 22.0459 49.7996 22.095 50.0106 22.095H78.5575C79.9996 22.095 81.3827 22.6679 82.4024 23.6876C83.4221 24.7073 83.995 26.0904 83.995 27.5325V30.2513H22.8231C21.7415 30.2513 20.7043 30.6809 19.9395 31.4457C19.1747 32.2105 18.745 33.2478 18.745 34.3294C18.745 35.411 19.1747 36.4483 19.9395 37.213C20.7043 37.9778 21.7415 38.4075 22.8231 38.4075H87.9535C88.7217 38.4068 89.4812 38.5689 90.1822 38.8831C90.8832 39.1973 91.5096 39.6564 92.0203 40.2303C92.5309 40.8041 92.9142 41.4796 93.1448 42.2123C93.3755 42.945 93.4482 43.7183 93.3584 44.4812L89.4325 77.8294C89.4325 80.3531 88.43 82.7734 86.6454 84.5579C84.8609 86.3425 82.4406 87.345 79.9169 87.345H17.3856C14.8619 87.345 12.4416 86.3425 10.6571 84.5579C8.87254 82.7734 7.87 80.3531 7.87 77.8294V20.7356C7.87 18.2126 8.8705 15.7875 10.6594 14.0094Z" fill="#C83737"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M42.5039 38.4075H22.8231C21.7415 38.4075 20.7043 37.9778 19.9395 37.213C19.1747 36.4482 18.745 35.411 18.745 34.3294C18.745 33.2478 19.1747 32.2105 19.9395 31.4457C20.7043 30.6809 21.7415 30.2513 22.8231 30.2513H83.995V27.5325C83.995 26.0904 83.4221 24.7073 82.4024 23.6876C81.3827 22.6679 79.9996 22.095 78.5575 22.095H50.0106C49.7996 22.095 49.5914 22.0459 49.4027 21.9515C49.2139 21.8571 49.0498 21.7201 48.9231 21.5513L44.0294 15.0263C42.235 12.6338 39.4075 11.22 36.4169 11.22H17.3856C14.8624 11.2213 12.443 12.2246 10.6594 14.0094C8.8705 15.7875 7.87 18.2126 7.87 20.7356V47.9602C16.6861 44.4618 28.757 40.875 42.5039 38.4075Z" fill="#EC8A8A"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

610
dms/static/description/index.html Executable file
View File

@ -0,0 +1,610 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>Document Management System</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: gray; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic, pre.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="document-management-system">
<h1 class="title">Document Management System</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:966c4331ff7c75b1ea8cb1d065c878d81250957cd305a5d6422def133e2a7d63
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/lgpl-3.0-standalone.html"><img alt="License: LGPL-3" src="https://img.shields.io/badge/licence-LGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/dms/tree/17.0/dms"><img alt="OCA/dms" src="https://img.shields.io/badge/github-OCA%2Fdms-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/dms-17-0/dms-17-0-dms"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/dms&amp;target_branch=17.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>DMS is a module for creating, managing and viewing document files
directly within Odoo. This module is only the basis for an entire
ecosystem of apps that extend and seamlessly integrate with the document
management system.</p>
<p>This module adds portal functionality for directories and files for
allowed users, both portal or internal users. You can get as well a
tokenized link from a directory or a file for sharing it with any
anonymous user.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#installation" id="toc-entry-1">Installation</a><ul>
<li><a class="reference internal" href="#preview" id="toc-entry-2">Preview</a></li>
</ul>
</li>
<li><a class="reference internal" href="#configuration" id="toc-entry-3">Configuration</a></li>
<li><a class="reference internal" href="#configuration-1" id="toc-entry-4">Configuration</a><ul>
<li><a class="reference internal" href="#create-a-storage" id="toc-entry-5">1. Create a storage</a></li>
<li><a class="reference internal" href="#create-an-access-group" id="toc-entry-6">2. Create an access group</a></li>
<li><a class="reference internal" href="#create-a-directory" id="toc-entry-7">3. Create a directory</a></li>
</ul>
</li>
<li><a class="reference internal" href="#migration" id="toc-entry-8">Migration</a></li>
<li><a class="reference internal" href="#file-wizard-selection" id="toc-entry-9">File Wizard Selection</a></li>
<li><a class="reference internal" href="#usage" id="toc-entry-10">Usage</a><ul>
<li><a class="reference internal" href="#portal-functionality" id="toc-entry-11">Portal functionality</a></li>
</ul>
</li>
<li><a class="reference internal" href="#known-issues-roadmap" id="toc-entry-12">Known issues / Roadmap</a></li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-13">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-14">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-15">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-16">Contributors</a></li>
<li><a class="reference internal" href="#other-credits" id="toc-entry-17">Other credits</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-18">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="installation">
<h1><a class="toc-backref" href="#toc-entry-1">Installation</a></h1>
<div class="section" id="preview">
<h2><a class="toc-backref" href="#toc-entry-2">Preview</a></h2>
<p><tt class="docutils literal"><span class="pre">python-magic</span></tt> library is recommended to be installed for having whole
support to get proper file types and file preview.</p>
</div>
</div>
<div class="section" id="configuration">
<h1><a class="toc-backref" href="#toc-entry-3">Configuration</a></h1>
</div>
<div class="section" id="configuration-1">
<h1><a class="toc-backref" href="#toc-entry-4">Configuration</a></h1>
<p>To configure this module, you need to:</p>
<div class="section" id="create-a-storage">
<h2><a class="toc-backref" href="#toc-entry-5">1. Create a storage</a></h2>
<ol class="arabic simple">
<li>Go to <em>Documents -&gt; Configuration -&gt; Storages</em>.</li>
<li>Create a new document storage. You can choose between three options
on <tt class="docutils literal">Save Type</tt>:<ul>
<li><tt class="docutils literal">Database</tt>: Store the files on the database as a field</li>
<li><tt class="docutils literal">Attachment</tt>: Store the files as attachments</li>
<li><tt class="docutils literal">File</tt>: Store the files on the file system</li>
</ul>
</li>
</ol>
</div>
<div class="section" id="create-an-access-group">
<h2><a class="toc-backref" href="#toc-entry-6">2. Create an access group</a></h2>
<ol class="arabic simple">
<li>Next, create an administrative access group. Go to <em>Configuration -&gt;
Access Groups</em>.<ul>
<li>Create a new group, name it appropriately, and turn on all three
permissions (Create, Write and Unlink. Read is implied and always
enabled).</li>
<li>Add any other top-level administrative users to the group if needed
(your user should already be there).</li>
<li>You can create other groups in here later for fine-grained access
control.</li>
</ul>
</li>
</ol>
</div>
<div class="section" id="create-a-directory">
<h2><a class="toc-backref" href="#toc-entry-7">3. Create a directory</a></h2>
<ol class="arabic simple">
<li>Afterward, go to <em>Documents -&gt; Directories</em>.</li>
<li>Create a new directory, mark it as root and select the previously
created setting.<ul>
<li>Select the <em>Groups</em> tab and add your administrative group created
above. If your directory was already created before the group, you
can also add it in the access groups (<em>Configuration -&gt; Access
Groups</em>).</li>
</ul>
</li>
<li>In the directory settings, you can also add other access groups
(created above) that will be able to:<ul>
<li>read</li>
<li>create</li>
<li>write</li>
<li>delete</li>
</ul>
</li>
</ol>
</div>
</div>
<div class="section" id="migration">
<h1><a class="toc-backref" href="#toc-entry-8">Migration</a></h1>
<p>If you need to modify the storage <tt class="docutils literal">Save Type</tt> you might want to
migrate the file data. To achieve it, you need to:</p>
<ol class="arabic simple">
<li>Go to <em>Documents -&gt; Configuration -&gt; Storage</em> and select the storage
you want to modify</li>
<li>Modify the save type</li>
<li>Press the button Migrate files if you want to migrate all the files
at once</li>
<li>Press the button Manual File Migration to specify files one by one</li>
</ol>
<p>You can check all the files that still need to be migrated from all
storages and migrate them manually on <em>Documents -&gt; Configuration -&gt;
Migration</em></p>
</div>
<div class="section" id="file-wizard-selection">
<h1><a class="toc-backref" href="#toc-entry-9">File Wizard Selection</a></h1>
<p>There is an action called <tt class="docutils literal">action_dms_file_wizard_selector</tt> to open a
wizard to list files in kanban view. This can be used (example
dms_attachment_link module) to add a button in kanban view with the
action we need.</p>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#toc-entry-10">Usage</a></h1>
<p>The best way to manage the documents is to switch to the Documents view.
Existing documents can be managed there and new documents can be
created.</p>
<div class="section" id="portal-functionality">
<h2><a class="toc-backref" href="#toc-entry-11">Portal functionality</a></h2>
<p>You can add any portal user to DMS access groups, and then allow that
group in directories, so they will see in the portal such directories
and their files. Another possibility is to click on “Share” button
inside a directory or a file for obtaining a tokenized link for single
access to that resource, no matter if logged or not.</p>
</div>
</div>
<div class="section" id="known-issues-roadmap">
<h1><a class="toc-backref" href="#toc-entry-12">Known issues / Roadmap</a></h1>
<ul class="simple">
<li>Files preview in portal</li>
<li>Allow to download folder in portal and create zip file with all
content</li>
<li>Save in cache own_root directories and update in every
create/write/unlink function</li>
<li>Add a migration procedure for converting an storage to attachment one
for populating existing records with attachments as folders</li>
<li>Add a link from attachment view in chatter to linked documents</li>
<li>If Inherit permissions from related record (the
inherit_access_from_parent_record field from storage) is changed when
directories already exist, inconsistencies may occur because groups
defined in the directories and subdirectories will still exist, all
groups in these directories should be removed before changing.</li>
<li>Since portal users can read <tt class="docutils literal">dms.storage</tt> records, if your module
extends this model to another storage backend that needs using
secrets, remember to forbid access to the secrets fields by other
means. It would be nice to be able to remove that rule at some point.</li>
<li>Searchpanel in files: Highlight items (shading) without records when
filtering something (by name for example).</li>
<li>Accessing the clipboard (for example copy share link of
file/directory) is limited to secure connections. It also happens in
any part of Odoo.</li>
</ul>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#toc-entry-13">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/dms/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/dms/issues/new?body=module:%20dms%0Aversion:%2017.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#toc-entry-14">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#toc-entry-15">Authors</a></h2>
<ul class="simple">
<li>MuK IT</li>
<li>Tecnativa</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#toc-entry-16">Contributors</a></h2>
<ul class="simple">
<li>Mathias Markl &lt;<a class="reference external" href="mailto:mathias.markl&#64;mukit.at">mathias.markl&#64;mukit.at</a>&gt;</li>
<li>Enric Tobella &lt;<a class="reference external" href="mailto:etobella&#64;creublanca.es">etobella&#64;creublanca.es</a>&gt;</li>
<li>Antoni Romera</li>
<li>Gelu Boros &lt;<a class="reference external" href="mailto:gelu.boros&#64;rgbconsulting.com">gelu.boros&#64;rgbconsulting.com</a>&gt;</li>
<li><a class="reference external" href="https://www.tecnativa.com">Tecnativa</a>:<ul>
<li>Víctor Martínez</li>
<li>Pedro M. Baeza</li>
<li>Jairo Llopis</li>
</ul>
</li>
<li><a class="reference external" href="https://www.elegosoft.com">Elego</a>:<ul>
<li>Yu Weng &lt;<a class="reference external" href="mailto:yweng&#64;elegosoft.com">yweng&#64;elegosoft.com</a>&gt;</li>
<li>Philip Witte &lt;<a class="reference external" href="mailto:phillip.witte&#64;elegosoft.com">phillip.witte&#64;elegosoft.com</a>&gt;</li>
<li>Khanh Bui &lt;<a class="reference external" href="mailto:khanh.bui&#64;mail.elegosoft.com">khanh.bui&#64;mail.elegosoft.com</a>&gt;</li>
</ul>
</li>
<li><a class="reference external" href="https://www.subteno.com">Subteno</a>:<ul>
<li>Timothée Vannier &lt;<a class="reference external" href="mailto:tva&#64;subteno.com">tva&#64;subteno.com</a>&gt;</li>
</ul>
</li>
</ul>
</div>
<div class="section" id="other-credits">
<h2><a class="toc-backref" href="#toc-entry-17">Other credits</a></h2>
<p>Some pictures are based on or inspired by:</p>
<ul class="simple">
<li><a class="reference external" href="https://www.flaticon.com/authors/roundicons">Roundicons</a></li>
<li><a class="reference external" href="https://www.flaticon.com/authors/smashicons">Smashicons</a></li>
<li><a class="reference external" href="https://github.com/EmojiTwo/emojitwo">EmojiOne</a> : Portal DMS icon</li>
<li><a class="reference external" href="https://github.com/primer/octicons/">GitHub Octicons</a> : The main
DMS icon</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#toc-entry-18">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org">
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
</a>
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/dms/tree/17.0/dms">OCA/dms</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,12 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1008_2)">
<path d="M54 3L5 22L6.5 32.5L9.5 40L10.5 39.5L12 35L15 32.5L32.5 23.5L38 16L59.5 6.5V4.5L58 5H57V3L54.5 3.5L54 3Z" fill="#FBDBD0"/>
<path d="M13 59L11 37L11.5 36L14 32.5L23 28.5L32.5 23L36 18.5L38 16L61 6.5L59.5 16L55.5 30.5L14 59H13Z" fill="#C1DBF6"/>
<path d="M61.3151 5.90296L61.3155 5.90333C61.6617 6.31166 61.7685 6.68018 61.7936 6.93821C61.8062 7.06846 61.7983 7.17296 61.7878 7.24284C61.7826 7.27776 61.7768 7.30393 61.7727 7.32019C61.7707 7.32832 61.769 7.33394 61.7681 7.33693L61.7679 7.33778L61.7679 7.338L61.7678 7.33821L61.7677 7.33857L61.7675 7.33911L61.7674 7.3394L61.7643 7.34837L61.7618 7.3593L56.5987 30.4221L12.3444 61.661L12.3341 61.6121L12.2974 61.639L2.62867 20.6111L2.62906 20.611L2.6254 20.599L2.62539 20.5989L2.62538 20.5989L2.62532 20.5987L2.62491 20.5973L2.62307 20.5911L2.61557 20.5652C2.60896 20.5421 2.59925 20.5073 2.58716 20.4619C2.56297 20.3711 2.52925 20.2375 2.4917 20.0688C2.41656 19.731 2.32628 19.2531 2.26611 18.6938C2.14534 17.5714 2.14795 16.1398 2.6216 14.8583L2.62161 14.8583C3.10122 13.5603 3.92967 12.6648 4.64144 12.0926C4.99711 11.8067 5.32236 11.6025 5.55779 11.4702C5.67544 11.4041 5.77044 11.3561 5.83533 11.3249C5.86776 11.3093 5.89265 11.2979 5.90905 11.2906L5.92532 11.2834L23.5018 5.60147L23.5027 5.60126L23.5256 5.59594C23.5464 5.59128 23.578 5.58455 23.6193 5.57674C23.7019 5.56113 23.8233 5.54128 23.975 5.52508C24.2788 5.49264 24.7018 5.47516 25.1779 5.5347C26.1281 5.65349 27.2829 6.07705 28.1371 7.29954L28.1373 7.29989C28.5139 7.83635 28.9079 8.55923 29.21 9.15349C29.3604 9.44951 29.487 9.71156 29.576 9.89952C29.6204 9.99348 29.6555 10.0689 29.6793 10.1206L29.7065 10.18L29.7134 10.1952L29.7151 10.199L29.7155 10.1999L29.7156 10.2001L29.7157 10.2001L29.7907 10.3666L29.9633 10.307L53.0293 2.34204L53.0293 2.34208L53.0331 2.34068L53.0331 2.34067L53.0332 2.34064L53.0333 2.3406L53.0336 2.34051L53.0365 2.33944L53.0504 2.33463C53.0631 2.33034 53.0824 2.324 53.1073 2.31637C53.1574 2.30109 53.2299 2.28078 53.3176 2.26136C53.4948 2.22217 53.7267 2.18829 53.9592 2.20261C54.1922 2.21697 54.41 2.27862 54.5774 2.41514C54.7403 2.54801 54.8788 2.77075 54.9212 3.15587L54.9497 3.41461L55.1924 3.32044L57.4985 2.42557L57.1831 4.58306L57.1319 4.93309L57.4582 4.79647L59.7852 3.82223L59.5263 5.50966L59.4722 5.86247L59.8013 5.72443L60.0975 5.60023C60.1011 5.59938 60.1055 5.59835 60.1107 5.59718C60.1287 5.59316 60.1559 5.58754 60.1906 5.58185C60.2603 5.57043 60.3582 5.55899 60.47 5.55899C60.7509 5.55899 61.0838 5.62869 61.3151 5.90296ZM6.59316 22.903L6.43985 22.9625L6.46857 23.1244L9.35108 39.3709L9.74667 39.313L8.139 25.4484L56.4503 5.21947L56.5773 5.16629L56.5729 5.02866L56.5344 3.81316L56.5255 3.53088L56.2622 3.63304L56.3345 3.8195C56.2622 3.63304 56.2621 3.63306 56.262 3.6331L56.2615 3.6333L56.2594 3.63411L56.2512 3.63731L56.2184 3.65L56.0898 3.6999L55.5928 3.89271L53.7456 4.60938L47.4818 7.03952L31.4277 13.268L15.3735 19.4965L9.1097 21.9267L7.26248 22.6434L6.76551 22.8362L6.63688 22.8861L6.60417 22.8988L6.59592 22.902L6.59385 22.9028L6.59333 22.903C6.59322 22.903 6.59316 22.903 6.6655 23.0895L6.59316 22.903ZM10.0533 37.282L10.4476 37.3005C10.6223 36.4776 10.9443 35.3837 11.5322 34.4093C12.1129 33.4531 12.9088 32.6731 13.5645 32.1301C13.8915 31.8593 14.1818 31.6489 14.3898 31.5065C14.4938 31.4354 14.5771 31.3813 14.634 31.3452C14.6625 31.3272 14.6843 31.3137 14.6989 31.3048L14.7136 31.2959L14.7164 31.2945L14.7476 31.2792L14.8695 31.2195L15.3351 30.9916C15.7377 30.7944 16.3162 30.5109 17.0202 30.1657C18.428 29.4753 20.3374 28.5381 22.3435 27.5507C26.3543 25.5764 30.7553 23.3997 32.3042 22.5955L32.3042 22.5954C33.0514 22.2071 33.6925 21.553 34.2321 20.8061C34.7728 20.0577 35.2199 19.2046 35.5753 18.4033C35.931 17.6014 36.1969 16.8467 36.3738 16.2927C36.4623 16.0156 36.5287 15.7883 36.573 15.6299C36.5902 15.5686 36.604 15.5176 36.6146 15.478L58.7886 6.15034L58.8918 6.10692L58.9087 5.99623L59.0557 5.03523L59.1096 4.68281L58.7808 4.82051L8.85876 25.7245L8.72001 25.7826L8.73733 25.932L10.0533 37.282ZM55.2191 29.5986L55.2823 29.5541L55.2992 29.4787L60.2242 7.47768L60.3089 7.09927L59.9514 7.24964L37.9284 16.5146L37.8428 16.5507L37.8151 16.6395C37.5248 17.5703 36.9978 19.0345 36.1989 20.4436C35.3979 21.8565 34.3365 23.1907 32.9878 23.8915C30.0006 25.4431 16.7408 31.9355 15.4411 32.5713L15.4313 32.5762L15.422 32.582C15.1367 32.7628 13.6545 33.7473 12.795 35.1643L12.7948 35.1645C12.0305 36.4292 11.799 38.0005 11.7434 38.4736L11.7407 38.4963L11.7432 38.5189L13.9182 58.2819L13.9552 58.6183L14.2321 58.4236L55.2191 29.5986Z" fill="white" stroke="#374874" stroke-width="0.4"/>
</g>
<defs>
<clipPath id="clip0_1008_2">
<rect width="64" height="64" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

1
dms/static/icons/file_ai.svg Executable file
View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><defs><clipPath id="_clipPath_DIUxPpL1A4o6jIKxrY3KVZ5F1CHA2mKL"><rect width="256" height="256"/></clipPath></defs><g clip-path="url(#_clipPath_DIUxPpL1A4o6jIKxrY3KVZ5F1CHA2mKL)"><rect width="256" height="256" style="fill:rgb(0,0,0)" fill-opacity="0"/><g><g><path d=" M 156.239 40 L 65.027 40 C 62.487 40 60.429 42.059 60.429 46.053 L 60.429 212.857 C 60.429 213.941 62.487 216 65.027 216 L 190.973 216 C 193.513 216 195.571 213.941 195.571 212.857 L 195.571 80.788 C 195.571 78.601 195.279 77.897 194.764 77.378 L 158.193 40.808 C 157.675 40.292 156.971 40 156.239 40 Z " fill="rgb(233,233,224)"/><path d=" M 190.973 216 L 65.027 216 C 62.487 216 60.429 213.941 60.429 211.402 L 60.429 162.571 L 195.571 162.571 L 195.571 211.402 C 195.571 213.941 193.513 216 190.973 216 Z " fill="rgb(255,193,79)"/><path d=" M 128.095 203.746 L 126.023 197.594 L 115.236 197.594 L 113.186 203.746 L 106.644 203.746 L 117.759 173.887 L 123.46 173.887 L 134.637 203.746 L 128.095 203.746 L 128.095 203.746 Z M 120.609 181.434 L 116.897 192.61 L 124.362 192.61 L 120.609 181.434 L 120.609 181.434 Z M 143.968 173.887 L 143.968 203.746 L 137.815 203.746 L 137.815 173.887 L 143.968 173.887 L 143.968 173.887 Z " fill-rule="evenodd" fill="rgb(255,255,255)"/><path d=" M 157.857 74.571 L 157.857 76.02 C 148.205 79.063 140.562 86.649 137.422 96.26 C 135.263 94.516 132.557 93.429 129.571 93.429 C 123.729 93.429 118.851 97.451 117.446 102.857 L 107.571 102.857 L 107.571 96.571 L 88.714 96.571 L 88.714 115.429 L 107.571 115.429 L 107.571 109.143 L 116.752 109.143 C 115.696 117.512 110.513 124.788 102.958 128.512 C 100.858 124.493 96.697 121.714 91.857 121.714 C 84.924 121.714 79.286 127.353 79.286 134.286 C 79.286 141.219 84.924 146.857 91.857 146.857 C 98.602 146.857 104.077 141.508 104.372 134.836 C 112.716 131.218 118.977 124.191 121.733 115.749 C 123.889 117.487 126.592 118.571 129.571 118.571 C 135.414 118.571 140.292 114.549 141.697 109.143 L 151.571 109.143 L 151.571 115.429 L 170.429 115.429 L 170.429 96.571 L 151.571 96.571 L 151.571 102.857 L 142.36 102.857 C 143.551 93.369 150.041 85.527 158.772 82.372 C 160.632 87.001 165.142 90.286 170.429 90.286 C 177.362 90.286 183 84.647 183 77.714 L 183 74.571 L 157.857 74.571 Z M 101.286 109.143 L 95 109.143 L 95 102.857 L 101.286 102.857 L 101.286 109.143 Z M 91.857 140.571 C 88.391 140.571 85.571 137.752 85.571 134.286 C 85.571 130.819 88.391 128 91.857 128 C 95.324 128 98.143 130.819 98.143 134.286 C 98.143 137.752 95.324 140.571 91.857 140.571 Z M 157.857 102.857 L 164.143 102.857 L 164.143 109.143 L 157.857 109.143 L 157.857 102.857 Z M 129.571 112.286 C 126.105 112.286 123.286 109.467 123.286 106 C 123.286 102.533 126.105 99.714 129.571 99.714 C 133.038 99.714 135.857 102.533 135.857 106 C 135.857 109.467 133.038 112.286 129.571 112.286 Z M 170.429 84 C 168.156 84 166.176 82.777 165.073 80.97 C 165.802 80.904 166.538 80.857 167.286 80.857 L 175.869 80.857 C 174.785 82.733 172.751 84 170.429 84 Z " fill="rgb(200,189,184)"/><path d=" M 157.857 40.475 L 157.857 77.714 L 195.097 77.714 L 157.857 40.475 Z " fill="rgb(217,215,202)"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

1
dms/static/icons/file_aj.svg Executable file
View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><defs><clipPath id="_clipPath_6nAp4GoljLPqc74aMxxhpy3ZhrUJqOMK"><rect width="256" height="256"/></clipPath></defs><g clip-path="url(#_clipPath_6nAp4GoljLPqc74aMxxhpy3ZhrUJqOMK)"><rect width="256" height="256" style="fill:rgb(0,0,0)" fill-opacity="0"/><g><g><path d=" M 156.239 40 L 65.027 40 C 62.487 40 60.429 42.059 60.429 46.053 L 60.429 212.857 C 60.429 213.941 62.487 216 65.027 216 L 190.973 216 C 193.513 216 195.571 213.941 195.571 212.857 L 195.571 80.788 C 195.571 78.601 195.279 77.897 194.764 77.378 L 158.193 40.808 C 157.675 40.292 156.971 40 156.239 40 Z " fill="rgb(233,233,224)"/><path d=" M 157.857 40.475 L 157.857 77.714 L 195.097 77.714 L 157.857 40.475 Z " fill="rgb(217,215,202)"/><path d=" M 190.973 216 L 65.027 216 C 62.487 216 60.429 213.941 60.429 211.402 L 60.429 162.571 L 195.571 162.571 L 195.571 211.402 C 195.571 213.941 193.513 216 190.973 216 Z " fill="rgb(93,93,93)"/><g><path d=" M 112.936 91.207 C 111.707 89.978 109.721 89.978 108.492 91.207 L 89.635 110.064 C 88.406 111.293 88.406 113.279 89.635 114.508 L 108.492 133.365 C 109.105 133.978 109.91 134.286 110.714 134.286 C 111.519 134.286 112.323 133.978 112.936 133.365 C 114.165 132.136 114.165 130.15 112.936 128.921 L 96.301 112.286 L 112.936 95.651 C 114.165 94.422 114.165 92.435 112.936 91.207 Z " fill="rgb(93,93,93)"/><path d=" M 169.508 110.064 L 161.722 102.278 L 150.651 91.207 C 149.422 89.978 147.435 89.978 146.207 91.207 C 144.978 92.435 144.978 94.422 146.207 95.651 L 162.842 112.286 L 146.207 128.921 C 144.978 130.15 144.978 132.136 146.207 133.365 C 146.819 133.978 147.624 134.286 148.429 134.286 C 149.233 134.286 150.038 133.978 150.651 133.365 L 169.508 114.508 C 170.737 113.279 170.737 111.293 169.508 110.064 Z " fill="rgb(93,93,93)"/></g></g></g><path d=" M 122.595 203.746 L 120.523 197.594 L 109.736 197.594 L 107.686 203.746 L 101.144 203.746 L 112.259 173.887 L 117.96 173.887 L 129.137 203.746 L 122.595 203.746 L 122.595 203.746 Z M 115.109 181.434 L 111.397 192.61 L 118.862 192.61 L 115.109 181.434 L 115.109 181.434 Z M 143.964 194.559 L 143.964 173.887 L 150.116 173.887 L 150.116 194.559 L 150.116 194.559 Q 150.116 197.409 148.855 199.593 L 148.855 199.593 L 148.855 199.593 Q 147.594 201.777 145.297 202.967 L 145.297 202.967 L 145.297 202.967 Q 143 204.156 140.108 204.156 L 140.108 204.156 L 140.108 204.156 Q 135.371 204.156 132.726 201.747 L 132.726 201.747 L 132.726 201.747 Q 130.08 199.337 130.08 194.928 L 130.08 194.928 L 136.273 194.928 L 136.273 194.928 Q 136.273 197.122 137.196 198.168 L 137.196 198.168 L 137.196 198.168 Q 138.119 199.214 140.108 199.214 L 140.108 199.214 L 140.108 199.214 Q 141.872 199.214 142.918 198.004 L 142.918 198.004 L 142.918 198.004 Q 143.964 196.794 143.964 194.559 L 143.964 194.559 L 143.964 194.559 Z " fill-rule="evenodd" fill="rgb(255,255,255)"/></g></svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

1
dms/static/icons/file_avi.svg Executable file
View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><defs><clipPath id="_clipPath_FLHZ43wzdKFmTHD21bIUvTuw3Pzi3Qlw"><rect width="256" height="256"/></clipPath></defs><g clip-path="url(#_clipPath_FLHZ43wzdKFmTHD21bIUvTuw3Pzi3Qlw)"><rect width="256" height="256" style="fill:rgb(0,0,0)" fill-opacity="0"/><g><g><path d=" M 156.239 40 L 65.027 40 C 62.487 40 60.429 42.059 60.429 46.053 L 60.429 212.857 C 60.429 213.941 62.487 216 65.027 216 L 190.973 216 C 193.513 216 195.571 213.941 195.571 212.857 L 195.571 80.788 C 195.571 78.601 195.279 77.897 194.764 77.378 L 158.193 40.808 C 157.675 40.292 156.971 40 156.239 40 Z " fill="rgb(233,233,224)"/><path d=" M 157.857 40.475 L 157.857 77.714 L 195.097 77.714 L 157.857 40.475 Z " fill="rgb(217,215,202)"/><path d=" M 190.973 216 L 65.027 216 C 62.487 216 60.429 213.941 60.429 211.402 L 60.429 162.571 L 195.571 162.571 L 195.571 211.402 C 195.571 213.941 193.513 216 190.973 216 Z " fill="rgb(215,94,114)"/><path d=" M 114.595 203.746 L 112.523 197.594 L 101.736 197.594 L 99.686 203.746 L 93.144 203.746 L 104.259 173.887 L 109.96 173.887 L 121.137 203.746 L 114.595 203.746 L 114.595 203.746 Z M 107.109 181.434 L 103.397 192.61 L 110.862 192.61 L 107.109 181.434 L 107.109 181.434 Z M 128.232 173.887 L 134.959 196.343 L 141.727 173.887 L 148.576 173.887 L 138.179 203.746 L 131.76 203.746 L 121.403 173.887 L 128.232 173.887 L 128.232 173.887 Z M 157.928 173.887 L 157.928 203.746 L 151.775 203.746 L 151.775 173.887 L 157.928 173.887 L 157.928 173.887 Z " fill-rule="evenodd" fill="rgb(255,255,255)"/><path d=" M 113.857 128 L 113.857 105.855 L 113.857 84 L 148.429 106 L 113.857 128 Z " fill="rgb(200,189,184)"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

1
dms/static/icons/file_c.svg Executable file
View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><defs><clipPath id="_clipPath_QDq7UHuMAP1DndjZvSJ7oBHBPdwuwQhy"><rect width="256" height="256"/></clipPath></defs><g clip-path="url(#_clipPath_QDq7UHuMAP1DndjZvSJ7oBHBPdwuwQhy)"><rect width="256" height="256" style="fill:rgb(0,0,0)" fill-opacity="0"/><g><g><path d=" M 156.239 40 L 65.027 40 C 62.487 40 60.429 42.059 60.429 46.053 L 60.429 212.857 C 60.429 213.941 62.487 216 65.027 216 L 190.973 216 C 193.513 216 195.571 213.941 195.571 212.857 L 195.571 80.788 C 195.571 78.601 195.279 77.897 194.764 77.378 L 158.193 40.808 C 157.675 40.292 156.971 40 156.239 40 Z " fill="rgb(233,233,224)"/><path d=" M 157.857 40.475 L 157.857 77.714 L 195.097 77.714 L 157.857 40.475 Z " fill="rgb(217,215,202)"/><path d=" M 190.973 216 L 65.027 216 C 62.487 216 60.429 213.941 60.429 211.402 L 60.429 162.571 L 195.571 162.571 L 195.571 211.402 C 195.571 213.941 193.513 216 190.973 216 Z " fill="rgb(128,140,155)"/></g></g><path d=" M 132.954 195.8 L 139.106 195.8 L 139.106 195.8 Q 138.758 200.619 135.548 203.388 L 135.548 203.388 L 135.548 203.388 Q 132.339 206.156 127.089 206.156 L 127.089 206.156 L 127.089 206.156 Q 121.347 206.156 118.055 202.291 L 118.055 202.291 L 118.055 202.291 Q 114.764 198.425 114.764 191.678 L 114.764 191.678 L 114.764 189.853 L 114.764 189.853 Q 114.764 185.546 116.281 182.265 L 116.281 182.265 L 116.281 182.265 Q 117.799 178.983 120.619 177.23 L 120.619 177.23 L 120.619 177.23 Q 123.438 175.477 127.171 175.477 L 127.171 175.477 L 127.171 175.477 Q 132.339 175.477 135.497 178.245 L 135.497 178.245 L 135.497 178.245 Q 138.655 181.014 139.147 186.018 L 139.147 186.018 L 132.995 186.018 L 132.995 186.018 Q 132.77 183.126 131.385 181.824 L 131.385 181.824 L 131.385 181.824 Q 130.001 180.521 127.171 180.521 L 127.171 180.521 L 127.171 180.521 Q 124.095 180.521 122.567 182.726 L 122.567 182.726 L 122.567 182.726 Q 121.039 184.931 120.998 189.565 L 120.998 189.565 L 120.998 191.821 L 120.998 191.821 Q 120.998 196.661 122.464 198.896 L 122.464 198.896 L 122.464 198.896 Q 123.931 201.132 127.089 201.132 L 127.089 201.132 L 127.089 201.132 Q 129.939 201.132 131.344 199.83 L 131.344 199.83 L 131.344 199.83 Q 132.749 198.527 132.954 195.8 L 132.954 195.8 L 132.954 195.8 Z " fill="rgb(255,255,255)"/><path d=" M 131.94 106.301 L 131.94 106.301 L 131.94 106.301 Q 127.32 106.301 124.86 109.121 L 124.86 109.121 L 124.86 109.121 Q 122.4 111.941 122.4 117.101 L 122.4 117.101 L 122.4 117.101 Q 122.4 122.321 125.13 125.141 L 125.13 125.141 L 125.13 125.141 Q 127.86 127.961 132.48 127.961 L 132.48 127.961 L 132.48 127.961 Q 134.94 127.961 136.65 127.301 L 136.65 127.301 L 136.65 127.301 Q 138.36 126.641 139.92 125.801 L 139.92 125.801 L 139.92 125.801 Q 140.94 126.641 141.51 127.811 L 141.51 127.811 L 141.51 127.811 Q 142.08 128.981 142.08 130.541 L 142.08 130.541 L 142.08 130.541 Q 142.08 133.001 139.35 134.711 L 139.35 134.711 L 139.35 134.711 Q 136.62 136.421 131.04 136.421 L 131.04 136.421 L 131.04 136.421 Q 127.02 136.421 123.48 135.281 L 123.48 135.281 L 123.48 135.281 Q 119.94 134.141 117.3 131.771 L 117.3 131.771 L 117.3 131.771 Q 114.66 129.401 113.13 125.771 L 113.13 125.771 L 113.13 125.771 Q 111.6 122.141 111.6 117.101 L 111.6 117.101 L 111.6 117.101 Q 111.6 112.421 113.07 108.851 L 113.07 108.851 L 113.07 108.851 Q 114.54 105.281 117.09 102.821 L 117.09 102.821 L 117.09 102.821 Q 119.64 100.361 123.06 99.101 L 123.06 99.101 L 123.06 99.101 Q 126.48 97.841 130.38 97.841 L 130.38 97.841 L 130.38 97.841 Q 135.9 97.841 138.93 99.641 L 138.93 99.641 L 138.93 99.641 Q 141.96 101.441 141.96 104.261 L 141.96 104.261 L 141.96 104.261 Q 141.96 105.821 141.18 106.961 L 141.18 106.961 L 141.18 106.961 Q 140.4 108.101 139.38 108.761 L 139.38 108.761 L 139.38 108.761 Q 137.82 107.741 136.05 107.021 L 136.05 107.021 L 136.05 107.021 Q 134.28 106.301 131.94 106.301 Z " fill="rgb(128,140,155)"/></g></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

1
dms/static/icons/file_cbl.svg Executable file
View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><defs><clipPath id="_clipPath_TQmEGhsjtrLEyjf6zmhvf3bpAZU65XHf"><rect width="256" height="256"/></clipPath></defs><g clip-path="url(#_clipPath_TQmEGhsjtrLEyjf6zmhvf3bpAZU65XHf)"><rect width="256" height="256" style="fill:rgb(0,0,0)" fill-opacity="0"/><g><g><path d=" M 156.239 40 L 65.027 40 C 62.487 40 60.429 42.059 60.429 46.053 L 60.429 212.857 C 60.429 213.941 62.487 216 65.027 216 L 190.973 216 C 193.513 216 195.571 213.941 195.571 212.857 L 195.571 80.788 C 195.571 78.601 195.279 77.897 194.764 77.378 L 158.193 40.808 C 157.675 40.292 156.971 40 156.239 40 Z " fill="rgb(233,233,224)"/><path d=" M 157.857 40.475 L 157.857 77.714 L 195.097 77.714 L 157.857 40.475 Z " fill="rgb(217,215,202)"/><path d=" M 190.973 216 L 65.027 216 C 62.487 216 60.429 213.941 60.429 211.402 L 60.429 162.571 L 195.571 162.571 L 195.571 211.402 C 195.571 213.941 193.513 216 190.973 216 Z " fill="rgb(93,93,93)"/><g><path d=" M 112.936 91.207 C 111.707 89.978 109.721 89.978 108.492 91.207 L 89.635 110.064 C 88.406 111.293 88.406 113.279 89.635 114.508 L 108.492 133.365 C 109.105 133.978 109.91 134.286 110.714 134.286 C 111.519 134.286 112.323 133.978 112.936 133.365 C 114.165 132.136 114.165 130.15 112.936 128.921 L 96.301 112.286 L 112.936 95.651 C 114.165 94.422 114.165 92.435 112.936 91.207 Z " fill="rgb(93,93,93)"/><path d=" M 169.508 110.064 L 161.722 102.278 L 150.651 91.207 C 149.422 89.978 147.435 89.978 146.207 91.207 C 144.978 92.435 144.978 94.422 146.207 95.651 L 162.842 112.286 L 146.207 128.921 C 144.978 130.15 144.978 132.136 146.207 133.365 C 146.819 133.978 147.624 134.286 148.429 134.286 C 149.233 134.286 150.038 133.978 150.651 133.365 L 169.508 114.508 C 170.737 113.279 170.737 111.293 169.508 110.064 Z " fill="rgb(93,93,93)"/></g></g></g><path d=" M 107.954 195.8 L 114.106 195.8 L 114.106 195.8 Q 113.758 200.619 110.548 203.388 L 110.548 203.388 L 110.548 203.388 Q 107.339 206.156 102.089 206.156 L 102.089 206.156 L 102.089 206.156 Q 96.347 206.156 93.055 202.291 L 93.055 202.291 L 93.055 202.291 Q 89.764 198.425 89.764 191.678 L 89.764 191.678 L 89.764 189.853 L 89.764 189.853 Q 89.764 185.546 91.281 182.265 L 91.281 182.265 L 91.281 182.265 Q 92.799 178.983 95.619 177.23 L 95.619 177.23 L 95.619 177.23 Q 98.438 175.477 102.171 175.477 L 102.171 175.477 L 102.171 175.477 Q 107.339 175.477 110.497 178.245 L 110.497 178.245 L 110.497 178.245 Q 113.655 181.014 114.147 186.018 L 114.147 186.018 L 107.995 186.018 L 107.995 186.018 Q 107.77 183.126 106.385 181.824 L 106.385 181.824 L 106.385 181.824 Q 105.001 180.521 102.171 180.521 L 102.171 180.521 L 102.171 180.521 Q 99.095 180.521 97.567 182.726 L 97.567 182.726 L 97.567 182.726 Q 96.039 184.931 95.998 189.565 L 95.998 189.565 L 95.998 191.821 L 95.998 191.821 Q 95.998 196.661 97.464 198.896 L 97.464 198.896 L 97.464 198.896 Q 98.931 201.132 102.089 201.132 L 102.089 201.132 L 102.089 201.132 Q 104.939 201.132 106.344 199.83 L 106.344 199.83 L 106.344 199.83 Q 107.749 198.527 107.954 195.8 L 107.954 195.8 L 107.954 195.8 Z M 129.754 205.746 L 118.146 205.746 L 118.146 175.887 L 128.605 175.887 L 128.605 175.887 Q 134.04 175.887 136.85 177.968 L 136.85 177.968 L 136.85 177.968 Q 139.659 180.05 139.659 184.069 L 139.659 184.069 L 139.659 184.069 Q 139.659 186.264 138.531 187.935 L 138.531 187.935 L 138.531 187.935 Q 137.403 189.606 135.394 190.386 L 135.394 190.386 L 135.394 190.386 Q 137.69 190.96 139.013 192.703 L 139.013 192.703 L 139.013 192.703 Q 140.336 194.446 140.336 196.969 L 140.336 196.969 L 140.336 196.969 Q 140.336 201.275 137.588 203.49 L 137.588 203.49 L 137.588 203.49 Q 134.84 205.705 129.754 205.746 L 129.754 205.746 L 129.754 205.746 Z M 129.938 192.744 L 124.299 192.744 L 124.299 200.804 L 129.569 200.804 L 129.569 200.804 Q 131.743 200.804 132.963 199.768 L 132.963 199.768 L 132.963 199.768 Q 134.184 198.732 134.184 196.907 L 134.184 196.907 L 134.184 196.907 Q 134.184 192.806 129.938 192.744 L 129.938 192.744 L 129.938 192.744 Z M 124.299 180.87 L 124.299 188.396 L 128.852 188.396 L 128.852 188.396 Q 133.507 188.314 133.507 184.685 L 133.507 184.685 L 133.507 184.685 Q 133.507 182.654 132.328 181.762 L 132.328 181.762 L 132.328 181.762 Q 131.148 180.87 128.605 180.87 L 128.605 180.87 L 124.299 180.87 L 124.299 180.87 Z M 151.103 175.887 L 151.103 200.804 L 164.166 200.804 L 164.166 205.746 L 144.95 205.746 L 144.95 175.887 L 151.103 175.887 L 151.103 175.887 Z " fill-rule="evenodd" fill="rgb(255,255,255)"/></g></svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

1
dms/static/icons/file_cc.svg Executable file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.5 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.4 KiB

1
dms/static/icons/file_cs.svg Executable file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.2 KiB

1
dms/static/icons/file_css.svg Executable file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.5 KiB

1
dms/static/icons/file_csv.svg Executable file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.5 KiB

1
dms/static/icons/file_dbf.svg Executable file
View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><defs><clipPath id="_clipPath_AC8gC2Fy7eYKCxX4InFHVsjb6C7TwY0y"><rect width="256" height="256"/></clipPath></defs><g clip-path="url(#_clipPath_AC8gC2Fy7eYKCxX4InFHVsjb6C7TwY0y)"><rect width="256" height="256" style="fill:rgb(0,0,0)" fill-opacity="0"/><g><g><path d=" M 156.239 40 L 65.027 40 C 62.487 40 60.429 42.059 60.429 46.053 L 60.429 212.857 C 60.429 213.941 62.487 216 65.027 216 L 190.973 216 C 193.513 216 195.571 213.941 195.571 212.857 L 195.571 80.788 C 195.571 78.601 195.279 77.897 194.764 77.378 L 158.193 40.808 C 157.675 40.292 156.971 40 156.239 40 Z " fill="rgb(233,233,224)"/><path d=" M 157.857 40.475 L 157.857 77.714 L 195.097 77.714 L 157.857 40.475 Z " fill="rgb(217,215,202)"/><path d=" M 190.973 216 L 65.027 216 C 62.487 216 60.429 213.941 60.429 211.402 L 60.429 162.571 L 195.571 162.571 L 195.571 211.402 C 195.571 213.941 193.513 216 190.973 216 Z " fill="rgb(233,99,96)"/><path d=" M 117 90.286 L 117 77.714 L 79.286 77.714 L 79.286 90.286 L 79.286 96.571 L 79.286 102.857 L 79.286 109.143 L 79.286 115.429 L 79.286 121.714 L 79.286 128 L 79.286 134.286 L 79.286 146.857 L 110.714 146.857 L 117 146.857 L 183 146.857 L 183 134.286 L 183 128 L 183 121.714 L 183 115.429 L 183 109.143 L 183 102.857 L 183 90.286 L 117 90.286 Z M 85.571 84 L 110.714 84 L 110.714 90.286 L 85.571 90.286 L 85.571 84 Z M 85.571 96.571 L 110.714 96.571 L 110.714 102.857 L 85.571 102.857 L 85.571 96.571 Z M 85.571 109.143 L 110.714 109.143 L 110.714 115.429 L 85.571 115.429 L 85.571 109.143 Z M 85.571 121.714 L 110.714 121.714 L 110.714 128 L 85.571 128 L 85.571 121.714 Z M 110.714 140.571 L 85.571 140.571 L 85.571 134.286 L 110.714 134.286 L 110.714 140.571 Z M 176.714 140.571 L 117 140.571 L 117 134.286 L 176.714 134.286 L 176.714 140.571 Z M 176.714 128 L 117 128 L 117 121.714 L 176.714 121.714 L 176.714 128 Z M 176.714 115.429 L 117 115.429 L 117 109.143 L 176.714 109.143 L 176.714 115.429 Z M 117 102.857 L 117 96.571 L 176.714 96.571 L 176.714 102.857 L 117 102.857 Z " fill="rgb(200,189,184)"/></g></g><path d=" M 99.915 205.746 L 90.666 205.746 L 90.666 175.887 L 99.854 175.887 L 99.854 175.887 Q 103.791 175.887 106.898 177.661 L 106.898 177.661 L 106.898 177.661 Q 110.005 179.435 111.748 182.706 L 111.748 182.706 L 111.748 182.706 Q 113.491 185.977 113.491 190.14 L 113.491 190.14 L 113.491 191.514 L 113.491 191.514 Q 113.491 195.677 111.779 198.917 L 111.779 198.917 L 111.779 198.917 Q 110.066 202.157 106.949 203.941 L 106.949 203.941 L 106.949 203.941 Q 103.832 205.726 99.915 205.746 L 99.915 205.746 L 99.915 205.746 Z M 99.854 180.87 L 96.818 180.87 L 96.818 200.804 L 99.792 200.804 L 99.792 200.804 Q 103.401 200.804 105.309 198.445 L 105.309 198.445 L 105.309 198.445 Q 107.216 196.087 107.257 191.698 L 107.257 191.698 L 107.257 190.119 L 107.257 190.119 Q 107.257 185.566 105.37 183.218 L 105.37 183.218 L 105.37 183.218 Q 103.483 180.87 99.854 180.87 L 99.854 180.87 L 99.854 180.87 Z M 129.569 205.746 L 117.962 205.746 L 117.962 175.887 L 128.421 175.887 L 128.421 175.887 Q 133.855 175.887 136.665 177.968 L 136.665 177.968 L 136.665 177.968 Q 139.475 180.05 139.475 184.069 L 139.475 184.069 L 139.475 184.069 Q 139.475 186.264 138.347 187.935 L 138.347 187.935 L 138.347 187.935 Q 137.219 189.606 135.209 190.386 L 135.209 190.386 L 135.209 190.386 Q 137.506 190.96 138.829 192.703 L 138.829 192.703 L 138.829 192.703 Q 140.151 194.446 140.151 196.969 L 140.151 196.969 L 140.151 196.969 Q 140.151 201.275 137.403 203.49 L 137.403 203.49 L 137.403 203.49 Q 134.655 205.705 129.569 205.746 L 129.569 205.746 L 129.569 205.746 Z M 129.754 192.744 L 124.114 192.744 L 124.114 200.804 L 129.385 200.804 L 129.385 200.804 Q 131.559 200.804 132.779 199.768 L 132.779 199.768 L 132.779 199.768 Q 133.999 198.732 133.999 196.907 L 133.999 196.907 L 133.999 196.907 Q 133.999 192.806 129.754 192.744 L 129.754 192.744 L 129.754 192.744 Z M 124.114 180.87 L 124.114 188.396 L 128.667 188.396 L 128.667 188.396 Q 133.322 188.314 133.322 184.685 L 133.322 184.685 L 133.322 184.685 Q 133.322 182.654 132.143 181.762 L 132.143 181.762 L 132.143 181.762 Q 130.964 180.87 128.421 180.87 L 128.421 180.87 L 124.114 180.87 L 124.114 180.87 Z M 162.73 188.581 L 162.73 193.544 L 150.918 193.544 L 150.918 205.746 L 144.766 205.746 L 144.766 175.887 L 164.207 175.887 L 164.207 180.87 L 150.918 180.87 L 150.918 188.581 L 162.73 188.581 L 162.73 188.581 Z " fill-rule="evenodd" fill="rgb(255,255,255)"/></g></svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

1
dms/static/icons/file_dll.svg Executable file
View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><defs><clipPath id="_clipPath_JRbDI2Nk1iQLWnj5TvFr9peayMg5GuJ2"><rect width="256" height="256"/></clipPath></defs><g clip-path="url(#_clipPath_JRbDI2Nk1iQLWnj5TvFr9peayMg5GuJ2)"><rect width="256" height="256" style="fill:rgb(0,0,0)" fill-opacity="0"/><g><g><path d=" M 156.239 38 L 65.027 38 C 62.487 38 60.429 40.059 60.429 44.053 L 60.429 210.857 C 60.429 211.941 62.487 214 65.027 214 L 190.973 214 C 193.513 214 195.571 211.941 195.571 210.857 L 195.571 78.788 C 195.571 76.601 195.279 75.897 194.764 75.378 L 158.193 38.808 C 157.675 38.292 156.971 38 156.239 38 Z " fill="rgb(233,233,224)"/><path d=" M 190.973 214 L 65.027 214 C 62.487 214 60.429 211.941 60.429 209.402 L 60.429 160.571 L 195.571 160.571 L 195.571 209.402 C 195.571 211.941 193.513 214 190.973 214 Z " fill="rgb(62,71,83)"/><path d=" M 157.857 38.475 L 157.857 75.714 L 195.097 75.714 L 157.857 38.475 Z " fill="rgb(217,215,202)"/></g></g><path d=" M 102.415 203.746 L 93.166 203.746 L 93.166 173.887 L 102.354 173.887 L 102.354 173.887 Q 106.291 173.887 109.398 175.661 L 109.398 175.661 L 109.398 175.661 Q 112.505 177.435 114.248 180.706 L 114.248 180.706 L 114.248 180.706 Q 115.991 183.977 115.991 188.14 L 115.991 188.14 L 115.991 189.514 L 115.991 189.514 Q 115.991 193.677 114.279 196.917 L 114.279 196.917 L 114.279 196.917 Q 112.566 200.157 109.449 201.941 L 109.449 201.941 L 109.449 201.941 Q 106.332 203.726 102.415 203.746 L 102.415 203.746 L 102.415 203.746 Z M 102.354 178.87 L 99.318 178.87 L 99.318 198.804 L 102.292 198.804 L 102.292 198.804 Q 105.901 198.804 107.809 196.445 L 107.809 196.445 L 107.809 196.445 Q 109.716 194.087 109.757 189.698 L 109.757 189.698 L 109.757 188.119 L 109.757 188.119 Q 109.757 183.566 107.87 181.218 L 107.87 181.218 L 107.87 181.218 Q 105.983 178.87 102.354 178.87 L 102.354 178.87 L 102.354 178.87 Z M 126.614 173.887 L 126.614 198.804 L 139.678 198.804 L 139.678 203.746 L 120.462 203.746 L 120.462 173.887 L 126.614 173.887 L 126.614 173.887 Z M 149.357 173.887 L 149.357 198.804 L 162.421 198.804 L 162.421 203.746 L 143.205 203.746 L 143.205 173.887 L 149.357 173.887 L 149.357 173.887 Z " fill-rule="evenodd" fill="rgb(255,255,255)"/><g><g><g><g><path d=" M 128 98.25 C 122.625 98.25 118.25 102.625 118.25 108 C 118.25 113.375 122.625 117.75 128 117.75 C 133.375 117.75 137.75 113.375 137.75 108 C 137.75 102.625 133.375 98.25 128 98.25 Z " fill="rgb(200,189,184)"/><path d=" M 164.364 101.37 L 160.061 100.541 C 158.134 100.174 156.623 98.913 155.905 97.093 C 155.186 95.263 155.44 93.31 156.603 91.727 L 159.304 88.042 C 160.253 86.752 160.113 84.958 158.979 83.827 L 153.146 77.993 C 152.047 76.891 150.312 76.729 149.028 77.6 L 145.398 80.053 C 143.779 81.155 141.823 81.334 140.019 80.554 C 138.222 79.767 137.016 78.207 136.72 76.267 L 136.025 71.756 C 135.78 70.17 134.415 69 132.81 69 L 124.561 69 C 123.005 69 121.666 70.105 121.37 71.636 L 120.301 77.193 C 119.94 79.075 118.718 80.576 116.947 81.308 C 115.175 82.046 113.245 81.847 111.659 80.771 L 106.972 77.6 C 105.689 76.729 103.96 76.891 102.855 77.993 L 97.021 83.827 C 95.887 84.958 95.747 86.752 96.696 88.042 L 99.397 91.73 C 100.56 93.31 100.814 95.263 100.096 97.093 C 99.377 98.913 97.866 100.174 95.936 100.541 L 91.636 101.37 C 90.105 101.666 89 103.005 89 104.561 L 89 112.81 C 89 114.415 90.17 115.78 91.756 116.024 L 96.267 116.72 C 98.207 117.015 99.767 118.221 100.554 120.018 C 101.337 121.816 101.155 123.775 100.053 125.4 L 97.6 129.027 C 96.725 130.314 96.891 132.043 97.993 133.145 L 103.827 138.979 C 104.961 140.116 106.752 140.246 108.042 139.304 L 111.73 136.603 C 113.31 135.443 115.26 135.192 117.093 135.904 C 118.913 136.622 120.174 138.134 120.541 140.064 L 121.37 144.364 C 121.666 145.895 123.005 147 124.561 147 L 132.81 147 C 134.415 147 135.78 145.83 136.024 144.244 L 136.512 141.072 C 136.817 139.086 138.055 137.51 139.908 136.746 C 141.747 135.979 143.746 136.217 145.368 137.406 L 147.958 139.304 C 149.242 140.247 151.039 140.117 152.173 138.979 L 158.007 133.145 C 159.109 132.044 159.275 130.315 158.4 129.028 L 155.947 125.398 C 154.845 123.776 154.663 121.816 155.446 120.019 C 156.233 118.222 157.793 117.016 159.733 116.72 L 164.244 116.025 C 165.83 115.781 167 114.416 167 112.81 L 167 104.562 C 167 103.005 165.895 101.666 164.364 101.37 Z M 128 124.25 C 119.04 124.25 111.75 116.96 111.75 108 C 111.75 99.04 119.04 91.75 128 91.75 C 136.96 91.75 144.25 99.04 144.25 108 C 144.25 116.96 136.96 124.25 128 124.25 Z " fill="rgb(200,189,184)"/></g></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

1
dms/static/icons/file_doc.svg Executable file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.1 KiB

1
dms/static/icons/file_docx.svg Executable file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.3 KiB

1
dms/static/icons/file_dwg.svg Executable file
View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><defs><clipPath id="_clipPath_D4Sj8YJrR0Fd21hr8GyZRcCPEVvEhAsA"><rect width="256" height="256"/></clipPath></defs><g clip-path="url(#_clipPath_D4Sj8YJrR0Fd21hr8GyZRcCPEVvEhAsA)"><rect width="256" height="256" style="fill:rgb(0,0,0)" fill-opacity="0"/><g><g><path d=" M 156.239 40 L 65.027 40 C 62.487 40 60.429 42.059 60.429 46.053 L 60.429 212.857 C 60.429 213.941 62.487 216 65.027 216 L 190.973 216 C 193.513 216 195.571 213.941 195.571 212.857 L 195.571 80.788 C 195.571 78.601 195.279 77.897 194.764 77.378 L 158.193 40.808 C 157.675 40.292 156.971 40 156.239 40 Z " fill="rgb(233,233,224)"/><path d=" M 157.857 40.475 L 157.857 77.714 L 195.097 77.714 L 157.857 40.475 Z " fill="rgb(217,215,202)"/><path d=" M 190.973 216 L 65.027 216 C 62.487 216 60.429 213.941 60.429 211.402 L 60.429 162.571 L 195.571 162.571 L 195.571 211.402 C 195.571 213.941 193.513 216 190.973 216 Z " fill="rgb(134,151,203)"/><rect x="79.286" y="80.857" width="47.143" height="47.347" transform="matrix(1,0,0,1,0,0)" fill="rgb(200,189,184)"/><rect x="148.429" y="84" width="6.286" height="22" transform="matrix(1,0,0,1,0,0)" fill="rgb(173,162,158)"/><rect x="148.429" y="131.143" width="6.286" height="22" transform="matrix(1,0,0,1,0,0)" fill="rgb(173,162,158)"/><path d=" M 167.286 134.286 L 135.857 134.286 L 135.857 102.857 L 167.286 102.857 L 167.286 134.286 Z M 142.143 128 L 161 128 L 161 109.143 L 142.143 109.143 L 142.143 128 Z " fill="rgb(173,162,158)"/><rect x="164.143" y="115.429" width="22" height="6.286" transform="matrix(1,0,0,1,0,0)" fill="rgb(173,162,158)"/><rect x="117" y="115.429" width="22" height="6.286" transform="matrix(1,0,0,1,0,0)" fill="rgb(173,162,158)"/></g></g><path d=" M 92.415 203.746 L 83.166 203.746 L 83.166 173.887 L 92.354 173.887 L 92.354 173.887 Q 96.291 173.887 99.398 175.661 L 99.398 175.661 L 99.398 175.661 Q 102.505 177.435 104.248 180.706 L 104.248 180.706 L 104.248 180.706 Q 105.991 183.977 105.991 188.14 L 105.991 188.14 L 105.991 189.514 L 105.991 189.514 Q 105.991 193.677 104.279 196.917 L 104.279 196.917 L 104.279 196.917 Q 102.566 200.157 99.449 201.941 L 99.449 201.941 L 99.449 201.941 Q 96.332 203.726 92.415 203.746 L 92.415 203.746 L 92.415 203.746 Z M 92.354 178.87 L 89.318 178.87 L 89.318 198.804 L 92.292 198.804 L 92.292 198.804 Q 95.901 198.804 97.809 196.445 L 97.809 196.445 L 97.809 196.445 Q 99.716 194.087 99.757 189.698 L 99.757 189.698 L 99.757 188.119 L 99.757 188.119 Q 99.757 183.566 97.87 181.218 L 97.87 181.218 L 97.87 181.218 Q 95.983 178.87 92.354 178.87 L 92.354 178.87 L 92.354 178.87 Z M 128.816 173.887 L 133.718 195.133 L 137.737 173.887 L 143.869 173.887 L 137.245 203.746 L 131.052 203.746 L 126.191 183.771 L 121.331 203.746 L 115.138 203.746 L 108.514 173.887 L 114.646 173.887 L 118.686 195.092 L 123.607 173.887 L 128.816 173.887 L 128.816 173.887 Z M 170.816 188.058 L 170.816 199.973 L 170.816 199.973 Q 169.155 201.962 166.12 203.059 L 166.12 203.059 L 166.12 203.059 Q 163.085 204.156 159.394 204.156 L 159.394 204.156 L 159.394 204.156 Q 155.518 204.156 152.595 202.464 L 152.595 202.464 L 152.595 202.464 Q 149.673 200.772 148.083 197.553 L 148.083 197.553 L 148.083 197.553 Q 146.494 194.333 146.453 189.985 L 146.453 189.985 L 146.453 187.955 L 146.453 187.955 Q 146.453 183.484 147.96 180.213 L 147.96 180.213 L 147.96 180.213 Q 149.468 176.942 152.308 175.209 L 152.308 175.209 L 152.308 175.209 Q 155.148 173.477 158.963 173.477 L 158.963 173.477 L 158.963 173.477 Q 164.274 173.477 167.269 176.009 L 167.269 176.009 L 167.269 176.009 Q 170.263 178.542 170.816 183.382 L 170.816 183.382 L 164.828 183.382 L 164.828 183.382 Q 164.418 180.818 163.013 179.629 L 163.013 179.629 L 163.013 179.629 Q 161.608 178.439 159.147 178.439 L 159.147 178.439 L 159.147 178.439 Q 156.01 178.439 154.369 180.798 L 154.369 180.798 L 154.369 180.798 Q 152.729 183.156 152.708 187.812 L 152.708 187.812 L 152.708 189.719 L 152.708 189.719 Q 152.708 194.415 154.492 196.814 L 154.492 196.814 L 154.492 196.814 Q 156.276 199.214 159.722 199.214 L 159.722 199.214 L 159.722 199.214 Q 163.188 199.214 164.664 197.737 L 164.664 197.737 L 164.664 192.59 L 159.065 192.59 L 159.065 188.058 L 170.816 188.058 L 170.816 188.058 Z " fill-rule="evenodd" fill="rgb(255,255,255)"/></g></svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

1
dms/static/icons/file_eml.svg Executable file
View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><defs><clipPath id="_clipPath_eMGLtDmIvhgzWzMJn5Zi3pkjzRQYkoFv"><rect width="256" height="256"/></clipPath></defs><g clip-path="url(#_clipPath_eMGLtDmIvhgzWzMJn5Zi3pkjzRQYkoFv)"><rect width="256" height="256" style="fill:rgb(0,0,0)" fill-opacity="0"/><path d=" M 156.239 40 L 65.027 40 C 62.487 40 60.429 42.059 60.429 46.053 L 60.429 212.857 C 60.429 213.941 62.487 216 65.027 216 L 190.973 216 C 193.513 216 195.571 213.941 195.571 212.857 L 195.571 80.788 C 195.571 78.601 195.279 77.897 194.764 77.378 L 158.193 40.808 C 157.675 40.292 156.971 40 156.239 40 Z " fill="rgb(233,233,224)"/><path d=" M 157.857 40.475 L 157.857 77.714 L 195.097 77.714 L 157.857 40.475 Z " fill="rgb(217,215,202)"/><path d=" M 190.973 216 L 65.027 216 C 62.487 216 60.429 213.941 60.429 211.402 L 60.429 162.571 L 195.571 162.571 L 195.571 211.402 C 195.571 213.941 193.513 216 190.973 216 Z " fill="rgb(74,134,232)"/><path d=" M 157.767 134.37 L 157.767 104.603 Q 156.526 105.999 155.092 107.161 Q 144.705 115.146 138.581 120.262 Q 136.604 121.929 135.364 122.859 Q 134.124 123.789 132.012 124.739 Q 129.899 125.688 128.039 125.688 L 127.961 125.688 Q 126.101 125.688 123.988 124.739 Q 121.876 123.789 120.636 122.859 Q 119.396 121.929 117.419 120.262 Q 111.295 115.146 100.908 107.161 Q 99.474 105.999 98.233 104.603 L 98.233 134.37 Q 98.233 134.874 98.602 135.242 Q 98.97 135.61 99.474 135.61 L 156.526 135.61 Q 157.03 135.61 157.398 135.242 Q 157.767 134.874 157.767 134.37 Z M 157.767 93.635 L 157.767 92.685 L 157.747 92.181 L 157.631 91.697 L 157.418 91.348 L 157.069 91.057 L 156.526 90.96 L 99.474 90.96 Q 98.97 90.96 98.602 91.329 Q 98.233 91.697 98.233 92.201 Q 98.233 98.712 103.931 103.208 Q 111.411 109.099 119.473 115.495 Q 119.706 115.688 120.83 116.638 Q 121.954 117.588 122.613 118.091 Q 123.271 118.595 124.337 119.312 Q 125.403 120.029 126.295 120.378 Q 127.186 120.727 127.961 120.727 L 128.039 120.727 Q 128.814 120.727 129.705 120.378 Q 130.597 120.029 131.663 119.312 Q 132.729 118.595 133.387 118.091 Q 134.046 117.588 135.17 116.638 Q 136.294 115.688 136.527 115.495 Q 144.589 109.099 152.069 103.208 Q 154.162 101.541 155.964 98.731 Q 157.767 95.921 157.767 93.635 Z M 162.728 92.201 L 162.728 134.37 Q 162.728 136.928 160.906 138.75 Q 159.084 140.571 156.526 140.571 L 99.474 140.571 Q 96.916 140.571 95.094 138.75 Q 93.272 136.928 93.272 134.37 L 93.272 92.201 Q 93.272 89.643 95.094 87.821 Q 96.916 85.999 99.474 85.999 L 156.526 85.999 Q 159.084 85.999 160.906 87.821 Q 162.728 89.643 162.728 92.201 Z " fill="rgb(74,134,232)"/><path d=" M 105.631 185.881 L 105.631 190.701 L 93.818 190.701 L 93.818 198.699 L 107.682 198.699 L 107.682 203.641 L 87.666 203.641 L 87.666 173.782 L 107.641 173.782 L 107.641 178.765 L 93.818 178.765 L 93.818 185.881 L 105.631 185.881 L 105.631 185.881 Z M 111.291 173.782 L 119.33 173.782 L 127 195.438 L 134.629 173.782 L 142.709 173.782 L 142.709 203.641 L 136.536 203.641 L 136.536 195.479 L 137.151 181.39 L 129.092 203.641 L 124.867 203.641 L 116.828 181.411 L 117.443 195.479 L 117.443 203.641 L 111.291 203.641 L 111.291 173.782 L 111.291 173.782 Z M 154.234 173.782 L 154.234 198.699 L 167.298 198.699 L 167.298 203.641 L 148.082 203.641 L 148.082 173.782 L 154.234 173.782 L 154.234 173.782 Z " fill-rule="evenodd" fill="rgb(255,255,255)"/></g></svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

1
dms/static/icons/file_eps.svg Executable file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.1 KiB

1
dms/static/icons/file_exe.svg Executable file
View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><defs><clipPath id="_clipPath_fuCqdWSzYoowVCmecSUlBS0SWUOdLKXJ"><rect width="256" height="256"/></clipPath></defs><g clip-path="url(#_clipPath_fuCqdWSzYoowVCmecSUlBS0SWUOdLKXJ)"><rect width="256" height="256" style="fill:rgb(0,0,0)" fill-opacity="0"/><g><g><path d=" M 156.239 40 L 65.027 40 C 62.487 40 60.429 42.059 60.429 46.053 L 60.429 212.857 C 60.429 213.941 62.487 216 65.027 216 L 190.973 216 C 193.513 216 195.571 213.941 195.571 212.857 L 195.571 80.788 C 195.571 78.601 195.279 77.897 194.764 77.378 L 158.193 40.808 C 157.675 40.292 156.971 40 156.239 40 Z " fill="rgb(233,233,224)"/><path d=" M 157.857 40.475 L 157.857 77.714 L 195.097 77.714 L 157.857 40.475 Z " fill="rgb(217,215,202)"/><path d=" M 190.973 216 L 65.027 216 C 62.487 216 60.429 213.941 60.429 211.402 L 60.429 162.571 L 195.571 162.571 L 195.571 211.402 C 195.571 213.941 193.513 216 190.973 216 Z " fill="rgb(151,119,168)"/><path d=" M 145.286 140.571 C 144.975 140.571 144.657 140.524 144.346 140.427 C 142.69 139.908 141.766 138.145 142.284 136.489 L 157.999 86.203 C 158.517 84.547 160.28 83.623 161.937 84.141 C 163.593 84.66 164.517 86.423 163.998 88.079 L 148.284 138.365 C 147.866 139.71 146.625 140.571 145.286 140.571 Z " fill="rgb(151,119,168)"/><circle vector-effect="non-scaling-stroke" cx="131.14285714285717" cy="101.28571428571428" r="4.714285714285722" fill="rgb(151,119,168)"/><circle vector-effect="non-scaling-stroke" cx="131.14285714285717" cy="123.28571428571428" r="4.714285714285722" fill="rgb(151,119,168)"/><path d=" M 113.857 134.286 L 107.571 134.286 C 95.44 134.286 85.571 124.417 85.571 112.286 C 85.571 100.154 95.44 90.286 107.571 90.286 L 113.857 90.286 C 115.592 90.286 117 91.694 117 93.429 C 117 95.163 115.592 96.571 113.857 96.571 L 107.571 96.571 C 98.907 96.571 91.857 103.621 91.857 112.286 C 91.857 120.951 98.907 128 107.571 128 L 113.857 128 C 115.592 128 117 129.408 117 131.143 C 117 132.878 115.592 134.286 113.857 134.286 Z " fill="rgb(151,119,168)"/></g></g><path d=" M 110.631 185.986 L 110.631 190.806 L 98.818 190.806 L 98.818 198.804 L 112.682 198.804 L 112.682 203.746 L 92.666 203.746 L 92.666 173.887 L 112.641 173.887 L 112.641 178.87 L 98.818 178.87 L 98.818 185.986 L 110.631 185.986 L 110.631 185.986 Z M 121.377 173.887 L 126.976 184.182 L 132.574 173.887 L 139.649 173.887 L 130.954 188.693 L 139.875 203.746 L 132.718 203.746 L 126.976 193.287 L 121.233 203.746 L 114.076 203.746 L 122.997 188.693 L 114.302 173.887 L 121.377 173.887 L 121.377 173.887 Z M 160.937 185.986 L 160.937 190.806 L 149.124 190.806 L 149.124 198.804 L 162.987 198.804 L 162.987 203.746 L 142.972 203.746 L 142.972 173.887 L 162.946 173.887 L 162.946 178.87 L 149.124 178.87 L 149.124 185.986 L 160.937 185.986 L 160.937 185.986 Z " fill-rule="evenodd" fill="rgb(255,255,255)"/></g></svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

1
dms/static/icons/file_f.svg Executable file
View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><defs><clipPath id="_clipPath_5oTXkrN31uMQuyZv3FF9Doe4IqTHZzDZ"><rect width="256" height="256"/></clipPath></defs><g clip-path="url(#_clipPath_5oTXkrN31uMQuyZv3FF9Doe4IqTHZzDZ)"><rect width="256" height="256" style="fill:rgb(0,0,0)" fill-opacity="0"/><g><g><path d=" M 156.239 40 L 65.027 40 C 62.487 40 60.429 42.059 60.429 46.053 L 60.429 212.857 C 60.429 213.941 62.487 216 65.027 216 L 190.973 216 C 193.513 216 195.571 213.941 195.571 212.857 L 195.571 80.788 C 195.571 78.601 195.279 77.897 194.764 77.378 L 158.193 40.808 C 157.675 40.292 156.971 40 156.239 40 Z " fill="rgb(233,233,224)"/><path d=" M 157.857 40.475 L 157.857 77.714 L 195.097 77.714 L 157.857 40.475 Z " fill="rgb(217,215,202)"/><path d=" M 190.973 216 L 65.027 216 C 62.487 216 60.429 213.941 60.429 211.402 L 60.429 162.571 L 195.571 162.571 L 195.571 211.402 C 195.571 213.941 193.513 216 190.973 216 Z " fill="rgb(93,93,93)"/><g><path d=" M 112.936 91.207 C 111.707 89.978 109.721 89.978 108.492 91.207 L 89.635 110.064 C 88.406 111.293 88.406 113.279 89.635 114.508 L 108.492 133.365 C 109.105 133.978 109.91 134.286 110.714 134.286 C 111.519 134.286 112.323 133.978 112.936 133.365 C 114.165 132.136 114.165 130.15 112.936 128.921 L 96.301 112.286 L 112.936 95.651 C 114.165 94.422 114.165 92.435 112.936 91.207 Z " fill="rgb(93,93,93)"/><path d=" M 169.508 110.064 L 161.722 102.278 L 150.651 91.207 C 149.422 89.978 147.435 89.978 146.207 91.207 C 144.978 92.435 144.978 94.422 146.207 95.651 L 162.842 112.286 L 146.207 128.921 C 144.978 130.15 144.978 132.136 146.207 133.365 C 146.819 133.978 147.624 134.286 148.429 134.286 C 149.233 134.286 150.038 133.978 150.651 133.365 L 169.508 114.508 C 170.737 113.279 170.737 111.293 169.508 110.064 Z " fill="rgb(93,93,93)"/></g></g></g><path d=" M 135.631 188.581 L 135.631 193.544 L 123.818 193.544 L 123.818 205.746 L 117.666 205.746 L 117.666 175.887 L 137.107 175.887 L 137.107 180.87 L 123.818 180.87 L 123.818 188.581 L 135.631 188.581 L 135.631 188.581 Z " fill="rgb(255,255,255)"/></g></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

1
dms/static/icons/file_fla.svg Executable file
View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><defs><clipPath id="_clipPath_2IuqJwMePGGPEksxk56V3JdmHKDktmlJ"><rect width="256" height="256"/></clipPath></defs><g clip-path="url(#_clipPath_2IuqJwMePGGPEksxk56V3JdmHKDktmlJ)"><rect width="256" height="256" style="fill:rgb(0,0,0)" fill-opacity="0"/><g><g><path d=" M 156.239 40 L 65.027 40 C 62.487 40 60.429 42.059 60.429 46.053 L 60.429 212.857 C 60.429 213.941 62.487 216 65.027 216 L 190.973 216 C 193.513 216 195.571 213.941 195.571 212.857 L 195.571 80.788 C 195.571 78.601 195.279 77.897 194.764 77.378 L 158.193 40.808 C 157.675 40.292 156.971 40 156.239 40 Z " fill="rgb(233,233,224)"/><path d=" M 157.857 40.475 L 157.857 77.714 L 195.097 77.714 L 157.857 40.475 Z " fill="rgb(217,215,202)"/><path d=" M 190.973 216 L 65.027 216 C 62.487 216 60.429 213.941 60.429 211.402 L 60.429 162.571 L 195.571 162.571 L 195.571 211.402 C 195.571 213.941 193.513 216 190.973 216 Z " fill="rgb(206,60,59)"/><circle vector-effect="non-scaling-stroke" cx="120.14285714285714" cy="90.28571428571439" r="6.285714285714278" fill="rgb(200,189,184)"/><g><path d=" M 149.953 83.777 L 147.731 81.555 C 141.203 75.024 132.519 71.429 123.286 71.429 C 114.052 71.429 105.368 75.024 98.841 81.555 C 92.31 88.083 88.714 96.766 88.714 106 C 88.714 115.234 92.31 123.917 98.841 130.445 C 105.368 136.976 114.052 140.571 123.286 140.571 C 132.519 140.571 141.203 136.976 147.731 130.445 L 149.953 128.223 L 127.73 106 L 149.953 83.777 Z M 140.942 128.101 C 135.945 132.111 129.782 134.286 123.286 134.286 C 115.73 134.286 108.627 131.344 103.285 126.001 C 97.942 120.658 95 113.555 95 106 C 95 98.445 97.942 91.342 103.285 85.999 C 108.627 80.656 115.73 77.714 123.286 77.714 C 129.782 77.714 135.945 79.889 140.942 83.899 L 118.842 106 L 140.942 128.101 Z " fill="rgb(200,189,184)"/><path d=" M 151.571 102.857 L 142.143 102.857 C 140.405 102.857 139 104.262 139 106 C 139 107.738 140.405 109.143 142.143 109.143 L 151.571 109.143 C 153.309 109.143 154.714 107.738 154.714 106 C 154.714 104.262 153.309 102.857 151.571 102.857 Z " fill="rgb(200,189,184)"/><path d=" M 170.429 102.857 L 164.143 102.857 C 162.405 102.857 161 104.262 161 106 C 161 107.738 162.405 109.143 164.143 109.143 L 170.429 109.143 C 172.167 109.143 173.571 107.738 173.571 106 C 173.571 104.262 172.167 102.857 170.429 102.857 Z " fill="rgb(200,189,184)"/></g></g></g><path d=" M 110.131 186.581 L 110.131 191.544 L 98.318 191.544 L 98.318 203.746 L 92.166 203.746 L 92.166 173.887 L 111.607 173.887 L 111.607 178.87 L 98.318 178.87 L 98.318 186.581 L 110.131 186.581 L 110.131 186.581 Z M 121.328 173.887 L 121.328 198.804 L 134.392 198.804 L 134.392 203.746 L 115.176 203.746 L 115.176 173.887 L 121.328 173.887 L 121.328 173.887 Z M 156.848 203.746 L 154.776 197.594 L 143.989 197.594 L 141.938 203.746 L 135.396 203.746 L 146.512 173.887 L 152.213 173.887 L 163.39 203.746 L 156.848 203.746 L 156.848 203.746 Z M 149.362 181.434 L 145.65 192.61 L 153.115 192.61 L 149.362 181.434 L 149.362 181.434 Z " fill-rule="evenodd" fill="rgb(255,255,255)"/></g></svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

Some files were not shown because too many files have changed in this diff Show More