Resolve README conflict
|
|
@ -0,0 +1,128 @@
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
wheels/
|
||||||
|
pip-wheel-metadata/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# github action
|
||||||
|
.github/workflows/*yaml
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
# zuhair-project
|
||||||
|
|
@ -0,0 +1,166 @@
|
||||||
|
==========================
|
||||||
|
Document Management System
|
||||||
|
==========================
|
||||||
|
|
||||||
|
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
|
!! This file is generated by oca-gen-addon-readme !!
|
||||||
|
!! changes will be overwritten. !!
|
||||||
|
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
|
|
||||||
|
.. |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/14.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-14-0/dms-14-0-dms
|
||||||
|
:alt: Translate me on Weblate
|
||||||
|
.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
|
||||||
|
:target: https://runbot.odoo-community.org/runbot/292/14.0
|
||||||
|
:alt: Try me on Runbot
|
||||||
|
|
||||||
|
|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.
|
||||||
|
|
||||||
|
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
|
||||||
|
#. Afterwards go to *Documents -> Directories*.
|
||||||
|
#. Create a new directory, mark it as root and select the previously created setting.
|
||||||
|
#. On the Directory you can also define the access groups 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*
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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 smashing it by providing a detailed and welcomed
|
||||||
|
`feedback <https://github.com/OCA/dms/issues/new?body=module:%20dms%0Aversion:%2014.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
|
||||||
|
|
||||||
|
Other credits
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
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. Adding new features by an Expert Co.
|
||||||
|
|
||||||
|
|
||||||
|
.. image:: https://odoo-community.org/logo.png
|
||||||
|
:alt: Odoo Community Association
|
||||||
|
:target: https://odoo-community.org
|
||||||
|
|
||||||
|
.. image:: https://odoo-community.org/logo.png
|
||||||
|
:alt: Odoo Community Association
|
||||||
|
:target: https://exp-sa.com
|
||||||
|
|
||||||
|
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/14.0/dms>`_ project on GitHub.
|
||||||
|
|
||||||
|
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
from . import controllers
|
||||||
|
from . import models
|
||||||
|
from . import wizard
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
# 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 Odex25""",
|
||||||
|
"version": "14.0.4.0.0",
|
||||||
|
"category": "Odex25-DMS/Odex25-DMS",
|
||||||
|
"license": "LGPL-3",
|
||||||
|
"website": "https://github.com/OCA/dms",
|
||||||
|
"author": "MuK IT, Tecnativa, Odoo Community Association (OCA)",
|
||||||
|
"depends": [
|
||||||
|
'base',
|
||||||
|
'web',
|
||||||
|
'attachment_indexation',
|
||||||
|
'digest',
|
||||||
|
"web_drop_target",
|
||||||
|
"mail",
|
||||||
|
"http_routing",
|
||||||
|
"portal",
|
||||||
|
"mail_preview_base",
|
||||||
|
"project",
|
||||||
|
"board",
|
||||||
|
'documents'
|
||||||
|
],
|
||||||
|
"data": [
|
||||||
|
"security/security.xml",
|
||||||
|
"security/ir.model.access.csv",
|
||||||
|
"actions/file.xml",
|
||||||
|
"data/documents_data.xml",
|
||||||
|
"data/mail_templates.xml",
|
||||||
|
"template/assets.xml",
|
||||||
|
"template/onboarding.xml",
|
||||||
|
"views/documents_views.xml",
|
||||||
|
# 'views/assets.xml',
|
||||||
|
'views/templates.xml',
|
||||||
|
'views/activity_views.xml',
|
||||||
|
"views/menu.xml",
|
||||||
|
"views/tag.xml",
|
||||||
|
"views/category.xml",
|
||||||
|
# "views/dms_file.xml",
|
||||||
|
"views/directory.xml",
|
||||||
|
"views/attach.xml",
|
||||||
|
"views/storage.xml",
|
||||||
|
"views/settings_view.xml",
|
||||||
|
"views/dms_access_groups_views.xml",
|
||||||
|
"views/res_config_settings.xml",
|
||||||
|
"views/dms_portal_templates.xml",
|
||||||
|
'wizard/request_activity_views.xml',
|
||||||
|
],
|
||||||
|
"qweb": ["static/src/xml/views.xml"],
|
||||||
|
"images": ["static/description/banner.png"],
|
||||||
|
"application": True,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?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>
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
from . import main
|
||||||
|
from . import portal
|
||||||
|
|
@ -0,0 +1,517 @@
|
||||||
|
# Copyright 2017-2019 MuK IT GmbH
|
||||||
|
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
|
||||||
|
from odoo import http
|
||||||
|
import base64
|
||||||
|
import zipfile
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from contextlib import ExitStack
|
||||||
|
from odoo.exceptions import AccessError
|
||||||
|
from odoo.http import request, content_disposition
|
||||||
|
from odoo.tools.translate import _
|
||||||
|
from odoo.tools import image_process
|
||||||
|
from odoo.addons.web.controllers.main import Binary
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
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.ref(
|
||||||
|
"dms.document_onboarding_directory_panel"
|
||||||
|
)._render(
|
||||||
|
{
|
||||||
|
"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.ref("dms.document_onboarding_file_panel")._render(
|
||||||
|
{
|
||||||
|
"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=""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ShareRoute(http.Controller):
|
||||||
|
|
||||||
|
# util methods #################################################################################
|
||||||
|
|
||||||
|
def binary_content(self, id, env=None, field='datas', share_id=None, share_token=None,
|
||||||
|
download=False, unique=False, filename_field='name'):
|
||||||
|
env = env or request.env
|
||||||
|
record = env['documents.document'].browse(int(id))
|
||||||
|
filehash = None
|
||||||
|
|
||||||
|
if share_id:
|
||||||
|
share = env['documents.share'].sudo().browse(int(share_id))
|
||||||
|
record = share._get_documents_and_check_access(share_token, [int(id)], operation='read')
|
||||||
|
if not record or not record.exists():
|
||||||
|
return (404, [], None)
|
||||||
|
|
||||||
|
#check access right
|
||||||
|
try:
|
||||||
|
last_update = record['__last_update']
|
||||||
|
except AccessError:
|
||||||
|
return (404, [], None)
|
||||||
|
|
||||||
|
mimetype = False
|
||||||
|
if record.type == 'url' and record.url:
|
||||||
|
module_resource_path = record.url
|
||||||
|
filename = os.path.basename(module_resource_path)
|
||||||
|
status = 301
|
||||||
|
content = module_resource_path
|
||||||
|
else:
|
||||||
|
status, content, filename, mimetype, filehash = env['ir.http']._binary_record_content(
|
||||||
|
record, field=field, filename=None, filename_field=filename_field,
|
||||||
|
default_mimetype='application/octet-stream')
|
||||||
|
status, headers, content = env['ir.http']._binary_set_headers(
|
||||||
|
status, content, filename, mimetype, unique, filehash=filehash, download=download)
|
||||||
|
|
||||||
|
return status, headers, content
|
||||||
|
|
||||||
|
def _get_file_response(self, id, field='datas', share_id=None, share_token=None):
|
||||||
|
"""
|
||||||
|
returns the http response to download one file.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
status, headers, content = self.binary_content(
|
||||||
|
id, field=field, share_id=share_id, share_token=share_token, download=True)
|
||||||
|
|
||||||
|
if status != 200:
|
||||||
|
return request.env['ir.http']._response_by_status(status, headers, content)
|
||||||
|
else:
|
||||||
|
content_base64 = base64.b64decode(content)
|
||||||
|
headers.append(('Content-Length', len(content_base64)))
|
||||||
|
response = request.make_response(content_base64, headers)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def _make_zip(self, name, documents):
|
||||||
|
"""returns zip files for the Document Inspector and the portal.
|
||||||
|
|
||||||
|
:param name: the name to give to the zip file.
|
||||||
|
:param documents: files (documents.document) to be zipped.
|
||||||
|
:return: a http response to download a zip file.
|
||||||
|
"""
|
||||||
|
stream = io.BytesIO()
|
||||||
|
try:
|
||||||
|
with zipfile.ZipFile(stream, 'w') as doc_zip:
|
||||||
|
for document in documents:
|
||||||
|
if document.type != 'binary':
|
||||||
|
continue
|
||||||
|
status, content, filename, mimetype, filehash = request.env['ir.http']._binary_record_content(
|
||||||
|
document, field='datas', filename=None, filename_field='name',
|
||||||
|
default_mimetype='application/octet-stream')
|
||||||
|
doc_zip.writestr(filename, base64.b64decode(content),
|
||||||
|
compress_type=zipfile.ZIP_DEFLATED)
|
||||||
|
except zipfile.BadZipfile:
|
||||||
|
logger.exception("BadZipfile exception")
|
||||||
|
|
||||||
|
content = stream.getvalue()
|
||||||
|
headers = [
|
||||||
|
('Content-Type', 'zip'),
|
||||||
|
('X-Content-Type-Options', 'nosniff'),
|
||||||
|
('Content-Length', len(content)),
|
||||||
|
('Content-Disposition', content_disposition(name))
|
||||||
|
]
|
||||||
|
return request.make_response(content, headers)
|
||||||
|
|
||||||
|
# Download & upload routes #####################################################################
|
||||||
|
|
||||||
|
@http.route('/documents/upload_attachment', type='http', methods=['POST'], auth="user")
|
||||||
|
def upload_document(self, folder_id, ufile, document_id=False, partner_id=False, owner_id=False):
|
||||||
|
try:
|
||||||
|
files = request.httprequest.files.getlist('ufile')
|
||||||
|
result = {'success': _("All files uploaded")}
|
||||||
|
tag_ids = request.params.pop('tag_ids', None)
|
||||||
|
tag_ids = tag_ids.split(',') if tag_ids else []
|
||||||
|
if document_id:
|
||||||
|
document = request.env['documents.document'].browse(int(document_id))
|
||||||
|
ufile = files[0]
|
||||||
|
try:
|
||||||
|
data = base64.encodebytes(ufile.read())
|
||||||
|
mimetype = ufile.content_type
|
||||||
|
document.write({
|
||||||
|
'name': ufile.filename,
|
||||||
|
'datas': data,
|
||||||
|
'mimetype': mimetype,
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("Fail to upload document %s" % ufile.filename)
|
||||||
|
result = {'error': str(e)}
|
||||||
|
else:
|
||||||
|
vals_list = []
|
||||||
|
for ufile in files:
|
||||||
|
try:
|
||||||
|
mimetype = ufile.content_type
|
||||||
|
datas = base64.encodebytes(ufile.read())
|
||||||
|
vals = {
|
||||||
|
'name': ufile.filename,
|
||||||
|
'mimetype': mimetype,
|
||||||
|
'datas': datas,
|
||||||
|
'folder_id': int(folder_id),
|
||||||
|
'tag_ids': tag_ids,
|
||||||
|
'partner_id': int(partner_id)
|
||||||
|
}
|
||||||
|
if owner_id:
|
||||||
|
vals['owner_id'] = int(owner_id)
|
||||||
|
vals_list.append(vals)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("Fail to upload document %s" % ufile.filename)
|
||||||
|
result = {'error': str(e)}
|
||||||
|
cids = request.httprequest.cookies.get('cids', str(request.env.user.company_id.id))
|
||||||
|
allowed_company_ids = [int(cid) for cid in cids.split(',')]
|
||||||
|
documents = request.env['documents.document'].with_context(allowed_company_ids=allowed_company_ids).create(vals_list)
|
||||||
|
result['ids'] = documents.ids
|
||||||
|
|
||||||
|
return json.dumps(result)
|
||||||
|
except Exception as e:
|
||||||
|
msg = "Fail to upload document %s" % str(e)
|
||||||
|
result = {'error': msg}
|
||||||
|
logger.exception(msg)
|
||||||
|
# return json.dumps(result)
|
||||||
|
|
||||||
|
@http.route('/documents/pdf_split', type='http', methods=['POST'], auth="user")
|
||||||
|
def pdf_split(self, new_files=None, ufile=None, archive=False, vals=None):
|
||||||
|
"""Used to split and/or merge pdf documents.
|
||||||
|
|
||||||
|
The data can come from different sources: multiple existing documents
|
||||||
|
(at least one must be provided) and any number of extra uploaded files.
|
||||||
|
|
||||||
|
:param new_files: the array that represents the new pdf structure:
|
||||||
|
[{
|
||||||
|
'name': 'New File Name',
|
||||||
|
'new_pages': [{
|
||||||
|
'old_file_type': 'document' or 'file',
|
||||||
|
'old_file_index': document_id or index in ufile,
|
||||||
|
'old_page_number': 5,
|
||||||
|
}],
|
||||||
|
}]
|
||||||
|
:param ufile: extra uploaded files that are not existing documents
|
||||||
|
:param archive: whether to archive the original documents
|
||||||
|
:param vals: values for the create of the new documents.
|
||||||
|
"""
|
||||||
|
vals = json.loads(vals)
|
||||||
|
new_files = json.loads(new_files)
|
||||||
|
# find original documents
|
||||||
|
document_ids = set()
|
||||||
|
for new_file in new_files:
|
||||||
|
for page in new_file['new_pages']:
|
||||||
|
if page['old_file_type'] == 'document':
|
||||||
|
document_ids.add(page['old_file_index'])
|
||||||
|
documents = request.env['documents.document'].browse(document_ids)
|
||||||
|
|
||||||
|
with ExitStack() as stack:
|
||||||
|
files = request.httprequest.files.getlist('ufile')
|
||||||
|
open_files = [stack.enter_context(io.BytesIO(file.read())) for file in files]
|
||||||
|
|
||||||
|
# merge together data from existing documents and from extra uploads
|
||||||
|
document_id_index_map = {}
|
||||||
|
current_index = len(open_files)
|
||||||
|
for document in documents:
|
||||||
|
open_files.append(stack.enter_context(io.BytesIO(base64.b64decode(document.datas))))
|
||||||
|
document_id_index_map[document.id] = current_index
|
||||||
|
current_index += 1
|
||||||
|
|
||||||
|
# update new_files structure with the new indices from documents
|
||||||
|
for new_file in new_files:
|
||||||
|
for page in new_file['new_pages']:
|
||||||
|
if page.pop('old_file_type') == 'document':
|
||||||
|
page['old_file_index'] = document_id_index_map[page['old_file_index']]
|
||||||
|
|
||||||
|
# apply the split/merge
|
||||||
|
new_documents = documents._pdf_split(new_files=new_files, open_files=open_files, vals=vals)
|
||||||
|
|
||||||
|
# archive original documents if needed
|
||||||
|
if archive == 'true':
|
||||||
|
documents.write({'active': False})
|
||||||
|
|
||||||
|
response = request.make_response(json.dumps(new_documents.ids), [('Content-Type', 'application/json')])
|
||||||
|
return response
|
||||||
|
|
||||||
|
@http.route(['/documents/content/<int:id>'], type='http', auth='user')
|
||||||
|
def documents_content(self, id):
|
||||||
|
return self._get_file_response(id)
|
||||||
|
|
||||||
|
@http.route(['/documents/image/<int:id>',
|
||||||
|
'/documents/image/<int:id>/<int:width>x<int:height>',
|
||||||
|
], type='http', auth="public")
|
||||||
|
def content_image(self, id=None, field='datas', share_id=None, width=0, height=0, crop=False, share_token=None,
|
||||||
|
unique=False, **kwargs):
|
||||||
|
status, headers, image_base64 = self.binary_content(
|
||||||
|
id=id, field=field, share_id=share_id, share_token=share_token, unique=unique)
|
||||||
|
if status != 200:
|
||||||
|
return request.env['ir.http']._response_by_status(status, headers, image_base64)
|
||||||
|
|
||||||
|
try:
|
||||||
|
image_base64 = image_process(image_base64, size=(int(width), int(height)), crop=crop)
|
||||||
|
except Exception:
|
||||||
|
return request.not_found()
|
||||||
|
|
||||||
|
if not image_base64:
|
||||||
|
return request.not_found()
|
||||||
|
|
||||||
|
content = base64.b64decode(image_base64)
|
||||||
|
headers = http.set_safe_image_headers(headers, content)
|
||||||
|
response = request.make_response(content, headers)
|
||||||
|
response.status_code = status
|
||||||
|
return response
|
||||||
|
|
||||||
|
@http.route(['/document/zip'], type='http', auth='user')
|
||||||
|
def get_zip(self, file_ids=None, zip_name=None, token=None, **kwargs):
|
||||||
|
"""Route to get the zip file of the selection in the document's Kanban view.
|
||||||
|
|
||||||
|
:param file_ids: IDs of the files to zip (either comma-separated or list format).
|
||||||
|
:param zip_name: Name of the zip file.
|
||||||
|
:param token: Optional token for file tracking.
|
||||||
|
"""
|
||||||
|
if not file_ids:
|
||||||
|
# If file_ids is None, check for list-style query parameters
|
||||||
|
file_ids = request.httprequest.args.getlist('file_ids[]')
|
||||||
|
|
||||||
|
if file_ids:
|
||||||
|
if isinstance(file_ids, list): # Case: file_ids[]=1262&file_ids[]=1256
|
||||||
|
ids_list = [int(x) for x in file_ids]
|
||||||
|
else: # Case: file_ids=1262,1256,1261
|
||||||
|
ids_list = [int(x) for x in file_ids.split(',')]
|
||||||
|
else:
|
||||||
|
ids_list = []
|
||||||
|
|
||||||
|
env = request.env
|
||||||
|
response = self._make_zip(zip_name, env['documents.document'].browse(ids_list))
|
||||||
|
|
||||||
|
if token:
|
||||||
|
response.set_cookie('fileToken', token)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
@http.route(["/document/download/all/<int:share_id>/<access_token>"], type='http', auth='public')
|
||||||
|
def share_download_all(self, access_token=None, share_id=None):
|
||||||
|
"""
|
||||||
|
:param share_id: id of the share, the name of the share will be the name of the zip file share.
|
||||||
|
:param access_token: share access token
|
||||||
|
:returns the http response for a zip file if the token and the ID are valid.
|
||||||
|
"""
|
||||||
|
env = request.env
|
||||||
|
try:
|
||||||
|
share = env['documents.share'].sudo().browse(share_id)
|
||||||
|
documents = share._get_documents_and_check_access(access_token, operation='read')
|
||||||
|
if documents:
|
||||||
|
return self._make_zip((share.name or 'unnamed-link') + '.zip', documents)
|
||||||
|
else:
|
||||||
|
return request.not_found()
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to zip share link id: %s" % share_id)
|
||||||
|
return request.not_found()
|
||||||
|
|
||||||
|
@http.route(["/document/avatar/<int:share_id>/<access_token>"], type='http', auth='public')
|
||||||
|
def get_avatar(self, access_token=None, share_id=None):
|
||||||
|
"""
|
||||||
|
:param share_id: id of the share.
|
||||||
|
:param access_token: share access token
|
||||||
|
:returns the picture of the share author for the front-end view.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
env = request.env
|
||||||
|
share = env['documents.share'].sudo().browse(share_id)
|
||||||
|
if share._get_documents_and_check_access(access_token, document_ids=[], operation='read') is not False:
|
||||||
|
image = env['res.users'].sudo().browse(share.create_uid.id).image_128
|
||||||
|
|
||||||
|
if not image:
|
||||||
|
binary = Binary()
|
||||||
|
return binary.placeholder()
|
||||||
|
|
||||||
|
return base64.b64decode(image)
|
||||||
|
else:
|
||||||
|
return request.not_found()
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to download portrait")
|
||||||
|
return request.not_found()
|
||||||
|
|
||||||
|
@http.route(["/document/thumbnail/<int:share_id>/<access_token>/<int:id>"],
|
||||||
|
type='http', auth='public')
|
||||||
|
def get_thumbnail(self, id=None, access_token=None, share_id=None):
|
||||||
|
"""
|
||||||
|
:param id: id of the document
|
||||||
|
:param access_token: token of the share link
|
||||||
|
:param share_id: id of the share link
|
||||||
|
:return: the thumbnail of the document for the portal view.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
thumbnail = self._get_file_response(id, share_id=share_id, share_token=access_token, field='thumbnail')
|
||||||
|
return thumbnail
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to download thumbnail id: %s" % id)
|
||||||
|
return request.not_found()
|
||||||
|
|
||||||
|
# single file download route.
|
||||||
|
@http.route(["/document/download/<int:share_id>/<access_token>/<int:id>"],
|
||||||
|
type='http', auth='public')
|
||||||
|
def download_one(self, id=None, access_token=None, share_id=None, **kwargs):
|
||||||
|
"""
|
||||||
|
used to download a single file from the portal multi-file page.
|
||||||
|
|
||||||
|
:param id: id of the file
|
||||||
|
:param access_token: token of the share link
|
||||||
|
:param share_id: id of the share link
|
||||||
|
:return: a portal page to preview and download a single file.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
document = self._get_file_response(id, share_id=share_id, share_token=access_token, field='datas')
|
||||||
|
return document or request.not_found()
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to download document %s" % id)
|
||||||
|
|
||||||
|
return request.not_found()
|
||||||
|
|
||||||
|
# Upload file(s) route.
|
||||||
|
@http.route(["/document/upload/<int:share_id>/<token>/",
|
||||||
|
"/document/upload/<int:share_id>/<token>/<int:document_id>"],
|
||||||
|
type='http', auth='public', methods=['POST'], csrf=False)
|
||||||
|
def upload_attachment(self, share_id, token, document_id=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Allows public upload if provided with the right token and share_Link.
|
||||||
|
|
||||||
|
:param share_id: id of the share.
|
||||||
|
:param token: share access token.
|
||||||
|
:param document_id: id of a document request to directly upload its content
|
||||||
|
:return if files are uploaded, recalls the share portal with the updated content.
|
||||||
|
"""
|
||||||
|
share = http.request.env['documents.share'].sudo().browse(share_id)
|
||||||
|
if not share.can_upload or (not document_id and share.action != 'downloadupload'):
|
||||||
|
return http.request.not_found()
|
||||||
|
|
||||||
|
available_documents = share._get_documents_and_check_access(
|
||||||
|
token, [document_id] if document_id else [], operation='write')
|
||||||
|
folder = share.folder_id
|
||||||
|
folder_id = folder.id or False
|
||||||
|
button_text = share.name or _('Share link')
|
||||||
|
chatter_message = _('''<b> File uploaded by: </b> %s <br/>
|
||||||
|
<b> Link created by: </b> %s <br/>
|
||||||
|
<a class="btn btn-primary" href="/web#id=%s&model=documents.share&view_type=form" target="_blank">
|
||||||
|
<b>%s</b>
|
||||||
|
</a>
|
||||||
|
''') % (
|
||||||
|
http.request.env.user.name,
|
||||||
|
share.create_uid.name,
|
||||||
|
share_id,
|
||||||
|
button_text,
|
||||||
|
)
|
||||||
|
if document_id and available_documents:
|
||||||
|
if available_documents.type != 'empty':
|
||||||
|
return http.request.not_found()
|
||||||
|
try:
|
||||||
|
file = request.httprequest.files.getlist('requestFile')[0]
|
||||||
|
data = file.read()
|
||||||
|
mimetype = file.content_type
|
||||||
|
write_vals = {
|
||||||
|
'mimetype': mimetype,
|
||||||
|
'name': file.filename,
|
||||||
|
'type': 'binary',
|
||||||
|
'datas': base64.b64encode(data),
|
||||||
|
}
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to read uploaded file")
|
||||||
|
else:
|
||||||
|
available_documents.with_context(binary_field_real_user=http.request.env.user).write(write_vals)
|
||||||
|
available_documents.message_post(body=chatter_message)
|
||||||
|
elif not document_id and available_documents is not False:
|
||||||
|
try:
|
||||||
|
for file in request.httprequest.files.getlist('files'):
|
||||||
|
data = file.read()
|
||||||
|
mimetype = file.content_type
|
||||||
|
document_dict = {
|
||||||
|
'mimetype': mimetype,
|
||||||
|
'name': file.filename,
|
||||||
|
'datas': base64.b64encode(data),
|
||||||
|
'tag_ids': [(6, 0, share.tag_ids.ids)],
|
||||||
|
'partner_id': share.partner_id.id,
|
||||||
|
'owner_id': share.owner_id.id,
|
||||||
|
'folder_id': folder_id,
|
||||||
|
}
|
||||||
|
document = request.env['documents.document'].with_user(share.create_uid).with_context(binary_field_real_user=http.request.env.user).create(document_dict)
|
||||||
|
document.message_post(body=chatter_message)
|
||||||
|
if share.activity_option:
|
||||||
|
document.documents_set_activity(settings_record=share)
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to upload document")
|
||||||
|
else:
|
||||||
|
return http.request.not_found()
|
||||||
|
return """<script type='text/javascript'>
|
||||||
|
window.open("/document/share/%s/%s", "_self");
|
||||||
|
</script>""" % (share_id, token)
|
||||||
|
|
||||||
|
# Frontend portals #############################################################################
|
||||||
|
|
||||||
|
# share portals route.
|
||||||
|
@http.route(['/document/share/<int:share_id>/<token>'], type='http', auth='public')
|
||||||
|
def share_portal(self, share_id=None, token=None):
|
||||||
|
"""
|
||||||
|
Leads to a public portal displaying downloadable files for anyone with the token.
|
||||||
|
|
||||||
|
:param share_id: id of the share link
|
||||||
|
:param token: share access token
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
share = http.request.env['documents.share'].sudo().browse(share_id)
|
||||||
|
available_documents = share._get_documents_and_check_access(token, operation='read')
|
||||||
|
if available_documents is False:
|
||||||
|
if share._check_token(token):
|
||||||
|
options = {
|
||||||
|
'expiration_date': share.date_deadline,
|
||||||
|
'author': share.create_uid.name,
|
||||||
|
}
|
||||||
|
return request.render('dms.not_available', options)
|
||||||
|
else:
|
||||||
|
return request.not_found()
|
||||||
|
|
||||||
|
options = {
|
||||||
|
'base_url': http.request.env["ir.config_parameter"].sudo().get_param("web.base.url"),
|
||||||
|
'token': str(token),
|
||||||
|
'upload': share.action == 'downloadupload',
|
||||||
|
'share_id': str(share.id),
|
||||||
|
'author': share.create_uid.name,
|
||||||
|
}
|
||||||
|
if share.type == 'ids' and len(available_documents) == 1:
|
||||||
|
options.update(document=available_documents[0], request_upload=True)
|
||||||
|
return request.render('dms.share_single', options)
|
||||||
|
else:
|
||||||
|
options.update(all_button='binary' in [document.type for document in available_documents],
|
||||||
|
document_ids=available_documents,
|
||||||
|
request_upload=share.action == 'downloadupload' or share.type == 'ids')
|
||||||
|
return request.render('dms.share_page', options)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to generate the multi file share portal")
|
||||||
|
return request.not_found()
|
||||||
|
|
@ -0,0 +1,201 @@
|
||||||
|
# Copyright 2020-2021 Tecnativa - Víctor Martínez
|
||||||
|
import base64
|
||||||
|
|
||||||
|
from odoo import _, http
|
||||||
|
from odoo.http import request
|
||||||
|
from odoo.osv.expression import OR
|
||||||
|
|
||||||
|
from odoo.addons.portal.controllers.portal import CustomerPortal
|
||||||
|
from odoo.addons.web.controllers.main import content_disposition, 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_portal_layout_values(self):
|
||||||
|
values = super()._prepare_portal_layout_values()
|
||||||
|
ids = request.env["dms.directory"]._get_own_root_directories()
|
||||||
|
values.update({"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])
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
<record id="digest_tip_documents_0" model="digest.tip">
|
||||||
|
<field name="name">Tip: Become a paperless company</field>
|
||||||
|
<field name="sequence">300</field>
|
||||||
|
<field name="group_id" ref="documents.group_dms_user" />
|
||||||
|
<field name="tip_description" type="html">
|
||||||
|
<div>
|
||||||
|
% set record = object.env['documents.share'].search([('alias_name', '!=', False)], limit=1)
|
||||||
|
<b class="tip_title">Tip: Become a paperless company</b>
|
||||||
|
% if record and record.alias_domain
|
||||||
|
<p class="tip_content">An easy way to process incoming mails is to configure your scanner to send PDFs to ${record.alias_id.display_name}. Scanned files will appear automatically in your workspace. Then, process your documents in bulk with the split tool: launch user defined actions, request a signature, convert to vendor bills with AI, etc.</p>
|
||||||
|
% else
|
||||||
|
<p class="tip_content">An easy way to process incoming mails is to configure your scanner to send PDFs to your workspace email. Scanned files will appear automatically in your workspace. Then, process your documents in bulk with the split tool: launch user defined actions, request a signature, convert to vendor bills with AI, etc.</p>
|
||||||
|
% endif
|
||||||
|
<img src="/documents/static/src/img/documents-paperless.png" class="illustration_border" />
|
||||||
|
</div>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,296 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
|
||||||
|
<!-- activity types -->
|
||||||
|
|
||||||
|
<record id="mail_documents_activity_data_Inbox" model="mail.activity.type">
|
||||||
|
<field name="name">Inbox</field>
|
||||||
|
<field name="res_model_id" ref="dms.model_documents_document"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="mail_documents_activity_data_tv" model="mail.activity.type">
|
||||||
|
<field name="name">To validate</field>
|
||||||
|
<field name="res_model_id" ref="dms.model_documents_document"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="mail_documents_activity_data_md" model="mail.activity.type">
|
||||||
|
<field name="name">Requested Document</field>
|
||||||
|
<field name="category">upload_file</field>
|
||||||
|
<field name="res_model_id" ref="dms.model_documents_document"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- <!– Folders –>-->
|
||||||
|
|
||||||
|
<!-- <record id="documents_internal_folder" model="documents.folder">-->
|
||||||
|
<!-- <field name="name">Internal</field>-->
|
||||||
|
<!-- <field name="description" type="html">-->
|
||||||
|
<!-- <p>Categorize, share and keep track of all your internal documents.</p>-->
|
||||||
|
<!-- <p>Incoming letters sent to inbox email alias will be added to your inbox automatically.</p>-->
|
||||||
|
<!-- </field>-->
|
||||||
|
<!-- <field name="sequence">1</field>-->
|
||||||
|
<!-- </record>-->
|
||||||
|
|
||||||
|
<!-- <record id="documents_finance_folder" model="documents.folder">-->
|
||||||
|
<!-- <field name="name">Finance</field>-->
|
||||||
|
<!-- <field name="description" type="html">-->
|
||||||
|
<!-- <center>-->
|
||||||
|
<!-- <p>Automate your inbox using scanned documents or emails sent to <strong>inbox-financial</strong> email alias.</p>-->
|
||||||
|
<!-- </center>-->
|
||||||
|
<!-- </field>-->
|
||||||
|
<!-- <field name="sequence">11</field>-->
|
||||||
|
<!-- </record>-->
|
||||||
|
|
||||||
|
<!-- <record id="documents_marketing_folder" model="documents.folder">-->
|
||||||
|
<!-- <field name="name">Marketing</field>-->
|
||||||
|
<!-- <field name="sequence">13</field>-->
|
||||||
|
<!-- </record>-->
|
||||||
|
|
||||||
|
<!-- <!– Facets internal –>-->
|
||||||
|
|
||||||
|
<!-- <record id="documents_internal_status" model="documents.facet">-->
|
||||||
|
<!-- <field name="name">Status</field>-->
|
||||||
|
<!-- <field name="sequence">1</field>-->
|
||||||
|
<!-- <field name="folder_id" ref="documents_internal_folder"/>-->
|
||||||
|
<!-- </record>-->
|
||||||
|
|
||||||
|
<!-- <record id="documents_internal_knowledge" model="documents.facet">-->
|
||||||
|
<!-- <field name="name">Knowledge</field>-->
|
||||||
|
<!-- <field name="sequence">6</field>-->
|
||||||
|
<!-- <field name="folder_id" ref="documents_internal_folder"/>-->
|
||||||
|
<!-- </record>-->
|
||||||
|
|
||||||
|
<!-- <record id="documents_internal_template" model="documents.facet">-->
|
||||||
|
<!-- <field name="name">Templates</field>-->
|
||||||
|
<!-- <field name="sequence">6</field>-->
|
||||||
|
<!-- <field name="folder_id" ref="documents_internal_folder"/>-->
|
||||||
|
<!-- </record>-->
|
||||||
|
|
||||||
|
<!-- <!– Facets finance –>-->
|
||||||
|
|
||||||
|
<!-- <record id="documents_finance_status" model="documents.facet">-->
|
||||||
|
<!-- <field name="name">Status</field>-->
|
||||||
|
<!-- <field name="sequence">1</field>-->
|
||||||
|
<!-- <field name="folder_id" ref="documents_finance_folder"/>-->
|
||||||
|
<!-- </record>-->
|
||||||
|
|
||||||
|
<!-- <record id="documents_finance_documents" model="documents.facet">-->
|
||||||
|
<!-- <field name="name">Documents</field>-->
|
||||||
|
<!-- <field name="sequence">5</field>-->
|
||||||
|
<!-- <field name="folder_id" ref="documents_finance_folder"/>-->
|
||||||
|
<!-- </record>-->
|
||||||
|
|
||||||
|
<!-- <record id="documents_finance_fiscal_year" model="documents.facet">-->
|
||||||
|
<!-- <field name="name">Fiscal years</field>-->
|
||||||
|
<!-- <field name="sequence">5</field>-->
|
||||||
|
<!-- <field name="folder_id" ref="documents_finance_folder"/>-->
|
||||||
|
<!-- </record>-->
|
||||||
|
|
||||||
|
|
||||||
|
<!-- <!– Facets marketing –>-->
|
||||||
|
|
||||||
|
<!-- <record id="documents_marketing_assets" model="documents.facet">-->
|
||||||
|
<!-- <field name="name">Assets</field>-->
|
||||||
|
<!-- <field name="sequence">5</field>-->
|
||||||
|
<!-- <field name="folder_id" ref="documents_marketing_folder"/>-->
|
||||||
|
<!-- </record>-->
|
||||||
|
|
||||||
|
<!-- <!– tags internal –>-->
|
||||||
|
|
||||||
|
<!-- <record id="documents_internal_status_inbox" model="documents.tag">-->
|
||||||
|
<!-- <field name="name">Inbox</field>-->
|
||||||
|
<!-- <field name="facet_id" ref="documents_internal_status"/>-->
|
||||||
|
<!-- <field name="sequence">2</field>-->
|
||||||
|
<!-- </record>-->
|
||||||
|
|
||||||
|
<!-- <record id="documents_internal_status_tc" model="documents.tag">-->
|
||||||
|
<!-- <field name="name">To Validate</field>-->
|
||||||
|
<!-- <field name="facet_id" ref="documents_internal_status"/>-->
|
||||||
|
<!-- <field name="sequence">3</field>-->
|
||||||
|
<!-- </record>-->
|
||||||
|
|
||||||
|
<!-- <record id="documents_internal_status_validated" model="documents.tag">-->
|
||||||
|
<!-- <field name="name">Validated</field>-->
|
||||||
|
<!-- <field name="facet_id" ref="documents_internal_status"/>-->
|
||||||
|
<!-- <field name="sequence">5</field>-->
|
||||||
|
<!-- </record>-->
|
||||||
|
|
||||||
|
<!-- <record id="documents_internal_status_deprecated" model="documents.tag">-->
|
||||||
|
<!-- <field name="name">Deprecated</field>-->
|
||||||
|
<!-- <field name="facet_id" ref="documents_internal_status"/>-->
|
||||||
|
<!-- <field name="sequence">6</field>-->
|
||||||
|
<!-- </record>-->
|
||||||
|
|
||||||
|
<!-- <record id="documents_internal_knowledge_hr" model="documents.tag">-->
|
||||||
|
<!-- <field name="name">HR</field>-->
|
||||||
|
<!-- <field name="facet_id" ref="documents_internal_knowledge"/>-->
|
||||||
|
<!-- <field name="sequence">9</field>-->
|
||||||
|
<!-- </record>-->
|
||||||
|
|
||||||
|
<!-- <record id="documents_internal_knowledge_sales" model="documents.tag">-->
|
||||||
|
<!-- <field name="name">Sales</field>-->
|
||||||
|
<!-- <field name="facet_id" ref="documents_internal_knowledge"/>-->
|
||||||
|
<!-- <field name="sequence">9</field>-->
|
||||||
|
<!-- </record>-->
|
||||||
|
|
||||||
|
<!-- <record id="documents_internal_knowledge_legal" model="documents.tag">-->
|
||||||
|
<!-- <field name="name">Legal</field>-->
|
||||||
|
<!-- <field name="facet_id" ref="documents_internal_knowledge"/>-->
|
||||||
|
<!-- <field name="sequence">9</field>-->
|
||||||
|
<!-- </record>-->
|
||||||
|
|
||||||
|
<!-- <record id="documents_internal_knowledge_other" model="documents.tag">-->
|
||||||
|
<!-- <field name="name">Other</field>-->
|
||||||
|
<!-- <field name="facet_id" ref="documents_internal_knowledge"/>-->
|
||||||
|
<!-- <field name="sequence">10</field>-->
|
||||||
|
<!-- </record>-->
|
||||||
|
|
||||||
|
<!-- <record id="documents_internal_template_presentations" model="documents.tag">-->
|
||||||
|
<!-- <field name="name">Presentations</field>-->
|
||||||
|
<!-- <field name="facet_id" ref="documents_internal_template"/>-->
|
||||||
|
<!-- <field name="sequence">10</field>-->
|
||||||
|
<!-- </record>-->
|
||||||
|
|
||||||
|
<!-- <record id="documents_internal_template_contracts" model="documents.tag">-->
|
||||||
|
<!-- <field name="name">Contracts</field>-->
|
||||||
|
<!-- <field name="facet_id" ref="documents_internal_template"/>-->
|
||||||
|
<!-- <field name="sequence">10</field>-->
|
||||||
|
<!-- </record>-->
|
||||||
|
|
||||||
|
<!-- <record id="documents_internal_template_project" model="documents.tag">-->
|
||||||
|
<!-- <field name="name">Project</field>-->
|
||||||
|
<!-- <field name="facet_id" ref="documents_internal_template"/>-->
|
||||||
|
<!-- <field name="sequence">10</field>-->
|
||||||
|
<!-- </record>-->
|
||||||
|
|
||||||
|
<!-- <record id="documents_internal_template_text" model="documents.tag">-->
|
||||||
|
<!-- <field name="name">Text</field>-->
|
||||||
|
<!-- <field name="facet_id" ref="documents_internal_template"/>-->
|
||||||
|
<!-- <field name="sequence">10</field>-->
|
||||||
|
<!-- </record>-->
|
||||||
|
|
||||||
|
<!-- <!– tags finance –>-->
|
||||||
|
|
||||||
|
<!-- <record id="documents_finance_status_inbox" model="documents.tag">-->
|
||||||
|
<!-- <field name="name">Inbox</field>-->
|
||||||
|
<!-- <field name="facet_id" ref="documents_finance_status"/>-->
|
||||||
|
<!-- <field name="sequence">6</field>-->
|
||||||
|
<!-- </record>-->
|
||||||
|
|
||||||
|
<!-- <record id="documents_finance_status_tc" model="documents.tag">-->
|
||||||
|
<!-- <field name="name">To Validate</field>-->
|
||||||
|
<!-- <field name="facet_id" ref="documents_finance_status"/>-->
|
||||||
|
<!-- <field name="sequence">6</field>-->
|
||||||
|
<!-- </record>-->
|
||||||
|
|
||||||
|
<!-- <record id="documents_finance_status_validated" model="documents.tag">-->
|
||||||
|
<!-- <field name="name">Validated</field>-->
|
||||||
|
<!-- <field name="facet_id" ref="documents_finance_status"/>-->
|
||||||
|
<!-- <field name="sequence">6</field>-->
|
||||||
|
<!-- </record>-->
|
||||||
|
|
||||||
|
<!-- <record id="documents_finance_documents_bill" model="documents.tag">-->
|
||||||
|
<!-- <field name="name">Bill</field>-->
|
||||||
|
<!-- <field name="facet_id" ref="documents_finance_documents"/>-->
|
||||||
|
<!-- <field name="sequence">5</field>-->
|
||||||
|
<!-- </record>-->
|
||||||
|
|
||||||
|
<!-- <record id="documents_finance_documents_expense" model="documents.tag">-->
|
||||||
|
<!-- <field name="name">Expense</field>-->
|
||||||
|
<!-- <field name="facet_id" ref="documents_finance_documents"/>-->
|
||||||
|
<!-- <field name="sequence">6</field>-->
|
||||||
|
<!-- </record>-->
|
||||||
|
|
||||||
|
<!-- <record id="documents_finance_documents_vat" model="documents.tag">-->
|
||||||
|
<!-- <field name="name">VAT</field>-->
|
||||||
|
<!-- <field name="facet_id" ref="documents_finance_documents"/>-->
|
||||||
|
<!-- <field name="sequence">7</field>-->
|
||||||
|
<!-- </record>-->
|
||||||
|
|
||||||
|
<!-- <record id="documents_finance_documents_fiscal" model="documents.tag">-->
|
||||||
|
<!-- <field name="name">Fiscal</field>-->
|
||||||
|
<!-- <field name="facet_id" ref="documents_finance_documents"/>-->
|
||||||
|
<!-- <field name="sequence">8</field>-->
|
||||||
|
<!-- </record>-->
|
||||||
|
|
||||||
|
<!-- <record id="documents_finance_documents_financial" model="documents.tag">-->
|
||||||
|
<!-- <field name="name">Financial</field>-->
|
||||||
|
<!-- <field name="facet_id" ref="documents_finance_documents"/>-->
|
||||||
|
<!-- <field name="sequence">9</field>-->
|
||||||
|
<!-- </record>-->
|
||||||
|
|
||||||
|
<!-- <record id="documents_finance_documents_Contracts" model="documents.tag">-->
|
||||||
|
<!-- <field name="name">Contracts</field>-->
|
||||||
|
<!-- <field name="facet_id" ref="documents_finance_documents"/>-->
|
||||||
|
<!-- <field name="sequence">10</field>-->
|
||||||
|
<!-- </record>-->
|
||||||
|
|
||||||
|
<!-- <record id="documents_finance_fiscal_year_2018" model="documents.tag">-->
|
||||||
|
<!-- <field name="name" eval="str(datetime.now().year)"/>-->
|
||||||
|
<!-- <field name="facet_id" ref="documents_finance_fiscal_year"/>-->
|
||||||
|
<!-- <field name="sequence">6</field>-->
|
||||||
|
<!-- </record>-->
|
||||||
|
|
||||||
|
<!-- <record id="documents_finance_fiscal_year_2017" model="documents.tag">-->
|
||||||
|
<!-- <field name="name" eval="str(datetime.now().year-1)"/>-->
|
||||||
|
<!-- <field name="facet_id" ref="documents_finance_fiscal_year"/>-->
|
||||||
|
<!-- <field name="sequence">5</field>-->
|
||||||
|
<!-- </record>-->
|
||||||
|
|
||||||
|
<!-- <!– tags marketing –>-->
|
||||||
|
|
||||||
|
<!-- <record id="documents_marketing_assets_ads" model="documents.tag">-->
|
||||||
|
<!-- <field name="name">Ads</field>-->
|
||||||
|
<!-- <field name="facet_id" ref="documents_marketing_assets"/>-->
|
||||||
|
<!-- <field name="sequence">10</field>-->
|
||||||
|
<!-- </record>-->
|
||||||
|
|
||||||
|
<!-- <record id="documents_marketing_assets_brochures" model="documents.tag">-->
|
||||||
|
<!-- <field name="name">Brochures</field>-->
|
||||||
|
<!-- <field name="facet_id" ref="documents_marketing_assets"/>-->
|
||||||
|
<!-- <field name="sequence">11</field>-->
|
||||||
|
<!-- </record>-->
|
||||||
|
|
||||||
|
<!-- <record id="documents_marketing_assets_images" model="documents.tag">-->
|
||||||
|
<!-- <field name="name">Images</field>-->
|
||||||
|
<!-- <field name="facet_id" ref="documents_marketing_assets"/>-->
|
||||||
|
<!-- <field name="sequence">12</field>-->
|
||||||
|
<!-- </record>-->
|
||||||
|
|
||||||
|
<!-- <record id="documents_marketing_assets_Videos" model="documents.tag">-->
|
||||||
|
<!-- <field name="name">Videos</field>-->
|
||||||
|
<!-- <field name="facet_id" ref="documents_marketing_assets"/>-->
|
||||||
|
<!-- <field name="sequence">13</field>-->
|
||||||
|
<!-- </record>-->
|
||||||
|
|
||||||
|
<!-- <!– share links –>-->
|
||||||
|
|
||||||
|
<!-- <record id="share_account_folder" model="documents.share">-->
|
||||||
|
<!-- <field name="name">Inbox Financial</field>-->
|
||||||
|
<!-- <field name="alias_name">inbox-financial</field>-->
|
||||||
|
<!-- <field name="type">domain</field>-->
|
||||||
|
<!-- <field name="folder_id" ref="documents_finance_folder"/>-->
|
||||||
|
<!-- <field name="action">downloadupload</field>-->
|
||||||
|
<!-- <field name="tag_ids" eval="[(6,0,[ref('documents_finance_status_inbox')])]"/>-->
|
||||||
|
<!-- <field name="email_drop">True</field>-->
|
||||||
|
<!-- <field name="activity_option">True</field>-->
|
||||||
|
<!-- <field name="activity_type_id" ref="mail_documents_activity_data_Inbox"/>-->
|
||||||
|
<!-- <field name="activity_date_deadline_range">7</field>-->
|
||||||
|
<!-- <field name="activity_date_deadline_range_type">days</field>-->
|
||||||
|
<!-- </record>-->
|
||||||
|
|
||||||
|
<!-- <record id="share_internal_folder" model="documents.share">-->
|
||||||
|
<!-- <field name="name">Inbox Internal</field>-->
|
||||||
|
<!-- <field name="alias_name">inbox</field>-->
|
||||||
|
<!-- <field name="type">domain</field>-->
|
||||||
|
<!-- <field name="folder_id" ref="documents_internal_folder"/>-->
|
||||||
|
<!-- <field name="action">downloadupload</field>-->
|
||||||
|
<!-- <field name="tag_ids" eval="[(6,0,[ref('documents_internal_status_inbox')])]"/>-->
|
||||||
|
<!-- <field name="email_drop">True</field>-->
|
||||||
|
<!-- <field name="activity_option">True</field>-->
|
||||||
|
<!-- <field name="activity_type_id" ref="mail_documents_activity_data_Inbox"/>-->
|
||||||
|
<!-- <field name="activity_date_deadline_range">7</field>-->
|
||||||
|
<!-- <field name="activity_date_deadline_range_type">days</field>-->
|
||||||
|
<!-- </record>-->
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
|
After Width: | Height: | Size: 72 KiB |
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
|
||||||
|
<!-- base data -->
|
||||||
|
|
||||||
|
<record id="documents_attachment_video_documents" model="documents.document">
|
||||||
|
<field name="name">Video: Odoo Documents</field>
|
||||||
|
<field name="type">url</field>
|
||||||
|
<field name="url">https://youtu.be/Ayab6wZ_U1A</field>
|
||||||
|
<field name="folder_id" ref="documents.documents_internal_folder"/>
|
||||||
|
<field name="tag_ids" eval="[(6,0,[ref('documents.documents_internal_template_presentations'),
|
||||||
|
ref('documents.documents_internal_status_validated')])]"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="documents_image_multi_pdf_document" model="documents.document">
|
||||||
|
<field name="name">Mails_inbox.pdf</field>
|
||||||
|
<field name="datas" type="base64" file="documents/data/files/Mails_inbox.pdf"/>
|
||||||
|
<field name="folder_id" ref="documents.documents_internal_folder"/>
|
||||||
|
<field name="tag_ids" eval="[(6,0,[ref('documents.documents_internal_status_inbox')])]"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="documents_invoice_png" model="documents.document">
|
||||||
|
<field name="name">invoice.png</field>
|
||||||
|
<field name="datas" type="base64" file="documents/data/files/invoice.png"/>
|
||||||
|
<field name="folder_id" ref="documents.documents_internal_folder"/>
|
||||||
|
<field name="tag_ids" eval="[(6,0,[ref('documents.documents_internal_status_inbox')])]"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<!--Email template -->
|
||||||
|
<record id="mail_template_document_request" model="mail.template">
|
||||||
|
<field name="name">Document Request: Send by email</field>
|
||||||
|
<field name="model_id" ref="model_documents_share"/>
|
||||||
|
<field name="subject">Document Request ${object.name != False and ': '+ object.name or ''}</field>
|
||||||
|
<field name="email_to">${object.owner_id.email_formatted | safe}</field>
|
||||||
|
<field name="body_html" type="html">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" style="padding-top: 16px; background-color: #F1F1F1; font-family:Verdana, Arial,sans-serif; color: #454748; width: 100%; border-collapse:separate;"><tr><td align="center">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" width="590" style="padding: 16px; background-color: white; color: #454748; border-collapse:separate;">
|
||||||
|
<tbody>
|
||||||
|
<!-- HEADER -->
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="min-width: 590px;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
|
||||||
|
<tr><td valign="middle">
|
||||||
|
<span style="font-size: 10px;">
|
||||||
|
Document Request: <br/>
|
||||||
|
% if object.name:
|
||||||
|
<span style="font-size: 20px; font-weight: bold;">
|
||||||
|
${object.name | safe}
|
||||||
|
</span>
|
||||||
|
% endif
|
||||||
|
</span><br/>
|
||||||
|
</td><td valign="middle" align="right">
|
||||||
|
<img src="/logo.png?company=${object.create_uid.company_id.id}" style="padding: 0px; margin: 0px; height: auto; width: 80px;" alt="${object.create_uid.company_id.name}"/>
|
||||||
|
</td></tr>
|
||||||
|
<tr><td colspan="2" style="text-align:center;">
|
||||||
|
<hr width="100%" style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin: 16px 0px 16px 0px;"/>
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- CONTENT -->
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="min-width: 590px;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: white; padding: 0px 8px 0px 8px; border-collapse:separate;">
|
||||||
|
<tr><td valign="top" style="font-size: 13px;">
|
||||||
|
<div>
|
||||||
|
Hello ${object.owner_id.name},
|
||||||
|
<br/><br/>
|
||||||
|
${object.create_uid.name} (${object.create_uid.email}) asks you to provide the following document:
|
||||||
|
<br/><br/>
|
||||||
|
<center>
|
||||||
|
<div>
|
||||||
|
% if object.name:
|
||||||
|
<b>${object.name | safe}</b>
|
||||||
|
% endif
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
% if object.activity_note:
|
||||||
|
<i>${object.activity_note | safe}</i>
|
||||||
|
% endif
|
||||||
|
</div>
|
||||||
|
<br/>
|
||||||
|
<div style="margin: 16px 0px 16px 0px;">
|
||||||
|
<a href="${object.full_url}"
|
||||||
|
style="background-color: #875A7B; padding: 20px 30px 20px 30px; text-decoration: none; color: #fff; border-radius: 5px; font-size:13px;">
|
||||||
|
Upload the requested document
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</center><br/>
|
||||||
|
Please provide us with the missing document before the link expires (planned on ${object.date_deadline}).
|
||||||
|
% if user and user.signature:
|
||||||
|
<br/>
|
||||||
|
${user.signature | safe}
|
||||||
|
<br/>
|
||||||
|
% endif
|
||||||
|
</div>
|
||||||
|
</td></tr>
|
||||||
|
<tr><td style="text-align:center;">
|
||||||
|
<hr width="100%" style="background-color:rgb(204,204,204);border:medium none;clear:both;display:block;font-size:0px;min-height:1px;line-height:0; margin: 16px 0px 16px 0px;"/>
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- FOOTER -->
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="min-width: 590px;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: white; font-size: 11px; padding: 0px 8px 0px 8px; border-collapse:separate;">
|
||||||
|
<tr><td valign="middle" align="left">
|
||||||
|
${object.create_uid.company_id.name}
|
||||||
|
</td></tr>
|
||||||
|
<tr><td valign="middle" align="left" style="opacity: 0.7;">
|
||||||
|
${object.create_uid.company_id.phone}
|
||||||
|
% if object.create_uid.company_id.email
|
||||||
|
| <a href="'mailto:%s' % ${object.create_uid.company_id.email}" style="text-decoration:none; color: #454748;">${object.create_uid.company_id.email}</a>
|
||||||
|
% endif
|
||||||
|
% if object.create_uid.company_id.website
|
||||||
|
| <a href="'%s' % ${object.create_uid.company_id.website}" style="text-decoration:none; color: #454748;">
|
||||||
|
${object.create_uid.company_id.website}
|
||||||
|
</a>
|
||||||
|
% endif
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td></tr>
|
||||||
|
<!-- POWERED BY -->
|
||||||
|
<tr><td align="center" style="min-width: 590px;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" width="590" style="min-width: 590px; background-color: #F1F1F1; color: #454748; padding: 8px; border-collapse:separate;">
|
||||||
|
<tr><td style="text-align: center; font-size: 13px;">
|
||||||
|
% if object.date_deadline:
|
||||||
|
This link expires on <b>${object.date_deadline}.</b><br/>
|
||||||
|
% endif
|
||||||
|
Powered by <a target="_blank" href="https://www.odoo.com/page/documents" style="color: #875A7B;">Odoo Documents</a>
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
</td></tr>
|
||||||
|
</table>
|
||||||
|
</field>
|
||||||
|
<field name="lang">${object.owner_id.lang}</field>
|
||||||
|
<field name="auto_delete" eval="True"/>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,204 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
|
||||||
|
<!-- Workflow -->
|
||||||
|
|
||||||
|
<!-- Internal -->
|
||||||
|
|
||||||
|
<!-- internal deprecate -->
|
||||||
|
|
||||||
|
<record id="documents_rule_internal_deprecate" model="documents.workflow.rule">
|
||||||
|
<field name="name">Deprecate</field>
|
||||||
|
<field name="sequence">7</field>
|
||||||
|
<field name="remove_activities">True</field>
|
||||||
|
<field name="domain" eval="['|', ('tag_ids', '=', False), ('tag_ids', 'in', [
|
||||||
|
ref('documents.documents_internal_status_validated'),
|
||||||
|
ref('documents.documents_internal_status_tc')])]"/>
|
||||||
|
<field name="domain_folder_id" ref="documents_internal_folder"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="documents_workflow_action_deprecate" model="documents.workflow.action">
|
||||||
|
<field name="workflow_rule_id" ref="documents_rule_internal_deprecate"/>
|
||||||
|
<field name="action">replace</field>
|
||||||
|
<field name="facet_id" ref="documents.documents_internal_status"/>
|
||||||
|
<field name="tag_id" ref="documents.documents_internal_status_deprecated"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- internal mark as draft -->
|
||||||
|
|
||||||
|
<record id="documents_rule_internal_mad" model="documents.workflow.rule">
|
||||||
|
<field name="name">Mark As Draft</field>
|
||||||
|
<field name="sequence">5</field>
|
||||||
|
<field name="condition_type">domain</field>
|
||||||
|
<field name="domain" eval="['|', ('tag_ids', '=', False), ('tag_ids', 'in', [
|
||||||
|
ref('documents.documents_internal_status_deprecated'),
|
||||||
|
ref('documents.documents_internal_status_tc')])]"/>
|
||||||
|
<field name="remove_activities">True</field>
|
||||||
|
<field name="domain_folder_id" ref="documents_internal_folder"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="documents_workflow_action_mad" model="documents.workflow.action">
|
||||||
|
<field name="workflow_rule_id" ref="documents_rule_internal_mad"/>
|
||||||
|
<field name="action">replace</field>
|
||||||
|
<field name="facet_id" ref="documents.documents_internal_status"/>
|
||||||
|
<field name="tag_id" ref="documents.documents_internal_status_inbox"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- internal ask for validation -->
|
||||||
|
|
||||||
|
<record id="documents_rule_internal_legal" model="documents.workflow.rule">
|
||||||
|
<field name="name">Send to Legal</field>
|
||||||
|
<field name="sequence">5</field>
|
||||||
|
<field name="condition_type">domain</field>
|
||||||
|
<field name="domain" eval="[('tag_ids', 'in', [
|
||||||
|
ref('documents.documents_internal_status_inbox'),
|
||||||
|
ref('documents.documents_internal_status_validated')])]"/>
|
||||||
|
<field name="domain_folder_id" ref="documents.documents_internal_folder"/>
|
||||||
|
<field name="activity_option">True</field>
|
||||||
|
<field name="activity_type_id" ref="documents.mail_documents_activity_data_tv"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="documents_workflow_action_afv" model="documents.workflow.action">
|
||||||
|
<field name="workflow_rule_id" ref="documents_rule_internal_legal"/>
|
||||||
|
<field name="action">replace</field>
|
||||||
|
<field name="facet_id" ref="documents.documents_internal_status"/>
|
||||||
|
<field name="tag_id" ref="documents.documents_internal_status_tc"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="documents_workflow_action_afv_legal" model="documents.workflow.action">
|
||||||
|
<field name="workflow_rule_id" ref="documents_rule_internal_legal"/>
|
||||||
|
<field name="action">add</field>
|
||||||
|
<field name="facet_id" ref="documents.documents_internal_knowledge"/>
|
||||||
|
<field name="tag_id" ref="documents.documents_internal_knowledge_legal"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- internal mark as bill -->
|
||||||
|
|
||||||
|
<record id="documents_rule_internal_mark" model="documents.workflow.rule">
|
||||||
|
<field name="sequence">2</field>
|
||||||
|
<field name="name">Mark As Bill</field>
|
||||||
|
<field name="condition_type">domain</field>
|
||||||
|
<field name="domain" eval="[('tag_ids', 'in', [
|
||||||
|
ref('documents.documents_internal_status_tc'),
|
||||||
|
ref('documents.documents_internal_status_inbox')])]"/>
|
||||||
|
<field name="remove_activities">True</field>
|
||||||
|
<field name="domain_folder_id" ref="documents.documents_internal_folder"/>
|
||||||
|
<field name="folder_id" ref="documents.documents_finance_folder"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="documents_workflow_action_mark_inbox" model="documents.workflow.action">
|
||||||
|
<field name="workflow_rule_id" ref="documents_rule_internal_mark"/>
|
||||||
|
<field name="action">add</field>
|
||||||
|
<field name="facet_id" ref="documents.documents_finance_status"/>
|
||||||
|
<field name="tag_id" ref="documents.documents_finance_status_inbox"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="documents_workflow_action_mark_bill" model="documents.workflow.action">
|
||||||
|
<field name="workflow_rule_id" ref="documents_rule_internal_mark"/>
|
||||||
|
<field name="action">add</field>
|
||||||
|
<field name="facet_id" ref="documents.documents_finance_documents"/>
|
||||||
|
<field name="tag_id" ref="documents.documents_finance_documents_bill"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Finance -->
|
||||||
|
|
||||||
|
<!-- finance validate -->
|
||||||
|
|
||||||
|
<record id="documents_rule_finance_validate" model="documents.workflow.rule">
|
||||||
|
<field name="sequence">6</field>
|
||||||
|
<field name="name">Validate</field>
|
||||||
|
<field name="condition_type">domain</field>
|
||||||
|
<field name="domain" eval="[('tag_ids', 'in', [
|
||||||
|
ref('documents.documents_finance_status_tc'),
|
||||||
|
ref('documents.documents_finance_status_inbox')])]"/>
|
||||||
|
<field name="remove_activities">True</field>
|
||||||
|
<field name="domain_folder_id" ref="documents.documents_finance_folder"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="documents_workflow_action_finance_validate" model="documents.workflow.action">
|
||||||
|
<field name="workflow_rule_id" ref="documents_rule_finance_validate"/>
|
||||||
|
<field name="action">replace</field>
|
||||||
|
<field name="facet_id" ref="documents.documents_finance_status"/>
|
||||||
|
<field name="tag_id" ref="documents.documents_finance_status_validated"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- finance ask for validation -->
|
||||||
|
|
||||||
|
<record id="documents_rule_finance_afv" model="documents.workflow.rule">
|
||||||
|
<field name="sequence">7</field>
|
||||||
|
<field name="name">Ask for Validation</field>
|
||||||
|
<field name="condition_type">domain</field>
|
||||||
|
<field name="domain" eval="[('tag_ids', 'in', [
|
||||||
|
ref('documents.documents_finance_status_inbox'),
|
||||||
|
ref('documents.documents_finance_status_validated')])]"/>
|
||||||
|
<field name="domain_folder_id" ref="documents_finance_folder"/>
|
||||||
|
<field name="activity_option">True</field>
|
||||||
|
<field name="activity_type_id" ref="documents.mail_documents_activity_data_tv"/>
|
||||||
|
<field name="domain_folder_id" ref="documents.documents_finance_folder"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="documents_workflow_action_finance_afv" model="documents.workflow.action">
|
||||||
|
<field name="workflow_rule_id" ref="documents_rule_finance_afv"/>
|
||||||
|
<field name="action">replace</field>
|
||||||
|
<field name="facet_id" ref="documents.documents_finance_status"/>
|
||||||
|
<field name="tag_id" ref="documents.documents_finance_status_tc"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- finance move to inbox -->
|
||||||
|
|
||||||
|
<record id="documents_rule_finance_mti" model="documents.workflow.rule">
|
||||||
|
<field name="sequence">7</field>
|
||||||
|
<field name="name">Move To Inbox</field>
|
||||||
|
<field name="condition_type">domain</field>
|
||||||
|
<field name="domain" eval="['|', ('tag_ids', '=', False), ('tag_ids', 'in', [
|
||||||
|
ref('documents.documents_finance_status_validated')])]"/>
|
||||||
|
<field name="domain_folder_id" ref="documents_finance_folder"/>
|
||||||
|
<field name="activity_option">True</field>
|
||||||
|
<field name="activity_type_id" ref="documents.mail_documents_activity_data_tv"/>
|
||||||
|
<field name="domain_folder_id" ref="documents.documents_finance_folder"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="documents_workflow_action_finance_mti" model="documents.workflow.action">
|
||||||
|
<field name="workflow_rule_id" ref="documents_rule_finance_mti"/>
|
||||||
|
<field name="action">replace</field>
|
||||||
|
<field name="facet_id" ref="documents.documents_finance_status"/>
|
||||||
|
<field name="tag_id" ref="documents.documents_finance_status_inbox"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- finance set as 2018 contract-->
|
||||||
|
|
||||||
|
<record id="documents_rule_finance_2018contracts" model="documents.workflow.rule">
|
||||||
|
<field name="sequence">8</field>
|
||||||
|
<field name="name" eval="'Set As ' + str(datetime.now().year) + ' Contracts'"/>
|
||||||
|
<field name="domain_folder_id" ref="documents_finance_folder"/>
|
||||||
|
<field name="required_tag_ids" eval="[(4, ref('documents.documents_finance_status_inbox'))]"/>
|
||||||
|
<field name="activity_option">True</field>
|
||||||
|
<field name="activity_type_id" ref="documents.mail_documents_activity_data_tv"/>
|
||||||
|
<field name="domain_folder_id" ref="documents.documents_finance_folder"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="documents_workflow_action_finance_status_validated" model="documents.workflow.action">
|
||||||
|
<field name="workflow_rule_id" ref="documents_rule_finance_2018contracts"/>
|
||||||
|
<field name="action">replace</field>
|
||||||
|
<field name="facet_id" ref="documents.documents_finance_status"/>
|
||||||
|
<field name="tag_id" ref="documents.documents_finance_status_validated"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="documents_workflow_action_finance_2018contracts" model="documents.workflow.action">
|
||||||
|
<field name="workflow_rule_id" ref="documents_rule_finance_2018contracts"/>
|
||||||
|
<field name="action">replace</field>
|
||||||
|
<field name="facet_id" ref="documents.documents_finance_fiscal_year"/>
|
||||||
|
<field name="tag_id" ref="documents.documents_finance_fiscal_year_2018"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="documents_workflow_action_finance_DocumentContract" model="documents.workflow.action">
|
||||||
|
<field name="workflow_rule_id" ref="documents_rule_finance_2018contracts"/>
|
||||||
|
<field name="action">replace</field>
|
||||||
|
<field name="facet_id" ref="documents.documents_finance_documents"/>
|
||||||
|
<field name="tag_id" ref="documents.documents_finance_documents_Contracts"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
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
|
||||||
|
from . import dms_settings
|
||||||
|
from . import attach
|
||||||
|
# new
|
||||||
|
from . import document
|
||||||
|
from . import share
|
||||||
|
from . import res_partner
|
||||||
|
from . import documents_mixin
|
||||||
|
from . import mail_activity
|
||||||
|
from . import tags
|
||||||
|
from . import workflow
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||||
|
|
||||||
|
from odoo import fields, models , api
|
||||||
|
|
||||||
|
|
||||||
|
class AbstractDmsMixin(models.AbstractModel):
|
||||||
|
_name = "abstract.dms.mixin"
|
||||||
|
_description = "Abstract Dms Mixin"
|
||||||
|
|
||||||
|
name = fields.Char(string="Name", required=True, index=True)
|
||||||
|
# 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=True,
|
||||||
|
)
|
||||||
|
storage_id_save_type = fields.Selection(related="storage_id.save_type", store=False)
|
||||||
|
color = fields.Integer(string="Color", 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):
|
||||||
|
"""Remove the limit of records (default is 200 since js)."""
|
||||||
|
kwargs.update(limit=False)
|
||||||
|
_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
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,169 @@
|
||||||
|
# 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(string="Parent Path", index=True)
|
||||||
|
|
||||||
|
# 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")
|
||||||
|
perm_download = fields.Boolean(string="Download Access")
|
||||||
|
|
||||||
|
# Permissions computed including parent group
|
||||||
|
perm_inclusive_create = fields.Boolean(
|
||||||
|
string="Inherited Create Access",
|
||||||
|
compute="_compute_inclusive_permissions",
|
||||||
|
store=True,
|
||||||
|
)
|
||||||
|
perm_inclusive_write = fields.Boolean(
|
||||||
|
string="Inherited Write Access",
|
||||||
|
compute="_compute_inclusive_permissions",
|
||||||
|
store=True,
|
||||||
|
)
|
||||||
|
perm_inclusive_unlink = fields.Boolean(
|
||||||
|
string="Inherited Unlink Access",
|
||||||
|
compute="_compute_inclusive_permissions",
|
||||||
|
store=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", string="Users", store=True)
|
||||||
|
count_directories = fields.Integer(
|
||||||
|
compute="_compute_count_directories", string="Count Directories"
|
||||||
|
)
|
||||||
|
parent_group_id = fields.Many2one(
|
||||||
|
comodel_name="dms.access.group",
|
||||||
|
string="Parent Group",
|
||||||
|
ondelete="cascade",
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
@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",
|
||||||
|
"perm_download",
|
||||||
|
)
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
from odoo import _, api, fields, models, tools
|
||||||
|
|
||||||
|
|
||||||
|
class FileAttach(models.Model):
|
||||||
|
_name = "dms.file.attach"
|
||||||
|
_description = "File"
|
||||||
|
|
||||||
|
file_project_ids = fields.One2many(
|
||||||
|
comodel_name='documents.document',
|
||||||
|
inverse_name='project_id',
|
||||||
|
compute="_compute_project_field",
|
||||||
|
string='',
|
||||||
|
required=False)
|
||||||
|
|
||||||
|
file_transaction_ids = fields.One2many(
|
||||||
|
comodel_name='documents.document',
|
||||||
|
inverse_name='attach_id',
|
||||||
|
string='',
|
||||||
|
required=False)
|
||||||
|
|
||||||
|
def _compute_project_field(self):
|
||||||
|
print("******************")
|
||||||
|
related_recordset = self.env["documents.document"].search([])
|
||||||
|
self.file_project_ids = ()
|
||||||
|
s_list = []
|
||||||
|
value = {}
|
||||||
|
for val in self:
|
||||||
|
User_input = related_recordset.search([])
|
||||||
|
for rec in User_input:
|
||||||
|
data = {
|
||||||
|
'file_code': rec.file_code,
|
||||||
|
'write_date': rec.write_date,
|
||||||
|
'create_uid': rec.create_uid,
|
||||||
|
}
|
||||||
|
print(data)
|
||||||
|
s_list.append((0, 0, data))
|
||||||
|
self.file_project_ids = s_list
|
||||||
|
print(self.file_project_ids)
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
# Copyright 2021 Tecnativa - Jairo Llopis
|
||||||
|
# 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."""
|
||||||
|
# result = super().unlink()
|
||||||
|
# self.env["documents.document"].sudo().search(
|
||||||
|
# [("res_model", "=", self._name), ("res_id", "in", self.ids)]
|
||||||
|
# ).unlink()
|
||||||
|
# self.env["dms.directory"].sudo().search(
|
||||||
|
# [("res_model", "=", self._name), ("res_id", "in", self.ids)]
|
||||||
|
# ).unlink()
|
||||||
|
# return result
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
# 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(string="Name", 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)
|
||||||
|
parent_id = fields.Many2one(
|
||||||
|
comodel_name="dms.category",
|
||||||
|
string="Parent Category",
|
||||||
|
ondelete="cascade",
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
child_category_ids = fields.One2many(
|
||||||
|
comodel_name="dms.category",
|
||||||
|
inverse_name="parent_id",
|
||||||
|
string="Child Categories",
|
||||||
|
)
|
||||||
|
|
||||||
|
parent_path = fields.Char(string="Parent Path", index=True)
|
||||||
|
tag_ids = fields.One2many(
|
||||||
|
comodel_name="documents.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="documents.document",
|
||||||
|
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", string="Count Tags")
|
||||||
|
|
||||||
|
count_directories = fields.Integer(
|
||||||
|
compute="_compute_count_directories", string="Count Directories"
|
||||||
|
)
|
||||||
|
|
||||||
|
count_files = fields.Integer(compute="_compute_count_files", string="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)
|
||||||
|
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
# 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
|
||||||
|
|
@ -0,0 +1,836 @@
|
||||||
|
# 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
|
||||||
|
from odoo.osv import expression
|
||||||
|
import logging
|
||||||
|
from odoo import _, api, fields, models, tools
|
||||||
|
from odoo.exceptions import UserError, ValidationError
|
||||||
|
from odoo.osv.expression import OR
|
||||||
|
from odoo.tools import consteq
|
||||||
|
from collections import OrderedDict, defaultdict
|
||||||
|
|
||||||
|
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=True)
|
||||||
|
is_root_directory = fields.Boolean(
|
||||||
|
string="Is Root Directory",
|
||||||
|
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.""",
|
||||||
|
)
|
||||||
|
parent_folder_id = fields.Many2one('dms.directory',
|
||||||
|
string="Parent Workspace",
|
||||||
|
ondelete="cascade",
|
||||||
|
help="A workspace will inherit the tags of its parent workspace")
|
||||||
|
description = fields.Html(string="Description", translate=True)
|
||||||
|
|
||||||
|
# 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,
|
||||||
|
)
|
||||||
|
project_id = fields.Many2one('project.project', string='project.project')
|
||||||
|
settings = fields.Many2one(
|
||||||
|
comodel_name='dms.settings',
|
||||||
|
string="Settings",
|
||||||
|
store=True,
|
||||||
|
auto_join=True,
|
||||||
|
ondelete='restrict',
|
||||||
|
compute='_compute_settings'
|
||||||
|
)
|
||||||
|
|
||||||
|
def _compute_settings(self, write=True):
|
||||||
|
if write:
|
||||||
|
for record in self:
|
||||||
|
if not record.is_root_directory:
|
||||||
|
record.settings = record.parent_id.settings
|
||||||
|
else:
|
||||||
|
self.ensure_one()
|
||||||
|
if self.is_root_directory:
|
||||||
|
return {'settings': self.settings.id}
|
||||||
|
else:
|
||||||
|
return {'settings': self.parent_id.settings.id}
|
||||||
|
|
||||||
|
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=True,
|
||||||
|
store=True,
|
||||||
|
readonly=False,
|
||||||
|
compute="_compute_parent_id",
|
||||||
|
copy=True,
|
||||||
|
default=lambda self: self._default_parent_id(),
|
||||||
|
)
|
||||||
|
|
||||||
|
self_id = fields.Many2one(
|
||||||
|
comodel_name="dms.directory",
|
||||||
|
string="Self Directory",
|
||||||
|
store=True,
|
||||||
|
readonly=False,
|
||||||
|
compute="_compute_self_id",
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
complete_name = fields.Char(
|
||||||
|
"Complete Name", compute="_compute_complete_name", store=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="documents.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",
|
||||||
|
string="Starred",
|
||||||
|
)
|
||||||
|
|
||||||
|
file_ids = fields.One2many(
|
||||||
|
comodel_name="documents.document",
|
||||||
|
inverse_name="folder_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", string="Count Elements"
|
||||||
|
)
|
||||||
|
|
||||||
|
count_total_directories = fields.Integer(string="Total Subdirectories"
|
||||||
|
)
|
||||||
|
|
||||||
|
count_total_files = fields.Integer(string="Total Files"
|
||||||
|
)
|
||||||
|
user_specific = fields.Boolean(string="Own Documents Only",
|
||||||
|
help="Limit Read Groups to the documents of which they are owner.")
|
||||||
|
|
||||||
|
count_total_elements = fields.Integer(
|
||||||
|
compute="_compute_count_total_elements", string="Total Elements"
|
||||||
|
)
|
||||||
|
|
||||||
|
size = fields.Integer(compute="_compute_size", string="Size")
|
||||||
|
|
||||||
|
inherit_group_ids = fields.Boolean(string="Inherit Groups")
|
||||||
|
|
||||||
|
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):
|
||||||
|
super()._compute_access_url()
|
||||||
|
for item in self:
|
||||||
|
item.access_url = "/my/dms/directory/%s" % (item.id)
|
||||||
|
|
||||||
|
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 consteq(current_directory.access_token, access_token):
|
||||||
|
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):
|
||||||
|
return (
|
||||||
|
self.env["dms.directory"]
|
||||||
|
.search([("is_hidden", "=", False), ("parent_id", "=", False)])
|
||||||
|
.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"].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.recompute()
|
||||||
|
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
# 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.model
|
||||||
|
def _search_panel_domain(self, field, operator, parent_id, comodel_domain=False):
|
||||||
|
if not comodel_domain:
|
||||||
|
comodel_domain = []
|
||||||
|
files_ids = self.search([("parent_id", operator, parent_id)]).ids
|
||||||
|
return expression.AND([comodel_domain, [(field, "in", files_ids)]])
|
||||||
|
@api.model
|
||||||
|
def search_panel_select_range(self, field_name, **kwargs):
|
||||||
|
operator, directory_id = self._search_panel_directory(**kwargs)
|
||||||
|
if field_name == 'parent_id':
|
||||||
|
enable_counters = kwargs.get('enable_counters', False)
|
||||||
|
fields = ['display_name', 'name', 'description', 'parent_id']
|
||||||
|
available_folders = self.env['dms.directory'].search([])
|
||||||
|
folder_domain = expression.OR(
|
||||||
|
[[('parent_id', 'parent_of', available_folders.ids)], [('id', 'in', available_folders.ids)]])
|
||||||
|
# also fetches the ancestors of the available folders to display the complete folder tree for all available folders.
|
||||||
|
DocumentFolder = self.env['dms.directory'].sudo().with_context(hierarchical_naming=False)
|
||||||
|
records = DocumentFolder.search_read(folder_domain, fields)
|
||||||
|
domain_image = {}
|
||||||
|
if enable_counters:
|
||||||
|
model_domain = expression.AND([
|
||||||
|
kwargs.get('search_domain', []),
|
||||||
|
kwargs.get('category_domain', []),
|
||||||
|
kwargs.get('filter_domain', []),
|
||||||
|
[(field_name, '!=', False)]
|
||||||
|
])
|
||||||
|
domain_image = self._search_panel_domain_image(field_name, model_domain, enable_counters)
|
||||||
|
comodel_domain = kwargs.pop("comodel_domain", [])
|
||||||
|
# domain_image = self._search_panel_domain(
|
||||||
|
# "file_ids", operator, directory_id, comodel_domain
|
||||||
|
# )
|
||||||
|
|
||||||
|
values_range = OrderedDict()
|
||||||
|
for record in records:
|
||||||
|
record['display_name'] = record['name']
|
||||||
|
record_id = record['id']
|
||||||
|
if enable_counters:
|
||||||
|
image_element = domain_image.get(record_id)
|
||||||
|
record['__count'] = image_element['__count'] if image_element else 0
|
||||||
|
value = record['parent_id']
|
||||||
|
record['parent_id'] = value and value[0]
|
||||||
|
values_range[record_id] = record
|
||||||
|
|
||||||
|
if enable_counters:
|
||||||
|
self._search_panel_global_counters(values_range, 'parent_id')
|
||||||
|
|
||||||
|
return {
|
||||||
|
'parent_field': 'parent_id',
|
||||||
|
'values': list(values_range.values()),
|
||||||
|
}
|
||||||
|
|
||||||
|
return super(DmsDirectory, self).search_panel_select_range(field_name)
|
||||||
|
|
||||||
|
# @api.model
|
||||||
|
# def search_panel_select_range(self, field_name, **kwargs):
|
||||||
|
# if field_name == 'parent_id':
|
||||||
|
# enable_counters = kwargs.get('enable_counters', False)
|
||||||
|
# fields = ['display_name', 'name', 'description', 'self_id']
|
||||||
|
# available_folders = self.env['dms.directory'].search([])
|
||||||
|
# folder_domain = expression.OR(
|
||||||
|
# [[('self_id', 'child_of', available_folders.ids)], [('id', 'in', available_folders.ids)]])
|
||||||
|
# # also fetches the ancestors of the available folders to display the complete folder tree for all available folders.
|
||||||
|
# DocumentFolder = self.env['dms.directory'].sudo().with_context(hierarchical_naming=False)
|
||||||
|
# records = DocumentFolder.search_read(folder_domain, fields)
|
||||||
|
# domain_image = {}
|
||||||
|
# if enable_counters:
|
||||||
|
# model_domain = expression.AND([
|
||||||
|
# kwargs.get('search_domain', []),
|
||||||
|
# kwargs.get('category_domain', []),
|
||||||
|
# kwargs.get('filter_domain', []),
|
||||||
|
# [(field_name, '!=', False)]
|
||||||
|
# ])
|
||||||
|
# domain_image = self._search_panel_domain_image(field_name, model_domain, enable_counters)
|
||||||
|
|
||||||
|
# values_range = OrderedDict()
|
||||||
|
# for record in records:
|
||||||
|
# # print("***********",record)
|
||||||
|
# record['display_name'] = record['name']
|
||||||
|
# record_id = record['id']
|
||||||
|
# if enable_counters:
|
||||||
|
# image_element = domain_image.get(record_id)
|
||||||
|
# record['__count'] = image_element['__count'] if image_element else 0
|
||||||
|
# value = record['self_id']
|
||||||
|
# record['self_id'] = value and value[0]
|
||||||
|
# values_range[record_id] = record
|
||||||
|
# print(list(values_range.values()))
|
||||||
|
# if enable_counters:
|
||||||
|
# pass
|
||||||
|
# self._search_panel_global_counters(values_range, 'self_id')
|
||||||
|
# print(list(values_range.values()))
|
||||||
|
|
||||||
|
# return {
|
||||||
|
# 'parent_field': 'self_id',
|
||||||
|
# 'values': list(values_range.values()),
|
||||||
|
# }
|
||||||
|
|
||||||
|
# return super(DmsDirectory, self).search_panel_select_range(field_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_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["documents.document"].sudo()
|
||||||
|
for record in self:
|
||||||
|
# Avoid NewId
|
||||||
|
if not record.id:
|
||||||
|
record.size = 0
|
||||||
|
continue
|
||||||
|
recs = sudo_model.search_read(
|
||||||
|
domain=[("folder_id", "child_of", record.id)],
|
||||||
|
fields=["file_size"],
|
||||||
|
)
|
||||||
|
record.size = sum(rec.get("file_size", 0) for rec in recs)
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
def _compute_self_id(self):
|
||||||
|
for record in self:
|
||||||
|
record.self_id = record.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"]._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["documents.document"].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 vals.get("storage_id"):
|
||||||
|
for item in self:
|
||||||
|
if item.storage_id.id != vals["storage_id"]:
|
||||||
|
raise UserError(_("It is not possible to change the storage."))
|
||||||
|
if vals.get("parent_id"):
|
||||||
|
parent = self.browse([vals["parent_id"]])
|
||||||
|
for item in self:
|
||||||
|
if item.parent_id.storage_id != parent.storage_id:
|
||||||
|
raise UserError(
|
||||||
|
_("It is not possible to change parent to other 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.recompute()
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
if not self.env.user.has_group("dms.group_dms_delete_directory") and not self.env.user.id == 1:
|
||||||
|
raise ValidationError(
|
||||||
|
_("You are not allowed to delete a folder/directory!")
|
||||||
|
)
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,617 @@
|
||||||
|
# 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 odoo import _, api, fields, models, tools
|
||||||
|
from odoo.exceptions import 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.",
|
||||||
|
)
|
||||||
|
file_code = fields.Char(
|
||||||
|
string="File Code",
|
||||||
|
required=False)
|
||||||
|
version = fields.Char(
|
||||||
|
string="File version",
|
||||||
|
required=False)
|
||||||
|
|
||||||
|
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=True,
|
||||||
|
)
|
||||||
|
# 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,
|
||||||
|
string="Path Names",
|
||||||
|
readonly=True,
|
||||||
|
store=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
path_json = fields.Text(
|
||||||
|
compute="_compute_path",
|
||||||
|
compute_sudo=True,
|
||||||
|
string="Path Json",
|
||||||
|
readonly=True,
|
||||||
|
store=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
tag_ids = fields.Many2many(
|
||||||
|
comodel_name="documents.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",
|
||||||
|
string="Content",
|
||||||
|
attachment=False,
|
||||||
|
prefetch=False,
|
||||||
|
required=True,
|
||||||
|
store=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
extension = fields.Char(
|
||||||
|
compute="_compute_extension", string="Extension", readonly=True, store=True
|
||||||
|
)
|
||||||
|
|
||||||
|
mimetype = fields.Char(
|
||||||
|
compute="_compute_mimetype", string="Type", readonly=True, store=True
|
||||||
|
)
|
||||||
|
|
||||||
|
size = fields.Integer(string="Size", readonly=True)
|
||||||
|
|
||||||
|
checksum = fields.Char(string="Checksum/SHA1", readonly=True, index=True)
|
||||||
|
|
||||||
|
content_binary = fields.Binary(
|
||||||
|
string="Content Binary", attachment=False, prefetch=False, invisible=True
|
||||||
|
)
|
||||||
|
|
||||||
|
save_type = fields.Char(
|
||||||
|
compute="_compute_save_type",
|
||||||
|
string="Current Save Type",
|
||||||
|
invisible=True,
|
||||||
|
prefetch=False,
|
||||||
|
)
|
||||||
|
settings = fields.Many2one(
|
||||||
|
comodel_name='dms.settings',
|
||||||
|
string="Settings",
|
||||||
|
store=True,
|
||||||
|
auto_join=True,
|
||||||
|
ondelete='restrict',
|
||||||
|
# compute='_compute_settings',
|
||||||
|
tracking=True)
|
||||||
|
|
||||||
|
migration = fields.Char(
|
||||||
|
compute="_compute_migration",
|
||||||
|
string="Migration Status",
|
||||||
|
readonly=True,
|
||||||
|
prefetch=False,
|
||||||
|
compute_sudo=True,
|
||||||
|
)
|
||||||
|
attach_id = fields.Many2one(
|
||||||
|
comodel_name='dms.file.attach',
|
||||||
|
related='directory_id.project_id',
|
||||||
|
string='',
|
||||||
|
required=False)
|
||||||
|
project_id = fields.Many2one(
|
||||||
|
comodel_name='project.project',
|
||||||
|
related='directory_id.project_id',
|
||||||
|
string='',
|
||||||
|
store=True,
|
||||||
|
required=False)
|
||||||
|
require_migration = fields.Boolean(
|
||||||
|
compute="_compute_migration", store=True, compute_sudo=True
|
||||||
|
)
|
||||||
|
|
||||||
|
content_file = fields.Binary(
|
||||||
|
attachment=True, string="Content File", 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"):
|
||||||
|
if one.mimetype.startswith("image/"):
|
||||||
|
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):
|
||||||
|
super()._compute_access_url()
|
||||||
|
for item in self:
|
||||||
|
item.access_url = "/my/dms/file/%s/download" % (item.id)
|
||||||
|
|
||||||
|
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",
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_human_size(self):
|
||||||
|
return human_size(self.size)
|
||||||
|
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
# Helper
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
|
||||||
|
# def _compute_settings(self, write=True):
|
||||||
|
# if write:
|
||||||
|
# for record in self:
|
||||||
|
# record.settings = record.directory.settings
|
||||||
|
# else:
|
||||||
|
# self.ensure_one()
|
||||||
|
# return {'settings': self.directory.settings.id}
|
||||||
|
|
||||||
|
@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:
|
||||||
|
info = (index, record_count, dms_file.migration)
|
||||||
|
_logger.info(_("Migrate File %s of %s [ %s ]") % info)
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
# 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):
|
||||||
|
operator, directory_id = self._search_panel_directory(**kwargs)
|
||||||
|
if directory_id and field_name == "directory_id":
|
||||||
|
domain = [("parent_id", operator, directory_id)]
|
||||||
|
values = (
|
||||||
|
self.env["dms.directory"]
|
||||||
|
.with_context(directory_short_name=True)
|
||||||
|
.search_read(domain, ["display_name", "parent_id"])
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"parent_field": "parent_id",
|
||||||
|
"values": values if len(values) > 1 else [],
|
||||||
|
}
|
||||||
|
context = {}
|
||||||
|
if field_name == "directory_id":
|
||||||
|
context["directory_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.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
current_dir = current_dir.parent_id
|
||||||
|
record.update(
|
||||||
|
{
|
||||||
|
"path_names": "/".join(path_names),
|
||||||
|
"path_json": json.dumps(path_json),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.depends("name")
|
||||||
|
def _compute_extension(self):
|
||||||
|
for record in self:
|
||||||
|
record.extension = file.guess_extension(record.name)
|
||||||
|
|
||||||
|
@api.depends("content")
|
||||||
|
def _compute_mimetype(self):
|
||||||
|
for record in self:
|
||||||
|
binary = base64.b64decode(record.content or "")
|
||||||
|
record.mimetype = guess_mimetype(binary)
|
||||||
|
|
||||||
|
@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:
|
||||||
|
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)
|
||||||
|
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
# Locking fields and functions
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
|
||||||
|
locked_by = fields.Many2one(comodel_name="res.users", string="Locked by")
|
||||||
|
|
||||||
|
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})
|
||||||
|
|
@ -0,0 +1,256 @@
|
||||||
|
# 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 = "folder_id"
|
||||||
|
|
||||||
|
res_model = fields.Char(string="Linked attachments model", index=True, store=True)
|
||||||
|
res_id = fields.Integer(
|
||||||
|
string="Linked attachments record ID", index=True, 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"].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 or self.env.user.has_group('base.group_system'):
|
||||||
|
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 or self.env.user.has_group('base.group_system'):
|
||||||
|
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 = [(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
|
||||||
|
group_ids = [i for i in group["res_id"] if i] # Hack to remove None res_id
|
||||||
|
related_ok = model.browse(group_ids)._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 or self.env.user.has_group('base.group_system') 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 or self.env.user.has_group('base.group_system'):
|
||||||
|
# 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):
|
||||||
|
try:
|
||||||
|
# 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()
|
||||||
|
# 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
|
||||||
|
except Exception as e:
|
||||||
|
msg = "Fail to upload document %s" % str(e)
|
||||||
|
# logger.exception(msg)
|
||||||
|
# AccessError(msg)
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from odoo import _
|
||||||
|
from odoo import models, api, fields
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(models.Model):
|
||||||
|
_name = 'dms.settings'
|
||||||
|
_description = "MuK Documents Settings"
|
||||||
|
# _inherit = 'dms.model'
|
||||||
|
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
# Database
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
|
||||||
|
name = fields.Char(
|
||||||
|
string="Name",
|
||||||
|
required=True)
|
||||||
|
|
||||||
|
save_type = fields.Selection(
|
||||||
|
selection=[("database", _('Database'))],
|
||||||
|
string="Save Type",
|
||||||
|
default="database",
|
||||||
|
required=True,
|
||||||
|
help="The save type is used to determine how a file is saved to the system.")
|
||||||
|
|
||||||
|
system_locks = fields.Boolean(
|
||||||
|
string="System Locks",
|
||||||
|
default=True,
|
||||||
|
help="Indicates if files and directories should be automatically locked while system operations take place.")
|
||||||
|
|
||||||
|
show_tree = fields.Boolean(
|
||||||
|
string="Show Structure",
|
||||||
|
default=True,
|
||||||
|
help="Indicates if directories inside of this settings object are visible on document views.")
|
||||||
|
|
||||||
|
settings_directories = fields.One2many(
|
||||||
|
comodel_name='dms.directory',
|
||||||
|
inverse_name='settings',
|
||||||
|
string="Directories",
|
||||||
|
copy=False,
|
||||||
|
readonly=True)
|
||||||
|
|
||||||
|
settings_files = fields.One2many(
|
||||||
|
comodel_name='dms.file',
|
||||||
|
inverse_name='settings',
|
||||||
|
string="Files",
|
||||||
|
copy=False,
|
||||||
|
readonly=True)
|
||||||
|
|
||||||
|
count_settings_directories = fields.Integer(
|
||||||
|
# compute='_compute_count_settings_directories',
|
||||||
|
string="Directories")
|
||||||
|
|
||||||
|
count_settings_files = fields.Integer(
|
||||||
|
compute='_compute_count_settings_files',
|
||||||
|
string="Files")
|
||||||
|
|
||||||
|
root_directories = fields.One2many(
|
||||||
|
comodel_name='dms.directory',
|
||||||
|
inverse_name='settings',
|
||||||
|
string="Root Directories",
|
||||||
|
domain=[['is_root_directory', '=', True]])
|
||||||
|
|
||||||
|
top_directories = fields.One2many(
|
||||||
|
comodel_name='dms.directory',
|
||||||
|
inverse_name='settings',
|
||||||
|
# compute='_compute_top_directories',
|
||||||
|
search='_search_top_directories',
|
||||||
|
string="Top Directories",
|
||||||
|
help="Directories which have no parent or the user has no access right to those parents.")
|
||||||
|
|
||||||
|
top_files = fields.One2many(
|
||||||
|
comodel_name='dms.file',
|
||||||
|
string="Top Files",
|
||||||
|
compute='_compute_top_files',
|
||||||
|
search='_search_top_files',
|
||||||
|
help="Files which parent aren't readable by the user.")
|
||||||
|
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
# Functions
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
|
||||||
|
def notify_change(self, values, *largs, **kwargs):
|
||||||
|
super(Settings, self).notify_change(values, *largs, **kwargs)
|
||||||
|
for record in self:
|
||||||
|
directories = record.root_directories
|
||||||
|
operation = directories.generate_operation_key()
|
||||||
|
if record.system_locks:
|
||||||
|
directories.lock_tree(operation=operation, refresh=True)
|
||||||
|
for directory in directories:
|
||||||
|
directory.with_context(operation=operation).notify_change(values)
|
||||||
|
if record.system_locks:
|
||||||
|
directories.unlock_operation(operation=operation, refresh=True)
|
||||||
|
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
# Search
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _search_top_directories(self, operator, operand):
|
||||||
|
directories = self.env['dms.directory'].search([('name', operator, operand)])
|
||||||
|
directories = directories.filtered(
|
||||||
|
lambda d: d.is_root_directory or
|
||||||
|
not d.parent_directory.check_access('read'))
|
||||||
|
return [('id', 'in', directories.mapped('settings.id'))]
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _search_top_files(self, operator, operand):
|
||||||
|
files = self.env['dms.file'].search([('name', operator, operand)])
|
||||||
|
files = files.filtered(lambda f: not f.directory.check_access('read'))
|
||||||
|
return [('id', 'in', files.mapped('settings.id'))]
|
||||||
|
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
# Read, View
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
|
||||||
|
# @api.depends('settings_directories')
|
||||||
|
# def _compute_count_settings_directories(self):
|
||||||
|
# for record in self:
|
||||||
|
# record.count_settings_directories = len(record.settings_directories)
|
||||||
|
#
|
||||||
|
@api.depends('settings_files')
|
||||||
|
def _compute_count_settings_files(self):
|
||||||
|
for record in self:
|
||||||
|
record.count_settings_files = len(record.settings_files)
|
||||||
|
|
||||||
|
# @api.depends('settings_directories')
|
||||||
|
# def _compute_top_directories(self):
|
||||||
|
# for record in self:
|
||||||
|
# # access_ids = self.env['dms.directory']._get_complete_access_ids("read")
|
||||||
|
# access_ids = self.env['dms.directory']
|
||||||
|
# record.top_directories = record.settings_directories.filtered(
|
||||||
|
# lambda d: d.is_root_directory or not d.parent_directory.id in access_ids)
|
||||||
|
|
||||||
|
@api.depends('settings_files')
|
||||||
|
def _compute_top_files(self):
|
||||||
|
for record in self:
|
||||||
|
# access_ids = self.env['dms.directory']._get_complete_access_ids("read")
|
||||||
|
access_ids = self.env['dms.directory']
|
||||||
|
record.top_files = record.settings_files.filtered(lambda f: not f.directory.id in access_ids)
|
||||||
|
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
# Create, Update, Delete
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
|
||||||
|
@api.onchange('save_type')
|
||||||
|
def _onchange_save_type(self):
|
||||||
|
if self._origin.id:
|
||||||
|
warning = {
|
||||||
|
'title': (_('Information')),
|
||||||
|
'message': (_('Changing the save settings can cause a heavy migration process.'))
|
||||||
|
}
|
||||||
|
return {'warning': warning}
|
||||||
|
|
||||||
|
def _check_notification(self, vals, *largs, **kwargs):
|
||||||
|
if 'save_type' in vals:
|
||||||
|
self.suspend_security().notify_change({'save_type': vals['save_type']})
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from odoo import models
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentMixin(models.AbstractModel):
|
||||||
|
"""
|
||||||
|
Inherit this mixin to automatically create a `documents.document` when
|
||||||
|
an `ir.attachment` is linked to a record.
|
||||||
|
Override this mixin's methods to specify an owner, a folder or tags
|
||||||
|
for the document.
|
||||||
|
"""
|
||||||
|
_name = 'documents.mixin'
|
||||||
|
_description = "Documents creation mixin"
|
||||||
|
|
||||||
|
def _get_document_vals(self, attachment):
|
||||||
|
"""
|
||||||
|
Return values used to create a `documents.document`
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
document_vals = {}
|
||||||
|
if self._check_create_documents():
|
||||||
|
document_vals = {
|
||||||
|
'attachment_id': attachment.id,
|
||||||
|
'name': attachment.name or self.display_name,
|
||||||
|
'folder_id': self._get_document_folder().id,
|
||||||
|
'owner_id': self._get_document_owner().id,
|
||||||
|
'tag_ids': [(6, 0, self._get_document_tags().ids)],
|
||||||
|
}
|
||||||
|
return document_vals
|
||||||
|
|
||||||
|
def _get_document_owner(self):
|
||||||
|
return self.env.user
|
||||||
|
|
||||||
|
def _get_document_tags(self):
|
||||||
|
return self.env['documents.tag']
|
||||||
|
|
||||||
|
def _get_document_folder(self):
|
||||||
|
return self.env['dms.directory']
|
||||||
|
|
||||||
|
def _check_create_documents(self):
|
||||||
|
return bool(self and self._get_document_folder())
|
||||||
|
|
@ -0,0 +1,239 @@
|
||||||
|
# Copyright 2021 Tecnativa - Víctor Martínez
|
||||||
|
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
|
||||||
|
import base64
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
|
||||||
|
from odoo import api, models, _
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
from odoo import models, api
|
||||||
|
from PyPDF2 import PdfFileWriter, PdfFileReader
|
||||||
|
|
||||||
|
|
||||||
|
# class IrAttachment(models.Model):
|
||||||
|
# _inherit = "ir.attachment"
|
||||||
|
#
|
||||||
|
# @api.model
|
||||||
|
# def _pdf_split(self, new_files=None, open_files=None):
|
||||||
|
# """Creates and returns new pdf attachments based on existing data.
|
||||||
|
#
|
||||||
|
# :param new_files: the array that represents the new pdf structure:
|
||||||
|
# [{
|
||||||
|
# 'name': 'New File Name',
|
||||||
|
# 'new_pages': [{
|
||||||
|
# 'old_file_index': 7,
|
||||||
|
# 'old_page_number': 5,
|
||||||
|
# }],
|
||||||
|
# }]
|
||||||
|
# :param open_files: array of open file objects.
|
||||||
|
# :returns: the new PDF attachments
|
||||||
|
# """
|
||||||
|
# vals_list = []
|
||||||
|
# pdf_from_files = [PdfFileReader(open_file, strict=False) for open_file in open_files]
|
||||||
|
# for new_file in new_files:
|
||||||
|
# output = PdfFileWriter()
|
||||||
|
# for page in new_file['new_pages']:
|
||||||
|
# input_pdf = pdf_from_files[int(page['old_file_index'])]
|
||||||
|
# page_index = page['old_page_number'] - 1
|
||||||
|
# output.addPage(input_pdf.getPage(page_index))
|
||||||
|
# with io.BytesIO() as stream:
|
||||||
|
# output.write(stream)
|
||||||
|
# vals_list.append({
|
||||||
|
# 'name': new_file['name'] + ".pdf",
|
||||||
|
# 'datas': base64.b64encode(stream.getvalue()),
|
||||||
|
# })
|
||||||
|
# return self.create(vals_list)
|
||||||
|
#
|
||||||
|
# def _create_document(self, vals):
|
||||||
|
# """
|
||||||
|
# Implemented by bridge modules that create new documents if attachments are linked to
|
||||||
|
# their business models.
|
||||||
|
#
|
||||||
|
# :param vals: the create/write dictionary of ir attachment
|
||||||
|
# :return True if new documents are created
|
||||||
|
# """
|
||||||
|
# # Special case for documents
|
||||||
|
# if vals.get('res_model') == 'documents.document' and vals.get('res_id'):
|
||||||
|
# document = self.env['documents.document'].browse(vals['res_id'])
|
||||||
|
# if document.exists() and not document.attachment_id:
|
||||||
|
# document.attachment_id = self[0].id
|
||||||
|
# return False
|
||||||
|
#
|
||||||
|
# # Generic case for all other models
|
||||||
|
# res_model = vals.get('res_model')
|
||||||
|
# res_id = vals.get('res_id')
|
||||||
|
# model = self.env.get(res_model)
|
||||||
|
# if model is not None and res_id and issubclass(type(model), self.pool['documents.mixin']):
|
||||||
|
# vals_list = [
|
||||||
|
# model.browse(res_id)._get_document_vals(attachment)
|
||||||
|
# for attachment in self
|
||||||
|
# if not attachment.res_field
|
||||||
|
# ]
|
||||||
|
# vals_list = [vals for vals in vals_list if vals] # Remove empty values
|
||||||
|
# self.env['documents.document'].create(vals_list)
|
||||||
|
# return True
|
||||||
|
# return False
|
||||||
|
#
|
||||||
|
# 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"].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,
|
||||||
|
# }
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# def _dms_operations(self):
|
||||||
|
# for attachment in self:
|
||||||
|
# print(attachment,"__________________________")
|
||||||
|
# if not attachment.res_model or not attachment.res_id:
|
||||||
|
# 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["documents.document"].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
|
||||||
|
|
||||||
|
|
||||||
|
class IrAttachment2(models.Model):
|
||||||
|
_inherit = ['ir.attachment']
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _pdf_split(self, new_files=None, open_files=None):
|
||||||
|
"""Creates and returns new pdf attachments based on existing data.
|
||||||
|
|
||||||
|
:param new_files: the array that represents the new pdf structure:
|
||||||
|
[{
|
||||||
|
'name': 'New File Name',
|
||||||
|
'new_pages': [{
|
||||||
|
'old_file_index': 7,
|
||||||
|
'old_page_number': 5,
|
||||||
|
}],
|
||||||
|
}]
|
||||||
|
:param open_files: array of open file objects.
|
||||||
|
:returns: the new PDF attachments
|
||||||
|
"""
|
||||||
|
vals_list = []
|
||||||
|
pdf_from_files = [PdfFileReader(open_file, strict=False) for open_file in open_files]
|
||||||
|
for new_file in new_files:
|
||||||
|
output = PdfFileWriter()
|
||||||
|
for page in new_file['new_pages']:
|
||||||
|
input_pdf = pdf_from_files[int(page['old_file_index'])]
|
||||||
|
page_index = page['old_page_number'] - 1
|
||||||
|
output.addPage(input_pdf.getPage(page_index))
|
||||||
|
with io.BytesIO() as stream:
|
||||||
|
output.write(stream)
|
||||||
|
vals_list.append({
|
||||||
|
'name': new_file['name'] + ".pdf",
|
||||||
|
'datas': base64.b64encode(stream.getvalue()),
|
||||||
|
})
|
||||||
|
return self.create(vals_list)
|
||||||
|
|
||||||
|
def _create_document(self, vals):
|
||||||
|
"""
|
||||||
|
Implemented by bridge modules that create new documents if attachments are linked to
|
||||||
|
their business models.
|
||||||
|
|
||||||
|
:param vals: the create/write dictionary of ir attachment
|
||||||
|
:return True if new documents are created
|
||||||
|
"""
|
||||||
|
# Special case for documents
|
||||||
|
if vals.get('res_model') == 'documents.document' and vals.get('res_id'):
|
||||||
|
document = self.env['documents.document'].browse(vals['res_id'])
|
||||||
|
if document.exists() and not document.attachment_id:
|
||||||
|
document.attachment_id = self[0].id
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Generic case for all other models
|
||||||
|
res_model = vals.get('res_model')
|
||||||
|
res_id = vals.get('res_id')
|
||||||
|
model = self.env.get(res_model)
|
||||||
|
if model is not None and res_id and issubclass(type(model), self.pool['documents.mixin']):
|
||||||
|
vals_list = [
|
||||||
|
model.browse(res_id)._get_document_vals(attachment)
|
||||||
|
for attachment in self
|
||||||
|
if not attachment.res_field
|
||||||
|
]
|
||||||
|
vals_list = [vals for vals in vals_list if vals] # Remove empty values
|
||||||
|
self.env['documents.document'].create(vals_list)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
# def create(self, vals):
|
||||||
|
# attachment = super(IrAttachment2, self).create(vals)
|
||||||
|
# print("😀😀😀😀😀😀😀😀😀😀😀")
|
||||||
|
# extension = os.path.splitext(attachment['name'])[1]
|
||||||
|
# # print(extension)
|
||||||
|
# # if vals['mimetype'] == 'image/png':
|
||||||
|
# # raise ValidationError(
|
||||||
|
# # _("😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀"))
|
||||||
|
#
|
||||||
|
# # print(vals)
|
||||||
|
# # the context can indicate that this new attachment is created from documents, and therefore
|
||||||
|
# # doesn't need a new document to contain it.
|
||||||
|
# if not self._context.get('no_document') and not attachment.res_field:
|
||||||
|
# attachment.sudo()._create_document(dict(vals, res_model=attachment.res_model, res_id=attachment.res_id))
|
||||||
|
# return attachment
|
||||||
|
|
||||||
|
def write(self, vals):
|
||||||
|
if not self._context.get('no_document'):
|
||||||
|
self.filtered(lambda a: not (vals.get('res_field') or a.res_field)).sudo()._create_document(vals)
|
||||||
|
return super(IrAttachment2, self).write(vals)
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from odoo import api, models, fields, _
|
||||||
|
|
||||||
|
|
||||||
|
class MailActivityType(models.Model):
|
||||||
|
_inherit = "mail.activity.type"
|
||||||
|
|
||||||
|
tag_ids = fields.Many2many('documents.tag')
|
||||||
|
folder_id = fields.Many2one('dms.directory',
|
||||||
|
help="By defining a folder, the upload activities will generate a document")
|
||||||
|
default_user_id = fields.Many2one('res.users', string="Default User")
|
||||||
|
|
||||||
|
|
||||||
|
class MailActivity(models.Model):
|
||||||
|
_inherit = 'mail.activity'
|
||||||
|
|
||||||
|
def _action_done(self, feedback=False, attachment_ids=None):
|
||||||
|
if attachment_ids:
|
||||||
|
for record in self:
|
||||||
|
document = self.env['documents.document'].search([('request_activity_id', '=', record.id)], limit=1)
|
||||||
|
if document and not document.attachment_id:
|
||||||
|
self.env['documents.document'].search([('attachment_id', '=', attachment_ids[0])]).unlink()
|
||||||
|
if not feedback:
|
||||||
|
feedback = _("Document Request: %s Uploaded by: %s") % (document.name, self.env.user.name)
|
||||||
|
document.write({'attachment_id': attachment_ids[0], 'request_activity_id': False})
|
||||||
|
|
||||||
|
return super(MailActivity, self)._action_done(feedback=feedback, attachment_ids=attachment_ids)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def create(self, values):
|
||||||
|
activity = super(MailActivity, self).create(values)
|
||||||
|
activity_type = activity.activity_type_id
|
||||||
|
if activity_type.category == 'upload_file' and activity.res_model != 'documents.document':
|
||||||
|
if activity_type.folder_id:
|
||||||
|
self.env['documents.document'].create({
|
||||||
|
'res_model': activity.res_model,
|
||||||
|
'res_id': activity.res_id,
|
||||||
|
'owner_id': activity_type.default_user_id.id,
|
||||||
|
'folder_id': activity_type.folder_id.id,
|
||||||
|
'tag_ids': [(6, 0, activity_type.tag_ids.ids if activity_type.tag_ids else [])],
|
||||||
|
'name': activity.summary or activity.res_name or 'upload file request',
|
||||||
|
'request_activity_id': activity.id,
|
||||||
|
})
|
||||||
|
return activity
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
# 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
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
# 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()
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
# 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"),
|
||||||
|
],
|
||||||
|
string="Documents Onboarding State",
|
||||||
|
default="not_done",
|
||||||
|
)
|
||||||
|
|
||||||
|
documents_onboarding_storage_state = fields.Selection(
|
||||||
|
selection=[
|
||||||
|
("not_done", "Not done"),
|
||||||
|
("just_done", "Just done"),
|
||||||
|
("done", "Done"),
|
||||||
|
("closed", "Closed"),
|
||||||
|
],
|
||||||
|
string="Documents Onboarding Storage State",
|
||||||
|
default="not_done",
|
||||||
|
)
|
||||||
|
|
||||||
|
documents_onboarding_directory_state = fields.Selection(
|
||||||
|
selection=[
|
||||||
|
("not_done", "Not done"),
|
||||||
|
("just_done", "Just done"),
|
||||||
|
("done", "Done"),
|
||||||
|
("closed", "Closed"),
|
||||||
|
],
|
||||||
|
string="Documents Onboarding Directory State",
|
||||||
|
default="not_done",
|
||||||
|
)
|
||||||
|
|
||||||
|
documents_onboarding_file_state = fields.Selection(
|
||||||
|
selection=[
|
||||||
|
("not_done", "Not done"),
|
||||||
|
("just_done", "Just done"),
|
||||||
|
("done", "Done"),
|
||||||
|
("closed", "Closed"),
|
||||||
|
],
|
||||||
|
string="Documents Onboarding File State",
|
||||||
|
default="not_done",
|
||||||
|
)
|
||||||
|
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
# Functions
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
|
||||||
|
def get_and_update_documents_onboarding_state(self):
|
||||||
|
return self.get_and_update_onbarding_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_folder_id": directory and directory.id},
|
||||||
|
}
|
||||||
|
return action
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def action_close_documents_onboarding(self):
|
||||||
|
self.env.user.company_id.documents_onboarding_state = "closed"
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
# 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",
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from odoo import api, models, fields, _
|
||||||
|
|
||||||
|
|
||||||
|
class Partner(models.Model):
|
||||||
|
_inherit = "res.partner"
|
||||||
|
|
||||||
|
document_count = fields.Integer('Document Count', compute='_compute_document_count')
|
||||||
|
|
||||||
|
def _compute_document_count(self):
|
||||||
|
read_group_var = self.env['documents.document'].read_group(
|
||||||
|
[('partner_id', 'in', self.ids)],
|
||||||
|
fields=['partner_id'],
|
||||||
|
groupby=['partner_id'])
|
||||||
|
|
||||||
|
document_count_dict = dict((d['partner_id'][0], d['partner_id_count']) for d in read_group_var)
|
||||||
|
for record in self:
|
||||||
|
record.document_count = document_count_dict.get(record.id, 0)
|
||||||
|
|
||||||
|
def action_see_documents(self):
|
||||||
|
self.ensure_one()
|
||||||
|
return {
|
||||||
|
'name': _('Documents'),
|
||||||
|
'res_model': 'documents.document',
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'views': [(False, 'kanban')],
|
||||||
|
'view_mode': 'kanban',
|
||||||
|
'context': {
|
||||||
|
"search_default_partner_id": self.id,
|
||||||
|
"default_partner_id": self.id,
|
||||||
|
"searchpanel_default_folder_id": False
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,218 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from ast import literal_eval
|
||||||
|
|
||||||
|
from odoo import models, fields, api, exceptions
|
||||||
|
from odoo.tools.translate import _
|
||||||
|
from odoo.tools import consteq
|
||||||
|
|
||||||
|
from odoo.osv import expression
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentShare(models.Model):
|
||||||
|
_name = 'documents.share'
|
||||||
|
_inherit = ['mail.thread', 'mail.alias.mixin']
|
||||||
|
_description = 'Documents Share'
|
||||||
|
|
||||||
|
folder_id = fields.Many2one('dms.directory', string="Workspace", required=True)
|
||||||
|
name = fields.Char(string="Name")
|
||||||
|
|
||||||
|
access_token = fields.Char(required=True, default=lambda x: str(uuid.uuid4()))
|
||||||
|
full_url = fields.Char(string="URL", compute='_compute_full_url')
|
||||||
|
date_deadline = fields.Date(string="Valid Until")
|
||||||
|
state = fields.Selection([
|
||||||
|
('live', "Live"),
|
||||||
|
('expired', "Expired"),
|
||||||
|
], default='live', compute='_compute_state', string="Status")
|
||||||
|
can_upload = fields.Boolean(compute='_compute_can_upload')
|
||||||
|
|
||||||
|
type = fields.Selection([
|
||||||
|
('ids', "Document list"),
|
||||||
|
('domain', "Domain"),
|
||||||
|
], default='ids', string="Share type")
|
||||||
|
# type == 'ids'
|
||||||
|
document_ids = fields.Many2many('documents.document', string='Shared Documents')
|
||||||
|
# type == 'domain'
|
||||||
|
domain = fields.Char()
|
||||||
|
|
||||||
|
action = fields.Selection([
|
||||||
|
('download', "Download"),
|
||||||
|
('downloadupload', "Download and Upload"),
|
||||||
|
], default='download', string="Allows to")
|
||||||
|
tag_ids = fields.Many2many('documents.tag', string="Shared Tags")
|
||||||
|
partner_id = fields.Many2one('res.partner', string="Contact")
|
||||||
|
owner_id = fields.Many2one('res.users', string="Document Owner")
|
||||||
|
email_drop = fields.Boolean(string='Upload by Email')
|
||||||
|
|
||||||
|
# Activity
|
||||||
|
activity_option = fields.Boolean(string='Create a new activity')
|
||||||
|
activity_type_id = fields.Many2one('mail.activity.type', string="Activity type")
|
||||||
|
activity_summary = fields.Char('Summary')
|
||||||
|
activity_date_deadline_range = fields.Integer(string='Due Date In')
|
||||||
|
activity_date_deadline_range_type = fields.Selection([
|
||||||
|
('days', 'Days'),
|
||||||
|
('weeks', 'Weeks'),
|
||||||
|
('months', 'Months'),
|
||||||
|
], string='Due type', default='days')
|
||||||
|
activity_note = fields.Html(string="Note")
|
||||||
|
activity_user_id = fields.Many2one('res.users', string='Responsible')
|
||||||
|
|
||||||
|
_sql_constraints = [
|
||||||
|
('share_unique', 'unique (access_token)', "This access token already exists"),
|
||||||
|
]
|
||||||
|
|
||||||
|
def name_get(self):
|
||||||
|
name_array = []
|
||||||
|
for record in self:
|
||||||
|
name_array.append((record.id, record.name or "unnamed link"))
|
||||||
|
return name_array
|
||||||
|
|
||||||
|
def _get_documents(self, document_ids=None):
|
||||||
|
"""
|
||||||
|
:param list[int] document_ids: limit to the list of documents to fetch.
|
||||||
|
:return: recordset of the documents that can be accessed by the create_uid based on the settings
|
||||||
|
of the share link.
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
limited_self = self.with_user(self.create_uid)
|
||||||
|
Documents = limited_self.env['documents.document']
|
||||||
|
|
||||||
|
search_ids = set()
|
||||||
|
domains = [[('folder_id', '=', self.folder_id.id)]]
|
||||||
|
|
||||||
|
if document_ids is not None:
|
||||||
|
if not document_ids:
|
||||||
|
return Documents
|
||||||
|
search_ids = set(document_ids)
|
||||||
|
|
||||||
|
if self.type == 'domain':
|
||||||
|
record_domain = []
|
||||||
|
if self.domain:
|
||||||
|
record_domain = literal_eval(self.domain)
|
||||||
|
domains.append(record_domain)
|
||||||
|
else:
|
||||||
|
share_ids = limited_self.document_ids.ids
|
||||||
|
search_ids = search_ids.intersection(share_ids) if search_ids else share_ids
|
||||||
|
|
||||||
|
if search_ids or self.type != 'domain':
|
||||||
|
domains.append([('id', 'in', list(search_ids))])
|
||||||
|
|
||||||
|
search_domain = expression.AND(domains)
|
||||||
|
return Documents.search(search_domain)
|
||||||
|
|
||||||
|
def _get_writable_documents(self, documents):
|
||||||
|
"""
|
||||||
|
|
||||||
|
:param documents:
|
||||||
|
:return: the recordset of documents for which the create_uid has write access
|
||||||
|
False only if no write right.
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
try:
|
||||||
|
# checks the rights first in case of empty recordset
|
||||||
|
documents.with_user(self.create_uid).check_access_rights('write')
|
||||||
|
except exceptions.AccessError:
|
||||||
|
return False
|
||||||
|
return documents.with_user(self.create_uid)._filter_access_rules('write')
|
||||||
|
|
||||||
|
def _check_token(self, access_token):
|
||||||
|
if not access_token:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
return consteq(access_token, self.access_token)
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _get_documents_and_check_access(self, access_token, document_ids=None, operation='write'):
|
||||||
|
"""
|
||||||
|
:param str access_token: the access_token to be checked with the share link access_token
|
||||||
|
:param list[int] document_ids: limit to the list of documents to fetch and check from the share link.
|
||||||
|
:param str operation: access right to check on documents (read/write).
|
||||||
|
:return: Recordset[documents.document]: all the accessible requested documents
|
||||||
|
False if it fails access checks: False always means "no access right", if there are no documents but
|
||||||
|
the rights are valid, it still returns an empty recordset.
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
if not self._check_token(access_token):
|
||||||
|
return False
|
||||||
|
if self.state == 'expired':
|
||||||
|
return False
|
||||||
|
documents = self._get_documents(document_ids)
|
||||||
|
if operation == 'write':
|
||||||
|
return self._get_writable_documents(documents)
|
||||||
|
else:
|
||||||
|
return documents
|
||||||
|
|
||||||
|
def _compute_can_upload(self):
|
||||||
|
for record in self:
|
||||||
|
folder = record.folder_id
|
||||||
|
folder_has_groups = folder.group_ids.ids or folder.group_ids.ids
|
||||||
|
in_write_group = set(folder.group_ids.ids) & set(record.create_uid.groups_id.ids)
|
||||||
|
record.can_upload = in_write_group or not folder_has_groups
|
||||||
|
|
||||||
|
def _compute_state(self):
|
||||||
|
"""
|
||||||
|
changes the state based on the expiration date,
|
||||||
|
an expired share link cannot be used to upload or download files.
|
||||||
|
"""
|
||||||
|
for record in self:
|
||||||
|
record.state = 'live'
|
||||||
|
if record.date_deadline:
|
||||||
|
today = fields.Date.from_string(fields.Date.today())
|
||||||
|
exp_date = fields.Date.from_string(record.date_deadline)
|
||||||
|
diff_time = (exp_date - today).days
|
||||||
|
if diff_time <= 0:
|
||||||
|
record.state = 'expired'
|
||||||
|
|
||||||
|
@api.onchange('access_token')
|
||||||
|
def _compute_full_url(self):
|
||||||
|
base_url = self.env["ir.config_parameter"].sudo().get_param("web.base.url")
|
||||||
|
for record in self:
|
||||||
|
record.full_url = "%s/document/share/%s/%s" % (base_url, record.id, record.access_token)
|
||||||
|
|
||||||
|
def _alias_get_creation_values(self):
|
||||||
|
values = super(DocumentShare, self)._alias_get_creation_values()
|
||||||
|
values['alias_model_id'] = self.env['ir.model']._get('documents.document').id
|
||||||
|
if self.id:
|
||||||
|
values['alias_defaults'] = defaults = literal_eval(self.alias_defaults or "{}")
|
||||||
|
defaults.update({
|
||||||
|
'tag_ids': [(6, 0, self.tag_ids.ids)],
|
||||||
|
'folder_id': self.folder_id.id,
|
||||||
|
'partner_id': self.partner_id.id,
|
||||||
|
'create_share_id': self.id,
|
||||||
|
})
|
||||||
|
return values
|
||||||
|
|
||||||
|
def send_share_by_mail(self, template_xmlid):
|
||||||
|
self.ensure_one()
|
||||||
|
request_template = self.env.ref(template_xmlid, raise_if_not_found=False)
|
||||||
|
if request_template:
|
||||||
|
request_template.send_mail(self.id)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def create(self, vals):
|
||||||
|
if not vals.get('owner_id'):
|
||||||
|
vals['owner_id'] = self.env.uid
|
||||||
|
share = super(DocumentShare, self).create(vals)
|
||||||
|
return share
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def create_share(self, vals):
|
||||||
|
"""
|
||||||
|
creates a share link and returns a view.
|
||||||
|
:return: a form action that opens the share window to display the URL and the settings.
|
||||||
|
"""
|
||||||
|
|
||||||
|
share = self.create(vals)
|
||||||
|
view_id = self.env.ref('dms.share_view_form_popup').id
|
||||||
|
return {
|
||||||
|
'context': self._context,
|
||||||
|
'res_model': 'documents.share',
|
||||||
|
'target': 'new',
|
||||||
|
'name': _('Share selected records') if vals.get('type') == 'ids' else _('Share domain'),
|
||||||
|
'res_id': share.id,
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'views': [[view_id, 'form']],
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
# 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(string="Name", required=True)
|
||||||
|
|
||||||
|
save_type = fields.Selection(
|
||||||
|
selection=[
|
||||||
|
("database", _("Database")),
|
||||||
|
("file", _("Filestore")),
|
||||||
|
("attachment", _("Attachment")),
|
||||||
|
],
|
||||||
|
string="Save Type",
|
||||||
|
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="documents.document",
|
||||||
|
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",
|
||||||
|
)
|
||||||
|
|
||||||
|
@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)
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
# 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(string="Name", required=True, translate=True)
|
||||||
|
active = fields.Boolean(
|
||||||
|
default=True,
|
||||||
|
help="The active field allows you " "to hide the tag without removing it.",
|
||||||
|
)
|
||||||
|
folder_id = fields.Many2one('dms.directory', string="Workspace", store=True,
|
||||||
|
readonly=False)
|
||||||
|
facet_id = fields.Many2one('documents.facet', string="Category", ondelete='cascade', required=True)
|
||||||
|
sequence = fields.Integer('Sequence', default=10)
|
||||||
|
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)
|
||||||
|
folder_id = fields.Many2one(
|
||||||
|
'dms.directory',
|
||||||
|
string='',
|
||||||
|
required=False)
|
||||||
|
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="documents.document",
|
||||||
|
# relation="dms_file_tag_rel",
|
||||||
|
# column1="tid",
|
||||||
|
# column2="fid",
|
||||||
|
# string="Files",
|
||||||
|
# readonly=True,
|
||||||
|
)
|
||||||
|
count_directories = fields.Integer(
|
||||||
|
compute="_compute_count_directories", string="Count Directories"
|
||||||
|
)
|
||||||
|
count_files = fields.Integer(compute="_compute_count_files", string="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)
|
||||||
|
|
||||||
|
# @api.model
|
||||||
|
# def _get_tags(self, domain, folder_id):
|
||||||
|
# """
|
||||||
|
# fetches the tag and facet ids for the document selector (custom left sidebar of the kanban view)
|
||||||
|
# """
|
||||||
|
# documents = self.env['documents.document'].search(domain)
|
||||||
|
# # folders are searched with sudo() so we fetch the tags and facets from all the folder hierarchy (as tags
|
||||||
|
# # and facets are inherited from ancestor folders).
|
||||||
|
# folders = self.env['dms.directory'].sudo().search([('parent_folder_id', 'parent_of', folder_id)])
|
||||||
|
# self.flush(['sequence', 'name', 'facet_id'])
|
||||||
|
# self.env['documents.facet'].flush(['sequence', 'name', 'tooltip'])
|
||||||
|
# query = """
|
||||||
|
# SELECT facet.sequence AS group_sequence,
|
||||||
|
# facet.id AS group_id,
|
||||||
|
# facet.tooltip AS group_tooltip,
|
||||||
|
# documents_tag.sequence AS sequence,
|
||||||
|
# documents_tag.id AS id,
|
||||||
|
# COUNT(rel.documents_document_id) AS __count
|
||||||
|
# FROM documents_tag
|
||||||
|
# JOIN documents_facet facet ON dms.tag.facet_id = facet.id
|
||||||
|
# AND facet.folder_id = ANY(%s)
|
||||||
|
# LEFT JOIN document_tag_rel rel ON dms_tag.id = rel.documents_tag_id
|
||||||
|
# AND rel.documents_document_id = ANY(%s)
|
||||||
|
# GROUP BY facet.sequence, facet.name, facet.id, facet.tooltip, dms_tag.sequence, documents_tag.name, documents_tag.id
|
||||||
|
# ORDER BY facet.sequence, facet.name, facet.id, facet.tooltip, dms_tag.sequence, documents_tag.name, documents_tag.id
|
||||||
|
# """
|
||||||
|
# params = [
|
||||||
|
# list(folders.ids),
|
||||||
|
# list(documents.ids), # using Postgresql's ANY() with a list to prevent empty list of documents
|
||||||
|
# ]
|
||||||
|
# self.env.cr.execute(query, params)
|
||||||
|
# result = self.env.cr.dictfetchall()
|
||||||
|
#
|
||||||
|
# # Translating result
|
||||||
|
# groups = self.env['documents.facet'].browse({r['group_id'] for r in result})
|
||||||
|
# group_names = {group['id']: group['name'] for group in groups}
|
||||||
|
#
|
||||||
|
# tags = self.env['documents.tag'].browse({r['id'] for r in result})
|
||||||
|
# tags_names = {tag['id']: tag['name'] for tag in tags}
|
||||||
|
#
|
||||||
|
# for r in result:
|
||||||
|
# r['group_name'] = group_names.get(r['group_id'])
|
||||||
|
# r['display_name'] = tags_names.get(r['id'])
|
||||||
|
#
|
||||||
|
# return result
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from odoo import models, fields, api
|
||||||
|
from odoo.osv import expression
|
||||||
|
|
||||||
|
|
||||||
|
class TagsCategories(models.Model):
|
||||||
|
_name = "documents.facet"
|
||||||
|
_description = "Category"
|
||||||
|
_order = "sequence, name"
|
||||||
|
|
||||||
|
# the colors to be used to represent the display order of the facets (tag categories), the colors
|
||||||
|
# depend on the order and amount of fetched categories
|
||||||
|
# currently used in the searchPanel and the kanban view and should match across the two.
|
||||||
|
FACET_ORDER_COLORS = ['#F06050', '#6CC1ED', '#F7CD1F', '#814968', '#30C381', '#D6145F', '#475577', '#F4A460',
|
||||||
|
'#EB7E7F', '#2C8397']
|
||||||
|
|
||||||
|
folder_id = fields.Many2one('dms.directory', string="Workspace", ondelete="cascade")
|
||||||
|
name = fields.Char(required=True, translate=True)
|
||||||
|
tag_ids = fields.One2many('documents.tag', 'facet_id')
|
||||||
|
tooltip = fields.Char(help="Text shown when hovering on this tag category or its tags", string="Tooltip")
|
||||||
|
sequence = fields.Integer('Sequence', default=10)
|
||||||
|
|
||||||
|
_sql_constraints = [
|
||||||
|
('name_unique', 'unique (folder_id, name)', "Facet already exists in this folder"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class Tags(models.Model):
|
||||||
|
_name = "documents.tag"
|
||||||
|
_description = "Tag"
|
||||||
|
_order = "sequence, name"
|
||||||
|
|
||||||
|
folder_id = fields.Many2one('dms.directory', string="Workspace", store=True,
|
||||||
|
readonly=False)
|
||||||
|
facet_id = fields.Many2one('documents.facet', string="Category", ondelete='cascade', required=True)
|
||||||
|
category_id = fields.Many2one(
|
||||||
|
comodel_name="dms.category",
|
||||||
|
context="{'dms_category_show_path': True}",
|
||||||
|
string="Category",
|
||||||
|
ondelete="set null",
|
||||||
|
)
|
||||||
|
# active = fields.Boolean(
|
||||||
|
# string='',
|
||||||
|
# required=False)
|
||||||
|
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="documents.document",
|
||||||
|
relation="dms_file_tag_rel",
|
||||||
|
column1="tid",
|
||||||
|
column2="fid",
|
||||||
|
string="Files",
|
||||||
|
readonly=True,
|
||||||
|
)
|
||||||
|
count_directories = fields.Integer(
|
||||||
|
compute="_compute_count_directories", string="Count Directories"
|
||||||
|
)
|
||||||
|
count_files = fields.Integer(compute="_compute_count_files", string="Count Files")
|
||||||
|
|
||||||
|
@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)
|
||||||
|
|
||||||
|
name = fields.Char(required=True, translate=True)
|
||||||
|
sequence = fields.Integer('Sequence', default=10)
|
||||||
|
color = fields.Integer(string="Color Index", default=10)
|
||||||
|
|
||||||
|
_sql_constraints = [
|
||||||
|
('facet_name_unique', 'unique (facet_id, name)', "Tag already exists for this facet"),
|
||||||
|
]
|
||||||
|
|
||||||
|
def name_get(self):
|
||||||
|
names = []
|
||||||
|
if self._context.get('simple_name'):
|
||||||
|
return super(Tags, self).name_get()
|
||||||
|
for record in self:
|
||||||
|
names.append((record.id, "%s > %s" % (record.facet_id.name, record.name)))
|
||||||
|
return names
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _get_tags(self, domain, folder_id):
|
||||||
|
"""
|
||||||
|
fetches the tag and facet ids for the document selector (custom left sidebar of the kanban view)
|
||||||
|
"""
|
||||||
|
documents = self.env['documents.document'].search(domain)
|
||||||
|
# folders are searched with sudo() so we fetch the tags and facets from all the folder hierarchy (as tags
|
||||||
|
# and facets are inherited from ancestor folders).
|
||||||
|
folders = self.env['dms.directory'].sudo().search([('parent_folder_id', 'parent_of', folder_id)])
|
||||||
|
self.flush(['sequence', 'name', 'facet_id'])
|
||||||
|
self.env['documents.facet'].flush(['sequence', 'name', 'tooltip'])
|
||||||
|
query = """
|
||||||
|
SELECT facet.sequence AS group_sequence,
|
||||||
|
facet.id AS group_id,
|
||||||
|
facet.tooltip AS group_tooltip,
|
||||||
|
documents_tag.sequence AS sequence,
|
||||||
|
documents_tag.id AS id,
|
||||||
|
COUNT(rel.documents_document_id) AS __count
|
||||||
|
FROM documents_tag
|
||||||
|
JOIN documents_facet facet ON documents_tag.facet_id = facet.id
|
||||||
|
AND facet.folder_id = ANY(%s)
|
||||||
|
LEFT JOIN document_tag_rel rel ON documents_tag.id = rel.documents_tag_id
|
||||||
|
AND rel.documents_document_id = ANY(%s)
|
||||||
|
GROUP BY facet.sequence, facet.name, facet.id, facet.tooltip, documents_tag.sequence, documents_tag.name, documents_tag.id
|
||||||
|
ORDER BY facet.sequence, facet.name, facet.id, facet.tooltip, documents_tag.sequence, documents_tag.name, documents_tag.id
|
||||||
|
"""
|
||||||
|
params = [
|
||||||
|
list(folders.ids),
|
||||||
|
list(documents.ids), # using Postgresql's ANY() with a list to prevent empty list of documents
|
||||||
|
]
|
||||||
|
self.env.cr.execute(query, params)
|
||||||
|
result = self.env.cr.dictfetchall()
|
||||||
|
|
||||||
|
# Translating result
|
||||||
|
groups = self.env['documents.facet'].browse({r['group_id'] for r in result})
|
||||||
|
group_names = {group['id']: group['name'] for group in groups}
|
||||||
|
|
||||||
|
tags = self.env['documents.tag'].browse({r['id'] for r in result})
|
||||||
|
tags_names = {tag['id']: tag['name'] for tag in tags}
|
||||||
|
|
||||||
|
for r in result:
|
||||||
|
r['group_name'] = group_names.get(r['group_id'])
|
||||||
|
r['display_name'] = tags_names.get(r['id'])
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
|
from odoo import models, fields, api
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowActionRule(models.Model):
|
||||||
|
_name = "documents.workflow.rule"
|
||||||
|
_description = "A set of condition and actions which will be available to all attachments matching the conditions"
|
||||||
|
_order = "sequence"
|
||||||
|
|
||||||
|
domain_folder_id = fields.Many2one('dms.directory', string="Related Workspace", required=True, ondelete='cascade')
|
||||||
|
name = fields.Char(required=True, string="Action Button Name", translate=True)
|
||||||
|
note = fields.Char(string="Tooltip")
|
||||||
|
sequence = fields.Integer('Sequence', default=10)
|
||||||
|
|
||||||
|
# Conditions
|
||||||
|
condition_type = fields.Selection([
|
||||||
|
('criteria', "Criteria"),
|
||||||
|
('domain', "Domain"),
|
||||||
|
], default='criteria', string="Condition type")
|
||||||
|
|
||||||
|
# Domain
|
||||||
|
domain = fields.Char()
|
||||||
|
|
||||||
|
# Criteria
|
||||||
|
criteria_partner_id = fields.Many2one('res.partner', string="Contact")
|
||||||
|
criteria_owner_id = fields.Many2one('res.users', string="Owner")
|
||||||
|
required_tag_ids = fields.Many2many('documents.tag', 'required_tag_ids_rule_table', string="Required Tags")
|
||||||
|
excluded_tag_ids = fields.Many2many('documents.tag', 'excluded_tag_ids_rule_table', string="Excluded Tags")
|
||||||
|
limited_to_single_record = fields.Boolean(string="One record limit", compute='_compute_limited_to_single_record')
|
||||||
|
|
||||||
|
# Actions
|
||||||
|
partner_id = fields.Many2one('res.partner', string="Set Contact")
|
||||||
|
user_id = fields.Many2one('res.users', string="Set Owner")
|
||||||
|
tag_action_ids = fields.One2many('documents.workflow.action', 'workflow_rule_id', string='Set Tags')
|
||||||
|
folder_id = fields.Many2one('dms.directory', string="Move to Workspace")
|
||||||
|
has_business_option = fields.Boolean(compute='_get_business')
|
||||||
|
create_model = fields.Selection([], string="Create")
|
||||||
|
|
||||||
|
# Activity
|
||||||
|
remove_activities = fields.Boolean(string='Mark all as Done')
|
||||||
|
activity_option = fields.Boolean(string='Schedule Activity')
|
||||||
|
activity_type_id = fields.Many2one('mail.activity.type', string="Activity type")
|
||||||
|
activity_summary = fields.Char('Summary')
|
||||||
|
activity_date_deadline_range = fields.Integer(string='Due Date In')
|
||||||
|
activity_date_deadline_range_type = fields.Selection([
|
||||||
|
('days', 'Days'),
|
||||||
|
('weeks', 'Weeks'),
|
||||||
|
('months', 'Months'),
|
||||||
|
], string='Due type', default='days')
|
||||||
|
activity_note = fields.Html(string="Activity Note")
|
||||||
|
has_owner_activity = fields.Boolean(string="Set the activity on the document owner")
|
||||||
|
activity_user_id = fields.Many2one('res.users', string='Responsible')
|
||||||
|
|
||||||
|
@api.onchange('domain_folder_id')
|
||||||
|
def _on_domain_folder_id_change(self):
|
||||||
|
if self.domain_folder_id != self.required_tag_ids.mapped('folder_id'):
|
||||||
|
self.required_tag_ids = False
|
||||||
|
if self.domain_folder_id != self.excluded_tag_ids.mapped('folder_id'):
|
||||||
|
self.excluded_tag_ids = False
|
||||||
|
|
||||||
|
def _get_business(self):
|
||||||
|
"""
|
||||||
|
Checks if the workflow rule has available create models to display the option.
|
||||||
|
Implemented by the bridge models if the rule should only be available for a single record.
|
||||||
|
"""
|
||||||
|
for record in self:
|
||||||
|
record.has_business_option = len(self._fields['create_model'].selection)
|
||||||
|
|
||||||
|
def _compute_limited_to_single_record(self):
|
||||||
|
"""
|
||||||
|
Overwritten by bridge modules to define whether the rule is only available for one record at a time.
|
||||||
|
"""
|
||||||
|
self.update({'limited_to_single_record': False})
|
||||||
|
|
||||||
|
def create_record(self, documents=None):
|
||||||
|
"""
|
||||||
|
implemented by each link module to define specific fields for the new business model (create_values)
|
||||||
|
|
||||||
|
When creating/copying/writing an ir.attachment with a res_model and a res_id, add no_document=True
|
||||||
|
to the context to prevent the automatic creation of a document.
|
||||||
|
|
||||||
|
:param documents: the list of the documents of the selection
|
||||||
|
:return: the action dictionary that will be called after the workflow action is done or True.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def apply_actions(self, document_ids):
|
||||||
|
"""
|
||||||
|
called by the front-end Document Inspector to apply the actions to the selection of ID's.
|
||||||
|
|
||||||
|
:param document_ids: the list of documents to apply the action.
|
||||||
|
:return: if the action was to create a new business object, returns an action to open the view of the
|
||||||
|
newly created object, else returns True.
|
||||||
|
"""
|
||||||
|
documents = self.env['documents.document'].browse(document_ids)
|
||||||
|
|
||||||
|
# partner/owner/share_link/folder changes
|
||||||
|
document_dict = {}
|
||||||
|
if self.user_id:
|
||||||
|
document_dict['owner_id'] = self.user_id.id
|
||||||
|
if self.partner_id:
|
||||||
|
document_dict['partner_id'] = self.partner_id.id
|
||||||
|
if self.folder_id:
|
||||||
|
document_dict['folder_id'] = self.folder_id.id
|
||||||
|
|
||||||
|
documents.write(document_dict)
|
||||||
|
|
||||||
|
for document in documents:
|
||||||
|
if self.remove_activities:
|
||||||
|
document.activity_ids.action_feedback(
|
||||||
|
feedback="completed by rule: %s. %s" % (self.name, self.note or '')
|
||||||
|
)
|
||||||
|
|
||||||
|
# tag and facet actions
|
||||||
|
for tag_action in self.tag_action_ids:
|
||||||
|
tag_action.execute_tag_action(document)
|
||||||
|
|
||||||
|
if self.activity_option and self.activity_type_id:
|
||||||
|
documents.documents_set_activity(settings_record=self)
|
||||||
|
|
||||||
|
if self.create_model:
|
||||||
|
return self.with_company(documents.company_id).create_record(documents=documents)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowTagAction(models.Model):
|
||||||
|
_name = "documents.workflow.action"
|
||||||
|
_description = "Document Workflow Tag Action"
|
||||||
|
|
||||||
|
workflow_rule_id = fields.Many2one('documents.workflow.rule', ondelete='cascade')
|
||||||
|
|
||||||
|
action = fields.Selection([
|
||||||
|
('add', "Add"),
|
||||||
|
('replace', "Replace by"),
|
||||||
|
('remove', "Remove"),
|
||||||
|
], default='add', required=True)
|
||||||
|
|
||||||
|
facet_id = fields.Many2one('documents.facet', string="Category")
|
||||||
|
tag_id = fields.Many2one('documents.tag', string="Tag")
|
||||||
|
|
||||||
|
def execute_tag_action(self, document):
|
||||||
|
if self.action == 'add' and self.tag_id.id:
|
||||||
|
return document.write({'tag_ids': [(4, self.tag_id.id, False)]})
|
||||||
|
elif self.action == 'replace' and self.facet_id.id:
|
||||||
|
faceted_tags = self.env['documents.tag'].search([('facet_id', '=', self.facet_id.id)])
|
||||||
|
if faceted_tags.ids:
|
||||||
|
for tag in faceted_tags:
|
||||||
|
document.write({'tag_ids': [(3, tag.id, False)]})
|
||||||
|
return document.write({'tag_ids': [(4, self.tag_id.id, False)]})
|
||||||
|
elif self.action == 'remove':
|
||||||
|
if self.tag_id.id:
|
||||||
|
return document.write({'tag_ids': [(3, self.tag_id.id, False)]})
|
||||||
|
elif self.facet_id:
|
||||||
|
faceted_tags = self.env['documents.tag'].search([('facet_id', '=', self.facet_id.id)])
|
||||||
|
for tag in faceted_tags:
|
||||||
|
return document.write({'tag_ids': [(3, tag.id, False)]})
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
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_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_settings,dms_storage_user,model_dms_settings,group_dms_user,1,0,0,0
|
||||||
|
access_dms_settings,dms_storage_manager,model_dms_settings,group_dms_manager,1,1,1,1
|
||||||
|
|
||||||
|
access_dms_file_attach,dms_storage_user,model_dms_file_attach,group_dms_user,1,0,0,0
|
||||||
|
access_dms_attach,dms_storage_manager,model_dms_file_attach,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,1,1,1
|
||||||
|
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,1,1,1
|
||||||
|
|
||||||
|
|
||||||
|
access_documents_attachment_group_manager,documents_attachment_group_manager,model_documents_document,,1,1,1,1
|
||||||
|
access_documents_share_group_manager,documents_share_group_manager,model_documents_share,,1,1,1,1
|
||||||
|
access_documents_tag_group_manager,documents_tag_group_manager,model_documents_tag,,1,1,1,1
|
||||||
|
access_documents_facet_group_manager,documents_facet_group_manager,model_documents_facet,,1,1,1,1
|
||||||
|
access_documents_request_wizard,access.documents.request_wizard,model_documents_request_wizard,,1,1,1,0
|
||||||
|
|
||||||
|
access_documents_workflow_action_group_manager,documents_workflow_action_group_manager,model_documents_workflow_action,,1,1,1,1
|
||||||
|
access_documents_workflow_rule_group_manager,documents_workflow_rule_group_manager,model_documents_workflow_rule,,1,1,1,1
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,276 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<!-- <record id="module_category_documents_management" model="ir.module.category">
|
||||||
|
<field name="name">Documents</field>
|
||||||
|
<field name="description">Allows you to manage your documents.</field>
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="group_dms_user" model="res.groups">
|
||||||
|
<field name="name">User</field>
|
||||||
|
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
|
||||||
|
<field name="category_id" ref="dms.module_category_documents_management"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="group_dms_manager" model="res.groups">
|
||||||
|
<field name="name">Administrator</field>
|
||||||
|
<field name="category_id" ref="dms.module_category_documents_management"/>
|
||||||
|
<field name="implied_ids" eval="[(4, ref('group_dms_user'))]"/>
|
||||||
|
<field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
|
||||||
|
</record> -->
|
||||||
|
|
||||||
|
<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="dms.category_dms_security"/>
|
||||||
|
</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="dms.category_dms_security"/>
|
||||||
|
<field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]" />
|
||||||
|
</record>
|
||||||
|
<record id="group_dms_delete_file" model="res.groups">
|
||||||
|
<field name="name">Permission to delete Files</field>
|
||||||
|
</record>
|
||||||
|
<record id="group_dms_delete_directory" model="res.groups">
|
||||||
|
<field name="name">Permission to delete Folders/Directories</field>
|
||||||
|
</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">User can only edit and delete their own groups.</field>
|
||||||
|
<field name="model_id" ref="model_dms_access_group"/>
|
||||||
|
<field name="groups" eval="[(4, ref('base.group_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">Admins can edit and delete all groups.</field>
|
||||||
|
<field name="model_id" ref="model_dms_access_group"/>
|
||||||
|
<field name="groups" eval="[(4, ref('base.group_erp_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),('create_uid','=',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="active" 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),('create_uid','=',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),('create_uid','=',user.id)]</field>
|
||||||
|
</record>
|
||||||
|
<!-- document -->
|
||||||
|
<record id="rule_document_computed_create" model="ir.rule">
|
||||||
|
<field name="name">Apply computed create permissions.</field>
|
||||||
|
<field name="model_id" ref="model_documents_document"/>
|
||||||
|
<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_document_computed_read" model="ir.rule">
|
||||||
|
<field name="name">Apply computed read permissions.</field>
|
||||||
|
<field name="model_id" ref="model_documents_document"/>
|
||||||
|
<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_document_computed_unlink" model="ir.rule">
|
||||||
|
<field name="name">Apply computed unlink permissions.</field>
|
||||||
|
<field name="model_id" ref="model_documents_document"/>
|
||||||
|
<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_document_computed_write" model="ir.rule">
|
||||||
|
<field name="name">Apply computed write permissions.</field>
|
||||||
|
<field name="model_id" ref="model_documents_document"/>
|
||||||
|
<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="documents_document_global_rule" model="ir.rule">
|
||||||
|
<field name="name">Documents.document: global</field>
|
||||||
|
<field name="model_id" ref="model_documents_document"/>
|
||||||
|
<field name="domain_force">['|',
|
||||||
|
('folder_id.company_id', '=', False),
|
||||||
|
('folder_id.company_id', 'in', company_ids)]
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
<record id="documents_document_readonly_rule" model="ir.rule">
|
||||||
|
<field name="name">Documents.document: readonly rule</field>
|
||||||
|
<field name="model_id" ref="model_documents_document"/>
|
||||||
|
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
|
||||||
|
<field name="domain_force">[
|
||||||
|
'&',
|
||||||
|
('folder_id.group_ids', 'in', [g.id for g in user.groups_id]),
|
||||||
|
'|',
|
||||||
|
('folder_id.user_specific', '=', False),
|
||||||
|
('owner_id', '=', user.id)
|
||||||
|
]
|
||||||
|
</field>
|
||||||
|
<field name="perm_read" eval="True"/>
|
||||||
|
<field name="perm_write" eval="False"/>
|
||||||
|
<field name="perm_create" eval="False"/>
|
||||||
|
<field name="perm_unlink" eval="False"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="documents_document_write_rule" model="ir.rule">
|
||||||
|
<field name="name">Documents.document: folder write groups</field>
|
||||||
|
<field name="model_id" ref="model_documents_document"/>
|
||||||
|
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
|
||||||
|
<field name="domain_force">[
|
||||||
|
'|',
|
||||||
|
('folder_id.group_ids', 'in', [g.id for g in user.groups_id]),
|
||||||
|
'&',
|
||||||
|
('folder_id.group_ids', '=', False),
|
||||||
|
('folder_id.group_ids', '=', False)]
|
||||||
|
</field>
|
||||||
|
<field name="perm_read" eval="True"/>
|
||||||
|
<field name="perm_write" eval="True"/>
|
||||||
|
<field name="perm_create" eval="True"/>
|
||||||
|
<field name="perm_unlink" eval="False"/>
|
||||||
|
</record>
|
||||||
|
-->
|
||||||
|
<record id="documents_document_manager_rule" model="ir.rule">
|
||||||
|
<field name="name">Documents.document: manager rule</field>
|
||||||
|
<field name="model_id" ref="model_documents_document"/>
|
||||||
|
<field name="groups" eval="[(4, ref('dms.group_dms_manager'))]"/>
|
||||||
|
<field name="domain_force">[(1, '=', 1)]</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- share links rules -->
|
||||||
|
|
||||||
|
<record id="documents_share_folder_company_rule" model="ir.rule">
|
||||||
|
<field name="name">Documents.share: company</field>
|
||||||
|
<field name="model_id" ref="model_documents_share"/>
|
||||||
|
<field name="domain_force">['|',
|
||||||
|
('folder_id.company_id', '=', False),
|
||||||
|
('folder_id.company_id', 'in', company_ids)]
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="documents_share_folder_create_uid_rule" model="ir.rule">
|
||||||
|
<field name="name">Documents.share: create uid</field>
|
||||||
|
<field name="model_id" ref="model_documents_share"/>
|
||||||
|
<field name="groups" eval="[(4, ref('dms.group_dms_user'))]"/>
|
||||||
|
<field name="domain_force">[('create_uid', '=', user.id)]</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="documents_share_manager_rule" model="ir.rule">
|
||||||
|
<field name="name">Documents.share: manager rule</field>
|
||||||
|
<field name="model_id" ref="model_documents_share"/>
|
||||||
|
<field name="groups" eval="[(4, ref('dms.group_dms_manager'))]"/>
|
||||||
|
<field name="domain_force">[(1, '=', 1)]</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
|
@ -0,0 +1,528 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<!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 0.15.1: http://docutils.sourceforge.net/" />
|
||||||
|
<title>Document Management System</title>
|
||||||
|
<style type="text/css">
|
||||||
|
|
||||||
|
/*
|
||||||
|
:Author: David Goodger (goodger@python.org)
|
||||||
|
:Id: $Id: html4css1.css 7952 2016-07-26 18:15:59Z milde $
|
||||||
|
:Copyright: This stylesheet has been placed in the public domain.
|
||||||
|
|
||||||
|
Default cascading style sheet for the HTML output of Docutils.
|
||||||
|
|
||||||
|
See http://docutils.sf.net/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: grey; } /* 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 {
|
||||||
|
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. !!
|
||||||
|
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
|
||||||
|
<p><a class="reference external" 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" 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" href="https://github.com/OCA/dms/tree/14.0/dms"><img alt="OCA/dms" src="https://img.shields.io/badge/github-OCA%2Fdms-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/dms-14-0/dms-14-0-dms"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external" href="https://runbot.odoo-community.org/runbot/292/14.0"><img alt="Try me on Runbot" src="https://img.shields.io/badge/runbot-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="id1">Installation</a><ul>
|
||||||
|
<li><a class="reference internal" href="#preview" id="id2">Preview</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><a class="reference internal" href="#configuration" id="id3">Configuration</a><ul>
|
||||||
|
<li><a class="reference internal" href="#migration" id="id4">Migration</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><a class="reference internal" href="#usage" id="id5">Usage</a><ul>
|
||||||
|
<li><a class="reference internal" href="#portal-functionality" id="id6">Portal functionality</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><a class="reference internal" href="#known-issues-roadmap" id="id7">Known issues / Roadmap</a></li>
|
||||||
|
<li><a class="reference internal" href="#bug-tracker" id="id8">Bug Tracker</a></li>
|
||||||
|
<li><a class="reference internal" href="#credits" id="id9">Credits</a><ul>
|
||||||
|
<li><a class="reference internal" href="#authors" id="id10">Authors</a></li>
|
||||||
|
<li><a class="reference internal" href="#contributors" id="id11">Contributors</a></li>
|
||||||
|
<li><a class="reference internal" href="#other-credits" id="id12">Other credits</a></li>
|
||||||
|
<li><a class="reference internal" href="#maintainers" id="id13">Maintainers</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="section" id="installation">
|
||||||
|
<h1><a class="toc-backref" href="#id1">Installation</a></h1>
|
||||||
|
<div class="section" id="preview">
|
||||||
|
<h2><a class="toc-backref" href="#id2">Preview</a></h2>
|
||||||
|
<p><cite>mail_preview_base</cite> 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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section" id="configuration">
|
||||||
|
<h1><a class="toc-backref" href="#id3">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>Afterwards go to <em>Documents -> Directories</em>.</li>
|
||||||
|
<li>Create a new directory, mark it as root and select the previously created setting.</li>
|
||||||
|
<li><dl class="first docutils">
|
||||||
|
<dt>On the Directory you can also define the access groups 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="#id4">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>
|
||||||
|
<div class="section" id="usage">
|
||||||
|
<h1><a class="toc-backref" href="#id5">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="#id6">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="#id7">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>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="section" id="bug-tracker">
|
||||||
|
<h1><a class="toc-backref" href="#id8">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 smashing it by providing a detailed and welcomed
|
||||||
|
<a class="reference external" href="https://github.com/OCA/dms/issues/new?body=module:%20dms%0Aversion:%2014.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="#id9">Credits</a></h1>
|
||||||
|
<div class="section" id="authors">
|
||||||
|
<h2><a class="toc-backref" href="#id10">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="#id11">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>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="section" id="other-credits">
|
||||||
|
<h2><a class="toc-backref" href="#id12">Other credits</a></h2>
|
||||||
|
<p>Some pictures are based on or inspired by:</p>
|
||||||
|
<ul class="simple">
|
||||||
|
<li><a class="reference external" href="https://www.flaticon.com/authors/roundicons">Roundicons</a></li>
|
||||||
|
<li><a class="reference external" href="https://www.flaticon.com/authors/smashicons">Smashicons</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="section" id="maintainers">
|
||||||
|
<h2><a class="toc-backref" href="#id13">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/14.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>
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><defs><clipPath id="_clipPath_DIUxPpL1A4o6jIKxrY3KVZ5F1CHA2mKL"><rect width="256" height="256"/></clipPath></defs><g clip-path="url(#_clipPath_DIUxPpL1A4o6jIKxrY3KVZ5F1CHA2mKL)"><rect width="256" height="256" style="fill:rgb(0,0,0)" fill-opacity="0"/><g><g><path d=" M 156.239 40 L 65.027 40 C 62.487 40 60.429 42.059 60.429 46.053 L 60.429 212.857 C 60.429 213.941 62.487 216 65.027 216 L 190.973 216 C 193.513 216 195.571 213.941 195.571 212.857 L 195.571 80.788 C 195.571 78.601 195.279 77.897 194.764 77.378 L 158.193 40.808 C 157.675 40.292 156.971 40 156.239 40 Z " fill="rgb(233,233,224)"/><path d=" M 190.973 216 L 65.027 216 C 62.487 216 60.429 213.941 60.429 211.402 L 60.429 162.571 L 195.571 162.571 L 195.571 211.402 C 195.571 213.941 193.513 216 190.973 216 Z " fill="rgb(255,193,79)"/><path d=" M 128.095 203.746 L 126.023 197.594 L 115.236 197.594 L 113.186 203.746 L 106.644 203.746 L 117.759 173.887 L 123.46 173.887 L 134.637 203.746 L 128.095 203.746 L 128.095 203.746 Z M 120.609 181.434 L 116.897 192.61 L 124.362 192.61 L 120.609 181.434 L 120.609 181.434 Z M 143.968 173.887 L 143.968 203.746 L 137.815 203.746 L 137.815 173.887 L 143.968 173.887 L 143.968 173.887 Z " fill-rule="evenodd" fill="rgb(255,255,255)"/><path d=" M 157.857 74.571 L 157.857 76.02 C 148.205 79.063 140.562 86.649 137.422 96.26 C 135.263 94.516 132.557 93.429 129.571 93.429 C 123.729 93.429 118.851 97.451 117.446 102.857 L 107.571 102.857 L 107.571 96.571 L 88.714 96.571 L 88.714 115.429 L 107.571 115.429 L 107.571 109.143 L 116.752 109.143 C 115.696 117.512 110.513 124.788 102.958 128.512 C 100.858 124.493 96.697 121.714 91.857 121.714 C 84.924 121.714 79.286 127.353 79.286 134.286 C 79.286 141.219 84.924 146.857 91.857 146.857 C 98.602 146.857 104.077 141.508 104.372 134.836 C 112.716 131.218 118.977 124.191 121.733 115.749 C 123.889 117.487 126.592 118.571 129.571 118.571 C 135.414 118.571 140.292 114.549 141.697 109.143 L 151.571 109.143 L 151.571 115.429 L 170.429 115.429 L 170.429 96.571 L 151.571 96.571 L 151.571 102.857 L 142.36 102.857 C 143.551 93.369 150.041 85.527 158.772 82.372 C 160.632 87.001 165.142 90.286 170.429 90.286 C 177.362 90.286 183 84.647 183 77.714 L 183 74.571 L 157.857 74.571 Z M 101.286 109.143 L 95 109.143 L 95 102.857 L 101.286 102.857 L 101.286 109.143 Z M 91.857 140.571 C 88.391 140.571 85.571 137.752 85.571 134.286 C 85.571 130.819 88.391 128 91.857 128 C 95.324 128 98.143 130.819 98.143 134.286 C 98.143 137.752 95.324 140.571 91.857 140.571 Z M 157.857 102.857 L 164.143 102.857 L 164.143 109.143 L 157.857 109.143 L 157.857 102.857 Z M 129.571 112.286 C 126.105 112.286 123.286 109.467 123.286 106 C 123.286 102.533 126.105 99.714 129.571 99.714 C 133.038 99.714 135.857 102.533 135.857 106 C 135.857 109.467 133.038 112.286 129.571 112.286 Z M 170.429 84 C 168.156 84 166.176 82.777 165.073 80.97 C 165.802 80.904 166.538 80.857 167.286 80.857 L 175.869 80.857 C 174.785 82.733 172.751 84 170.429 84 Z " fill="rgb(200,189,184)"/><path d=" M 157.857 40.475 L 157.857 77.714 L 195.097 77.714 L 157.857 40.475 Z " fill="rgb(217,215,202)"/></g></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 3.3 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><defs><clipPath id="_clipPath_6nAp4GoljLPqc74aMxxhpy3ZhrUJqOMK"><rect width="256" height="256"/></clipPath></defs><g clip-path="url(#_clipPath_6nAp4GoljLPqc74aMxxhpy3ZhrUJqOMK)"><rect width="256" height="256" style="fill:rgb(0,0,0)" fill-opacity="0"/><g><g><path d=" M 156.239 40 L 65.027 40 C 62.487 40 60.429 42.059 60.429 46.053 L 60.429 212.857 C 60.429 213.941 62.487 216 65.027 216 L 190.973 216 C 193.513 216 195.571 213.941 195.571 212.857 L 195.571 80.788 C 195.571 78.601 195.279 77.897 194.764 77.378 L 158.193 40.808 C 157.675 40.292 156.971 40 156.239 40 Z " fill="rgb(233,233,224)"/><path d=" M 157.857 40.475 L 157.857 77.714 L 195.097 77.714 L 157.857 40.475 Z " fill="rgb(217,215,202)"/><path d=" M 190.973 216 L 65.027 216 C 62.487 216 60.429 213.941 60.429 211.402 L 60.429 162.571 L 195.571 162.571 L 195.571 211.402 C 195.571 213.941 193.513 216 190.973 216 Z " fill="rgb(93,93,93)"/><g><path d=" M 112.936 91.207 C 111.707 89.978 109.721 89.978 108.492 91.207 L 89.635 110.064 C 88.406 111.293 88.406 113.279 89.635 114.508 L 108.492 133.365 C 109.105 133.978 109.91 134.286 110.714 134.286 C 111.519 134.286 112.323 133.978 112.936 133.365 C 114.165 132.136 114.165 130.15 112.936 128.921 L 96.301 112.286 L 112.936 95.651 C 114.165 94.422 114.165 92.435 112.936 91.207 Z " fill="rgb(93,93,93)"/><path d=" M 169.508 110.064 L 161.722 102.278 L 150.651 91.207 C 149.422 89.978 147.435 89.978 146.207 91.207 C 144.978 92.435 144.978 94.422 146.207 95.651 L 162.842 112.286 L 146.207 128.921 C 144.978 130.15 144.978 132.136 146.207 133.365 C 146.819 133.978 147.624 134.286 148.429 134.286 C 149.233 134.286 150.038 133.978 150.651 133.365 L 169.508 114.508 C 170.737 113.279 170.737 111.293 169.508 110.064 Z " fill="rgb(93,93,93)"/></g></g></g><path d=" M 122.595 203.746 L 120.523 197.594 L 109.736 197.594 L 107.686 203.746 L 101.144 203.746 L 112.259 173.887 L 117.96 173.887 L 129.137 203.746 L 122.595 203.746 L 122.595 203.746 Z M 115.109 181.434 L 111.397 192.61 L 118.862 192.61 L 115.109 181.434 L 115.109 181.434 Z M 143.964 194.559 L 143.964 173.887 L 150.116 173.887 L 150.116 194.559 L 150.116 194.559 Q 150.116 197.409 148.855 199.593 L 148.855 199.593 L 148.855 199.593 Q 147.594 201.777 145.297 202.967 L 145.297 202.967 L 145.297 202.967 Q 143 204.156 140.108 204.156 L 140.108 204.156 L 140.108 204.156 Q 135.371 204.156 132.726 201.747 L 132.726 201.747 L 132.726 201.747 Q 130.08 199.337 130.08 194.928 L 130.08 194.928 L 136.273 194.928 L 136.273 194.928 Q 136.273 197.122 137.196 198.168 L 137.196 198.168 L 137.196 198.168 Q 138.119 199.214 140.108 199.214 L 140.108 199.214 L 140.108 199.214 Q 141.872 199.214 142.918 198.004 L 142.918 198.004 L 142.918 198.004 Q 143.964 196.794 143.964 194.559 L 143.964 194.559 L 143.964 194.559 Z " fill-rule="evenodd" fill="rgb(255,255,255)"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 3.0 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><defs><clipPath id="_clipPath_FLHZ43wzdKFmTHD21bIUvTuw3Pzi3Qlw"><rect width="256" height="256"/></clipPath></defs><g clip-path="url(#_clipPath_FLHZ43wzdKFmTHD21bIUvTuw3Pzi3Qlw)"><rect width="256" height="256" style="fill:rgb(0,0,0)" fill-opacity="0"/><g><g><path d=" M 156.239 40 L 65.027 40 C 62.487 40 60.429 42.059 60.429 46.053 L 60.429 212.857 C 60.429 213.941 62.487 216 65.027 216 L 190.973 216 C 193.513 216 195.571 213.941 195.571 212.857 L 195.571 80.788 C 195.571 78.601 195.279 77.897 194.764 77.378 L 158.193 40.808 C 157.675 40.292 156.971 40 156.239 40 Z " fill="rgb(233,233,224)"/><path d=" M 157.857 40.475 L 157.857 77.714 L 195.097 77.714 L 157.857 40.475 Z " fill="rgb(217,215,202)"/><path d=" M 190.973 216 L 65.027 216 C 62.487 216 60.429 213.941 60.429 211.402 L 60.429 162.571 L 195.571 162.571 L 195.571 211.402 C 195.571 213.941 193.513 216 190.973 216 Z " fill="rgb(215,94,114)"/><path d=" M 114.595 203.746 L 112.523 197.594 L 101.736 197.594 L 99.686 203.746 L 93.144 203.746 L 104.259 173.887 L 109.96 173.887 L 121.137 203.746 L 114.595 203.746 L 114.595 203.746 Z M 107.109 181.434 L 103.397 192.61 L 110.862 192.61 L 107.109 181.434 L 107.109 181.434 Z M 128.232 173.887 L 134.959 196.343 L 141.727 173.887 L 148.576 173.887 L 138.179 203.746 L 131.76 203.746 L 121.403 173.887 L 128.232 173.887 L 128.232 173.887 Z M 157.928 173.887 L 157.928 203.746 L 151.775 203.746 L 151.775 173.887 L 157.928 173.887 L 157.928 173.887 Z " fill-rule="evenodd" fill="rgb(255,255,255)"/><path d=" M 113.857 128 L 113.857 105.855 L 113.857 84 L 148.429 106 L 113.857 128 Z " fill="rgb(200,189,184)"/></g></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><defs><clipPath id="_clipPath_QDq7UHuMAP1DndjZvSJ7oBHBPdwuwQhy"><rect width="256" height="256"/></clipPath></defs><g clip-path="url(#_clipPath_QDq7UHuMAP1DndjZvSJ7oBHBPdwuwQhy)"><rect width="256" height="256" style="fill:rgb(0,0,0)" fill-opacity="0"/><g><g><path d=" M 156.239 40 L 65.027 40 C 62.487 40 60.429 42.059 60.429 46.053 L 60.429 212.857 C 60.429 213.941 62.487 216 65.027 216 L 190.973 216 C 193.513 216 195.571 213.941 195.571 212.857 L 195.571 80.788 C 195.571 78.601 195.279 77.897 194.764 77.378 L 158.193 40.808 C 157.675 40.292 156.971 40 156.239 40 Z " fill="rgb(233,233,224)"/><path d=" M 157.857 40.475 L 157.857 77.714 L 195.097 77.714 L 157.857 40.475 Z " fill="rgb(217,215,202)"/><path d=" M 190.973 216 L 65.027 216 C 62.487 216 60.429 213.941 60.429 211.402 L 60.429 162.571 L 195.571 162.571 L 195.571 211.402 C 195.571 213.941 193.513 216 190.973 216 Z " fill="rgb(128,140,155)"/></g></g><path d=" M 132.954 195.8 L 139.106 195.8 L 139.106 195.8 Q 138.758 200.619 135.548 203.388 L 135.548 203.388 L 135.548 203.388 Q 132.339 206.156 127.089 206.156 L 127.089 206.156 L 127.089 206.156 Q 121.347 206.156 118.055 202.291 L 118.055 202.291 L 118.055 202.291 Q 114.764 198.425 114.764 191.678 L 114.764 191.678 L 114.764 189.853 L 114.764 189.853 Q 114.764 185.546 116.281 182.265 L 116.281 182.265 L 116.281 182.265 Q 117.799 178.983 120.619 177.23 L 120.619 177.23 L 120.619 177.23 Q 123.438 175.477 127.171 175.477 L 127.171 175.477 L 127.171 175.477 Q 132.339 175.477 135.497 178.245 L 135.497 178.245 L 135.497 178.245 Q 138.655 181.014 139.147 186.018 L 139.147 186.018 L 132.995 186.018 L 132.995 186.018 Q 132.77 183.126 131.385 181.824 L 131.385 181.824 L 131.385 181.824 Q 130.001 180.521 127.171 180.521 L 127.171 180.521 L 127.171 180.521 Q 124.095 180.521 122.567 182.726 L 122.567 182.726 L 122.567 182.726 Q 121.039 184.931 120.998 189.565 L 120.998 189.565 L 120.998 191.821 L 120.998 191.821 Q 120.998 196.661 122.464 198.896 L 122.464 198.896 L 122.464 198.896 Q 123.931 201.132 127.089 201.132 L 127.089 201.132 L 127.089 201.132 Q 129.939 201.132 131.344 199.83 L 131.344 199.83 L 131.344 199.83 Q 132.749 198.527 132.954 195.8 L 132.954 195.8 L 132.954 195.8 Z " fill="rgb(255,255,255)"/><path d=" M 131.94 106.301 L 131.94 106.301 L 131.94 106.301 Q 127.32 106.301 124.86 109.121 L 124.86 109.121 L 124.86 109.121 Q 122.4 111.941 122.4 117.101 L 122.4 117.101 L 122.4 117.101 Q 122.4 122.321 125.13 125.141 L 125.13 125.141 L 125.13 125.141 Q 127.86 127.961 132.48 127.961 L 132.48 127.961 L 132.48 127.961 Q 134.94 127.961 136.65 127.301 L 136.65 127.301 L 136.65 127.301 Q 138.36 126.641 139.92 125.801 L 139.92 125.801 L 139.92 125.801 Q 140.94 126.641 141.51 127.811 L 141.51 127.811 L 141.51 127.811 Q 142.08 128.981 142.08 130.541 L 142.08 130.541 L 142.08 130.541 Q 142.08 133.001 139.35 134.711 L 139.35 134.711 L 139.35 134.711 Q 136.62 136.421 131.04 136.421 L 131.04 136.421 L 131.04 136.421 Q 127.02 136.421 123.48 135.281 L 123.48 135.281 L 123.48 135.281 Q 119.94 134.141 117.3 131.771 L 117.3 131.771 L 117.3 131.771 Q 114.66 129.401 113.13 125.771 L 113.13 125.771 L 113.13 125.771 Q 111.6 122.141 111.6 117.101 L 111.6 117.101 L 111.6 117.101 Q 111.6 112.421 113.07 108.851 L 113.07 108.851 L 113.07 108.851 Q 114.54 105.281 117.09 102.821 L 117.09 102.821 L 117.09 102.821 Q 119.64 100.361 123.06 99.101 L 123.06 99.101 L 123.06 99.101 Q 126.48 97.841 130.38 97.841 L 130.38 97.841 L 130.38 97.841 Q 135.9 97.841 138.93 99.641 L 138.93 99.641 L 138.93 99.641 Q 141.96 101.441 141.96 104.261 L 141.96 104.261 L 141.96 104.261 Q 141.96 105.821 141.18 106.961 L 141.18 106.961 L 141.18 106.961 Q 140.4 108.101 139.38 108.761 L 139.38 108.761 L 139.38 108.761 Q 137.82 107.741 136.05 107.021 L 136.05 107.021 L 136.05 107.021 Q 134.28 106.301 131.94 106.301 Z " fill="rgb(128,140,155)"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 4.0 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><defs><clipPath id="_clipPath_TQmEGhsjtrLEyjf6zmhvf3bpAZU65XHf"><rect width="256" height="256"/></clipPath></defs><g clip-path="url(#_clipPath_TQmEGhsjtrLEyjf6zmhvf3bpAZU65XHf)"><rect width="256" height="256" style="fill:rgb(0,0,0)" fill-opacity="0"/><g><g><path d=" M 156.239 40 L 65.027 40 C 62.487 40 60.429 42.059 60.429 46.053 L 60.429 212.857 C 60.429 213.941 62.487 216 65.027 216 L 190.973 216 C 193.513 216 195.571 213.941 195.571 212.857 L 195.571 80.788 C 195.571 78.601 195.279 77.897 194.764 77.378 L 158.193 40.808 C 157.675 40.292 156.971 40 156.239 40 Z " fill="rgb(233,233,224)"/><path d=" M 157.857 40.475 L 157.857 77.714 L 195.097 77.714 L 157.857 40.475 Z " fill="rgb(217,215,202)"/><path d=" M 190.973 216 L 65.027 216 C 62.487 216 60.429 213.941 60.429 211.402 L 60.429 162.571 L 195.571 162.571 L 195.571 211.402 C 195.571 213.941 193.513 216 190.973 216 Z " fill="rgb(93,93,93)"/><g><path d=" M 112.936 91.207 C 111.707 89.978 109.721 89.978 108.492 91.207 L 89.635 110.064 C 88.406 111.293 88.406 113.279 89.635 114.508 L 108.492 133.365 C 109.105 133.978 109.91 134.286 110.714 134.286 C 111.519 134.286 112.323 133.978 112.936 133.365 C 114.165 132.136 114.165 130.15 112.936 128.921 L 96.301 112.286 L 112.936 95.651 C 114.165 94.422 114.165 92.435 112.936 91.207 Z " fill="rgb(93,93,93)"/><path d=" M 169.508 110.064 L 161.722 102.278 L 150.651 91.207 C 149.422 89.978 147.435 89.978 146.207 91.207 C 144.978 92.435 144.978 94.422 146.207 95.651 L 162.842 112.286 L 146.207 128.921 C 144.978 130.15 144.978 132.136 146.207 133.365 C 146.819 133.978 147.624 134.286 148.429 134.286 C 149.233 134.286 150.038 133.978 150.651 133.365 L 169.508 114.508 C 170.737 113.279 170.737 111.293 169.508 110.064 Z " fill="rgb(93,93,93)"/></g></g></g><path d=" M 107.954 195.8 L 114.106 195.8 L 114.106 195.8 Q 113.758 200.619 110.548 203.388 L 110.548 203.388 L 110.548 203.388 Q 107.339 206.156 102.089 206.156 L 102.089 206.156 L 102.089 206.156 Q 96.347 206.156 93.055 202.291 L 93.055 202.291 L 93.055 202.291 Q 89.764 198.425 89.764 191.678 L 89.764 191.678 L 89.764 189.853 L 89.764 189.853 Q 89.764 185.546 91.281 182.265 L 91.281 182.265 L 91.281 182.265 Q 92.799 178.983 95.619 177.23 L 95.619 177.23 L 95.619 177.23 Q 98.438 175.477 102.171 175.477 L 102.171 175.477 L 102.171 175.477 Q 107.339 175.477 110.497 178.245 L 110.497 178.245 L 110.497 178.245 Q 113.655 181.014 114.147 186.018 L 114.147 186.018 L 107.995 186.018 L 107.995 186.018 Q 107.77 183.126 106.385 181.824 L 106.385 181.824 L 106.385 181.824 Q 105.001 180.521 102.171 180.521 L 102.171 180.521 L 102.171 180.521 Q 99.095 180.521 97.567 182.726 L 97.567 182.726 L 97.567 182.726 Q 96.039 184.931 95.998 189.565 L 95.998 189.565 L 95.998 191.821 L 95.998 191.821 Q 95.998 196.661 97.464 198.896 L 97.464 198.896 L 97.464 198.896 Q 98.931 201.132 102.089 201.132 L 102.089 201.132 L 102.089 201.132 Q 104.939 201.132 106.344 199.83 L 106.344 199.83 L 106.344 199.83 Q 107.749 198.527 107.954 195.8 L 107.954 195.8 L 107.954 195.8 Z M 129.754 205.746 L 118.146 205.746 L 118.146 175.887 L 128.605 175.887 L 128.605 175.887 Q 134.04 175.887 136.85 177.968 L 136.85 177.968 L 136.85 177.968 Q 139.659 180.05 139.659 184.069 L 139.659 184.069 L 139.659 184.069 Q 139.659 186.264 138.531 187.935 L 138.531 187.935 L 138.531 187.935 Q 137.403 189.606 135.394 190.386 L 135.394 190.386 L 135.394 190.386 Q 137.69 190.96 139.013 192.703 L 139.013 192.703 L 139.013 192.703 Q 140.336 194.446 140.336 196.969 L 140.336 196.969 L 140.336 196.969 Q 140.336 201.275 137.588 203.49 L 137.588 203.49 L 137.588 203.49 Q 134.84 205.705 129.754 205.746 L 129.754 205.746 L 129.754 205.746 Z M 129.938 192.744 L 124.299 192.744 L 124.299 200.804 L 129.569 200.804 L 129.569 200.804 Q 131.743 200.804 132.963 199.768 L 132.963 199.768 L 132.963 199.768 Q 134.184 198.732 134.184 196.907 L 134.184 196.907 L 134.184 196.907 Q 134.184 192.806 129.938 192.744 L 129.938 192.744 L 129.938 192.744 Z M 124.299 180.87 L 124.299 188.396 L 128.852 188.396 L 128.852 188.396 Q 133.507 188.314 133.507 184.685 L 133.507 184.685 L 133.507 184.685 Q 133.507 182.654 132.328 181.762 L 132.328 181.762 L 132.328 181.762 Q 131.148 180.87 128.605 180.87 L 128.605 180.87 L 124.299 180.87 L 124.299 180.87 Z M 151.103 175.887 L 151.103 200.804 L 164.166 200.804 L 164.166 205.746 L 144.95 205.746 L 144.95 175.887 L 151.103 175.887 L 151.103 175.887 Z " fill-rule="evenodd" fill="rgb(255,255,255)"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><defs><clipPath id="_clipPath_AC8gC2Fy7eYKCxX4InFHVsjb6C7TwY0y"><rect width="256" height="256"/></clipPath></defs><g clip-path="url(#_clipPath_AC8gC2Fy7eYKCxX4InFHVsjb6C7TwY0y)"><rect width="256" height="256" style="fill:rgb(0,0,0)" fill-opacity="0"/><g><g><path d=" M 156.239 40 L 65.027 40 C 62.487 40 60.429 42.059 60.429 46.053 L 60.429 212.857 C 60.429 213.941 62.487 216 65.027 216 L 190.973 216 C 193.513 216 195.571 213.941 195.571 212.857 L 195.571 80.788 C 195.571 78.601 195.279 77.897 194.764 77.378 L 158.193 40.808 C 157.675 40.292 156.971 40 156.239 40 Z " fill="rgb(233,233,224)"/><path d=" M 157.857 40.475 L 157.857 77.714 L 195.097 77.714 L 157.857 40.475 Z " fill="rgb(217,215,202)"/><path d=" M 190.973 216 L 65.027 216 C 62.487 216 60.429 213.941 60.429 211.402 L 60.429 162.571 L 195.571 162.571 L 195.571 211.402 C 195.571 213.941 193.513 216 190.973 216 Z " fill="rgb(233,99,96)"/><path d=" M 117 90.286 L 117 77.714 L 79.286 77.714 L 79.286 90.286 L 79.286 96.571 L 79.286 102.857 L 79.286 109.143 L 79.286 115.429 L 79.286 121.714 L 79.286 128 L 79.286 134.286 L 79.286 146.857 L 110.714 146.857 L 117 146.857 L 183 146.857 L 183 134.286 L 183 128 L 183 121.714 L 183 115.429 L 183 109.143 L 183 102.857 L 183 90.286 L 117 90.286 Z M 85.571 84 L 110.714 84 L 110.714 90.286 L 85.571 90.286 L 85.571 84 Z M 85.571 96.571 L 110.714 96.571 L 110.714 102.857 L 85.571 102.857 L 85.571 96.571 Z M 85.571 109.143 L 110.714 109.143 L 110.714 115.429 L 85.571 115.429 L 85.571 109.143 Z M 85.571 121.714 L 110.714 121.714 L 110.714 128 L 85.571 128 L 85.571 121.714 Z M 110.714 140.571 L 85.571 140.571 L 85.571 134.286 L 110.714 134.286 L 110.714 140.571 Z M 176.714 140.571 L 117 140.571 L 117 134.286 L 176.714 134.286 L 176.714 140.571 Z M 176.714 128 L 117 128 L 117 121.714 L 176.714 121.714 L 176.714 128 Z M 176.714 115.429 L 117 115.429 L 117 109.143 L 176.714 109.143 L 176.714 115.429 Z M 117 102.857 L 117 96.571 L 176.714 96.571 L 176.714 102.857 L 117 102.857 Z " fill="rgb(200,189,184)"/></g></g><path d=" M 99.915 205.746 L 90.666 205.746 L 90.666 175.887 L 99.854 175.887 L 99.854 175.887 Q 103.791 175.887 106.898 177.661 L 106.898 177.661 L 106.898 177.661 Q 110.005 179.435 111.748 182.706 L 111.748 182.706 L 111.748 182.706 Q 113.491 185.977 113.491 190.14 L 113.491 190.14 L 113.491 191.514 L 113.491 191.514 Q 113.491 195.677 111.779 198.917 L 111.779 198.917 L 111.779 198.917 Q 110.066 202.157 106.949 203.941 L 106.949 203.941 L 106.949 203.941 Q 103.832 205.726 99.915 205.746 L 99.915 205.746 L 99.915 205.746 Z M 99.854 180.87 L 96.818 180.87 L 96.818 200.804 L 99.792 200.804 L 99.792 200.804 Q 103.401 200.804 105.309 198.445 L 105.309 198.445 L 105.309 198.445 Q 107.216 196.087 107.257 191.698 L 107.257 191.698 L 107.257 190.119 L 107.257 190.119 Q 107.257 185.566 105.37 183.218 L 105.37 183.218 L 105.37 183.218 Q 103.483 180.87 99.854 180.87 L 99.854 180.87 L 99.854 180.87 Z M 129.569 205.746 L 117.962 205.746 L 117.962 175.887 L 128.421 175.887 L 128.421 175.887 Q 133.855 175.887 136.665 177.968 L 136.665 177.968 L 136.665 177.968 Q 139.475 180.05 139.475 184.069 L 139.475 184.069 L 139.475 184.069 Q 139.475 186.264 138.347 187.935 L 138.347 187.935 L 138.347 187.935 Q 137.219 189.606 135.209 190.386 L 135.209 190.386 L 135.209 190.386 Q 137.506 190.96 138.829 192.703 L 138.829 192.703 L 138.829 192.703 Q 140.151 194.446 140.151 196.969 L 140.151 196.969 L 140.151 196.969 Q 140.151 201.275 137.403 203.49 L 137.403 203.49 L 137.403 203.49 Q 134.655 205.705 129.569 205.746 L 129.569 205.746 L 129.569 205.746 Z M 129.754 192.744 L 124.114 192.744 L 124.114 200.804 L 129.385 200.804 L 129.385 200.804 Q 131.559 200.804 132.779 199.768 L 132.779 199.768 L 132.779 199.768 Q 133.999 198.732 133.999 196.907 L 133.999 196.907 L 133.999 196.907 Q 133.999 192.806 129.754 192.744 L 129.754 192.744 L 129.754 192.744 Z M 124.114 180.87 L 124.114 188.396 L 128.667 188.396 L 128.667 188.396 Q 133.322 188.314 133.322 184.685 L 133.322 184.685 L 133.322 184.685 Q 133.322 182.654 132.143 181.762 L 132.143 181.762 L 132.143 181.762 Q 130.964 180.87 128.421 180.87 L 128.421 180.87 L 124.114 180.87 L 124.114 180.87 Z M 162.73 188.581 L 162.73 193.544 L 150.918 193.544 L 150.918 205.746 L 144.766 205.746 L 144.766 175.887 L 164.207 175.887 L 164.207 180.87 L 150.918 180.87 L 150.918 188.581 L 162.73 188.581 L 162.73 188.581 Z " fill-rule="evenodd" fill="rgb(255,255,255)"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 4.6 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><defs><clipPath id="_clipPath_JRbDI2Nk1iQLWnj5TvFr9peayMg5GuJ2"><rect width="256" height="256"/></clipPath></defs><g clip-path="url(#_clipPath_JRbDI2Nk1iQLWnj5TvFr9peayMg5GuJ2)"><rect width="256" height="256" style="fill:rgb(0,0,0)" fill-opacity="0"/><g><g><path d=" M 156.239 38 L 65.027 38 C 62.487 38 60.429 40.059 60.429 44.053 L 60.429 210.857 C 60.429 211.941 62.487 214 65.027 214 L 190.973 214 C 193.513 214 195.571 211.941 195.571 210.857 L 195.571 78.788 C 195.571 76.601 195.279 75.897 194.764 75.378 L 158.193 38.808 C 157.675 38.292 156.971 38 156.239 38 Z " fill="rgb(233,233,224)"/><path d=" M 190.973 214 L 65.027 214 C 62.487 214 60.429 211.941 60.429 209.402 L 60.429 160.571 L 195.571 160.571 L 195.571 209.402 C 195.571 211.941 193.513 214 190.973 214 Z " fill="rgb(62,71,83)"/><path d=" M 157.857 38.475 L 157.857 75.714 L 195.097 75.714 L 157.857 38.475 Z " fill="rgb(217,215,202)"/></g></g><path d=" M 102.415 203.746 L 93.166 203.746 L 93.166 173.887 L 102.354 173.887 L 102.354 173.887 Q 106.291 173.887 109.398 175.661 L 109.398 175.661 L 109.398 175.661 Q 112.505 177.435 114.248 180.706 L 114.248 180.706 L 114.248 180.706 Q 115.991 183.977 115.991 188.14 L 115.991 188.14 L 115.991 189.514 L 115.991 189.514 Q 115.991 193.677 114.279 196.917 L 114.279 196.917 L 114.279 196.917 Q 112.566 200.157 109.449 201.941 L 109.449 201.941 L 109.449 201.941 Q 106.332 203.726 102.415 203.746 L 102.415 203.746 L 102.415 203.746 Z M 102.354 178.87 L 99.318 178.87 L 99.318 198.804 L 102.292 198.804 L 102.292 198.804 Q 105.901 198.804 107.809 196.445 L 107.809 196.445 L 107.809 196.445 Q 109.716 194.087 109.757 189.698 L 109.757 189.698 L 109.757 188.119 L 109.757 188.119 Q 109.757 183.566 107.87 181.218 L 107.87 181.218 L 107.87 181.218 Q 105.983 178.87 102.354 178.87 L 102.354 178.87 L 102.354 178.87 Z M 126.614 173.887 L 126.614 198.804 L 139.678 198.804 L 139.678 203.746 L 120.462 203.746 L 120.462 173.887 L 126.614 173.887 L 126.614 173.887 Z M 149.357 173.887 L 149.357 198.804 L 162.421 198.804 L 162.421 203.746 L 143.205 203.746 L 143.205 173.887 L 149.357 173.887 L 149.357 173.887 Z " fill-rule="evenodd" fill="rgb(255,255,255)"/><g><g><g><g><path d=" M 128 98.25 C 122.625 98.25 118.25 102.625 118.25 108 C 118.25 113.375 122.625 117.75 128 117.75 C 133.375 117.75 137.75 113.375 137.75 108 C 137.75 102.625 133.375 98.25 128 98.25 Z " fill="rgb(200,189,184)"/><path d=" M 164.364 101.37 L 160.061 100.541 C 158.134 100.174 156.623 98.913 155.905 97.093 C 155.186 95.263 155.44 93.31 156.603 91.727 L 159.304 88.042 C 160.253 86.752 160.113 84.958 158.979 83.827 L 153.146 77.993 C 152.047 76.891 150.312 76.729 149.028 77.6 L 145.398 80.053 C 143.779 81.155 141.823 81.334 140.019 80.554 C 138.222 79.767 137.016 78.207 136.72 76.267 L 136.025 71.756 C 135.78 70.17 134.415 69 132.81 69 L 124.561 69 C 123.005 69 121.666 70.105 121.37 71.636 L 120.301 77.193 C 119.94 79.075 118.718 80.576 116.947 81.308 C 115.175 82.046 113.245 81.847 111.659 80.771 L 106.972 77.6 C 105.689 76.729 103.96 76.891 102.855 77.993 L 97.021 83.827 C 95.887 84.958 95.747 86.752 96.696 88.042 L 99.397 91.73 C 100.56 93.31 100.814 95.263 100.096 97.093 C 99.377 98.913 97.866 100.174 95.936 100.541 L 91.636 101.37 C 90.105 101.666 89 103.005 89 104.561 L 89 112.81 C 89 114.415 90.17 115.78 91.756 116.024 L 96.267 116.72 C 98.207 117.015 99.767 118.221 100.554 120.018 C 101.337 121.816 101.155 123.775 100.053 125.4 L 97.6 129.027 C 96.725 130.314 96.891 132.043 97.993 133.145 L 103.827 138.979 C 104.961 140.116 106.752 140.246 108.042 139.304 L 111.73 136.603 C 113.31 135.443 115.26 135.192 117.093 135.904 C 118.913 136.622 120.174 138.134 120.541 140.064 L 121.37 144.364 C 121.666 145.895 123.005 147 124.561 147 L 132.81 147 C 134.415 147 135.78 145.83 136.024 144.244 L 136.512 141.072 C 136.817 139.086 138.055 137.51 139.908 136.746 C 141.747 135.979 143.746 136.217 145.368 137.406 L 147.958 139.304 C 149.242 140.247 151.039 140.117 152.173 138.979 L 158.007 133.145 C 159.109 132.044 159.275 130.315 158.4 129.028 L 155.947 125.398 C 154.845 123.776 154.663 121.816 155.446 120.019 C 156.233 118.222 157.793 117.016 159.733 116.72 L 164.244 116.025 C 165.83 115.781 167 114.416 167 112.81 L 167 104.562 C 167 103.005 165.895 101.666 164.364 101.37 Z M 128 124.25 C 119.04 124.25 111.75 116.96 111.75 108 C 111.75 99.04 119.04 91.75 128 91.75 C 136.96 91.75 144.25 99.04 144.25 108 C 144.25 116.96 136.96 124.25 128 124.25 Z " fill="rgb(200,189,184)"/></g></g></g></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><defs><clipPath id="_clipPath_D4Sj8YJrR0Fd21hr8GyZRcCPEVvEhAsA"><rect width="256" height="256"/></clipPath></defs><g clip-path="url(#_clipPath_D4Sj8YJrR0Fd21hr8GyZRcCPEVvEhAsA)"><rect width="256" height="256" style="fill:rgb(0,0,0)" fill-opacity="0"/><g><g><path d=" M 156.239 40 L 65.027 40 C 62.487 40 60.429 42.059 60.429 46.053 L 60.429 212.857 C 60.429 213.941 62.487 216 65.027 216 L 190.973 216 C 193.513 216 195.571 213.941 195.571 212.857 L 195.571 80.788 C 195.571 78.601 195.279 77.897 194.764 77.378 L 158.193 40.808 C 157.675 40.292 156.971 40 156.239 40 Z " fill="rgb(233,233,224)"/><path d=" M 157.857 40.475 L 157.857 77.714 L 195.097 77.714 L 157.857 40.475 Z " fill="rgb(217,215,202)"/><path d=" M 190.973 216 L 65.027 216 C 62.487 216 60.429 213.941 60.429 211.402 L 60.429 162.571 L 195.571 162.571 L 195.571 211.402 C 195.571 213.941 193.513 216 190.973 216 Z " fill="rgb(134,151,203)"/><rect x="79.286" y="80.857" width="47.143" height="47.347" transform="matrix(1,0,0,1,0,0)" fill="rgb(200,189,184)"/><rect x="148.429" y="84" width="6.286" height="22" transform="matrix(1,0,0,1,0,0)" fill="rgb(173,162,158)"/><rect x="148.429" y="131.143" width="6.286" height="22" transform="matrix(1,0,0,1,0,0)" fill="rgb(173,162,158)"/><path d=" M 167.286 134.286 L 135.857 134.286 L 135.857 102.857 L 167.286 102.857 L 167.286 134.286 Z M 142.143 128 L 161 128 L 161 109.143 L 142.143 109.143 L 142.143 128 Z " fill="rgb(173,162,158)"/><rect x="164.143" y="115.429" width="22" height="6.286" transform="matrix(1,0,0,1,0,0)" fill="rgb(173,162,158)"/><rect x="117" y="115.429" width="22" height="6.286" transform="matrix(1,0,0,1,0,0)" fill="rgb(173,162,158)"/></g></g><path d=" M 92.415 203.746 L 83.166 203.746 L 83.166 173.887 L 92.354 173.887 L 92.354 173.887 Q 96.291 173.887 99.398 175.661 L 99.398 175.661 L 99.398 175.661 Q 102.505 177.435 104.248 180.706 L 104.248 180.706 L 104.248 180.706 Q 105.991 183.977 105.991 188.14 L 105.991 188.14 L 105.991 189.514 L 105.991 189.514 Q 105.991 193.677 104.279 196.917 L 104.279 196.917 L 104.279 196.917 Q 102.566 200.157 99.449 201.941 L 99.449 201.941 L 99.449 201.941 Q 96.332 203.726 92.415 203.746 L 92.415 203.746 L 92.415 203.746 Z M 92.354 178.87 L 89.318 178.87 L 89.318 198.804 L 92.292 198.804 L 92.292 198.804 Q 95.901 198.804 97.809 196.445 L 97.809 196.445 L 97.809 196.445 Q 99.716 194.087 99.757 189.698 L 99.757 189.698 L 99.757 188.119 L 99.757 188.119 Q 99.757 183.566 97.87 181.218 L 97.87 181.218 L 97.87 181.218 Q 95.983 178.87 92.354 178.87 L 92.354 178.87 L 92.354 178.87 Z M 128.816 173.887 L 133.718 195.133 L 137.737 173.887 L 143.869 173.887 L 137.245 203.746 L 131.052 203.746 L 126.191 183.771 L 121.331 203.746 L 115.138 203.746 L 108.514 173.887 L 114.646 173.887 L 118.686 195.092 L 123.607 173.887 L 128.816 173.887 L 128.816 173.887 Z M 170.816 188.058 L 170.816 199.973 L 170.816 199.973 Q 169.155 201.962 166.12 203.059 L 166.12 203.059 L 166.12 203.059 Q 163.085 204.156 159.394 204.156 L 159.394 204.156 L 159.394 204.156 Q 155.518 204.156 152.595 202.464 L 152.595 202.464 L 152.595 202.464 Q 149.673 200.772 148.083 197.553 L 148.083 197.553 L 148.083 197.553 Q 146.494 194.333 146.453 189.985 L 146.453 189.985 L 146.453 187.955 L 146.453 187.955 Q 146.453 183.484 147.96 180.213 L 147.96 180.213 L 147.96 180.213 Q 149.468 176.942 152.308 175.209 L 152.308 175.209 L 152.308 175.209 Q 155.148 173.477 158.963 173.477 L 158.963 173.477 L 158.963 173.477 Q 164.274 173.477 167.269 176.009 L 167.269 176.009 L 167.269 176.009 Q 170.263 178.542 170.816 183.382 L 170.816 183.382 L 164.828 183.382 L 164.828 183.382 Q 164.418 180.818 163.013 179.629 L 163.013 179.629 L 163.013 179.629 Q 161.608 178.439 159.147 178.439 L 159.147 178.439 L 159.147 178.439 Q 156.01 178.439 154.369 180.798 L 154.369 180.798 L 154.369 180.798 Q 152.729 183.156 152.708 187.812 L 152.708 187.812 L 152.708 189.719 L 152.708 189.719 Q 152.708 194.415 154.492 196.814 L 154.492 196.814 L 154.492 196.814 Q 156.276 199.214 159.722 199.214 L 159.722 199.214 L 159.722 199.214 Q 163.188 199.214 164.664 197.737 L 164.664 197.737 L 164.664 192.59 L 159.065 192.59 L 159.065 188.058 L 170.816 188.058 L 170.816 188.058 Z " fill-rule="evenodd" fill="rgb(255,255,255)"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 4.4 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><defs><clipPath id="_clipPath_eMGLtDmIvhgzWzMJn5Zi3pkjzRQYkoFv"><rect width="256" height="256"/></clipPath></defs><g clip-path="url(#_clipPath_eMGLtDmIvhgzWzMJn5Zi3pkjzRQYkoFv)"><rect width="256" height="256" style="fill:rgb(0,0,0)" fill-opacity="0"/><path d=" M 156.239 40 L 65.027 40 C 62.487 40 60.429 42.059 60.429 46.053 L 60.429 212.857 C 60.429 213.941 62.487 216 65.027 216 L 190.973 216 C 193.513 216 195.571 213.941 195.571 212.857 L 195.571 80.788 C 195.571 78.601 195.279 77.897 194.764 77.378 L 158.193 40.808 C 157.675 40.292 156.971 40 156.239 40 Z " fill="rgb(233,233,224)"/><path d=" M 157.857 40.475 L 157.857 77.714 L 195.097 77.714 L 157.857 40.475 Z " fill="rgb(217,215,202)"/><path d=" M 190.973 216 L 65.027 216 C 62.487 216 60.429 213.941 60.429 211.402 L 60.429 162.571 L 195.571 162.571 L 195.571 211.402 C 195.571 213.941 193.513 216 190.973 216 Z " fill="rgb(74,134,232)"/><path d=" M 157.767 134.37 L 157.767 104.603 Q 156.526 105.999 155.092 107.161 Q 144.705 115.146 138.581 120.262 Q 136.604 121.929 135.364 122.859 Q 134.124 123.789 132.012 124.739 Q 129.899 125.688 128.039 125.688 L 127.961 125.688 Q 126.101 125.688 123.988 124.739 Q 121.876 123.789 120.636 122.859 Q 119.396 121.929 117.419 120.262 Q 111.295 115.146 100.908 107.161 Q 99.474 105.999 98.233 104.603 L 98.233 134.37 Q 98.233 134.874 98.602 135.242 Q 98.97 135.61 99.474 135.61 L 156.526 135.61 Q 157.03 135.61 157.398 135.242 Q 157.767 134.874 157.767 134.37 Z M 157.767 93.635 L 157.767 92.685 L 157.747 92.181 L 157.631 91.697 L 157.418 91.348 L 157.069 91.057 L 156.526 90.96 L 99.474 90.96 Q 98.97 90.96 98.602 91.329 Q 98.233 91.697 98.233 92.201 Q 98.233 98.712 103.931 103.208 Q 111.411 109.099 119.473 115.495 Q 119.706 115.688 120.83 116.638 Q 121.954 117.588 122.613 118.091 Q 123.271 118.595 124.337 119.312 Q 125.403 120.029 126.295 120.378 Q 127.186 120.727 127.961 120.727 L 128.039 120.727 Q 128.814 120.727 129.705 120.378 Q 130.597 120.029 131.663 119.312 Q 132.729 118.595 133.387 118.091 Q 134.046 117.588 135.17 116.638 Q 136.294 115.688 136.527 115.495 Q 144.589 109.099 152.069 103.208 Q 154.162 101.541 155.964 98.731 Q 157.767 95.921 157.767 93.635 Z M 162.728 92.201 L 162.728 134.37 Q 162.728 136.928 160.906 138.75 Q 159.084 140.571 156.526 140.571 L 99.474 140.571 Q 96.916 140.571 95.094 138.75 Q 93.272 136.928 93.272 134.37 L 93.272 92.201 Q 93.272 89.643 95.094 87.821 Q 96.916 85.999 99.474 85.999 L 156.526 85.999 Q 159.084 85.999 160.906 87.821 Q 162.728 89.643 162.728 92.201 Z " fill="rgb(74,134,232)"/><path d=" M 105.631 185.881 L 105.631 190.701 L 93.818 190.701 L 93.818 198.699 L 107.682 198.699 L 107.682 203.641 L 87.666 203.641 L 87.666 173.782 L 107.641 173.782 L 107.641 178.765 L 93.818 178.765 L 93.818 185.881 L 105.631 185.881 L 105.631 185.881 Z M 111.291 173.782 L 119.33 173.782 L 127 195.438 L 134.629 173.782 L 142.709 173.782 L 142.709 203.641 L 136.536 203.641 L 136.536 195.479 L 137.151 181.39 L 129.092 203.641 L 124.867 203.641 L 116.828 181.411 L 117.443 195.479 L 117.443 203.641 L 111.291 203.641 L 111.291 173.782 L 111.291 173.782 Z M 154.234 173.782 L 154.234 198.699 L 167.298 198.699 L 167.298 203.641 L 148.082 203.641 L 148.082 173.782 L 154.234 173.782 L 154.234 173.782 Z " fill-rule="evenodd" fill="rgb(255,255,255)"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><defs><clipPath id="_clipPath_fuCqdWSzYoowVCmecSUlBS0SWUOdLKXJ"><rect width="256" height="256"/></clipPath></defs><g clip-path="url(#_clipPath_fuCqdWSzYoowVCmecSUlBS0SWUOdLKXJ)"><rect width="256" height="256" style="fill:rgb(0,0,0)" fill-opacity="0"/><g><g><path d=" M 156.239 40 L 65.027 40 C 62.487 40 60.429 42.059 60.429 46.053 L 60.429 212.857 C 60.429 213.941 62.487 216 65.027 216 L 190.973 216 C 193.513 216 195.571 213.941 195.571 212.857 L 195.571 80.788 C 195.571 78.601 195.279 77.897 194.764 77.378 L 158.193 40.808 C 157.675 40.292 156.971 40 156.239 40 Z " fill="rgb(233,233,224)"/><path d=" M 157.857 40.475 L 157.857 77.714 L 195.097 77.714 L 157.857 40.475 Z " fill="rgb(217,215,202)"/><path d=" M 190.973 216 L 65.027 216 C 62.487 216 60.429 213.941 60.429 211.402 L 60.429 162.571 L 195.571 162.571 L 195.571 211.402 C 195.571 213.941 193.513 216 190.973 216 Z " fill="rgb(151,119,168)"/><path d=" M 145.286 140.571 C 144.975 140.571 144.657 140.524 144.346 140.427 C 142.69 139.908 141.766 138.145 142.284 136.489 L 157.999 86.203 C 158.517 84.547 160.28 83.623 161.937 84.141 C 163.593 84.66 164.517 86.423 163.998 88.079 L 148.284 138.365 C 147.866 139.71 146.625 140.571 145.286 140.571 Z " fill="rgb(151,119,168)"/><circle vector-effect="non-scaling-stroke" cx="131.14285714285717" cy="101.28571428571428" r="4.714285714285722" fill="rgb(151,119,168)"/><circle vector-effect="non-scaling-stroke" cx="131.14285714285717" cy="123.28571428571428" r="4.714285714285722" fill="rgb(151,119,168)"/><path d=" M 113.857 134.286 L 107.571 134.286 C 95.44 134.286 85.571 124.417 85.571 112.286 C 85.571 100.154 95.44 90.286 107.571 90.286 L 113.857 90.286 C 115.592 90.286 117 91.694 117 93.429 C 117 95.163 115.592 96.571 113.857 96.571 L 107.571 96.571 C 98.907 96.571 91.857 103.621 91.857 112.286 C 91.857 120.951 98.907 128 107.571 128 L 113.857 128 C 115.592 128 117 129.408 117 131.143 C 117 132.878 115.592 134.286 113.857 134.286 Z " fill="rgb(151,119,168)"/></g></g><path d=" M 110.631 185.986 L 110.631 190.806 L 98.818 190.806 L 98.818 198.804 L 112.682 198.804 L 112.682 203.746 L 92.666 203.746 L 92.666 173.887 L 112.641 173.887 L 112.641 178.87 L 98.818 178.87 L 98.818 185.986 L 110.631 185.986 L 110.631 185.986 Z M 121.377 173.887 L 126.976 184.182 L 132.574 173.887 L 139.649 173.887 L 130.954 188.693 L 139.875 203.746 L 132.718 203.746 L 126.976 193.287 L 121.233 203.746 L 114.076 203.746 L 122.997 188.693 L 114.302 173.887 L 121.377 173.887 L 121.377 173.887 Z M 160.937 185.986 L 160.937 190.806 L 149.124 190.806 L 149.124 198.804 L 162.987 198.804 L 162.987 203.746 L 142.972 203.746 L 142.972 173.887 L 162.946 173.887 L 162.946 178.87 L 149.124 178.87 L 149.124 185.986 L 160.937 185.986 L 160.937 185.986 Z " fill-rule="evenodd" fill="rgb(255,255,255)"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 3.0 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><defs><clipPath id="_clipPath_5oTXkrN31uMQuyZv3FF9Doe4IqTHZzDZ"><rect width="256" height="256"/></clipPath></defs><g clip-path="url(#_clipPath_5oTXkrN31uMQuyZv3FF9Doe4IqTHZzDZ)"><rect width="256" height="256" style="fill:rgb(0,0,0)" fill-opacity="0"/><g><g><path d=" M 156.239 40 L 65.027 40 C 62.487 40 60.429 42.059 60.429 46.053 L 60.429 212.857 C 60.429 213.941 62.487 216 65.027 216 L 190.973 216 C 193.513 216 195.571 213.941 195.571 212.857 L 195.571 80.788 C 195.571 78.601 195.279 77.897 194.764 77.378 L 158.193 40.808 C 157.675 40.292 156.971 40 156.239 40 Z " fill="rgb(233,233,224)"/><path d=" M 157.857 40.475 L 157.857 77.714 L 195.097 77.714 L 157.857 40.475 Z " fill="rgb(217,215,202)"/><path d=" M 190.973 216 L 65.027 216 C 62.487 216 60.429 213.941 60.429 211.402 L 60.429 162.571 L 195.571 162.571 L 195.571 211.402 C 195.571 213.941 193.513 216 190.973 216 Z " fill="rgb(93,93,93)"/><g><path d=" M 112.936 91.207 C 111.707 89.978 109.721 89.978 108.492 91.207 L 89.635 110.064 C 88.406 111.293 88.406 113.279 89.635 114.508 L 108.492 133.365 C 109.105 133.978 109.91 134.286 110.714 134.286 C 111.519 134.286 112.323 133.978 112.936 133.365 C 114.165 132.136 114.165 130.15 112.936 128.921 L 96.301 112.286 L 112.936 95.651 C 114.165 94.422 114.165 92.435 112.936 91.207 Z " fill="rgb(93,93,93)"/><path d=" M 169.508 110.064 L 161.722 102.278 L 150.651 91.207 C 149.422 89.978 147.435 89.978 146.207 91.207 C 144.978 92.435 144.978 94.422 146.207 95.651 L 162.842 112.286 L 146.207 128.921 C 144.978 130.15 144.978 132.136 146.207 133.365 C 146.819 133.978 147.624 134.286 148.429 134.286 C 149.233 134.286 150.038 133.978 150.651 133.365 L 169.508 114.508 C 170.737 113.279 170.737 111.293 169.508 110.064 Z " fill="rgb(93,93,93)"/></g></g></g><path d=" M 135.631 188.581 L 135.631 193.544 L 123.818 193.544 L 123.818 205.746 L 117.666 205.746 L 117.666 175.887 L 137.107 175.887 L 137.107 180.87 L 123.818 180.87 L 123.818 188.581 L 135.631 188.581 L 135.631 188.581 Z " fill="rgb(255,255,255)"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 2.2 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><defs><clipPath id="_clipPath_2IuqJwMePGGPEksxk56V3JdmHKDktmlJ"><rect width="256" height="256"/></clipPath></defs><g clip-path="url(#_clipPath_2IuqJwMePGGPEksxk56V3JdmHKDktmlJ)"><rect width="256" height="256" style="fill:rgb(0,0,0)" fill-opacity="0"/><g><g><path d=" M 156.239 40 L 65.027 40 C 62.487 40 60.429 42.059 60.429 46.053 L 60.429 212.857 C 60.429 213.941 62.487 216 65.027 216 L 190.973 216 C 193.513 216 195.571 213.941 195.571 212.857 L 195.571 80.788 C 195.571 78.601 195.279 77.897 194.764 77.378 L 158.193 40.808 C 157.675 40.292 156.971 40 156.239 40 Z " fill="rgb(233,233,224)"/><path d=" M 157.857 40.475 L 157.857 77.714 L 195.097 77.714 L 157.857 40.475 Z " fill="rgb(217,215,202)"/><path d=" M 190.973 216 L 65.027 216 C 62.487 216 60.429 213.941 60.429 211.402 L 60.429 162.571 L 195.571 162.571 L 195.571 211.402 C 195.571 213.941 193.513 216 190.973 216 Z " fill="rgb(206,60,59)"/><circle vector-effect="non-scaling-stroke" cx="120.14285714285714" cy="90.28571428571439" r="6.285714285714278" fill="rgb(200,189,184)"/><g><path d=" M 149.953 83.777 L 147.731 81.555 C 141.203 75.024 132.519 71.429 123.286 71.429 C 114.052 71.429 105.368 75.024 98.841 81.555 C 92.31 88.083 88.714 96.766 88.714 106 C 88.714 115.234 92.31 123.917 98.841 130.445 C 105.368 136.976 114.052 140.571 123.286 140.571 C 132.519 140.571 141.203 136.976 147.731 130.445 L 149.953 128.223 L 127.73 106 L 149.953 83.777 Z M 140.942 128.101 C 135.945 132.111 129.782 134.286 123.286 134.286 C 115.73 134.286 108.627 131.344 103.285 126.001 C 97.942 120.658 95 113.555 95 106 C 95 98.445 97.942 91.342 103.285 85.999 C 108.627 80.656 115.73 77.714 123.286 77.714 C 129.782 77.714 135.945 79.889 140.942 83.899 L 118.842 106 L 140.942 128.101 Z " fill="rgb(200,189,184)"/><path d=" M 151.571 102.857 L 142.143 102.857 C 140.405 102.857 139 104.262 139 106 C 139 107.738 140.405 109.143 142.143 109.143 L 151.571 109.143 C 153.309 109.143 154.714 107.738 154.714 106 C 154.714 104.262 153.309 102.857 151.571 102.857 Z " fill="rgb(200,189,184)"/><path d=" M 170.429 102.857 L 164.143 102.857 C 162.405 102.857 161 104.262 161 106 C 161 107.738 162.405 109.143 164.143 109.143 L 170.429 109.143 C 172.167 109.143 173.571 107.738 173.571 106 C 173.571 104.262 172.167 102.857 170.429 102.857 Z " fill="rgb(200,189,184)"/></g></g></g><path d=" M 110.131 186.581 L 110.131 191.544 L 98.318 191.544 L 98.318 203.746 L 92.166 203.746 L 92.166 173.887 L 111.607 173.887 L 111.607 178.87 L 98.318 178.87 L 98.318 186.581 L 110.131 186.581 L 110.131 186.581 Z M 121.328 173.887 L 121.328 198.804 L 134.392 198.804 L 134.392 203.746 L 115.176 203.746 L 115.176 173.887 L 121.328 173.887 L 121.328 173.887 Z M 156.848 203.746 L 154.776 197.594 L 143.989 197.594 L 141.938 203.746 L 135.396 203.746 L 146.512 173.887 L 152.213 173.887 L 163.39 203.746 L 156.848 203.746 L 156.848 203.746 Z M 149.362 181.434 L 145.65 192.61 L 153.115 192.61 L 149.362 181.434 L 149.362 181.434 Z " fill-rule="evenodd" fill="rgb(255,255,255)"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 3.2 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><defs><clipPath id="_clipPath_qJ4JQ2vHPl9yNMOIFjFdX7hbAxrDctK3"><rect width="256" height="256"/></clipPath></defs><g clip-path="url(#_clipPath_qJ4JQ2vHPl9yNMOIFjFdX7hbAxrDctK3)"><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)"/><circle vector-effect="non-scaling-stroke" cx="99.49742857142849" cy="85.35457142857132" r="14.35971428571429" fill="rgb(243,213,91)"/><path d=" M 60.429 162.571 L 95 162.571 L 195.571 162.571 L 195.571 128 L 164.143 98.143 L 131.143 134.286 L 113.911 117.053 L 60.429 162.571 Z " fill="rgb(38,114,185)"/><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(24,94,159)"/></g></g><path d=" M 121.291 188.058 L 121.291 199.973 L 121.291 199.973 Q 119.63 201.962 116.595 203.059 L 116.595 203.059 L 116.595 203.059 Q 113.56 204.156 109.868 204.156 L 109.868 204.156 L 109.868 204.156 Q 105.992 204.156 103.07 202.464 L 103.07 202.464 L 103.07 202.464 Q 100.147 200.772 98.558 197.553 L 98.558 197.553 L 98.558 197.553 Q 96.969 194.333 96.928 189.985 L 96.928 189.985 L 96.928 187.955 L 96.928 187.955 Q 96.928 183.484 98.435 180.213 L 98.435 180.213 L 98.435 180.213 Q 99.942 176.942 102.783 175.209 L 102.783 175.209 L 102.783 175.209 Q 105.623 173.477 109.438 173.477 L 109.438 173.477 L 109.438 173.477 Q 114.749 173.477 117.743 176.009 L 117.743 176.009 L 117.743 176.009 Q 120.737 178.542 121.291 183.382 L 121.291 183.382 L 115.303 183.382 L 115.303 183.382 Q 114.893 180.818 113.488 179.629 L 113.488 179.629 L 113.488 179.629 Q 112.083 178.439 109.622 178.439 L 109.622 178.439 L 109.622 178.439 Q 106.484 178.439 104.844 180.798 L 104.844 180.798 L 104.844 180.798 Q 103.203 183.156 103.183 187.812 L 103.183 187.812 L 103.183 189.719 L 103.183 189.719 Q 103.183 194.415 104.967 196.814 L 104.967 196.814 L 104.967 196.814 Q 106.751 199.214 110.196 199.214 L 110.196 199.214 L 110.196 199.214 Q 113.662 199.214 115.139 197.737 L 115.139 197.737 L 115.139 192.59 L 109.54 192.59 L 109.54 188.058 L 121.291 188.058 L 121.291 188.058 Z M 132.816 173.887 L 132.816 203.746 L 126.664 203.746 L 126.664 173.887 L 132.816 173.887 L 132.816 173.887 Z M 156.482 186.581 L 156.482 191.544 L 144.67 191.544 L 144.67 203.746 L 138.518 203.746 L 138.518 173.887 L 157.959 173.887 L 157.959 178.87 L 144.67 178.87 L 144.67 186.581 L 156.482 186.581 L 156.482 186.581 Z " fill-rule="evenodd" fill="rgb(255,255,255)"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 3.1 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><defs><clipPath id="_clipPath_g24Gte2OrG30DAPUumNJM7BCqXYtetjX"><rect width="256" height="256"/></clipPath></defs><g clip-path="url(#_clipPath_g24Gte2OrG30DAPUumNJM7BCqXYtetjX)"><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(116,206,221)"/><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(116,206,221)"/><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(116,206,221)"/></g></g></g><path d=" M 124.291 189.058 L 124.291 200.973 L 124.291 200.973 Q 122.63 202.962 119.595 204.059 L 119.595 204.059 L 119.595 204.059 Q 116.56 205.156 112.868 205.156 L 112.868 205.156 L 112.868 205.156 Q 108.992 205.156 106.07 203.464 L 106.07 203.464 L 106.07 203.464 Q 103.147 201.772 101.558 198.553 L 101.558 198.553 L 101.558 198.553 Q 99.969 195.333 99.928 190.985 L 99.928 190.985 L 99.928 188.955 L 99.928 188.955 Q 99.928 184.484 101.435 181.213 L 101.435 181.213 L 101.435 181.213 Q 102.942 177.942 105.783 176.209 L 105.783 176.209 L 105.783 176.209 Q 108.623 174.477 112.438 174.477 L 112.438 174.477 L 112.438 174.477 Q 117.749 174.477 120.743 177.009 L 120.743 177.009 L 120.743 177.009 Q 123.737 179.542 124.291 184.382 L 124.291 184.382 L 118.303 184.382 L 118.303 184.382 Q 117.893 181.818 116.488 180.629 L 116.488 180.629 L 116.488 180.629 Q 115.083 179.439 112.622 179.439 L 112.622 179.439 L 112.622 179.439 Q 109.484 179.439 107.844 181.798 L 107.844 181.798 L 107.844 181.798 Q 106.203 184.156 106.183 188.812 L 106.183 188.812 L 106.183 190.719 L 106.183 190.719 Q 106.183 195.415 107.967 197.814 L 107.967 197.814 L 107.967 197.814 Q 109.751 200.214 113.196 200.214 L 113.196 200.214 L 113.196 200.214 Q 116.662 200.214 118.139 198.737 L 118.139 198.737 L 118.139 193.59 L 112.54 193.59 L 112.54 189.058 L 124.291 189.058 L 124.291 189.058 Z M 153.802 189.16 L 153.802 190.493 L 153.802 190.493 Q 153.802 194.902 152.243 198.225 L 152.243 198.225 L 152.243 198.225 Q 150.685 201.547 147.783 203.352 L 147.783 203.352 L 147.783 203.352 Q 144.881 205.156 141.128 205.156 L 141.128 205.156 L 141.128 205.156 Q 137.416 205.156 134.504 203.372 L 134.504 203.372 L 134.504 203.372 Q 131.592 201.588 129.992 198.276 L 129.992 198.276 L 129.992 198.276 Q 128.393 194.964 128.372 190.657 L 128.372 190.657 L 128.372 189.181 L 128.372 189.181 Q 128.372 184.771 129.961 181.418 L 129.961 181.418 L 129.961 181.418 Q 131.551 178.065 134.453 176.271 L 134.453 176.271 L 134.453 176.271 Q 137.354 174.477 141.087 174.477 L 141.087 174.477 L 141.087 174.477 Q 144.819 174.477 147.721 176.271 L 147.721 176.271 L 147.721 176.271 Q 150.623 178.065 152.212 181.418 L 152.212 181.418 L 152.212 181.418 Q 153.802 184.771 153.802 189.16 L 153.802 189.16 L 153.802 189.16 Z M 147.567 190.596 L 147.567 189.14 L 147.567 189.14 Q 147.567 184.443 145.886 182.003 L 145.886 182.003 L 145.886 182.003 Q 144.204 179.563 141.087 179.563 L 141.087 179.563 L 141.087 179.563 Q 137.99 179.563 136.309 181.972 L 136.309 181.972 L 136.309 181.972 Q 134.627 184.382 134.606 189.037 L 134.606 189.037 L 134.606 190.493 L 134.606 190.493 Q 134.606 195.066 136.288 197.589 L 136.288 197.589 L 136.288 197.589 Q 137.97 200.111 141.128 200.111 L 141.128 200.111 L 141.128 200.111 Q 144.225 200.111 145.886 197.681 L 145.886 197.681 L 145.886 197.681 Q 147.547 195.251 147.567 190.596 L 147.567 190.596 L 147.567 190.596 Z " fill-rule="evenodd" fill="rgb(255,255,255)"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><defs><clipPath id="_clipPath_rhplHpdI3DTbrTRaojPyWupcuif0b6PM"><rect width="256" height="256"/></clipPath></defs><g clip-path="url(#_clipPath_rhplHpdI3DTbrTRaojPyWupcuif0b6PM)"><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(236,102,48)"/><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(236,102,48)"/><path d=" M 169.508 110.064 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(236,102,48)"/></g></g></g><path d=" M 95.968 173.887 L 95.968 203.746 L 89.815 203.746 L 89.815 190.949 L 77.818 190.949 L 77.818 203.746 L 71.666 203.746 L 71.666 173.887 L 77.818 173.887 L 77.818 185.986 L 89.815 185.986 L 89.815 173.887 L 95.968 173.887 L 95.968 173.887 Z M 123.817 173.887 L 123.817 178.87 L 114.671 178.87 L 114.671 203.746 L 108.519 203.746 L 108.519 178.87 L 99.495 178.87 L 99.495 173.887 L 123.817 173.887 L 123.817 173.887 Z M 127.324 173.887 L 135.363 173.887 L 143.033 195.543 L 150.662 173.887 L 158.742 173.887 L 158.742 203.746 L 152.569 203.746 L 152.569 195.584 L 153.185 181.495 L 145.125 203.746 L 140.9 203.746 L 132.861 181.516 L 133.477 195.584 L 133.477 203.746 L 127.324 203.746 L 127.324 173.887 L 127.324 173.887 Z M 170.268 173.887 L 170.268 198.804 L 183.331 198.804 L 183.331 203.746 L 164.115 203.746 L 164.115 173.887 L 170.268 173.887 L 170.268 173.887 Z " fill-rule="evenodd" fill="rgb(255,255,255)"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><defs><clipPath id="_clipPath_cfbSQzBKfRUXVWtELZ5utsl7ZhZ3mlDk"><rect width="256" height="256"/></clipPath></defs><g clip-path="url(#_clipPath_cfbSQzBKfRUXVWtELZ5utsl7ZhZ3mlDk)"><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)"/><circle vector-effect="non-scaling-stroke" cx="99.49742857142849" cy="85.35457142857132" r="14.35971428571429" fill="rgb(243,213,91)"/><path d=" M 60.429 162.571 L 95 162.571 L 195.571 162.571 L 195.571 128 L 164.143 98.143 L 131.143 134.286 L 113.911 117.053 L 60.429 162.571 Z " fill="rgb(38,185,154)"/><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(20,160,133)"/></g></g><path d=" M 101.704 194.559 L 101.704 173.887 L 107.856 173.887 L 107.856 194.559 L 107.856 194.559 Q 107.856 197.409 106.595 199.593 L 106.595 199.593 L 106.595 199.593 Q 105.334 201.777 103.037 202.967 L 103.037 202.967 L 103.037 202.967 Q 100.74 204.156 97.849 204.156 L 97.849 204.156 L 97.849 204.156 Q 93.111 204.156 90.466 201.747 L 90.466 201.747 L 90.466 201.747 Q 87.82 199.337 87.82 194.928 L 87.82 194.928 L 94.014 194.928 L 94.014 194.928 Q 94.014 197.122 94.937 198.168 L 94.937 198.168 L 94.937 198.168 Q 95.859 199.214 97.849 199.214 L 97.849 199.214 L 97.849 199.214 Q 99.612 199.214 100.658 198.004 L 100.658 198.004 L 100.658 198.004 Q 101.704 196.794 101.704 194.559 L 101.704 194.559 L 101.704 194.559 Z M 124.673 193.226 L 119.279 193.226 L 119.279 203.746 L 113.127 203.746 L 113.127 173.887 L 124.775 173.887 L 124.775 173.887 Q 128.139 173.887 130.692 175.117 L 130.692 175.117 L 130.692 175.117 Q 133.245 176.348 134.619 178.614 L 134.619 178.614 L 134.619 178.614 Q 135.993 180.88 135.993 183.771 L 135.993 183.771 L 135.993 183.771 Q 135.993 188.16 132.989 190.693 L 132.989 190.693 L 132.989 190.693 Q 129.984 193.226 124.673 193.226 L 124.673 193.226 L 124.673 193.226 Z M 119.279 178.87 L 119.279 188.242 L 124.775 188.242 L 124.775 188.242 Q 127.216 188.242 128.498 187.094 L 128.498 187.094 L 128.498 187.094 Q 129.779 185.945 129.779 183.813 L 129.779 183.813 L 129.779 183.813 Q 129.779 181.618 128.487 180.265 L 128.487 180.265 L 128.487 180.265 Q 127.195 178.911 124.919 178.87 L 124.919 178.87 L 119.279 178.87 L 119.279 178.87 Z M 163.843 188.058 L 163.843 199.973 L 163.843 199.973 Q 162.182 201.962 159.146 203.059 L 159.146 203.059 L 159.146 203.059 Q 156.111 204.156 152.42 204.156 L 152.42 204.156 L 152.42 204.156 Q 148.544 204.156 145.622 202.464 L 145.622 202.464 L 145.622 202.464 Q 142.699 200.772 141.11 197.553 L 141.11 197.553 L 141.11 197.553 Q 139.521 194.333 139.479 189.985 L 139.479 189.985 L 139.479 187.955 L 139.479 187.955 Q 139.479 183.484 140.987 180.213 L 140.987 180.213 L 140.987 180.213 Q 142.494 176.942 145.334 175.209 L 145.334 175.209 L 145.334 175.209 Q 148.175 173.477 151.989 173.477 L 151.989 173.477 L 151.989 173.477 Q 157.301 173.477 160.295 176.009 L 160.295 176.009 L 160.295 176.009 Q 163.289 178.542 163.843 183.382 L 163.843 183.382 L 157.854 183.382 L 157.854 183.382 Q 157.444 180.818 156.04 179.629 L 156.04 179.629 L 156.04 179.629 Q 154.635 178.439 152.174 178.439 L 152.174 178.439 L 152.174 178.439 Q 149.036 178.439 147.396 180.798 L 147.396 180.798 L 147.396 180.798 Q 145.755 183.156 145.734 187.812 L 145.734 187.812 L 145.734 189.719 L 145.734 189.719 Q 145.734 194.415 147.519 196.814 L 147.519 196.814 L 147.519 196.814 Q 149.303 199.214 152.748 199.214 L 152.748 199.214 L 152.748 199.214 Q 156.214 199.214 157.69 197.737 L 157.69 197.737 L 157.69 192.59 L 152.092 192.59 L 152.092 188.058 L 163.843 188.058 L 163.843 188.058 Z " fill-rule="evenodd" fill="rgb(255,255,255)"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><defs><clipPath id="_clipPath_I1G28y1htKKMJgcF7pHEbsBWAMJckYyY"><rect width="256" height="256"/></clipPath></defs><g clip-path="url(#_clipPath_I1G28y1htKKMJgcF7pHEbsBWAMJckYyY)"><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(149,203,165)"/></g></g><path d=" M 97.333 174.887 L 105.372 174.887 L 113.042 196.543 L 120.671 174.887 L 128.751 174.887 L 128.751 204.746 L 122.578 204.746 L 122.578 196.584 L 123.193 182.495 L 115.133 204.746 L 110.909 204.746 L 102.87 182.516 L 103.485 196.584 L 103.485 204.746 L 97.333 204.746 L 97.333 174.887 L 97.333 174.887 Z M 143.373 204.746 L 134.124 204.746 L 134.124 174.887 L 143.311 174.887 L 143.311 174.887 Q 147.249 174.887 150.356 176.661 L 150.356 176.661 L 150.356 176.661 Q 153.463 178.435 155.206 181.706 L 155.206 181.706 L 155.206 181.706 Q 156.949 184.977 156.949 189.14 L 156.949 189.14 L 156.949 190.514 L 156.949 190.514 Q 156.949 194.677 155.236 197.917 L 155.236 197.917 L 155.236 197.917 Q 153.524 201.157 150.407 202.941 L 150.407 202.941 L 150.407 202.941 Q 147.29 204.726 143.373 204.746 L 143.373 204.746 L 143.373 204.746 Z M 143.311 179.87 L 140.276 179.87 L 140.276 199.804 L 143.25 199.804 L 143.25 199.804 Q 146.859 199.804 148.766 197.445 L 148.766 197.445 L 148.766 197.445 Q 150.674 195.087 150.715 190.698 L 150.715 190.698 L 150.715 189.119 L 150.715 189.119 Q 150.715 184.566 148.828 182.218 L 148.828 182.218 L 148.828 182.218 Q 146.941 179.87 143.311 179.87 L 143.311 179.87 L 143.311 179.87 Z " fill-rule="evenodd" fill="rgb(255,255,255)"/><g><path d="M 93.808 90.667 L 162.192 90.667 C 164.312 90.667 166.034 92.388 166.034 94.508 L 166.034 132.158 C 166.034 134.279 164.312 136 162.192 136 L 93.808 136 C 91.688 136 89.966 134.279 89.966 132.158 L 89.966 94.508 C 89.966 92.388 91.688 90.667 93.808 90.667 Z" style="fill:none;stroke:#95CBA5;stroke-width:4;"/><path d=" M 99.737 126.395 L 99.737 100.271 L 107.421 100.271 L 115.105 109.876 L 122.788 100.271 L 130.472 100.271 L 130.472 126.395 L 122.788 126.395 L 122.788 111.412 L 115.105 121.017 L 107.421 111.412 L 107.421 126.395 L 99.737 126.395 Z M 147.76 126.395 L 136.234 113.718 L 143.918 113.718 L 143.918 100.271 L 151.602 100.271 L 151.602 113.718 L 159.285 113.718 L 147.76 126.395 Z " fill="rgb(149,203,165)"/></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 3.1 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><defs><clipPath id="_clipPath_asVcXb5ArovgRsAnstkkJMmj7NXPC138"><rect width="256" height="256"/></clipPath></defs><g clip-path="url(#_clipPath_asVcXb5ArovgRsAnstkkJMmj7NXPC138)"><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 82.666 173.887 L 90.705 173.887 L 98.375 195.543 L 106.004 173.887 L 114.084 173.887 L 114.084 203.746 L 107.911 203.746 L 107.911 195.584 L 108.526 181.495 L 100.467 203.746 L 96.242 203.746 L 88.203 181.516 L 88.818 195.584 L 88.818 203.746 L 82.666 203.746 L 82.666 173.887 L 82.666 173.887 Z M 143.984 188.16 L 143.984 189.493 L 143.984 189.493 Q 143.984 193.902 142.426 197.225 L 142.426 197.225 L 142.426 197.225 Q 140.867 200.547 137.965 202.352 L 137.965 202.352 L 137.965 202.352 Q 135.063 204.156 131.311 204.156 L 131.311 204.156 L 131.311 204.156 Q 127.599 204.156 124.687 202.372 L 124.687 202.372 L 124.687 202.372 Q 121.774 200.588 120.175 197.276 L 120.175 197.276 L 120.175 197.276 Q 118.575 193.964 118.555 189.657 L 118.555 189.657 L 118.555 188.181 L 118.555 188.181 Q 118.555 183.771 120.144 180.418 L 120.144 180.418 L 120.144 180.418 Q 121.733 177.065 124.635 175.271 L 124.635 175.271 L 124.635 175.271 Q 127.537 173.477 131.27 173.477 L 131.27 173.477 L 131.27 173.477 Q 135.002 173.477 137.904 175.271 L 137.904 175.271 L 137.904 175.271 Q 140.806 177.065 142.395 180.418 L 142.395 180.418 L 142.395 180.418 Q 143.984 183.771 143.984 188.16 L 143.984 188.16 L 143.984 188.16 Z M 137.75 189.596 L 137.75 188.14 L 137.75 188.14 Q 137.75 183.443 136.068 181.003 L 136.068 181.003 L 136.068 181.003 Q 134.387 178.562 131.27 178.562 L 131.27 178.562 L 131.27 178.562 Q 128.173 178.562 126.491 180.972 L 126.491 180.972 L 126.491 180.972 Q 124.81 183.382 124.789 188.037 L 124.789 188.037 L 124.789 189.493 L 124.789 189.493 Q 124.789 194.066 126.471 196.589 L 126.471 196.589 L 126.471 196.589 Q 128.152 199.111 131.311 199.111 L 131.311 199.111 L 131.311 199.111 Q 134.407 199.111 136.068 196.681 L 136.068 196.681 L 136.068 196.681 Q 137.729 194.251 137.75 189.596 L 137.75 189.596 L 137.75 189.596 Z M 152.762 173.887 L 159.488 196.343 L 166.256 173.887 L 173.105 173.887 L 162.708 203.746 L 156.289 203.746 L 145.933 173.887 L 152.762 173.887 L 152.762 173.887 Z " fill-rule="evenodd" fill="rgb(255,255,255)"/><path d=" M 113.857 128 L 113.857 105.855 L 113.857 84 L 148.429 106 L 113.857 128 Z " fill="rgb(200,189,184)"/></g></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><defs><clipPath id="_clipPath_noUtHHGzPYvyzCKWl55c2UQn5veEgJAP"><rect width="256" height="256"/></clipPath></defs><g clip-path="url(#_clipPath_noUtHHGzPYvyzCKWl55c2UQn5veEgJAP)"><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,83,100)"/><path d=" M 117 128 C 116.478 128 115.96 127.871 115.488 127.613 C 114.483 127.06 113.857 126.004 113.857 124.857 L 113.857 80.857 C 113.857 79.71 114.483 78.654 115.488 78.101 C 116.497 77.551 117.72 77.592 118.691 78.205 L 153.262 100.205 C 154.164 100.783 154.714 101.782 154.714 102.857 C 154.714 103.932 154.164 104.931 153.259 105.51 L 118.688 127.51 C 118.175 127.833 117.588 128 117 128 Z M 120.143 86.58 L 120.143 119.131 L 145.716 102.857 L 120.143 86.58 Z " fill="rgb(200,189,184)"/><path d=" M 129.571 150 C 103.577 150 82.429 128.852 82.429 102.857 C 82.429 76.863 103.577 55.714 129.571 55.714 C 155.566 55.714 176.714 76.863 176.714 102.857 C 176.714 128.852 155.566 150 129.571 150 Z M 129.571 62 C 107.043 62 88.714 80.329 88.714 102.857 C 88.714 125.385 107.043 143.714 129.571 143.714 C 152.099 143.714 170.429 125.385 170.429 102.857 C 170.429 80.329 152.099 62 129.571 62 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><path d=" M 85.666 173.887 L 93.705 173.887 L 101.375 195.543 L 109.004 173.887 L 117.084 173.887 L 117.084 203.746 L 110.911 203.746 L 110.911 195.584 L 111.526 181.495 L 103.467 203.746 L 99.242 203.746 L 91.203 181.516 L 91.818 195.584 L 91.818 203.746 L 85.666 203.746 L 85.666 173.887 L 85.666 173.887 Z M 134.003 193.226 L 128.609 193.226 L 128.609 203.746 L 122.457 203.746 L 122.457 173.887 L 134.105 173.887 L 134.105 173.887 Q 137.469 173.887 140.022 175.117 L 140.022 175.117 L 140.022 175.117 Q 142.575 176.348 143.949 178.614 L 143.949 178.614 L 143.949 178.614 Q 145.323 180.88 145.323 183.771 L 145.323 183.771 L 145.323 183.771 Q 145.323 188.16 142.319 190.693 L 142.319 190.693 L 142.319 190.693 Q 139.314 193.226 134.003 193.226 L 134.003 193.226 L 134.003 193.226 Z M 128.609 178.87 L 128.609 188.242 L 134.105 188.242 L 134.105 188.242 Q 136.546 188.242 137.828 187.094 L 137.828 187.094 L 137.828 187.094 Q 139.109 185.945 139.109 183.813 L 139.109 183.813 L 139.109 183.813 Q 139.109 181.618 137.817 180.265 L 137.817 180.265 L 137.817 180.265 Q 136.525 178.911 134.249 178.87 L 134.249 178.87 L 128.609 178.87 L 128.609 178.87 Z M 166.446 173.887 L 166.446 192.508 L 169.83 192.508 L 169.83 197.286 L 166.446 197.286 L 166.446 203.746 L 160.52 203.746 L 160.52 197.286 L 148.276 197.286 L 148.01 193.554 L 160.458 173.887 L 166.446 173.887 L 166.446 173.887 Z M 160.13 182.644 L 153.916 192.508 L 160.52 192.508 L 160.52 181.967 L 160.13 182.644 L 160.13 182.644 Z " fill-rule="evenodd" fill="rgb(255,255,255)"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><defs><clipPath id="_clipPath_T4lYH0jQ4ugwp6GLbfIkOPUBVBRKI4bF"><rect width="256" height="256"/></clipPath></defs><g clip-path="url(#_clipPath_T4lYH0jQ4ugwp6GLbfIkOPUBVBRKI4bF)"><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 101.33 144.733 L 101.33 144.733 C 100.236 144.733 99.186 144.377 98.291 143.708 C 95.019 141.253 94.579 138.522 94.786 136.662 C 95.358 131.545 101.685 126.19 113.596 120.734 C 118.323 110.375 122.821 97.612 125.501 86.948 C 122.365 80.122 119.316 71.265 121.538 66.07 C 122.318 64.25 123.289 62.855 125.102 62.251 C 125.819 62.013 127.629 61.711 128.295 61.711 C 129.879 61.711 131.272 63.751 132.259 65.008 C 133.186 66.189 135.288 68.694 131.086 86.385 C 135.323 95.135 141.326 104.048 147.077 110.152 C 151.197 109.407 154.743 109.027 157.631 109.027 C 162.553 109.027 165.535 110.174 166.751 112.537 C 167.757 114.492 167.345 116.777 165.526 119.326 C 163.775 121.774 161.361 123.069 158.549 123.069 C 154.727 123.069 150.277 120.655 145.314 115.887 C 136.398 117.751 125.985 121.076 117.569 124.757 C 114.941 130.332 112.424 134.823 110.079 138.117 C 106.858 142.627 104.08 144.733 101.33 144.733 Z M 109.696 128.622 C 102.98 132.397 100.242 135.499 100.044 137.246 C 100.013 137.535 99.928 138.296 101.399 139.421 C 101.867 139.273 104.601 138.026 109.696 128.622 Z M 152.555 114.662 C 155.117 116.632 155.742 117.629 157.417 117.629 C 158.153 117.629 160.249 117.597 161.22 116.243 C 161.688 115.586 161.871 115.165 161.943 114.938 C 161.556 114.734 161.044 114.319 158.25 114.319 C 156.663 114.322 154.667 114.391 152.555 114.662 Z M 129.078 93.975 C 126.831 101.751 123.864 110.145 120.674 117.748 C 127.243 115.199 134.383 112.974 141.09 111.399 C 136.847 106.471 132.607 100.318 129.078 93.975 Z M 127.17 67.381 C 126.862 67.484 122.99 72.903 127.472 77.488 C 130.455 70.841 127.305 67.337 127.17 67.381 Z " fill="rgb(204,75,76)"/><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(204,75,76)"/></g></g><path d=" M 102.212 193.226 L 96.818 193.226 L 96.818 203.746 L 90.666 203.746 L 90.666 173.887 L 102.314 173.887 L 102.314 173.887 Q 105.678 173.887 108.231 175.117 L 108.231 175.117 L 108.231 175.117 Q 110.784 176.348 112.158 178.614 L 112.158 178.614 L 112.158 178.614 Q 113.532 180.88 113.532 183.771 L 113.532 183.771 L 113.532 183.771 Q 113.532 188.16 110.528 190.693 L 110.528 190.693 L 110.528 190.693 Q 107.523 193.226 102.212 193.226 L 102.212 193.226 L 102.212 193.226 Z M 96.818 178.87 L 96.818 188.242 L 102.314 188.242 L 102.314 188.242 Q 104.755 188.242 106.037 187.094 L 106.037 187.094 L 106.037 187.094 Q 107.318 185.945 107.318 183.813 L 107.318 183.813 L 107.318 183.813 Q 107.318 181.618 106.026 180.265 L 106.026 180.265 L 106.026 180.265 Q 104.734 178.911 102.458 178.87 L 102.458 178.87 L 96.818 178.87 L 96.818 178.87 Z M 127.006 203.746 L 117.757 203.746 L 117.757 173.887 L 126.944 173.887 L 126.944 173.887 Q 130.882 173.887 133.989 175.661 L 133.989 175.661 L 133.989 175.661 Q 137.096 177.435 138.839 180.706 L 138.839 180.706 L 138.839 180.706 Q 140.582 183.977 140.582 188.14 L 140.582 188.14 L 140.582 189.514 L 140.582 189.514 Q 140.582 193.677 138.87 196.917 L 138.87 196.917 L 138.87 196.917 Q 137.157 200.157 134.04 201.941 L 134.04 201.941 L 134.04 201.941 Q 130.923 203.726 127.006 203.746 L 127.006 203.746 L 127.006 203.746 Z M 126.944 178.87 L 123.909 178.87 L 123.909 198.804 L 126.883 198.804 L 126.883 198.804 Q 130.492 198.804 132.399 196.445 L 132.399 196.445 L 132.399 196.445 Q 134.307 194.087 134.348 189.698 L 134.348 189.698 L 134.348 188.119 L 134.348 188.119 Q 134.348 183.566 132.461 181.218 L 132.461 181.218 L 132.461 181.218 Q 130.574 178.87 126.944 178.87 L 126.944 178.87 L 126.944 178.87 Z M 163.018 186.581 L 163.018 191.544 L 151.205 191.544 L 151.205 203.746 L 145.053 203.746 L 145.053 173.887 L 164.494 173.887 L 164.494 178.87 L 151.205 178.87 L 151.205 186.581 L 163.018 186.581 L 163.018 186.581 Z " fill-rule="evenodd" fill="rgb(255,255,255)"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><defs><clipPath id="_clipPath_aHGuroPPSn3bQvMdTobuKZlOp1kZ4lEM"><rect width="256" height="256"/></clipPath></defs><g clip-path="url(#_clipPath_aHGuroPPSn3bQvMdTobuKZlOp1kZ4lEM)"><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(246,113,46)"/><path d=" M 164.143 134.286 L 88.714 134.286 L 88.714 84 L 164.143 84 L 164.143 134.286 Z M 95 128 L 157.857 128 L 157.857 90.286 L 95 90.286 L 95 128 Z " fill="rgb(200,189,184)"/><path d=" M 104.425 150 C 103.875 150 103.316 149.855 102.81 149.551 C 101.32 148.658 100.839 146.728 101.732 145.239 L 111.161 129.524 C 112.053 128.035 113.983 127.554 115.473 128.446 C 116.962 129.339 117.443 131.269 116.551 132.758 L 107.122 148.473 C 106.534 149.456 105.494 150 104.425 150 Z " fill="rgb(200,189,184)"/><path d=" M 148.432 150 C 147.363 150 146.323 149.456 145.735 148.476 L 136.307 132.761 C 135.414 131.272 135.895 129.342 137.385 128.449 C 138.874 127.56 140.804 128.038 141.697 129.527 L 151.125 145.242 C 152.018 146.731 151.537 148.661 150.047 149.554 C 149.541 149.855 148.982 150 148.432 150 Z " fill="rgb(200,189,184)"/><path d=" M 126.429 90.286 C 124.694 90.286 123.286 88.881 123.286 87.143 L 123.286 77.714 C 123.286 75.976 124.694 74.571 126.429 74.571 C 128.163 74.571 129.571 75.976 129.571 77.714 L 129.571 87.143 C 129.571 88.881 128.163 90.286 126.429 90.286 Z " fill="rgb(200,189,184)"/><rect x="95" y="90.286" width="62.857" height="37.714" transform="matrix(1,0,0,1,0,0)" fill="rgb(211,204,201)"/></g></g><path d=" M 100.712 193.226 L 95.318 193.226 L 95.318 203.746 L 89.166 203.746 L 89.166 173.887 L 100.814 173.887 L 100.814 173.887 Q 104.178 173.887 106.731 175.117 L 106.731 175.117 L 106.731 175.117 Q 109.284 176.348 110.658 178.614 L 110.658 178.614 L 110.658 178.614 Q 112.032 180.88 112.032 183.771 L 112.032 183.771 L 112.032 183.771 Q 112.032 188.16 109.028 190.693 L 109.028 190.693 L 109.028 190.693 Q 106.023 193.226 100.712 193.226 L 100.712 193.226 L 100.712 193.226 Z M 95.318 178.87 L 95.318 188.242 L 100.814 188.242 L 100.814 188.242 Q 103.255 188.242 104.537 187.094 L 104.537 187.094 L 104.537 187.094 Q 105.818 185.945 105.818 183.813 L 105.818 183.813 L 105.818 183.813 Q 105.818 181.618 104.526 180.265 L 104.526 180.265 L 104.526 180.265 Q 103.234 178.911 100.958 178.87 L 100.958 178.87 L 95.318 178.87 L 95.318 178.87 Z M 127.803 193.226 L 122.409 193.226 L 122.409 203.746 L 116.257 203.746 L 116.257 173.887 L 127.905 173.887 L 127.905 173.887 Q 131.269 173.887 133.822 175.117 L 133.822 175.117 L 133.822 175.117 Q 136.375 176.348 137.749 178.614 L 137.749 178.614 L 137.749 178.614 Q 139.123 180.88 139.123 183.771 L 139.123 183.771 L 139.123 183.771 Q 139.123 188.16 136.119 190.693 L 136.119 190.693 L 136.119 190.693 Q 133.114 193.226 127.803 193.226 L 127.803 193.226 L 127.803 193.226 Z M 122.409 178.87 L 122.409 188.242 L 127.905 188.242 L 127.905 188.242 Q 130.346 188.242 131.627 187.094 L 131.627 187.094 L 131.627 187.094 Q 132.909 185.945 132.909 183.813 L 132.909 183.813 L 132.909 183.813 Q 132.909 181.618 131.617 180.265 L 131.617 180.265 L 131.617 180.265 Q 130.325 178.911 128.049 178.87 L 128.049 178.87 L 122.409 178.87 L 122.409 178.87 Z M 165.824 173.887 L 165.824 178.87 L 156.678 178.87 L 156.678 203.746 L 150.525 203.746 L 150.525 178.87 L 141.502 178.87 L 141.502 173.887 L 165.824 173.887 L 165.824 173.887 Z " fill-rule="evenodd" fill="rgb(255,255,255)"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 4.2 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><defs><clipPath id="_clipPath_lMyuGubRIotblXMnQ7jMCoNnqqKJSGxl"><rect width="256" height="256"/></clipPath></defs><g clip-path="url(#_clipPath_lMyuGubRIotblXMnQ7jMCoNnqqKJSGxl)"><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(246,113,46)"/><path d=" M 164.143 134.286 L 88.714 134.286 L 88.714 84 L 164.143 84 L 164.143 134.286 Z M 95 128 L 157.857 128 L 157.857 90.286 L 95 90.286 L 95 128 Z " fill="rgb(200,189,184)"/><path d=" M 104.425 150 C 103.875 150 103.316 149.855 102.81 149.551 C 101.32 148.658 100.839 146.728 101.732 145.239 L 111.161 129.524 C 112.053 128.035 113.983 127.554 115.473 128.446 C 116.962 129.339 117.443 131.269 116.551 132.758 L 107.122 148.473 C 106.534 149.456 105.494 150 104.425 150 Z " fill="rgb(200,189,184)"/><path d=" M 148.432 150 C 147.363 150 146.323 149.456 145.735 148.476 L 136.307 132.761 C 135.414 131.272 135.895 129.342 137.385 128.449 C 138.874 127.56 140.804 128.038 141.697 129.527 L 151.125 145.242 C 152.018 146.731 151.537 148.661 150.047 149.554 C 149.541 149.855 148.982 150 148.432 150 Z " fill="rgb(200,189,184)"/><path d=" M 126.429 90.286 C 124.694 90.286 123.286 88.881 123.286 87.143 L 123.286 77.714 C 123.286 75.976 124.694 74.571 126.429 74.571 C 128.163 74.571 129.571 75.976 129.571 77.714 L 129.571 87.143 C 129.571 88.881 128.163 90.286 126.429 90.286 Z " fill="rgb(200,189,184)"/><rect x="95" y="90.286" width="62.857" height="37.714" transform="matrix(1,0,0,1,0,0)" fill="rgb(211,204,201)"/></g></g><path d=" M 87.712 193.444 L 82.318 193.444 L 82.318 203.965 L 76.166 203.965 L 76.166 174.105 L 87.814 174.105 L 87.814 174.105 Q 91.178 174.105 93.731 175.336 L 93.731 175.336 L 93.731 175.336 Q 96.284 176.566 97.658 178.833 L 97.658 178.833 L 97.658 178.833 Q 99.032 181.099 99.032 183.99 L 99.032 183.99 L 99.032 183.99 Q 99.032 188.379 96.028 190.912 L 96.028 190.912 L 96.028 190.912 Q 93.023 193.444 87.712 193.444 L 87.712 193.444 L 87.712 193.444 Z M 82.318 179.089 L 82.318 188.461 L 87.814 188.461 L 87.814 188.461 Q 90.255 188.461 91.537 187.313 L 91.537 187.313 L 91.537 187.313 Q 92.818 186.164 92.818 184.031 L 92.818 184.031 L 92.818 184.031 Q 92.818 181.837 91.526 180.483 L 91.526 180.483 L 91.526 180.483 Q 90.234 179.13 87.958 179.089 L 87.958 179.089 L 82.318 179.089 L 82.318 179.089 Z M 114.803 193.444 L 109.409 193.444 L 109.409 203.965 L 103.257 203.965 L 103.257 174.105 L 114.905 174.105 L 114.905 174.105 Q 118.269 174.105 120.822 175.336 L 120.822 175.336 L 120.822 175.336 Q 123.375 176.566 124.749 178.833 L 124.749 178.833 L 124.749 178.833 Q 126.123 181.099 126.123 183.99 L 126.123 183.99 L 126.123 183.99 Q 126.123 188.379 123.119 190.912 L 123.119 190.912 L 123.119 190.912 Q 120.114 193.444 114.803 193.444 L 114.803 193.444 L 114.803 193.444 Z M 109.409 179.089 L 109.409 188.461 L 114.905 188.461 L 114.905 188.461 Q 117.346 188.461 118.627 187.313 L 118.627 187.313 L 118.627 187.313 Q 119.909 186.164 119.909 184.031 L 119.909 184.031 L 119.909 184.031 Q 119.909 181.837 118.617 180.483 L 118.617 180.483 L 118.617 180.483 Q 117.325 179.13 115.049 179.089 L 115.049 179.089 L 109.409 179.089 L 109.409 179.089 Z M 152.824 174.105 L 152.824 179.089 L 143.678 179.089 L 143.678 203.965 L 137.525 203.965 L 137.525 179.089 L 128.502 179.089 L 128.502 174.105 L 152.824 174.105 L 152.824 174.105 Z M 161.417 174.105 L 167.016 184.4 L 172.614 174.105 L 179.689 174.105 L 170.994 188.912 L 179.915 203.965 L 172.758 203.965 L 167.016 193.506 L 161.273 203.965 L 154.116 203.965 L 163.037 188.912 L 154.342 174.105 L 161.417 174.105 L 161.417 174.105 Z " fill-rule="evenodd" fill="rgb(255,255,255)"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><defs><clipPath id="_clipPath_2OU59RX26Z25RRKMSSLcaTTjFJr3xE4G"><rect width="256" height="256"/></clipPath></defs><g clip-path="url(#_clipPath_2OU59RX26Z25RRKMSSLcaTTjFJr3xE4G)"><rect width="256" height="256" style="fill:rgb(0,0,0)" fill-opacity="0"/><path d=" M 156.239 40 L 65.027 40 C 62.487 40 60.429 42.059 60.429 46.053 L 60.429 212.857 C 60.429 213.941 62.487 216 65.027 216 L 190.973 216 C 193.513 216 195.571 213.941 195.571 212.857 L 195.571 80.788 C 195.571 78.601 195.279 77.897 194.764 77.378 L 158.193 40.808 C 157.675 40.292 156.971 40 156.239 40 Z " fill="rgb(233,233,224)"/><path d=" M 157.857 40.475 L 157.857 77.714 L 195.097 77.714 L 157.857 40.475 Z " fill="rgb(217,215,202)"/><path d=" M 190.973 216 L 65.027 216 C 62.487 216 60.429 213.941 60.429 211.402 L 60.429 162.571 L 195.571 162.571 L 195.571 211.402 C 195.571 213.941 193.513 216 190.973 216 Z " fill="rgb(248,198,61)"/><path d=" M 114.212 193.121 L 108.818 193.121 L 108.818 203.641 L 102.666 203.641 L 102.666 173.782 L 114.314 173.782 L 114.314 173.782 Q 117.678 173.782 120.231 175.012 L 120.231 175.012 L 120.231 175.012 Q 122.784 176.243 124.158 178.509 L 124.158 178.509 L 124.158 178.509 Q 125.532 180.775 125.532 183.667 L 125.532 183.667 L 125.532 183.667 Q 125.532 188.055 122.528 190.588 L 122.528 190.588 L 122.528 190.588 Q 119.523 193.121 114.212 193.121 L 114.212 193.121 L 114.212 193.121 Z M 108.818 178.765 L 108.818 188.137 L 114.314 188.137 L 114.314 188.137 Q 116.755 188.137 118.037 186.989 L 118.037 186.989 L 118.037 186.989 Q 119.318 185.84 119.318 183.708 L 119.318 183.708 L 119.318 183.708 Q 119.318 181.513 118.026 180.16 L 118.026 180.16 L 118.026 180.16 Q 116.734 178.806 114.458 178.765 L 114.458 178.765 L 108.818 178.765 L 108.818 178.765 Z M 133.879 173.782 L 140.052 187.235 L 146.266 173.782 L 152.992 173.782 L 143.189 192.813 L 143.189 203.641 L 136.935 203.641 L 136.935 192.813 L 127.132 173.782 L 133.879 173.782 L 133.879 173.782 Z " fill-rule="evenodd" fill="rgb(255,255,255)"/><g><g><linearGradient id="_lgradient_0" x1="0.12959359372586665" y1="0.1169963052650701" x2="0.7956443961566904" y2="0.7852749007964104" gradientTransform="matrix(51.243,0,0,51.358,93.449,75.258)" gradientUnits="userSpaceOnUse"><stop offset="0%" stop-opacity="1" style="stop-color:rgb(56,126,184)"/><stop offset="100%" stop-opacity="1" style="stop-color:rgb(54,105,148)"/></linearGradient><path d=" M 127.751 75.258 C 110.209 75.258 111.304 82.866 111.304 82.866 L 111.324 90.747 L 128.064 90.747 L 128.064 93.113 L 104.675 93.113 C 104.675 93.113 93.449 91.84 93.449 109.54 C 93.449 127.241 103.247 126.613 103.247 126.613 L 109.094 126.613 L 109.094 118.399 C 109.094 118.399 108.779 108.602 118.735 108.602 C 128.692 108.602 135.339 108.602 135.339 108.602 C 135.339 108.602 144.667 108.752 144.667 99.586 C 144.667 90.42 144.667 84.43 144.667 84.43 C 144.667 84.43 146.083 75.258 127.751 75.258 Z M 118.52 80.558 C 120.186 80.558 121.532 81.904 121.532 83.57 C 121.532 85.235 120.186 86.581 118.52 86.581 C 116.855 86.581 115.509 85.235 115.509 83.57 C 115.509 81.904 116.855 80.558 118.52 80.558 Z " fill="url(#_lgradient_0)"/><linearGradient id="_lgradient_1" x1="0.19127525011261282" y1="0.20315845977702884" x2="0.9066531095995977" y2="0.8885264992652997" gradientTransform="matrix(51.243,0,0,51.358,111.308,92.642)" gradientUnits="userSpaceOnUse"><stop offset="0%" stop-opacity="1" style="stop-color:rgb(255,224,82)"/><stop offset="100%" stop-opacity="1" style="stop-color:rgb(255,195,49)"/></linearGradient><path d=" M 128.249 144 C 145.791 144 144.696 136.393 144.696 136.393 L 144.676 128.511 L 127.936 128.511 L 127.936 126.145 L 151.325 126.145 C 151.325 126.145 162.551 127.418 162.551 109.718 C 162.551 92.018 152.753 92.645 152.753 92.645 L 146.906 92.645 L 146.906 100.859 C 146.906 100.859 147.221 110.657 137.265 110.657 C 127.308 110.657 120.661 110.657 120.661 110.657 C 120.661 110.657 111.333 110.506 111.333 119.672 C 111.333 128.838 111.333 134.828 111.333 134.828 C 111.333 134.828 109.917 144 128.249 144 Z M 137.48 138.7 C 135.814 138.7 134.468 137.354 134.468 135.689 C 134.468 134.023 135.814 132.677 137.48 132.677 C 139.145 132.677 140.491 134.023 140.491 135.689 C 140.491 137.354 139.145 138.7 137.48 138.7 Z " fill="url(#_lgradient_1)"/></g></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 4.4 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><defs><clipPath id="_clipPath_vTXj1UBRlX1G4H8YXsC5CxDRBKjcj0iC"><rect width="256" height="256"/></clipPath></defs><g clip-path="url(#_clipPath_vTXj1UBRlX1G4H8YXsC5CxDRBKjcj0iC)"><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(85,96,128)"/><g><path d=" M 129.571 115.429 L 129.571 109.143 L 135.857 109.143 L 135.857 102.857 L 129.571 102.857 L 129.571 96.571 L 135.857 96.571 L 135.857 90.286 L 129.571 90.286 L 129.571 84 L 135.857 84 L 135.857 77.714 L 129.571 77.714 L 129.571 71.429 L 135.857 71.429 L 135.857 65.143 L 129.571 65.143 L 129.571 58.857 L 123.286 58.857 L 123.286 65.143 L 117 65.143 L 117 71.429 L 123.286 71.429 L 123.286 77.714 L 117 77.714 L 117 84 L 123.286 84 L 123.286 90.286 L 117 90.286 L 117 96.571 L 123.286 96.571 L 123.286 102.857 L 117 102.857 L 117 109.143 L 123.286 109.143 L 123.286 115.429 L 110.714 115.429 L 110.714 131.143 C 110.714 139.808 117.764 146.857 126.429 146.857 C 135.093 146.857 142.143 139.808 142.143 131.143 L 142.143 115.429 L 129.571 115.429 Z M 135.857 131.143 C 135.857 136.341 131.627 140.571 126.429 140.571 C 121.23 140.571 117 136.341 117 131.143 L 117 121.714 L 135.857 121.714 L 135.857 131.143 Z " fill="rgb(200,189,184)"/><path d=" M 123.286 134.286 L 129.571 134.286 C 131.306 134.286 132.714 132.881 132.714 131.143 C 132.714 129.405 131.306 128 129.571 128 L 123.286 128 C 121.551 128 120.143 129.405 120.143 131.143 C 120.143 132.881 121.551 134.286 123.286 134.286 Z " fill="rgb(200,189,184)"/></g></g></g><path d=" M 105.318 203.746 L 99.72 192.815 L 94.818 192.815 L 94.818 203.746 L 88.666 203.746 L 88.666 173.887 L 99.761 173.887 L 99.761 173.887 Q 105.052 173.887 107.923 176.245 L 107.923 176.245 L 107.923 176.245 Q 110.794 178.604 110.794 182.91 L 110.794 182.91 L 110.794 182.91 Q 110.794 185.966 109.471 188.006 L 109.471 188.006 L 109.471 188.006 Q 108.148 190.047 105.462 191.257 L 105.462 191.257 L 111.922 203.459 L 111.922 203.746 L 105.318 203.746 L 105.318 203.746 Z M 94.818 178.87 L 94.818 187.832 L 99.781 187.832 L 99.781 187.832 Q 102.099 187.832 103.37 186.653 L 103.37 186.653 L 103.37 186.653 Q 104.642 185.474 104.642 183.402 L 104.642 183.402 L 104.642 183.402 Q 104.642 181.29 103.442 180.08 L 103.442 180.08 L 103.442 180.08 Q 102.242 178.87 99.761 178.87 L 99.761 178.87 L 94.818 178.87 L 94.818 178.87 Z M 134.398 203.746 L 132.327 197.594 L 121.54 197.594 L 119.489 203.746 L 112.947 203.746 L 124.063 173.887 L 129.764 173.887 L 140.94 203.746 L 134.398 203.746 L 134.398 203.746 Z M 126.913 181.434 L 123.201 192.61 L 130.666 192.61 L 126.913 181.434 L 126.913 181.434 Z M 160.382 203.746 L 154.783 192.815 L 149.882 192.815 L 149.882 203.746 L 143.729 203.746 L 143.729 173.887 L 154.824 173.887 L 154.824 173.887 Q 160.115 173.887 162.986 176.245 L 162.986 176.245 L 162.986 176.245 Q 165.857 178.604 165.857 182.91 L 165.857 182.91 L 165.857 182.91 Q 165.857 185.966 164.535 188.006 L 164.535 188.006 L 164.535 188.006 Q 163.212 190.047 160.525 191.257 L 160.525 191.257 L 166.985 203.459 L 166.985 203.746 L 160.382 203.746 L 160.382 203.746 Z M 149.882 178.87 L 149.882 187.832 L 154.845 187.832 L 154.845 187.832 Q 157.162 187.832 158.434 186.653 L 158.434 186.653 L 158.434 186.653 Q 159.705 185.474 159.705 183.402 L 159.705 183.402 L 159.705 183.402 Q 159.705 181.29 158.505 180.08 L 158.505 180.08 L 158.505 180.08 Q 157.306 178.87 154.824 178.87 L 154.824 178.87 L 149.882 178.87 L 149.882 178.87 Z " fill-rule="evenodd" fill="rgb(255,255,255)"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 4.3 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><defs><clipPath id="_clipPath_MipeIDFvj6U3h5wRDZn1ZwVQ0JfjCZwd"><rect width="256" height="256"/></clipPath></defs><g clip-path="url(#_clipPath_MipeIDFvj6U3h5wRDZn1ZwVQ0JfjCZwd)"><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(88,137,196)"/><path d=" M 162.238 75.084 C 160.783 74.138 158.838 74.546 157.889 75.998 C 153.385 82.9 145.383 92.228 141.577 96.034 L 132.815 104.796 L 123.927 95.908 L 132.683 87.143 C 138.249 81.577 152.392 70.039 158.19 67.965 C 158.316 67.946 158.476 67.918 158.542 67.902 C 160.18 67.535 161.141 65.963 160.852 64.31 C 160.56 62.657 158.916 61.529 157.26 61.752 L 156.371 61.947 C 148.705 64.502 132.934 78 128.236 82.702 L 105.654 105.283 C 105.356 105.271 105.06 105.239 104.759 105.239 C 95.421 105.239 87.589 111.918 85.955 121.004 L 85.615 121.233 L 85.553 122.918 C 85.446 125.879 84.603 135.653 78.189 137.205 C 76.564 137.611 75.492 139.17 75.697 140.826 L 76.206 143.714 L 98.143 143.714 L 98.206 143.714 L 98.206 143.714 L 104.762 143.62 C 115.344 143.62 123.955 135.009 123.955 124.427 C 123.955 123.82 123.921 123.223 123.864 122.629 L 146.018 100.478 C 150.119 96.377 158.354 86.781 163.15 79.433 C 164.099 77.978 163.69 76.033 162.238 75.084 Z M 104.762 137.334 L 93.011 137.334 L 93.058 137.429 L 87.837 137.429 C 90.625 133.214 91.486 127.884 91.747 124.716 L 92.005 123.132 C 92.674 116.513 98.159 111.522 104.765 111.522 C 105.145 111.522 105.503 111.601 105.877 111.635 L 106.402 111.701 C 109.687 112.122 112.55 113.76 114.574 116.136 C 114.756 116.353 114.945 116.56 115.111 116.789 C 115.369 117.135 115.598 117.503 115.821 117.874 C 115.979 118.141 116.126 118.414 116.268 118.691 C 116.466 119.087 116.657 119.489 116.815 119.907 C 116.909 120.159 116.978 120.423 117.057 120.683 C 117.198 121.145 117.343 121.607 117.434 122.095 L 117.449 122.176 C 117.578 122.912 117.676 123.657 117.676 124.427 C 117.669 131.542 111.88 137.334 104.762 137.334 Z M 120.193 113.062 C 120.171 113.034 120.152 113.009 120.13 112.98 C 118.229 110.431 115.715 108.376 112.801 107.025 L 119.473 100.352 L 128.361 109.24 L 121.853 115.746 C 121.378 114.803 120.819 113.907 120.193 113.062 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><path d=" M 100.318 203.746 L 94.72 192.815 L 89.818 192.815 L 89.818 203.746 L 83.666 203.746 L 83.666 173.887 L 94.761 173.887 L 94.761 173.887 Q 100.052 173.887 102.923 176.245 L 102.923 176.245 L 102.923 176.245 Q 105.794 178.604 105.794 182.91 L 105.794 182.91 L 105.794 182.91 Q 105.794 185.966 104.471 188.006 L 104.471 188.006 L 104.471 188.006 Q 103.148 190.047 100.462 191.257 L 100.462 191.257 L 106.922 203.459 L 106.922 203.746 L 100.318 203.746 L 100.318 203.746 Z M 89.818 178.87 L 89.818 187.832 L 94.781 187.832 L 94.781 187.832 Q 97.099 187.832 98.37 186.653 L 98.37 186.653 L 98.37 186.653 Q 99.642 185.474 99.642 183.402 L 99.642 183.402 L 99.642 183.402 Q 99.642 181.29 98.442 180.08 L 98.442 180.08 L 98.442 180.08 Q 97.242 178.87 94.761 178.87 L 94.761 178.87 L 89.818 178.87 L 89.818 178.87 Z M 129.398 203.746 L 127.327 197.594 L 116.54 197.594 L 114.489 203.746 L 107.947 203.746 L 119.063 173.887 L 124.764 173.887 L 135.94 203.746 L 129.398 203.746 L 129.398 203.746 Z M 121.913 181.434 L 118.201 192.61 L 125.666 192.61 L 121.913 181.434 L 121.913 181.434 Z M 157.084 173.887 L 161.985 195.133 L 166.005 173.887 L 172.137 173.887 L 165.513 203.746 L 159.319 203.746 L 154.459 183.771 L 149.599 203.746 L 143.405 203.746 L 136.781 173.887 L 142.913 173.887 L 146.953 195.092 L 151.875 173.887 L 157.084 173.887 L 157.084 173.887 Z " fill-rule="evenodd" fill="rgb(255,255,255)"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><defs><clipPath id="_clipPath_VbvJtGsXngEkjwHKSnY2rI1LAQin1v85"><rect width="256" height="256"/></clipPath></defs><g clip-path="url(#_clipPath_VbvJtGsXngEkjwHKSnY2rI1LAQin1v85)"><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></g><path d=" M 153.188 107.262 L 144.047 107.262 L 142.184 115.77 L 149.145 115.77 L 149.145 124.77 L 140.215 124.77 L 137.719 135.703 L 128.438 135.703 L 130.934 124.77 L 122.145 124.77 L 119.648 135.703 L 110.367 135.703 L 112.863 124.77 L 105.586 124.77 L 105.586 115.77 L 114.727 115.77 L 116.695 107.262 L 109.629 107.262 L 109.629 98.262 L 118.699 98.262 L 121.02 87.398 L 130.301 87.398 L 127.98 98.262 L 136.77 98.262 L 139.09 87.398 L 148.371 87.398 L 146.051 98.262 L 153.188 98.262 L 153.188 107.262 L 153.188 107.262 Z M 134.766 107.262 L 125.977 107.262 L 124.043 115.77 L 132.902 115.77 L 134.766 107.262 L 134.766 107.262 Z " fill-rule="evenodd" fill="rgb(93,93,93)"/><path d=" M 119.149 195.912 L 119.149 195.912 L 119.149 195.912 Q 119.149 194.169 117.919 193.236 L 117.919 193.236 L 117.919 193.236 Q 116.688 192.303 113.489 191.267 L 113.489 191.267 L 113.489 191.267 Q 110.29 190.231 108.424 189.227 L 108.424 189.227 L 108.424 189.227 Q 103.338 186.479 103.338 181.823 L 103.338 181.823 L 103.338 181.823 Q 103.338 179.403 104.702 177.506 L 104.702 177.506 L 104.702 177.506 Q 106.065 175.609 108.619 174.543 L 108.619 174.543 L 108.619 174.543 Q 111.172 173.477 114.351 173.477 L 114.351 173.477 L 114.351 173.477 Q 117.55 173.477 120.052 174.635 L 120.052 174.635 L 120.052 174.635 Q 122.554 175.794 123.938 177.906 L 123.938 177.906 L 123.938 177.906 Q 125.322 180.019 125.322 182.705 L 125.322 182.705 L 119.17 182.705 L 119.17 182.705 Q 119.17 180.654 117.878 179.516 L 117.878 179.516 L 117.878 179.516 Q 116.586 178.378 114.248 178.378 L 114.248 178.378 L 114.248 178.378 Q 111.992 178.378 110.741 179.332 L 110.741 179.332 L 110.741 179.332 Q 109.49 180.285 109.49 181.844 L 109.49 181.844 L 109.49 181.844 Q 109.49 183.3 110.957 184.284 L 110.957 184.284 L 110.957 184.284 Q 112.423 185.269 115.273 186.13 L 115.273 186.13 L 115.273 186.13 Q 120.523 187.709 122.923 190.047 L 122.923 190.047 L 122.923 190.047 Q 125.322 192.385 125.322 195.871 L 125.322 195.871 L 125.322 195.871 Q 125.322 199.747 122.39 201.952 L 122.39 201.952 L 122.39 201.952 Q 119.457 204.156 114.494 204.156 L 114.494 204.156 L 114.494 204.156 Q 111.049 204.156 108.219 202.895 L 108.219 202.895 L 108.219 202.895 Q 105.389 201.634 103.902 199.439 L 103.902 199.439 L 103.902 199.439 Q 102.415 197.245 102.415 194.354 L 102.415 194.354 L 108.588 194.354 L 108.588 194.354 Q 108.588 199.296 114.494 199.296 L 114.494 199.296 L 114.494 199.296 Q 116.688 199.296 117.919 198.404 L 117.919 198.404 L 117.919 198.404 Q 119.149 197.512 119.149 195.912 Z M 153.787 173.887 L 153.787 203.746 L 147.635 203.746 L 147.635 190.949 L 135.638 190.949 L 135.638 203.746 L 129.485 203.746 L 129.485 173.887 L 135.638 173.887 L 135.638 185.986 L 147.635 185.986 L 147.635 173.887 L 153.787 173.887 L 153.787 173.887 Z " fill-rule="evenodd" fill="rgb(255,255,255)"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><defs><clipPath id="_clipPath_q21FbrYhm4rXvezdMbXGpW3CD1Ri61Wi"><rect width="256" height="256"/></clipPath></defs><g clip-path="url(#_clipPath_q21FbrYhm4rXvezdMbXGpW3CD1Ri61Wi)"><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(85,96,128)"/><g><path d=" M 129.571 115.429 L 129.571 109.143 L 135.857 109.143 L 135.857 102.857 L 129.571 102.857 L 129.571 96.571 L 135.857 96.571 L 135.857 90.286 L 129.571 90.286 L 129.571 84 L 135.857 84 L 135.857 77.714 L 129.571 77.714 L 129.571 71.429 L 135.857 71.429 L 135.857 65.143 L 129.571 65.143 L 129.571 58.857 L 123.286 58.857 L 123.286 65.143 L 117 65.143 L 117 71.429 L 123.286 71.429 L 123.286 77.714 L 117 77.714 L 117 84 L 123.286 84 L 123.286 90.286 L 117 90.286 L 117 96.571 L 123.286 96.571 L 123.286 102.857 L 117 102.857 L 117 109.143 L 123.286 109.143 L 123.286 115.429 L 110.714 115.429 L 110.714 131.143 C 110.714 139.808 117.764 146.857 126.429 146.857 C 135.093 146.857 142.143 139.808 142.143 131.143 L 142.143 115.429 L 129.571 115.429 Z M 135.857 131.143 C 135.857 136.341 131.627 140.571 126.429 140.571 C 121.23 140.571 117 136.341 117 131.143 L 117 121.714 L 135.857 121.714 L 135.857 131.143 Z " fill="rgb(200,189,184)"/><path d=" M 123.286 134.286 L 129.571 134.286 C 131.306 134.286 132.714 132.881 132.714 131.143 C 132.714 129.405 131.306 128 129.571 128 L 123.286 128 C 121.551 128 120.143 129.405 120.143 131.143 C 120.143 132.881 121.551 134.286 123.286 134.286 Z " fill="rgb(200,189,184)"/></g></g></g><path d=" M 111.143 173.887 L 111.143 178.87 L 101.996 178.87 L 101.996 203.746 L 95.844 203.746 L 95.844 178.87 L 86.82 178.87 L 86.82 173.887 L 111.143 173.887 L 111.143 173.887 Z M 133.578 203.746 L 131.507 197.594 L 120.72 197.594 L 118.669 203.746 L 112.127 203.746 L 123.242 173.887 L 128.943 173.887 L 140.12 203.746 L 133.578 203.746 L 133.578 203.746 Z M 126.093 181.434 L 122.381 192.61 L 129.846 192.61 L 126.093 181.434 L 126.093 181.434 Z M 159.562 203.746 L 153.963 192.815 L 149.062 192.815 L 149.062 203.746 L 142.909 203.746 L 142.909 173.887 L 154.004 173.887 L 154.004 173.887 Q 159.295 173.887 162.166 176.245 L 162.166 176.245 L 162.166 176.245 Q 165.037 178.604 165.037 182.91 L 165.037 182.91 L 165.037 182.91 Q 165.037 185.966 163.714 188.006 L 163.714 188.006 L 163.714 188.006 Q 162.392 190.047 159.705 191.257 L 159.705 191.257 L 166.165 203.459 L 166.165 203.746 L 159.562 203.746 L 159.562 203.746 Z M 149.062 178.87 L 149.062 187.832 L 154.024 187.832 L 154.024 187.832 Q 156.342 187.832 157.613 186.653 L 157.613 186.653 L 157.613 186.653 Q 158.885 185.474 158.885 183.402 L 158.885 183.402 L 158.885 183.402 Q 158.885 181.29 157.685 180.08 L 157.685 180.08 L 157.685 180.08 Q 156.485 178.87 154.004 178.87 L 154.004 178.87 L 149.062 178.87 L 149.062 178.87 Z " fill-rule="evenodd" fill="rgb(255,255,255)"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 3.6 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 256 256" width="256" height="256"><defs><clipPath id="_clipPath_g8ot4TI3cAvYvsG3hKIZXTg6Y32xAlLo"><rect width="256" height="256"/></clipPath></defs><g clip-path="url(#_clipPath_g8ot4TI3cAvYvsG3hKIZXTg6Y32xAlLo)"><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 114.643 173.887 L 114.643 178.87 L 105.496 178.87 L 105.496 203.746 L 99.344 203.746 L 99.344 178.87 L 90.32 178.87 L 90.32 173.887 L 114.643 173.887 L 114.643 173.887 Z M 140.626 173.887 L 140.626 178.87 L 131.479 178.87 L 131.479 203.746 L 125.327 203.746 L 125.327 178.87 L 116.304 178.87 L 116.304 173.887 L 140.626 173.887 L 140.626 173.887 Z M 162.098 186.581 L 162.098 191.544 L 150.285 191.544 L 150.285 203.746 L 144.133 203.746 L 144.133 173.887 L 163.574 173.887 L 163.574 178.87 L 150.285 178.87 L 150.285 186.581 L 162.098 186.581 L 162.098 186.581 Z " fill-rule="evenodd" fill="rgb(255,255,255)"/><path d=" M 119.3 138.07 L 115.626 127.158 L 96.495 127.158 L 92.857 138.07 L 81.255 138.07 L 100.968 85.112 L 111.08 85.112 L 130.903 138.07 L 119.3 138.07 L 119.3 138.07 Z M 106.024 98.497 L 99.441 118.32 L 112.68 118.32 L 106.024 98.497 L 106.024 98.497 Z M 168.585 138.07 L 157.964 138.07 L 157.964 138.07 Q 157.236 136.651 156.909 134.542 L 156.909 134.542 L 156.909 134.542 Q 153.09 138.797 146.979 138.797 L 146.979 138.797 L 146.979 138.797 Q 141.196 138.797 137.395 135.451 L 137.395 135.451 L 137.395 135.451 Q 133.594 132.105 133.594 127.013 L 133.594 127.013 L 133.594 127.013 Q 133.594 120.757 138.232 117.411 L 138.232 117.411 L 138.232 117.411 Q 142.869 114.064 151.635 114.028 L 151.635 114.028 L 156.473 114.028 L 156.473 111.773 L 156.473 111.773 Q 156.473 109.045 155.072 107.408 L 155.072 107.408 L 155.072 107.408 Q 153.672 105.771 150.653 105.771 L 150.653 105.771 L 150.653 105.771 Q 147.998 105.771 146.488 107.044 L 146.488 107.044 L 146.488 107.044 Q 144.979 108.317 144.979 110.536 L 144.979 110.536 L 134.467 110.536 L 134.467 110.536 Q 134.467 107.117 136.577 104.207 L 136.577 104.207 L 136.577 104.207 Q 138.687 101.298 142.542 99.643 L 142.542 99.643 L 142.542 99.643 Q 146.397 97.988 151.199 97.988 L 151.199 97.988 L 151.199 97.988 Q 158.473 97.988 162.747 101.643 L 162.747 101.643 L 162.747 101.643 Q 167.021 105.299 167.021 111.918 L 167.021 111.918 L 167.021 128.977 L 167.021 128.977 Q 167.057 134.578 168.585 137.452 L 168.585 137.452 L 168.585 138.07 L 168.585 138.07 Z M 149.271 130.759 L 149.271 130.759 L 149.271 130.759 Q 151.599 130.759 153.563 129.723 L 153.563 129.723 L 153.563 129.723 Q 155.527 128.686 156.473 126.94 L 156.473 126.94 L 156.473 120.175 L 152.544 120.175 L 152.544 120.175 Q 144.652 120.175 144.142 125.631 L 144.142 125.631 L 144.106 126.249 L 144.106 126.249 Q 144.106 128.213 145.488 129.486 L 145.488 129.486 L 145.488 129.486 Q 146.87 130.759 149.271 130.759 Z " fill-rule="evenodd" fill="rgb(200,189,184)"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 5.1 KiB |