Add oca-dms submodule with 10 DMS modules
🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
|
|
@ -1,45 +0,0 @@
|
|||
# Add dms field for account
|
||||
|
||||
Odoo addon: account_dms_field
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install odoo-bringout-oca-dms-account_dms_field
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
This addon depends on:
|
||||
- account
|
||||
- dms_field
|
||||
|
||||
## Manifest Information
|
||||
|
||||
- **Name**: Add dms field for account
|
||||
- **Version**: 16.0.1.0.1
|
||||
- **Category**: Accounting/Accounting
|
||||
- **License**: LGPL-3
|
||||
- **Installable**: True
|
||||
|
||||
## Source
|
||||
|
||||
Based on [OCA/dms](https://github.com/OCA/dms) branch 16.0, addon `account_dms_field`.
|
||||
|
||||
## License
|
||||
|
||||
This package maintains the original LGPL-3 license from the upstream Odoo project.
|
||||
|
||||
## Documentation
|
||||
|
||||
- Overview: doc/OVERVIEW.md
|
||||
- Architecture: doc/ARCHITECTURE.md
|
||||
- Models: doc/MODELS.md
|
||||
- Controllers: doc/CONTROLLERS.md
|
||||
- Wizards: doc/WIZARDS.md
|
||||
- Install: doc/INSTALL.md
|
||||
- Usage: doc/USAGE.md
|
||||
- Configuration: doc/CONFIGURATION.md
|
||||
- Dependencies: doc/DEPENDENCIES.md
|
||||
- Troubleshooting: doc/TROUBLESHOOTING.md
|
||||
- FAQ: doc/FAQ.md
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
=========================
|
||||
Add dms field for account
|
||||
=========================
|
||||
|
||||
..
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:b7f18580a215d7c509b59f1e27607ad97ff06df4cc445bd45e838ead8c9e5164
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
.. |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-AGPL--3-blue.png
|
||||
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
|
||||
:alt: License: AGPL-3
|
||||
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fdms-lightgray.png?logo=github
|
||||
:target: https://github.com/OCA/dms/tree/16.0/account_dms_field
|
||||
:alt: OCA/dms
|
||||
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
|
||||
:target: https://translation.odoo-community.org/projects/dms-16-0/dms-16-0-account_dms_field
|
||||
: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=16.0
|
||||
:alt: Try me on Runboat
|
||||
|
||||
|badge1| |badge2| |badge3| |badge4| |badge5|
|
||||
|
||||
Add the Documents tab with the files in the account move form view.
|
||||
|
||||
**Table of contents**
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
#. *Go to Documents > Configuration > File templates* and create a new record.
|
||||
#. Set a storage, a model (account.move) and the access groups you want.
|
||||
#. Click on the "Documents" tab icon and a folder hierarchy will be created.
|
||||
#. You can set here the hierarchy of directories, subdirectories and files you need, this hierarchy will be used as a base when creating a new record (res.partner for example).
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
#. Go to the form view of an existing account move and click on the "Documents" tab icon, a hierarchy of
|
||||
folders and files linked to that record will be created.
|
||||
#. Create a new account.move. A hierarchy of folders and files linked to that record will be created.
|
||||
|
||||
|
||||
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:%20account_dms_field%0Aversion:%2016.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
|
||||
~~~~~~~
|
||||
|
||||
* Agenterp
|
||||
|
||||
Contributors
|
||||
~~~~~~~~~~~~
|
||||
|
||||
* `Agenterp <https://www.agenterp.com/>`_:
|
||||
|
||||
* Georg Notter
|
||||
|
||||
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.
|
||||
|
||||
.. |maintainer-victoralmau| image:: https://github.com/victoralmau.png?size=40px
|
||||
:target: https://github.com/victoralmau
|
||||
:alt: victoralmau
|
||||
|
||||
Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
|
||||
|
||||
|maintainer-victoralmau|
|
||||
|
||||
This module is part of the `OCA/dms <https://github.com/OCA/dms/tree/16.0/account_dms_field>`_ project on GitHub.
|
||||
|
||||
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
|
||||
|
|
@ -1 +0,0 @@
|
|||
from . import models
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"name": "Add dms field for account",
|
||||
"version": "16.0.1.0.1",
|
||||
"category": "Accounting/Accounting",
|
||||
"website": "https://github.com/OCA/dms",
|
||||
"author": "Agent ERP GmbH, Odoo Community Association (OCA)",
|
||||
"depends": ["account", "dms_field"],
|
||||
"data": ["views/account_move_view.xml"],
|
||||
"demo": ["demo/account_dms_data.xml"],
|
||||
"installable": True,
|
||||
"license": "LGPL-3",
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="read_access_account_move_group" model="dms.access.group">
|
||||
<field name="name">Everyone for Account DMS</field>
|
||||
<field name="group_ids" eval="[(4, ref('account.group_account_invoice'))]" />
|
||||
<field name="perm_create" eval="True" />
|
||||
<field name="perm_write" eval="True" />
|
||||
<field name="perm_unlink" eval="True" />
|
||||
</record>
|
||||
|
||||
<record id="field_template_account" model="dms.field.template">
|
||||
<field name="name">Account</field>
|
||||
<field name="storage_id" ref="dms.storage_demo" />
|
||||
<field name="model_id" ref="account.model_account_move" />
|
||||
<field name="user_field_id" ref="account.field_account_move__user_id" />
|
||||
<field name="group_ids" eval="[(4, ref('read_access_account_move_group'))]" />
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * account_dms_field
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 16.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#. module: account_dms_field
|
||||
#: model:ir.model.fields,field_description:account_dms_field.field_account_move__dms_directory_ids
|
||||
msgid "DMS Directories"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_dms_field
|
||||
#: model:ir.model,name:account_dms_field.model_dms_field_template
|
||||
msgid "Dms Field Template"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_dms_field
|
||||
#: model_terms:ir.ui.view,arch_db:account_dms_field.view_move_form_inherit_account_dms_field
|
||||
msgid "Documents"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_dms_field
|
||||
#: model:dms.access.group,name:account_dms_field.read_access_account_move_group
|
||||
msgid "Everyone for Account DMS"
|
||||
msgstr ""
|
||||
|
||||
#. module: account_dms_field
|
||||
#: model:ir.model,name:account_dms_field.model_account_move
|
||||
msgid "Journal Entry"
|
||||
msgstr ""
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * account_dms_field
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 16.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#. module: account_dms_field
|
||||
#: model:ir.model.fields,field_description:account_dms_field.field_account_move__dms_directory_ids
|
||||
msgid "DMS Directories"
|
||||
msgstr "DMS direktoriji"
|
||||
|
||||
#. module: account_dms_field
|
||||
#: model:ir.model,name:account_dms_field.model_dms_field_template
|
||||
msgid "Dms Field Template"
|
||||
msgstr "DMS templejt polja"
|
||||
|
||||
#. module: account_dms_field
|
||||
#: model_terms:ir.ui.view,arch_db:account_dms_field.view_move_form_inherit_account_dms_field
|
||||
msgid "Documents"
|
||||
msgstr "Dokumenti"
|
||||
|
||||
#. module: account_dms_field
|
||||
#: model:dms.access.group,name:account_dms_field.read_access_account_move_group
|
||||
msgid "Everyone for Account DMS"
|
||||
msgstr "Svi za Account DMS"
|
||||
|
||||
#. module: account_dms_field
|
||||
#: model:ir.model,name:account_dms_field.model_account_move
|
||||
msgid "Journal Entry"
|
||||
msgstr "Žurnal"
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * account_dms_field
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 16.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2024-10-17 16:06+0000\n"
|
||||
"Last-Translator: mymage <stefano.consolaro@mymage.it>\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: it\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 5.6.2\n"
|
||||
|
||||
#. module: account_dms_field
|
||||
#: model:ir.model.fields,field_description:account_dms_field.field_account_move__dms_directory_ids
|
||||
msgid "DMS Directories"
|
||||
msgstr "Cartelle DMS"
|
||||
|
||||
#. module: account_dms_field
|
||||
#: model:ir.model,name:account_dms_field.model_dms_field_template
|
||||
msgid "Dms Field Template"
|
||||
msgstr "Modello campo DMS"
|
||||
|
||||
#. module: account_dms_field
|
||||
#: model_terms:ir.ui.view,arch_db:account_dms_field.view_move_form_inherit_account_dms_field
|
||||
msgid "Documents"
|
||||
msgstr "Documenti"
|
||||
|
||||
#. module: account_dms_field
|
||||
#: model:dms.access.group,name:account_dms_field.read_access_account_move_group
|
||||
msgid "Everyone for Account DMS"
|
||||
msgstr "Tutti per conto DMS"
|
||||
|
||||
#. module: account_dms_field
|
||||
#: model:ir.model,name:account_dms_field.model_account_move
|
||||
msgid "Journal Entry"
|
||||
msgstr "Registrazione contabile"
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
from . import account_move
|
||||
from . import dms_field_template
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
from odoo import models
|
||||
|
||||
|
||||
class AccountMove(models.Model):
|
||||
_name = "account.move"
|
||||
_inherit = ["account.move", "dms.field.mixin"]
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
from odoo import models
|
||||
|
||||
|
||||
class DmsFieldTemplate(models.Model):
|
||||
_inherit = "dms.field.template"
|
||||
|
||||
def _prepare_directory_vals(self, directory, record):
|
||||
vals = super()._prepare_directory_vals(directory, record)
|
||||
if "/" not in vals["name"]:
|
||||
return vals
|
||||
vals["name"] = vals["name"].replace("/", "-")
|
||||
return vals
|
||||
|
Before Width: | Height: | Size: 9.2 KiB |
|
|
@ -1 +0,0 @@
|
|||
from . import test_account_dms_field
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
from odoo.addons.base.tests.common import BaseCommon
|
||||
|
||||
|
||||
class TestAccountDmsField(BaseCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.env = cls.env(context=dict(cls.env.context, test_dms_field=True))
|
||||
cls.template = cls.env.ref("account_dms_field.field_template_account")
|
||||
cls.storage = cls.template.storage_id
|
||||
cls.access_group = cls.template.group_ids
|
||||
cls.account_model = cls.env["account.move"]
|
||||
cls.partner = cls.env.ref("base.res_partner_12")
|
||||
cls.test_directory = cls.env["dms.directory"].create(
|
||||
{
|
||||
"name": "Test Directory",
|
||||
"parent_id": cls.template.dms_directory_ids[0].id,
|
||||
"storage_id": cls.template.storage_id.id,
|
||||
}
|
||||
)
|
||||
|
||||
def test_01_account_document_directory(self):
|
||||
account_move = self.account_model.create(
|
||||
{
|
||||
"partner_id": self.partner.id,
|
||||
}
|
||||
)
|
||||
account_move.invalidate_model()
|
||||
directory = account_move.dms_directory_ids
|
||||
# Assert that only one directory is created for the account move.
|
||||
self.assertEqual(len(directory), 1, "Directory length must be 1.")
|
||||
# Assert that the storage associated with the directory is the same as the
|
||||
# template's storage.
|
||||
self.assertEqual(
|
||||
directory.storage_id,
|
||||
self.storage,
|
||||
"Account move directory storage is different from the template storage.",
|
||||
)
|
||||
# Assert that the custom access group is present in the directory's group
|
||||
# list.
|
||||
self.assertIn(
|
||||
self.access_group,
|
||||
directory.group_ids,
|
||||
"Account move directory groups are different from the template groups.",
|
||||
)
|
||||
# Map the names of child directories related to the account move directory.
|
||||
child_directory_names = directory.mapped("child_directory_ids.name")
|
||||
# Assert that a specific child directory, "Test Directory", exists.
|
||||
self.assertIn(
|
||||
"Test Directory",
|
||||
child_directory_names,
|
||||
"Test Directory is not in the child directory of the account move "
|
||||
"directory.",
|
||||
)
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="view_move_form_inherit_account_dms_field" model="ir.ui.view">
|
||||
<field name="name">view.move.form.inherit.account.dms.field</field>
|
||||
<field name="model">account.move</field>
|
||||
<field name="inherit_id" ref="account.view_move_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//notebook" position="inside">
|
||||
<page
|
||||
name="documents"
|
||||
string="Documents"
|
||||
attrs="{'invisible': [('id', '=', False)]}"
|
||||
>
|
||||
<field name="dms_directory_ids" mode="dms_list" />
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
# Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
U[Users] -->|HTTP| V[Views and QWeb Templates]
|
||||
V --> C[Controllers]
|
||||
V --> W[Wizards – Transient Models]
|
||||
C --> M[Models and ORM]
|
||||
W --> M
|
||||
M --> R[Reports]
|
||||
DX[Data XML] --> M
|
||||
S[Security – ACLs and Groups] -. enforces .-> M
|
||||
|
||||
subgraph Account_dms_field Module - account_dms_field
|
||||
direction LR
|
||||
M:::layer
|
||||
W:::layer
|
||||
C:::layer
|
||||
V:::layer
|
||||
R:::layer
|
||||
S:::layer
|
||||
DX:::layer
|
||||
end
|
||||
|
||||
classDef layer fill:#eef8ff,stroke:#6ea8fe,stroke-width:1px
|
||||
```
|
||||
|
||||
Notes
|
||||
- Views include tree/form/kanban templates and report templates.
|
||||
- Controllers provide website/portal routes when present.
|
||||
- Wizards are UI flows implemented with `models.TransientModel`.
|
||||
- Data XML loads data/demo records; Security defines groups and access.
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# Configuration
|
||||
|
||||
Refer to Odoo settings for account_dms_field. Configure related models, access rights, and options as needed.
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# Controllers
|
||||
|
||||
This module does not define custom HTTP controllers.
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
# Dependencies
|
||||
|
||||
This addon depends on:
|
||||
|
||||
- [account](https://github.com/bringout/oca-ocb-accounting/tree/b11fb50e2ed11eec1e305a0df730b49554c01199/odoo-bringout-oca-ocb-account)
|
||||
- [dms_field](https://github.com/bringout/oca-technical)
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
# FAQ
|
||||
|
||||
- Q: Which Odoo version? A: 16.0 (OCA/OCB packaged).
|
||||
- Q: How to enable? A: Start server with --addon account_dms_field or install in UI.
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
# Install
|
||||
|
||||
```bash
|
||||
pip install odoo-bringout-oca-dms-account_dms_field"
|
||||
# or
|
||||
uv pip install odoo-bringout-oca-dms-account_dms_field"
|
||||
```
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
# Models
|
||||
|
||||
Detected core models and extensions in account_dms_field.
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class account_move
|
||||
class dms_field_template
|
||||
```
|
||||
|
||||
Notes
|
||||
- Classes show model technical names; fields omitted for brevity.
|
||||
- Items listed under _inherit are extensions of existing models.
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
# Overview
|
||||
|
||||
Packaged Odoo addon: account_dms_field. Provides features documented in upstream Odoo 16 under this addon.
|
||||
|
||||
- Source: OCA/OCB 16.0, addon account_dms_field
|
||||
- License: LGPL-3
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# Reports
|
||||
|
||||
This module does not define custom reports.
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
# Security
|
||||
|
||||
This module does not define custom security rules or access controls beyond Odoo defaults.
|
||||
|
||||
Default Odoo security applies:
|
||||
- Base user access through standard groups
|
||||
- Model access inherited from dependencies
|
||||
- No custom row-level security rules
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
# Troubleshooting
|
||||
|
||||
- Ensure Python and Odoo environment matches repo guidance.
|
||||
- Check database connectivity and logs if startup fails.
|
||||
- Validate that dependent addons listed in DEPENDENCIES.md are installed.
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
# Usage
|
||||
|
||||
Start Odoo including this addon (from repo root):
|
||||
|
||||
```bash
|
||||
python3 scripts/nix_odoo_web_server.py --db-name mydb --addon account_dms_field
|
||||
```
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# Wizards
|
||||
|
||||
This module does not include UI wizards.
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
[project]
|
||||
name = "odoo-bringout-oca-dms-account_dms_field"
|
||||
version = "16.0.0"
|
||||
description = "Add dms field for account - Odoo addon"
|
||||
authors = [
|
||||
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
|
||||
]
|
||||
dependencies = [
|
||||
"odoo-bringout-oca-ocb-account>=16.0.0",
|
||||
"odoo-bringout-oca-dms-dms_field>=16.0.0",
|
||||
"requests>=2.25.1"
|
||||
]
|
||||
readme = "README.md"
|
||||
requires-python = ">= 3.11"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Topic :: Office/Business",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
homepage = "https://github.com/bringout/0"
|
||||
repository = "https://github.com/bringout/0"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.metadata]
|
||||
allow-direct-references = true
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["account_dms_field"]
|
||||
|
||||
[tool.rye]
|
||||
managed = true
|
||||
dev-dependencies = [
|
||||
"pytest>=8.4.1",
|
||||
]
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
# Document Management System
|
||||
|
||||
Odoo addon: dms
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install odoo-bringout-oca-dms-dms
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
This addon depends on:
|
||||
- mail
|
||||
- http_routing
|
||||
- portal
|
||||
- base
|
||||
|
||||
## Manifest Information
|
||||
|
||||
- **Name**: Document Management System
|
||||
- **Version**: 16.0.1.8.6
|
||||
- **Category**: Document Management
|
||||
- **License**: LGPL-3
|
||||
- **Installable**: False
|
||||
|
||||
## Source
|
||||
|
||||
Based on [OCA/dms](https://github.com/OCA/dms) branch 16.0, addon `dms`.
|
||||
|
||||
## License
|
||||
|
||||
This package maintains the original LGPL-3 license from the upstream Odoo project.
|
||||
|
||||
## Documentation
|
||||
|
||||
- Overview: doc/OVERVIEW.md
|
||||
- Architecture: doc/ARCHITECTURE.md
|
||||
- Models: doc/MODELS.md
|
||||
- Controllers: doc/CONTROLLERS.md
|
||||
- Wizards: doc/WIZARDS.md
|
||||
- Install: doc/INSTALL.md
|
||||
- Usage: doc/USAGE.md
|
||||
- Configuration: doc/CONFIGURATION.md
|
||||
- Dependencies: doc/DEPENDENCIES.md
|
||||
- Troubleshooting: doc/TROUBLESHOOTING.md
|
||||
- FAQ: doc/FAQ.md
|
||||
|
|
@ -1,189 +0,0 @@
|
|||
==========================
|
||||
Document Management System
|
||||
==========================
|
||||
|
||||
..
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:4ca37da84fb902307a08168a40a5048ba34c2af01b05c97a139cca5eb19b8301
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
.. |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/16.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-16-0/dms-16-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=16.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
|
||||
~~~~~~~
|
||||
|
||||
``mail_preview_base`` is required for DMS but it is recommended to install all
|
||||
the other `mail_preview` modules from `social` OCA repository
|
||||
in order to improve the preview of files.
|
||||
|
||||
``python-magic`` library is recommended to be installed for having whole support
|
||||
to get proper file types and file preview.
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
To configure this module, you need to:
|
||||
|
||||
#. Go to *Documents -> Configuration -> Storages*.
|
||||
#. Create a new document storage. You can choose between two options on `Save Type`:
|
||||
* `Database`: Store the files on the database as a field
|
||||
* `Attachment`: Store the files as attachments
|
||||
#. 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.
|
||||
#. Afterwards go to *Documents -> Directories*.
|
||||
#. 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.
|
||||
#. On the Directory 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.
|
||||
In order to achieve it you need to:
|
||||
|
||||
#. Go to *Documents -> Configuration -> Storage* and select the storage you want to modify
|
||||
#. Modify the save type
|
||||
#. Press the button `Migrate files` if you want to migrate all the files at once
|
||||
#. Press the button `Manual File Migration` in order to specify files one by one
|
||||
|
||||
You can check all the files that still needs 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:%2016.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>
|
||||
|
||||
Other credits
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
The migration of this module from 15.0 to 16.0 was financially supported by `AgentERP <https://www.agenterp.com>`_
|
||||
|
||||
Some pictures are based on or inspired by:
|
||||
|
||||
* `Roundicons <https://www.flaticon.com/authors/roundicons>`_
|
||||
* `Smashicons <https://www.flaticon.com/authors/smashicons>`_
|
||||
|
||||
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/16.0/dms>`_ project on GitHub.
|
||||
|
||||
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
from . import controllers
|
||||
from . import models
|
||||
from . import wizards
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
# Copyright 2017-2019 MuK IT GmbH
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
|
||||
|
||||
{
|
||||
"name": "Document Management System",
|
||||
"summary": """Document Management System for Odoo""",
|
||||
"version": "16.0.1.8.6",
|
||||
"category": "Document Management",
|
||||
"license": "LGPL-3",
|
||||
"website": "https://github.com/OCA/dms",
|
||||
"author": "MuK IT, Tecnativa, Odoo Community Association (OCA)",
|
||||
"depends": [
|
||||
"mail",
|
||||
"http_routing",
|
||||
"portal",
|
||||
"base",
|
||||
],
|
||||
"data": [
|
||||
"security/security.xml",
|
||||
"security/ir.model.access.csv",
|
||||
"actions/file.xml",
|
||||
"template/onboarding.xml",
|
||||
"views/menu.xml",
|
||||
"views/tag.xml",
|
||||
"views/category.xml",
|
||||
"views/dms_file.xml",
|
||||
"views/directory.xml",
|
||||
"views/storage.xml",
|
||||
"views/dms_access_groups_views.xml",
|
||||
"views/res_config_settings.xml",
|
||||
"views/dms_portal_templates.xml",
|
||||
"wizards/wizard_dms_file_move_views.xml",
|
||||
"wizards/wizard_dms_share_views.xml",
|
||||
],
|
||||
"assets": {
|
||||
"mail.assets_messaging": [
|
||||
("include", "mail.assets_core_messaging"),
|
||||
"dms/static/src/models/*.js",
|
||||
],
|
||||
"web.assets_backend": [
|
||||
"dms/static/src/scss/*",
|
||||
"dms/static/src/js/fields/*",
|
||||
"dms/static/src/js/views/*.esm.js",
|
||||
"dms/static/src/js/views/*.xml",
|
||||
"dms/static/src/js/views/fields/binary/*",
|
||||
],
|
||||
"web.assets_frontend": ["dms/static/src/js/dms_portal_tour.js"],
|
||||
},
|
||||
"demo": [
|
||||
"demo/res_users.xml",
|
||||
"demo/access_group.xml",
|
||||
"demo/category.xml",
|
||||
"demo/tag.xml",
|
||||
"demo/storage.xml",
|
||||
"demo/directory.xml",
|
||||
"demo/file.xml",
|
||||
],
|
||||
"images": ["static/description/banner.png"],
|
||||
"application": True,
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
<?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>
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
from . import main
|
||||
from . import portal
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
# 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("/dms/document_onboarding/directory", auth="user", type="json")
|
||||
def document_onboarding_directory(self):
|
||||
company = request.env.user.company_id
|
||||
closed = company.documents_onboarding_state == "closed"
|
||||
check = request.env.user.has_group("dms.group_dms_manager")
|
||||
if check and not closed:
|
||||
return {
|
||||
"html": request.env["ir.qweb"]._render(
|
||||
request.env.ref("dms.document_onboarding_directory_panel").id,
|
||||
{
|
||||
"state": company.get_and_update_documents_onboarding_state(),
|
||||
"company": company,
|
||||
},
|
||||
)
|
||||
}
|
||||
return {}
|
||||
|
||||
@http.route("/dms/document_onboarding/file", auth="user", type="json")
|
||||
def document_onboarding_file(self):
|
||||
company = request.env.user.company_id
|
||||
closed = company.documents_onboarding_state == "closed"
|
||||
check = request.env.user.has_group("dms.group_dms_manager")
|
||||
if check and not closed:
|
||||
return {
|
||||
"html": request.env["ir.qweb"]._render(
|
||||
request.env.ref("dms.document_onboarding_file_panel").id,
|
||||
{
|
||||
"state": company.get_and_update_documents_onboarding_state(),
|
||||
"company": company,
|
||||
},
|
||||
)
|
||||
}
|
||||
return {}
|
||||
|
||||
@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=""
|
||||
)
|
||||
}
|
||||
|
|
@ -1,202 +0,0 @@
|
|||
# Copyright 2020-2021 Tecnativa - Víctor Martínez
|
||||
import base64
|
||||
|
||||
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):
|
||||
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
|
||||
):
|
||||
values = self._prepare_portal_layout_values()
|
||||
searchbar_sortings = {"name": {"label": _("Name"), "order": "name asc"}}
|
||||
# default sortby br
|
||||
if not sortby:
|
||||
sortby = "name"
|
||||
sort_br = searchbar_sortings[sortby]["order"]
|
||||
# search
|
||||
searchbar_inputs = {
|
||||
"name": {"input": "name", "label": _("Name")},
|
||||
}
|
||||
if not filterby:
|
||||
filterby = "name"
|
||||
# domain
|
||||
domain = [
|
||||
(
|
||||
"id",
|
||||
"in",
|
||||
request.env["dms.directory"]._get_own_root_directories(),
|
||||
)
|
||||
]
|
||||
# search
|
||||
if search and search_in:
|
||||
search_domain = []
|
||||
if search_in == "name":
|
||||
search_domain = OR([search_domain, [("name", "ilike", search)]])
|
||||
domain += search_domain
|
||||
# content according to pager and archive selected
|
||||
items = request.env["dms.directory"].search(domain, order=sort_br)
|
||||
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
|
||||
):
|
||||
ensure_db()
|
||||
# operations
|
||||
searchbar_sortings = {"name": {"label": _("Name"), "order": "name asc"}}
|
||||
# default sortby br
|
||||
if not sortby:
|
||||
sortby = "name"
|
||||
sort_br = searchbar_sortings[sortby]["order"]
|
||||
# search
|
||||
searchbar_inputs = {
|
||||
"name": {"input": "name", "label": _("Name")},
|
||||
}
|
||||
if not filterby:
|
||||
filterby = "name"
|
||||
# domain
|
||||
domain = [("is_hidden", "=", False), ("parent_id", "=", dms_directory_id)]
|
||||
# search
|
||||
if search and search_in:
|
||||
search_domain = []
|
||||
if search_in == "name":
|
||||
search_domain = OR([search_domain, [("name", "ilike", search)]])
|
||||
domain += search_domain
|
||||
# content according to pager and archive selected
|
||||
if access_token:
|
||||
dms_directory_items = (
|
||||
request.env["dms.directory"].sudo().search(domain, order=sort_br)
|
||||
)
|
||||
else:
|
||||
dms_directory_items = request.env["dms.directory"].search(
|
||||
domain, order=sort_br
|
||||
)
|
||||
request.session["my_dms_folder_history"] = dms_directory_items.ids
|
||||
res = self._dms_check_access("dms.directory", dms_directory_id, access_token)
|
||||
if not res:
|
||||
if access_token:
|
||||
return request.redirect("/")
|
||||
else:
|
||||
return request.redirect("/my")
|
||||
dms_directory_sudo = res
|
||||
# dms_files_count
|
||||
domain = [
|
||||
("is_hidden", "=", False),
|
||||
("directory_id", "=", dms_directory_id),
|
||||
]
|
||||
# search
|
||||
if search and search_in:
|
||||
search_domain = []
|
||||
if search_in == "name":
|
||||
search_domain = OR([search_domain, [("name", "ilike", search)]])
|
||||
domain += search_domain
|
||||
# items
|
||||
if access_token:
|
||||
dms_file_items = (
|
||||
request.env["dms.file"].sudo().search(domain, order=sort_br)
|
||||
)
|
||||
else:
|
||||
dms_file_items = request.env["dms.file"].search(domain, order=sort_br)
|
||||
request.session["my_dms_file_history"] = dms_file_items.ids
|
||||
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)
|
||||
|
||||
@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):
|
||||
"""Process user's consent acceptance or rejection."""
|
||||
ensure_db()
|
||||
# operations
|
||||
res = self._dms_check_access("dms.file", dms_file_id, access_token)
|
||||
if not res:
|
||||
if access_token:
|
||||
return request.redirect("/")
|
||||
else:
|
||||
return request.redirect("/my")
|
||||
|
||||
dms_file_sudo = res
|
||||
# It's necessary to prevent AccessError in ir_attachment .check() function
|
||||
if dms_file_sudo.attachment_id and request.env.user.has_group(
|
||||
"base.group_portal"
|
||||
):
|
||||
dms_file_sudo = dms_file_sudo.sudo()
|
||||
filecontent = base64.b64decode(dms_file_sudo.content)
|
||||
content_type = ["Content-Type", "application/octet-stream"]
|
||||
disposition_content = [
|
||||
"Content-Disposition",
|
||||
content_disposition(dms_file_sudo.name),
|
||||
]
|
||||
return request.make_response(filecontent, [content_type, disposition_content])
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
<?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'), ref('base.user_demo')])]"
|
||||
/>
|
||||
</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">Only admin user</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>
|
||||
</odoo>
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
<?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>
|
||||
|
|
@ -1,147 +0,0 @@
|
|||
<?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>
|
||||
|
|
@ -1,241 +0,0 @@
|
|||
<?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>
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
<?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>
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
<?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>
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
<?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>
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
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 category
|
||||
from . import tag
|
||||
|
||||
from . import res_company
|
||||
from . import res_config_settings
|
||||
from . import ir_attachment
|
||||
from . import mail_thread
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
# 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
|
||||
)
|
||||
|
|
@ -1,169 +0,0 @@
|
|||
# Copyright 2017-2019 MuK IT GmbH
|
||||
# Copyright 2020 RGB Consulting
|
||||
# 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(DmsAccessGroups, self).default_get(fields_list)
|
||||
if "explicit_user_ids" in res and res["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.mapped("group_ids.users")
|
||||
users |= record.mapped("explicit_user_ids")
|
||||
users |= record.mapped("parent_group_id.users")
|
||||
record.update({"users": users, "count_users": len(users)})
|
||||
|
||||
@api.constrains("parent_path")
|
||||
def _check_parent_recursiveness(self):
|
||||
"""Forbid recursive relationships."""
|
||||
for one in self:
|
||||
if not one.parent_group_id:
|
||||
continue
|
||||
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,
|
||||
}
|
||||
)
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Copyright 2021 Tecnativa - Jairo Llopis
|
||||
# Copyright 2024 Tecnativa - Víctor Martínez
|
||||
# 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)]
|
||||
self.env["dms.file"].sudo().search(domain).unlink()
|
||||
self.env["dms.directory"].sudo().search(domain).unlink()
|
||||
return result
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
# Copyright 2020 Creu Blanca
|
||||
# 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
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Category(models.Model):
|
||||
_name = "dms.category"
|
||||
_description = "Document Category"
|
||||
|
||||
_parent_store = True
|
||||
_parent_name = "parent_id"
|
||||
|
||||
_order = "complete_name asc"
|
||||
_rec_name = "complete_name"
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# Database
|
||||
# ----------------------------------------------------------
|
||||
|
||||
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(
|
||||
comodel_name="dms.category",
|
||||
string="Parent Category",
|
||||
ondelete="cascade",
|
||||
index="btree",
|
||||
)
|
||||
|
||||
child_category_ids = fields.One2many(
|
||||
comodel_name="dms.category",
|
||||
inverse_name="parent_id",
|
||||
string="Child Categories",
|
||||
)
|
||||
|
||||
parent_path = fields.Char(index="btree", unaccent=False)
|
||||
tag_ids = fields.One2many(
|
||||
comodel_name="dms.tag", inverse_name="category_id", string="Tags"
|
||||
)
|
||||
directory_ids = fields.One2many(
|
||||
comodel_name="dms.directory",
|
||||
inverse_name="category_id",
|
||||
string="Directories",
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
file_ids = fields.One2many(
|
||||
comodel_name="dms.file",
|
||||
inverse_name="category_id",
|
||||
string="Files",
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
count_categories = fields.Integer(
|
||||
compute="_compute_count_categories", string="Count Subcategories"
|
||||
)
|
||||
|
||||
count_tags = fields.Integer(compute="_compute_count_tags")
|
||||
|
||||
count_directories = fields.Integer(compute="_compute_count_directories")
|
||||
|
||||
count_files = fields.Integer(compute="_compute_count_files")
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# Constrains
|
||||
# ----------------------------------------------------------
|
||||
|
||||
_sql_constraints = [
|
||||
("name_uniq", "unique (name)", "Category name already exists!"),
|
||||
]
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# Read
|
||||
# ----------------------------------------------------------
|
||||
|
||||
@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("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)
|
||||
|
||||
def name_get(self):
|
||||
if not self.env.context.get("category_short_name", False):
|
||||
return super().name_get()
|
||||
vals = []
|
||||
for record in self:
|
||||
vals.append(tuple([record.id, record.name]))
|
||||
return vals
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# Create
|
||||
# ----------------------------------------------------------
|
||||
|
||||
@api.constrains("parent_id")
|
||||
def _check_category_recursion(self):
|
||||
if not self._check_recursion():
|
||||
raise ValidationError(_("Error! You cannot create recursive categories."))
|
||||
return True
|
||||
|
|
@ -1,800 +0,0 @@
|
|||
# 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 ast
|
||||
import base64
|
||||
import logging
|
||||
from ast import literal_eval
|
||||
from collections import defaultdict
|
||||
|
||||
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__)
|
||||
|
||||
|
||||
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=False,
|
||||
)
|
||||
|
||||
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=False,
|
||||
)
|
||||
|
||||
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
|
||||
else:
|
||||
directory_item = self
|
||||
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 name_get(self):
|
||||
if not self.env.context.get("directory_short_name", False):
|
||||
return super().name_get()
|
||||
vals = []
|
||||
for record in self:
|
||||
vals.append(tuple([record.id, record.name]))
|
||||
return vals
|
||||
|
||||
def toggle_starred(self):
|
||||
updates = defaultdict(set)
|
||||
for record in self:
|
||||
vals = {"starred": not record.starred}
|
||||
updates[tools.frozendict(vals)].add(record.id)
|
||||
with self.env.norecompute():
|
||||
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:
|
||||
elements = record.count_files
|
||||
elements += record.count_directories
|
||||
record.count_elements = elements
|
||||
|
||||
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:
|
||||
total_elements = record.count_total_files
|
||||
total_elements += record.count_total_directories
|
||||
record.count_total_elements = total_elements
|
||||
|
||||
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: 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:
|
||||
if record.storage_id.save_type != "attachment":
|
||||
continue
|
||||
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:
|
||||
childs = record.sudo().storage_id.root_directory_ids.name_get()
|
||||
else:
|
||||
childs = record.sudo().parent_id.child_directory_ids.name_get()
|
||||
if list(
|
||||
filter(
|
||||
lambda child: child[1] == record.name and child[0] != record.id,
|
||||
childs,
|
||||
)
|
||||
):
|
||||
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["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)})
|
||||
new = super().copy(default)
|
||||
for record in self.file_ids:
|
||||
record.copy({"directory_id": new.id})
|
||||
for record in self.child_directory_ids:
|
||||
record.copy({"parent_id": new.id})
|
||||
return new
|
||||
|
||||
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", None)
|
||||
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})
|
||||
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 parent to 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"]):
|
||||
with self.env.norecompute():
|
||||
res = super(DmsDirectory, self).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().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 childs (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
|
||||
|
|
@ -1,690 +0,0 @@
|
|||
# Copyright 2020 Antoni Romera
|
||||
# Copyright 2017-2019 MuK IT GmbH
|
||||
# Copyright 2021 Tecnativa - Víctor Martínez
|
||||
# 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 File(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,
|
||||
store=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, invisible=True)
|
||||
|
||||
save_type = fields.Char(
|
||||
compute="_compute_save_type",
|
||||
string="Current Save Type",
|
||||
invisible=True,
|
||||
prefetch=False,
|
||||
)
|
||||
|
||||
migration = fields.Char(
|
||||
compute="_compute_migration",
|
||||
string="Migration Status",
|
||||
readonly=True,
|
||||
prefetch=False,
|
||||
compute_sudo=True,
|
||||
)
|
||||
require_migration = fields.Boolean(
|
||||
compute="_compute_migration", store=True, compute_sudo=True
|
||||
)
|
||||
|
||||
content_file = fields.Binary(attachment=True, prefetch=False, invisible=True)
|
||||
|
||||
# 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):
|
||||
res = False
|
||||
if access_token:
|
||||
if self.access_token and consteq(self.access_token, access_token):
|
||||
return True
|
||||
else:
|
||||
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
|
||||
else:
|
||||
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 res
|
||||
|
||||
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,
|
||||
invisible=True,
|
||||
ondelete="cascade",
|
||||
index=True,
|
||||
)
|
||||
|
||||
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, logging=True):
|
||||
record_count = len(self)
|
||||
index = 1
|
||||
for dms_file in self:
|
||||
if 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})
|
||||
|
||||
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":
|
||||
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", False) == "dms.directory":
|
||||
active_id = self.env.context.get("active_id")
|
||||
# para saber que directorios, buscamos las posibles carpetas que nos interesan
|
||||
files = self.env["dms.file"].search(
|
||||
[["directory_id", "child_of", active_id]]
|
||||
)
|
||||
all_directories = files.mapped("directory_id")
|
||||
all_directories += files.mapped("directory_id.parent_id")
|
||||
domain.append(["id", "in", all_directories.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())}
|
||||
context = {}
|
||||
if field_name == "category_id":
|
||||
context["category_short_name"] = True
|
||||
return super(File, self.with_context(**context)).search_panel_select_range(
|
||||
field_name, **kwargs
|
||||
)
|
||||
|
||||
@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(
|
||||
File, self.with_context(directory_short_name=True)
|
||||
).search_panel_select_multi_range(
|
||||
field_name, comodel_domain=directory_comodel_domain, **kwargs
|
||||
)
|
||||
return super(
|
||||
File, 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),
|
||||
"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 = "{} > {}".format(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.name_get()
|
||||
if list(
|
||||
filter(
|
||||
lambda file: file[1] == record.name and file[0] != record.id, files
|
||||
)
|
||||
):
|
||||
raise ValidationError(_("A file with the same name already exists."))
|
||||
|
||||
@api.constrains("extension")
|
||||
def _check_extension(self):
|
||||
for record in self:
|
||||
if (
|
||||
record.extension
|
||||
and record.extension in self._get_forbidden_extensions()
|
||||
):
|
||||
raise ValidationError(_("The file has a forbidden file extension."))
|
||||
|
||||
@api.constrains("size")
|
||||
def _check_size(self):
|
||||
for record in self:
|
||||
if record.size and record.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)
|
||||
with self.env.norecompute():
|
||||
for vals, ids in updates.items():
|
||||
self.browse(ids).write(dict(vals))
|
||||
|
||||
def _create_model_attachment(self, vals):
|
||||
res_vals = vals.copy()
|
||||
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 [])
|
||||
if "directory_id" in default:
|
||||
model = self.env["dms.directory"]
|
||||
directory = model.browse(default["directory_id"])
|
||||
names = directory.sudo().file_ids.mapped("name")
|
||||
else:
|
||||
names = self.sudo().directory_id.file_ids.mapped("name")
|
||||
default.update({"name": file.unique_name(self.name, names, self.extension)})
|
||||
return super(File, self).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(File, self).create(new_vals_list)
|
||||
|
||||
def unlink(self):
|
||||
attachments = self.mapped("attachment_id")
|
||||
res = super().unlink()
|
||||
if not self.env.context.get("dms_file"):
|
||||
attachments.with_context(dms_file=True).unlink()
|
||||
return res
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# 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,
|
||||
}
|
||||
|
||||
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]
|
||||
|
|
@ -1,259 +0,0 @@
|
|||
# Copyright 2020 Creu Blanca
|
||||
# Copyright 2021 Tecnativa - Víctor Martínez
|
||||
# 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 = "{},{}".format(record.res_model, record.res_id)
|
||||
|
||||
def _compute_permissions(self):
|
||||
"""Get permissions for the current record.
|
||||
|
||||
⚠ Not very performant; only display field on form views.
|
||||
"""
|
||||
# Superuser unrestricted 🦸
|
||||
if self.env.su:
|
||||
self.update(
|
||||
{
|
||||
"permission_create": True,
|
||||
"permission_read": True,
|
||||
"permission_unlink": True,
|
||||
"permission_write": True,
|
||||
}
|
||||
)
|
||||
return
|
||||
# Update according to presence when applying ir.rule
|
||||
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 = "{}.{}".format(
|
||||
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:
|
||||
# Model not 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 = """
|
||||
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 {}
|
||||
""".format(
|
||||
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
|
||||
# Obtain and combine domains
|
||||
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_acccess_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 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
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
# Copyright 2021-2025 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
|
||||
|
||||
def unlink(self):
|
||||
if not self.env.context.get("dms_file"):
|
||||
self.env["dms.file"].search(
|
||||
[("attachment_id", "in", self.ids)]
|
||||
).with_context(dms_file=True).unlink()
|
||||
return super().unlink()
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
# 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 _message_post_process_attachments(
|
||||
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)._message_post_process_attachments(
|
||||
attachments, attachment_ids, message_data
|
||||
)
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
# Copyright 2017-2019 MuK IT GmbH.
|
||||
# Copyright 2020 Creu Blanca
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
|
||||
|
||||
import os
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.modules.module import get_resource_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):
|
||||
"""Obtain local disk path to record icon."""
|
||||
folders = ["static", "icons"]
|
||||
name = self._get_icon_placeholder_name()
|
||||
path = get_resource_path("dms", *folders, name)
|
||||
return path or get_resource_path("dms", *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 = (
|
||||
"/web/image/{}/{}/image_128/128x128?crop=1".format(one._name, one.id)
|
||||
if one.image_128
|
||||
else one._get_icon_url()
|
||||
)
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
# Copyright 2020 Creu Blanca
|
||||
# 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 ResCompany(models.Model):
|
||||
|
||||
_inherit = "res.company"
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# Database
|
||||
# ----------------------------------------------------------
|
||||
|
||||
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):
|
||||
return self._get_and_update_onboarding_state(
|
||||
"documents_onboarding_state", self.get_documents_steps_states_names()
|
||||
)
|
||||
|
||||
def get_documents_steps_states_names(self):
|
||||
return [
|
||||
"documents_onboarding_storage_state",
|
||||
"documents_onboarding_directory_state",
|
||||
"documents_onboarding_file_state",
|
||||
]
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# Actions
|
||||
# ----------------------------------------------------------
|
||||
|
||||
@api.model
|
||||
def action_open_documents_onboarding_storage(self):
|
||||
return self.env.ref("dms.action_dms_storage_new").read()[0]
|
||||
|
||||
@api.model
|
||||
def action_open_documents_onboarding_directory(self):
|
||||
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):
|
||||
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
|
||||
|
||||
@api.model
|
||||
def action_close_documents_onboarding(self):
|
||||
self.env.user.company_id.documents_onboarding_state = "closed"
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
# 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",
|
||||
)
|
||||
|
|
@ -1,153 +0,0 @@
|
|||
# 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"
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# Database
|
||||
# ----------------------------------------------------------
|
||||
|
||||
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 acess)",
|
||||
)
|
||||
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["ir.attachment"].clear_caches()
|
||||
return res
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
# 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)
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
To configure this module, you need to:
|
||||
|
||||
#. Go to *Documents -> Configuration -> Storages*.
|
||||
#. Create a new document storage. You can choose between two options on `Save Type`:
|
||||
* `Database`: Store the files on the database as a field
|
||||
* `Attachment`: Store the files as attachments
|
||||
#. 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.
|
||||
#. Afterwards go to *Documents -> Directories*.
|
||||
#. 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.
|
||||
#. On the Directory 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.
|
||||
In order to achieve it you need to:
|
||||
|
||||
#. Go to *Documents -> Configuration -> Storage* and select the storage you want to modify
|
||||
#. Modify the save type
|
||||
#. Press the button `Migrate files` if you want to migrate all the files at once
|
||||
#. Press the button `Manual File Migration` in order to specify files one by one
|
||||
|
||||
You can check all the files that still needs 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.
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
* 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>
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
The migration of this module from 15.0 to 16.0 was financially supported by `AgentERP <https://www.agenterp.com>`_
|
||||
|
||||
Some pictures are based on or inspired by:
|
||||
|
||||
* `Roundicons <https://www.flaticon.com/authors/roundicons>`_
|
||||
* `Smashicons <https://www.flaticon.com/authors/smashicons>`_
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
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.
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
Preview
|
||||
~~~~~~~
|
||||
|
||||
``mail_preview_base`` is required for DMS but it is recommended to install all
|
||||
the other `mail_preview` modules from `social` OCA repository
|
||||
in order to improve the preview of files.
|
||||
|
||||
``python-magic`` library is recommended to be installed for having whole support
|
||||
to get proper file types and file preview.
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
- 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.
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
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.
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
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,190 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!--
|
||||
|
||||
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).
|
||||
|
||||
-->
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 7 KiB |
|
Before Width: | Height: | Size: 7.8 KiB |
|
|
@ -1,566 +0,0 @@
|
|||
<!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:4ca37da84fb902307a08168a40a5048ba34c2af01b05c97a139cca5eb19b8301
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
|
||||
<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/16.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-16-0/dms-16-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&target_branch=16.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><ul>
|
||||
<li><a class="reference internal" href="#migration" id="toc-entry-4">Migration</a></li>
|
||||
<li><a class="reference internal" href="#file-wizard-selection" id="toc-entry-5">File Wizard Selection</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a class="reference internal" href="#usage" id="toc-entry-6">Usage</a><ul>
|
||||
<li><a class="reference internal" href="#portal-functionality" id="toc-entry-7">Portal functionality</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a class="reference internal" href="#known-issues-roadmap" id="toc-entry-8">Known issues / Roadmap</a></li>
|
||||
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-9">Bug Tracker</a></li>
|
||||
<li><a class="reference internal" href="#credits" id="toc-entry-10">Credits</a><ul>
|
||||
<li><a class="reference internal" href="#authors" id="toc-entry-11">Authors</a></li>
|
||||
<li><a class="reference internal" href="#contributors" id="toc-entry-12">Contributors</a></li>
|
||||
<li><a class="reference internal" href="#other-credits" id="toc-entry-13">Other credits</a></li>
|
||||
<li><a class="reference internal" href="#maintainers" id="toc-entry-14">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">mail_preview_base</tt> is required for DMS but it is recommended to install all
|
||||
the other <cite>mail_preview</cite> modules from <cite>social</cite> OCA repository
|
||||
in order to improve the preview of files.</p>
|
||||
<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>
|
||||
<p>To configure this module, you need to:</p>
|
||||
<ol class="arabic simple">
|
||||
<li>Go to <em>Documents -> Configuration -> Storages</em>.</li>
|
||||
<li><dl class="first docutils">
|
||||
<dt>Create a new document storage. You can choose between two options on <cite>Save Type</cite>:</dt>
|
||||
<dd><ul class="first last">
|
||||
<li><cite>Database</cite>: Store the files on the database as a field</li>
|
||||
<li><cite>Attachment</cite>: Store the files as attachments</li>
|
||||
</ul>
|
||||
</dd>
|
||||
</dl>
|
||||
</li>
|
||||
<li><dl class="first docutils">
|
||||
<dt>Next create an administrative access group. Go to <em>Configuration -> Access Groups</em>.</dt>
|
||||
<dd><ul class="first last">
|
||||
<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>
|
||||
</dd>
|
||||
</dl>
|
||||
</li>
|
||||
<li>Afterwards go to <em>Documents -> Directories</em>.</li>
|
||||
<li><dl class="first docutils">
|
||||
<dt>Create a new directory, mark it as root and select the previously created setting.</dt>
|
||||
<dd><ul class="first last">
|
||||
<li>Select the <em>Groups</em> tab and add your administrative group created above.</li>
|
||||
</ul>
|
||||
</dd>
|
||||
</dl>
|
||||
</li>
|
||||
<li><dl class="first docutils">
|
||||
<dt>On the Directory you can also add other access groups (created above) that will be able to:</dt>
|
||||
<dd><ul class="first last">
|
||||
<li>read</li>
|
||||
<li>create</li>
|
||||
<li>write</li>
|
||||
<li>delete</li>
|
||||
</ul>
|
||||
</dd>
|
||||
</dl>
|
||||
</li>
|
||||
</ol>
|
||||
<div class="section" id="migration">
|
||||
<h2><a class="toc-backref" href="#toc-entry-4">Migration</a></h2>
|
||||
<p>If you need to modify the storage Save Type you might want to migrate the file data.
|
||||
In order to achieve it you need to:</p>
|
||||
<ol class="arabic simple">
|
||||
<li>Go to <em>Documents -> Configuration -> Storage</em> and select the storage you want to modify</li>
|
||||
<li>Modify the save type</li>
|
||||
<li>Press the button <cite>Migrate files</cite> if you want to migrate all the files at once</li>
|
||||
<li>Press the button <cite>Manual File Migration</cite> in order to specify files one by one</li>
|
||||
</ol>
|
||||
<p>You can check all the files that still needs to be migrated from all storages
|
||||
and migrate them manually on <em>Documents -> Configuration -> Migration</em></p>
|
||||
</div>
|
||||
<div class="section" id="file-wizard-selection">
|
||||
<h2><a class="toc-backref" href="#toc-entry-5">File Wizard Selection</a></h2>
|
||||
<p>There is an action called <cite>action_dms_file_wizard_selector</cite> to open a wizard to list files in kanban view.
|
||||
This can be used (example <cite>dms_attachment_link</cite> module) to add a button in kanban view with the action we need.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" id="usage">
|
||||
<h1><a class="toc-backref" href="#toc-entry-6">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-7">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-8">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-9">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:%2016.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-10">Credits</a></h1>
|
||||
<div class="section" id="authors">
|
||||
<h2><a class="toc-backref" href="#toc-entry-11">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-12">Contributors</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Mathias Markl <<a class="reference external" href="mailto:mathias.markl@mukit.at">mathias.markl@mukit.at</a>></li>
|
||||
<li>Enric Tobella <<a class="reference external" href="mailto:etobella@creublanca.es">etobella@creublanca.es</a>></li>
|
||||
<li>Antoni Romera</li>
|
||||
<li>Gelu Boros <<a class="reference external" href="mailto:gelu.boros@rgbconsulting.com">gelu.boros@rgbconsulting.com</a>></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 <<a class="reference external" href="mailto:yweng@elegosoft.com">yweng@elegosoft.com</a>></li>
|
||||
<li>Philip Witte <<a class="reference external" href="mailto:phillip.witte@elegosoft.com">phillip.witte@elegosoft.com</a>></li>
|
||||
<li>Khanh Bui <<a class="reference external" href="mailto:khanh.bui@mail.elegosoft.com">khanh.bui@mail.elegosoft.com</a>></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="other-credits">
|
||||
<h2><a class="toc-backref" href="#toc-entry-13">Other credits</a></h2>
|
||||
<p>The migration of this module from 15.0 to 16.0 was financially supported by <a class="reference external" href="https://www.agenterp.com">AgentERP</a></p>
|
||||
<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>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="maintainers">
|
||||
<h2><a class="toc-backref" href="#toc-entry-14">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/16.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>
|
||||
|
|
@ -1 +0,0 @@
|
|||
<?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>
|
||||
|
Before Width: | Height: | Size: 3.3 KiB |
|
|
@ -1 +0,0 @@
|
|||
<?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>
|
||||
|
Before Width: | Height: | Size: 3 KiB |
|
|
@ -1 +0,0 @@
|
|||
<?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>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
|
|
@ -1 +0,0 @@
|
|||
<?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>
|
||||
|
Before Width: | Height: | Size: 4 KiB |
|
|
@ -1 +0,0 @@
|
|||
<?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>
|
||||
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
|
@ -1 +0,0 @@
|
|||
<?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>
|
||||
|
Before Width: | Height: | Size: 4.6 KiB |
|
|
@ -1 +0,0 @@
|
|||
<?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>
|
||||
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 8.3 KiB |
|
|
@ -1 +0,0 @@
|
|||
<?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>
|
||||
|
Before Width: | Height: | Size: 4.4 KiB |