Initial commit: OCA Storage packages (17 packages)

This commit is contained in:
Ernad Husremovic 2025-08-29 15:43:06 +02:00
commit 7a380f05d3
659 changed files with 41828 additions and 0 deletions

63
README.md Normal file
View file

@ -0,0 +1,63 @@
# OCA Storage
This repository contains **17** OCA packages for storage.
## Packages Included (17 packages)
- **odoo-bringout-oca-storage-fs_attachment** - From storage: fs_attachment
- **odoo-bringout-oca-storage-fs_base_multi_image** - From storage: fs_base_multi_image
- **odoo-bringout-oca-storage-fs_base_multi_media** - From storage: fs_base_multi_media
- **odoo-bringout-oca-storage-fs_file** - From storage: fs_file
- **odoo-bringout-oca-storage-fs_file_demo** - From storage: fs_file_demo
- **odoo-bringout-oca-storage-fs_image** - From storage: fs_image
- **odoo-bringout-oca-storage-fs_image_thumbnail** - From storage: fs_image_thumbnail
- **odoo-bringout-oca-storage-fs_product_brand_multi_image** - From storage: fs_product_brand_multi_image
- **odoo-bringout-oca-storage-fs_product_multi_image** - From storage: fs_product_multi_image
- **odoo-bringout-oca-storage-fs_product_multi_media** - From storage: fs_product_multi_media
- **odoo-bringout-oca-storage-fs_product_public_category_multi_image** - From storage: fs_product_public_category_multi_image
- **odoo-bringout-oca-storage-fs_storage** - From storage: fs_storage
- **odoo-bringout-oca-storage-fs_storage_backup** - From storage: fs_storage_backup
- **odoo-bringout-oca-storage-image_tag** - From storage: image_tag
- **odoo-bringout-oca-storage-storage_backend** - From storage: storage_backend
- **odoo-bringout-oca-storage-storage_backend_sftp** - From storage: storage_backend_sftp
- **odoo-bringout-oca-storage-storage_file** - From storage: storage_file
## Installation
Install any package from this category:
```bash
# Install from local directory
pip install packages/oca-storage/PACKAGE_NAME/
# Install in development mode
pip install -e packages/oca-storage/PACKAGE_NAME/
# Using uv (recommended for speed)
uv add packages/oca-storage/PACKAGE_NAME/
```
## Repository Structure
Each package in this repository follows the standard Odoo addon structure:
```
oca-storage/
├── odoo-bringout-oca-PROJECT-ADDON/
│ ├── ADDON_NAME/ # Complete addon code
│ │ ├── __init__.py
│ │ ├── __manifest__.py
│ │ └── ... (models, views, etc.)
│ ├── pyproject.toml # Python package configuration
│ └── README.md # Package documentation
└── ...
```
## Contributing
These packages are maintained as part of the [OCA (Odoo Community Association)](https://github.com/OCA) ecosystem.
## License
Each package maintains its original license as specified in the OCA repositories.

View file

@ -0,0 +1,46 @@
# Base Attachment Object Store
Odoo addon: fs_attachment
## Installation
```bash
pip install odoo-bringout-oca-storage-fs_attachment
```
## Dependencies
This addon depends on:
- fs_storage
## Manifest Information
- **Name**: Base Attachment Object Store
- **Version**: 16.0.1.3.5
- **Category**: Knowledge Management
- **License**: AGPL-3
- **Installable**: True
## Source
Based on [OCA/storage](https://github.com/OCA/storage) branch 16.0, addon `fs_attachment`.
## License
This package maintains the original AGPL-3 license from the upstream Odoo project.
## Documentation
- Overview: doc/OVERVIEW.md
- Architecture: doc/ARCHITECTURE.md
- Models: doc/MODELS.md
- Controllers: doc/CONTROLLERS.md
- Wizards: doc/WIZARDS.md
- Reports: doc/REPORTS.md
- Security: doc/SECURITY.md
- Install: doc/INSTALL.md
- Usage: doc/USAGE.md
- Configuration: doc/CONFIGURATION.md
- Dependencies: doc/DEPENDENCIES.md
- Troubleshooting: doc/TROUBLESHOOTING.md
- FAQ: doc/FAQ.md

View file

@ -0,0 +1,32 @@
# Architecture
```mermaid
flowchart TD
U[Users] -->|HTTP| V[Views and QWeb Templates]
V --> C[Controllers]
V --> W[Wizards Transient Models]
C --> M[Models and ORM]
W --> M
M --> R[Reports]
DX[Data XML] --> M
S[Security ACLs and Groups] -. enforces .-> M
subgraph Fs_attachment Module - fs_attachment
direction LR
M:::layer
W:::layer
C:::layer
V:::layer
R:::layer
S:::layer
DX:::layer
end
classDef layer fill:#eef8ff,stroke:#6ea8fe,stroke-width:1px
```
Notes
- Views include tree/form/kanban templates and report templates.
- Controllers provide website/portal routes when present.
- Wizards are UI flows implemented with `models.TransientModel`.
- Data XML loads data/demo records; Security defines groups and access.

View file

@ -0,0 +1,3 @@
# Configuration
Refer to Odoo settings for fs_attachment. Configure related models, access rights, and options as needed.

View file

@ -0,0 +1,3 @@
# Controllers
This module does not define custom HTTP controllers.

View file

@ -0,0 +1,5 @@
# Dependencies
This addon depends on:
- [fs_storage](../../odoo-bringout-oca-storage-fs_storage)

View file

@ -0,0 +1,4 @@
# FAQ
- Q: Which Odoo version? A: 16.0 (OCA/OCB packaged).
- Q: How to enable? A: Start server with --addon fs_attachment or install in UI.

View file

@ -0,0 +1,7 @@
# Install
```bash
pip install odoo-bringout-oca-storage-fs_attachment"
# or
uv pip install odoo-bringout-oca-storage-fs_attachment"
```

View file

@ -0,0 +1,17 @@
# Models
Detected core models and extensions in fs_attachment.
```mermaid
classDiagram
class fs_file_gc
class fs_storage
class ir_attachment
class ir_binary
class ir_model
class ir_model_fields
```
Notes
- Classes show model technical names; fields omitted for brevity.
- Items listed under _inherit are extensions of existing models.

View file

@ -0,0 +1,6 @@
# Overview
Packaged Odoo addon: fs_attachment. Provides features documented in upstream Odoo 16 under this addon.
- Source: OCA/OCB 16.0, addon fs_attachment
- License: LGPL-3

View file

@ -0,0 +1,3 @@
# Reports
This module does not define custom reports.

View file

@ -0,0 +1,73 @@
# Security
Access control and security definitions in fs_attachment.
## Access Control Lists (ACLs)
Model access permissions defined in:
- **[all_odoo_addons_repos.txt](../all_odoo_addons_repos.txt)**
- 318 model access rules
- **[bosnian_translations.json](../bosnian_translations.json)**
- 50 model access rules
- **[bosnian_translations_output.json](../bosnian_translations_output.json)**
- 444 model access rules
- **[CHANGELOG.md](../CHANGELOG.md)**
- 132 model access rules
- **[delete_all_odoo_addons.sh](../delete_all_odoo_addons.sh)**
- 50 model access rules
- **[delete_odoo_addons.sh](../delete_odoo_addons.sh)**
- 44 model access rules
- **[doc](../doc)**
- **[docker](../docker)**
- **[input](../input)**
- **[nix](../nix)**
- **[odoo.conf](../odoo.conf)**
- 58 model access rules
- **[odoo_packages_bez_l10n.txt](../odoo_packages_bez_l10n.txt)**
- 1947 model access rules
- **[odoo_packages_bringout.txt](../odoo_packages_bringout.txt)**
- 1947 model access rules
- **[odoo_packages.txt](../odoo_packages.txt)**
- 2085 model access rules
- **[output](../output)**
- **[packages](../packages)**
- **[PACKAGES.md](../PACKAGES.md)**
- 298 model access rules
- **[README.md](../README.md)**
- 338 model access rules
- **[scripts](../scripts)**
- **[temp](../temp)**
- **[TRANSLATION_BS_SUMMARY.md](../TRANSLATION_BS_SUMMARY.md)**
- 146 model access rules
- **[verify_deletions.sh](../verify_deletions.sh)**
- 55 model access rules
## Record Rules
Row-level security rules defined in:
## Security Groups & Configuration
Security groups and permissions defined in:
- **[fs_file_gc.xml](../fs_attachment/security/fs_file_gc.xml)**
```mermaid
graph TB
subgraph "Security Layers"
A[Users] --> B[Groups]
B --> C[Access Control Lists]
C --> D[Models]
B --> E[Record Rules]
E --> F[Individual Records]
end
```
Security files overview:
- **[fs_file_gc.xml](../fs_attachment/security/fs_file_gc.xml)**
- Security groups, categories, and XML-based rules
Notes
- Access Control Lists define which groups can access which models
- Record Rules provide row-level security (filter records by user/group)
- Security groups organize users and define permission sets
- All security is enforced at the ORM level by Odoo

View file

@ -0,0 +1,5 @@
# Troubleshooting
- Ensure Python and Odoo environment matches repo guidance.
- Check database connectivity and logs if startup fails.
- Validate that dependent addons listed in DEPENDENCIES.md are installed.

View file

@ -0,0 +1,7 @@
# Usage
Start Odoo including this addon (from repo root):
```bash
python3 scripts/nix_odoo_web_server.py --db-name mydb --addon fs_attachment
```

View file

@ -0,0 +1,3 @@
# Wizards
This module does not include UI wizards.

View file

@ -0,0 +1,455 @@
.. image:: https://odoo-community.org/readme-banner-image
:target: https://odoo-community.org/get-involved?utm_source=readme
:alt: Odoo Community Association
============================
Base Attachment Object Store
============================
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:03d52a1eb8acbea54afd494673cc996016973fa06cf64ae65384a78e13b6e5ac
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |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/license-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstorage-lightgray.png?logo=github
:target: https://github.com/OCA/storage/tree/16.0/fs_attachment
:alt: OCA/storage
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/storage-16-0/storage-16-0-fs_attachment
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/storage&target_branch=16.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
In some cases, you need to store attachment in another system that the Odoo's
filestore. For example, when your deployment is based on a multi-server
architecture to ensure redundancy and scalability, your attachments must
be stored in a way that they are accessible from all the servers. In this
way, you can use a shared storage system like NFS or a cloud storage like
S3 compliant storage, or....
This addon extend the storage mechanism of Odoo's attachments to allow
you to store them in any storage filesystem supported by the Python
library `fsspec <https://filesystem-spec.readthedocs.io/en/latest/>`_ and made
available via the `fs_storage` addon.
In contrast to Odoo, when a file is stored into an external storage, this
addon ensures that the filename keeps its meaning (In odoo the filename
into the filestore is the file content checksum). Concretely the filename
is based on the pattern:
'<name-without-extension>-<attachment-id>-<version>.<extension>'
This addon also adds on the attachments 2 new fields to use
to retrieve the file content from a URL:
* ``Internal URL``: URL to retrieve the file content from the Odoo's
filestore.
* ``Filesystem URL``: URL to retrieve the file content from the external
storage.
.. note::
The internal URL is always available, but the filesystem URL is only
available when the attachment is stored in an external storage.
Particular attention has been paid to limit as much as possible the consumption
of resources necessary to serve via Odoo the content stored in an external
filesystem. The implementation is based on an end-to-end streaming of content
between the external filesystem and the Odoo client application by default.
Nevertheless, if your content is available via a URL on the external filesystem,
you can configure the storage to use the x-sendfile mechanism to serve the
content if it's activated on your Odoo instance. In this case, the content
served by Odoo at the internal URL will be proxied to the filesystem URL
by nginx.
Last but not least, the addon adds a new method `open` on the attachment. This
method allows you to open the attachment as a file. For attachments stored into
the filestore or in an external filesystem, it allows you to directly read from
and write to the file and therefore minimize the memory consumption since data
are not kept into memory before being written into the database.
**Table of contents**
.. contents::
:local:
Usage
=====
Configuration
~~~~~~~~~~~~~
The configuration is done through the creation of a filesytem storage record
into odoo. To create a new storage, go to the menu
``Settings > Technical > FS Storage`` and click on ``Create``.
In addition to the common fields available to configure a storage, specifics
fields are available under the section 'Attachment' to configure the way
attachments will be stored in the filesystem.
* ``Optimizes Directory Path``: This option is useful if you need to prevent
having too many files in a single directory. It will create a directory
structure based on the attachment's checksum (with 2 levels of depth)
For example, if the checksum is ``123456789``, the file will be stored in the
directory ``/path/to/storage/12/34/my_file-1-0.txt``.
* ``Autovacuum GC``: This is used to automatically remove files from the filesystem
when it's no longer referenced in Odoo. Some storage backends (like S3) may
charge you for the storage of files, so it's important to remove them when
they're no longer needed. In some cases, this option is not desirable, for
example if you're using a storage backend to store images shared with others
systems (like your website) and you don't want to remove the files from the
storage while they're still referenced into the others systems.
This mechanism is based on a ``fs.file.gc`` model used to collect the files
to remove. This model is automatically populated by the ``ir.attachment``
model when a file is removed from the database. If you disable this option,
you'll have to manually take care of the records in the ``fs.file.gc`` for
your filesystem storage.
* ``Use As Default For Attachment``: This options allows you to declare the storage
as the default one for attachments. If you have multiple filesystem storage
configured, you can choose which one will be used by default for attachments.
Once activated, attachments created without specifying a storage will be
stored in this default storage.
* ``Force DB For Default Attachment Rules``: This option is useful if you want to
force the storage of some attachments in the database, even if you have a
default filesystem storage configured. This is specially useful when you're
using a storage backend like S3, where the latency of the network can be
high. This option is a JSON field that allows you to define the mimetypes and
the size limit below which the attachments will be stored in the database.
Small images (128, 256) are used in Odoo in list / kanban views. We
want them to be fast to read.
They are generally < 50KB (default configuration) so they don't take
that much space in database, but they'll be read much faster than from
the object storage.
The assets (application/javascript, text/css) are stored in database
as well whatever their size is:
* a database doesn't have thousands of them
* of course better for performance
* better portability of a database: when replicating a production
instance for dev, the assets are included
The default configuration is:
{"image/": 51200, "application/javascript": 0, "text/css": 0}
Where the key is the beginning of the mimetype to configure and the
value is the limit in size below which attachments are kept in DB.
0 means no limit.
Default configuration means:
* images mimetypes (image/png, image/jpeg, ...) below 50KB are
stored in database
* application/javascript are stored in database whatever their size
* text/css are stored in database whatever their size
This option is only available on the filesystem storage that is used
as default for attachments.
It is also possible to use different FS storages for attachments linked to
different resource fields/models. You can configure it either on the ``fs.storage``
directly, or in a server environment file:
* From the ``fs.storage``: Fields `model_ids` and `field_ids` will encode for which
models/fields use this storage as default storage for attachments having these resource
model/field. Note that if an attachment has both resource model and field, it will
first take the FS storage where the field is explicitely linked, then is not found,
the one where the model is explicitely linked.
* From a server environment file: In this case you just have to provide a comma-
separated list of models (under the `model_xmlids` key) or fields (under the
`field_xmlids` key). To do so, use the model/field XML ids provided by Odoo.
See the Server Environment section for a concrete example.
Another key feature of this module is the ability to get access to the attachments
from URLs.
* ``Base URL``: This is the base URL used to access the attachments from the
filesystem storage itself. If your storage doesn't provide a way to access
the files from a URL, you can leave this field empty.
* ``Is Directory Path In URL``: Normally the directory patch configured on the storage
is not included in the URL. If you want to include it, you can activate this option.
* ``Use X-Sendfile To Serve Internal Url``: If checked and odoo is behind a proxy
that supports x-sendfile, the content served by the attachment's internal URL
will be served by the proxy using the filesystem url path if defined (This field
is available on the attachment if the storage is configured with a base URL)
If not, the file will be served by odoo that will stream the content read from
the filesystem storage. This option is useful to avoid to serve files from odoo
and therefore to avoid to load the odoo process.
To be fully functional, this option requires the proxy to support x-sendfile
(apache) or x-accel-redirect (nginx). You must also configure your proxy by
adding for each storage a rule to redirect the url rooted at the 'storagge code'
to the server serving the files. For example, if you have a storage with the
code 'my_storage' and a server serving the files at the url 'http://myserver.com',
you must add the following rule in your proxy configuration:
.. code-block:: nginx
location /my_storage/ {
internal;
proxy_pass http://myserver.com;
}
With this configuration a call to '/web/content/<att.id>/<att.name><att.extension>"
for a file stored in the 'my_storage' storage will generate a response by odoo
with the URI
``/my_storage/<paht_in_storage>/<att.name>-<att.id>-<version><att.extension>``
in the headers ``X-Accel-Redirect`` and ``X-Sendfile`` and the proxy will redirect to
``http://myserver.com/<paht_in_storage>/<att.name>-<att.id>-<version><att.extension>``.
see https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/ for more
information.
* ``Use Filename Obfuscation``: If checked, the filename used to store the content
into the filesystem storage will be obfuscated. This is useful to avoid to
expose the real filename of the attachments outside of the Odoo database.
The filename will be obfuscated by using the checksum of the content. This option
is to avoid when the content of your filestore is shared with other systems
(like your website) and you want to keep a meaningful filename to ensure
SEO. This option is disabled by default.
Server Environment
~~~~~~~~~~~~~~~~~~
When you configure a storage through the use of server environment file, you can
provide values for the following keys:
* ``optimizes_directory_path``
* ``autovacuum_gc``
* ``base_url``
* ``is_directory_path_in_url``
* ``use_x_sendfile_to_serve_internal_url``
* ``use_as_default_for_attachments``
* ``force_db_for_default_attachment_rules``
* ``use_filename_obfuscation``
* ``model_xmlids``
* ``field_xmlids``
For example, the configuration of my storage with code `fsprod` used to store
the attachments by default could be:
.. code-block:: ini
[fs_storage.fsprod]
protocol=s3
options={"endpoint_url": "https://my_s3_server/", "key": "KEY", "secret": "SECRET"}
directory_path=my_bucket
use_as_default_for_attachments=True
use_filename_obfuscation=True
model_xmlids=base.model_res_lang,base.model_res_country
field_xmlids=base.field_res_partner__image_128
Advanced usage: Using attachment as a file
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The `open` method on the attachment can be used to open manipulate the attachment
as a file object. The object returned by the call to the method implements
methods from ``io.IOBase``. The method can ba called as any other python method.
In such a case, it's your responsibility to close the file at the end of your
process.
.. code-block:: python
attachment = self.env.create({"name": "test.txt"})
the_file = attachment.open("wb")
try:
the_file.write(b"content")
finally:
the_file.close()
The result of the call to `open` also works in a context ``with`` block. In such
a case, when the code exit the block, the file is automatically closed.
.. code-block:: python
attachment = self.env.create({"name": "test.txt"})
with attachment.open("wb") as the_file:
the_file.write(b"content")
It's always safer to prefer the second approach.
When your attachment is stored into the odoo filestore or into an external
filesystem storage, each time you call the open method, a new file is created.
This way of doing ensures that if the transaction is rolled back the original content
is preserved. Nevertheless you could have use cases where you would like to write
to the existing file directly. For example you could create an empty attachment
to store a csv report and then use the `open` method to write your content directly
into the new file. To support this kind a use cases, the parameter `new_version`
can be passed as `False` to avoid the creation of a new file.
.. code-block:: python
attachment = self.env.create({"name": "test.txt"})
with attachment.open("w", new_version=False) as f:
writer = csv.writer(f, delimiter=";")
....
Tips & Tricks
~~~~~~~~~~~~~
* When working in multi staging environments, the management of the attachments
can be tricky. For example, if you have a production instance and a staging
instance based on a backup of the production environment, you may want to have
the attachments shared between the two instances BUT you don't want to have
one instance removing or modifying the attachments of the other instance.
To do so, you can add on your staging instances a new storage and declare it
as the default storage to use for attachments. This way, all the new attachments
will be stored in this new storage but the attachments created on the production
instance will still be read from the production storage. Be careful to adapt the
configuration of your storage to the production environment to make it read only.
(The use of server environment files is a good way to do so).
Changelog
=========
16.0.1.0.13 (2024-05-10)
~~~~~~~~~~~~~~~~~~~~~~~~
**Bugfixes**
- No crash o missign file.
Prior to this change, Odoo was crashing as soon as access to a file stored into
an external filesytem was not possible. This can lead to a complete system block.
This change prevents this kind of blockage by ignoring access error to files
stored into external system on read operations. These kind of errors are logged
into the log files for traceability. (`#361 <https://github.com/OCA/storage/issues/361>`_)
16.0.1.0.8 (2023-12-20)
~~~~~~~~~~~~~~~~~~~~~~~
**Bugfixes**
- Fix the error retrieving attachment files when the storage is set to optimize directory paths. (`#312 <https://github.com/OCA/storage/issues/312>`_)
16.0.1.0.6 (2023-12-02)
~~~~~~~~~~~~~~~~~~~~~~~
**Bugfixes**
- Improve performance at creation of an attachment or when the attachment is updated.
Before this change, when the fs_url was computed the computed value was always
reassigned to the fs_url attribute even if the value was the same. In a lot of
cases the value was the same and the reassignment was not necessary. Unfortunately
this reassignment has as side effect to mark the record as dirty and generate a
SQL update statement at the end of the transaction. (`#307 <https://github.com/OCA/storage/issues/307>`_)
16.0.1.0.5 (2023-11-29)
~~~~~~~~~~~~~~~~~~~~~~~
**Bugfixes**
- When manipulating the file system api through a local variable named *fs*,
we observed some strange behavior when it was wrongly redefined in an
enclosing scope as in the following example: *with fs.open(...) as fs*.
This commit fixes this issue by renaming the local variable and therefore
avoiding the name clash. (`#306 <https://github.com/OCA/storage/issues/306>`_)
16.0.1.0.4 (2023-11-22)
~~~~~~~~~~~~~~~~~~~~~~~
**Bugfixes**
- Fix error when an url is computed for an attachment in a storage configure wihtout directory path. (`#302 <https://github.com/OCA/storage/issues/302>`_)
16.0.1.0.3 (2023-10-17)
~~~~~~~~~~~~~~~~~~~~~~~
**Bugfixes**
- Fix access to technical models to be able to upload attachments for users with basic access (`#289 <https://github.com/OCA/storage/issues/289>`_)
16.0.1.0.2 (2023-10-09)
~~~~~~~~~~~~~~~~~~~~~~~
**Bugfixes**
- Ensures python 3.9 compatibility. (`#285 <https://github.com/OCA/storage/issues/285>`_)
- If a storage is not used to store all the attachments by default, the call to the
`get_force_db_for_default_attachment_rules` method must return an empty dictionary. (`#286 <https://github.com/OCA/storage/issues/286>`_)
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/storage/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/storage/issues/new?body=module:%20fs_attachment%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* Camptocamp
* ACSONE SA/NV
Contributors
~~~~~~~~~~~~
Thierry Ducrest <thierry.ducrest@camptocamp.com>
Guewen Baconnier <guewen.baconnier@camptocamp.com>
Julien Coux <julien.coux@camptocamp.com>
Akim Juillerat <akim.juillerat@camptocamp.com>
Thomas Nowicki <thomas.nowicki@camptocamp.com>
Vincent Renaville <vincent.renaville@camptocamp.com>
Denis Leemann <denis.leemann@camptocamp.com>
Patrick Tombez <patrick.tombez@camptocamp.com>
Don Kendall <kendall@donkendall.com>
Stephane Mangin <stephane.mangin@camptocamp.com>
Laurent Mignon <laurent.mignon@acsone.eu>
Marie Lejeune <marie.lejeune@acsone.eu>
Wolfgang Pichler <wpichler@callino.at>
Nans Lefebvre <len@lambdao.dev>
Mohamed Alkobrosli <alkobroslymohamed@gmail.com>
Maintainers
~~~~~~~~~~~
This module is maintained by the OCA.
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
.. |maintainer-lmignon| image:: https://github.com/lmignon.png?size=40px
:target: https://github.com/lmignon
:alt: lmignon
Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
|maintainer-lmignon|
This module is part of the `OCA/storage <https://github.com/OCA/storage/tree/16.0/fs_attachment>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View file

@ -0,0 +1,2 @@
from . import models
from .hooks import pre_init_hook

View file

@ -0,0 +1,24 @@
# Copyright 2017-2021 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
{
"name": "Base Attachment Object Store",
"summary": "Store attachments on external object store",
"version": "16.0.1.3.5",
"author": "Camptocamp, ACSONE SA/NV, Odoo Community Association (OCA)",
"license": "AGPL-3",
"development_status": "Beta",
"category": "Knowledge Management",
"depends": ["fs_storage"],
"website": "https://github.com/OCA/storage",
"data": [
"security/fs_file_gc.xml",
"views/fs_storage.xml",
],
"external_dependencies": {"python": ["python_slugify"]},
"installable": True,
"auto_install": False,
"maintainers": ["lmignon"],
"pre_init_hook": "pre_init_hook",
}

View file

@ -0,0 +1,108 @@
# Copyright 2023 ACSONE SA/NV
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from __future__ import annotations
from odoo.http import STATIC_CACHE_LONG, Response, Stream, request
from odoo.tools import config
from .models.ir_attachment import IrAttachment
try:
from werkzeug.utils import secure_filename, send_file as _send_file
except ImportError:
from odoo.tools._vendor.send_file import send_file as _send_file
class FsStream(Stream):
fs_attachment = None
@classmethod
def from_fs_attachment(cls, attachment: IrAttachment) -> FsStream:
attachment.ensure_one()
if not attachment.fs_filename:
raise ValueError("Attachment is not stored into a filesystem storage")
return cls(
mimetype=attachment.mimetype,
download_name=attachment.name,
conditional=True,
etag=attachment.checksum,
type="fs",
size=attachment.file_size,
last_modified=attachment["__last_update"],
fs_attachment=attachment,
)
def read(self):
if self.type == "fs":
with self.fs_attachment.open("rb") as f:
return f.read()
return super().read()
def get_response(
self,
as_attachment=None,
immutable=None,
content_security_policy="default-src 'none'",
**send_file_kwargs,
):
if self.type != "fs":
return super().get_response(
as_attachment=as_attachment, immutable=immutable, **send_file_kwargs
)
if as_attachment is None:
as_attachment = self.as_attachment
if immutable is None:
immutable = self.immutable
# Sanitize the download_name before passing it
safe_download_name = secure_filename(self.download_name or "")
send_file_kwargs = {
"mimetype": self.mimetype,
"as_attachment": as_attachment,
"download_name": safe_download_name,
"conditional": self.conditional,
"etag": self.etag,
"last_modified": self.last_modified,
"max_age": STATIC_CACHE_LONG if immutable else self.max_age,
"environ": request.httprequest.environ,
"response_class": Response,
}
use_x_sendfile = self._fs_use_x_sendfile
# The file will be closed by werkzeug...
send_file_kwargs["use_x_sendfile"] = use_x_sendfile
if not use_x_sendfile:
f = self.fs_attachment.open("rb")
res = _send_file(f, **send_file_kwargs)
else:
x_accel_redirect = (
f"/{self.fs_attachment.fs_storage_code}{self.fs_attachment.fs_url_path}"
)
send_file_kwargs["use_x_sendfile"] = True
res = _send_file("", **send_file_kwargs)
# nginx specific headers
res.headers["X-Accel-Redirect"] = x_accel_redirect
# apache specific headers
res.headers["X-Sendfile"] = x_accel_redirect
res.headers["Content-Length"] = 0
if immutable and res.cache_control:
res.cache_control["immutable"] = None
res.headers["X-Content-Type-Options"] = "nosniff"
if content_security_policy:
res.headers["Content-Security-Policy"] = content_security_policy
return res
@classmethod
def _check_use_x_sendfile(cls, attachment: IrAttachment) -> bool:
return (
config["x_sendfile"]
and attachment.fs_url
and attachment.fs_storage_id.use_x_sendfile_to_serve_internal_url
)
@property
def _fs_use_x_sendfile(self) -> bool:
"""Return True if x-sendfile should be used to serve the file"""
return self._check_use_x_sendfile(self.fs_attachment)

View file

@ -0,0 +1,37 @@
# Copyright 2023 ACSONE SA/NV
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
import logging
from odoo.tools.sql import column_exists
_logger = logging.getLogger(__name__)
def pre_init_hook(cr):
"""Pre init hook."""
# add columns for computed fields to avoid useless computation by the ORM
# when installing the module
if column_exists(cr, "ir_attachment", "fs_storage_id"):
return # columns already added; update probably failed partway
_logger.info("Add columns for computed fields on ir_attachment")
cr.execute(
"""
ALTER TABLE ir_attachment
ADD COLUMN fs_storage_id INTEGER;
ALTER TABLE ir_attachment
ADD FOREIGN KEY (fs_storage_id) REFERENCES fs_storage(id);
"""
)
cr.execute(
"""
ALTER TABLE ir_attachment
ADD COLUMN fs_url VARCHAR;
"""
)
cr.execute(
"""
ALTER TABLE ir_attachment
ADD COLUMN fs_storage_code VARCHAR;
"""
)
_logger.info("Columns added on ir_attachment")

View file

@ -0,0 +1,419 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * fs_attachment
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: fs_attachment
#: model:ir.model,name:fs_attachment.model_ir_attachment
#: model_terms:ir.ui.view,arch_db:fs_attachment.fs_storage_form_view
msgid "Attachment"
msgstr "Prilog"
#. module: fs_attachment
#: model_terms:ir.ui.view,arch_db:fs_attachment.fs_storage_form_view
msgid "Attachment's Url"
msgstr "URL priloga"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__autovacuum_gc
msgid "Autovacuum Garbage Collection"
msgstr "Autovacuum čišćenje smeća"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__base_url
msgid "Base Url"
msgstr "Osnovna URL"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__base_url_for_files
msgid "Base Url For Files"
msgstr "Osnovna URL za datoteke"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc__create_uid
msgid "Created by"
msgstr "Kreirao"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc__create_date
msgid "Created on"
msgstr "Kreirano"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc__display_name
msgid "Display Name"
msgstr "Prikazani naziv"
#. module: fs_attachment
#: model:ir.model,name:fs_attachment.model_fs_storage
msgid "FS Storage"
msgstr "FS skladište"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__field_ids
msgid "Field"
msgstr "Polje"
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/fs_storage.py:0
#, python-format
msgid ""
"Field %(field)s already stored in another FS storage ('%(other_storage)s')"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__field_xmlids
msgid "Field Xmlids"
msgstr "XML ID-jevi polja"
#. module: fs_attachment
#: model:ir.model,name:fs_attachment.model_ir_model_fields
msgid "Fields"
msgstr "Polja"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_ir_attachment__fs_filename
msgid "File Name into the filesystem storage"
msgstr "Naziv datoteke u filesystem skladištu"
#. module: fs_attachment
#: model:ir.model,name:fs_attachment.model_ir_binary
msgid "File streaming helper model for controllers"
msgstr "File streaming helper model za kontrolere"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_ir_attachment__fs_storage_id
msgid "Filesystem Storage"
msgstr "Filesystem skladište"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_ir_attachment__fs_storage_code
msgid "Filesystem Storage Code"
msgstr "Kod filesystem skladišta"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_ir_attachment__fs_url
msgid "Filesystem URL"
msgstr "Filesystem URL"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_ir_attachment__fs_url_path
msgid "Filesystem URL Path"
msgstr "Putanja filesystem URL-a"
#. module: fs_attachment
#: model:ir.model,name:fs_attachment.model_fs_file_gc
msgid "Filesystem storage file garbage collector"
msgstr "Filesystem storage file garbage collector"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__force_db_for_default_attachment_rules
msgid "Force Db For Default Attachment Rules"
msgstr "Forsiraj DB za zadana pravila priloga"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc__id
msgid "ID"
msgstr "ID"
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__use_x_sendfile_to_serve_internal_url
msgid ""
"If checked and odoo is behind a proxy that supports x-sendfile, the content "
"served by the attachment's internal URL will be servedby the proxy using the"
" fs_url if defined. If not, the file will be served by odoo that will stream"
" the content read from the filesystem storage. This option is useful to "
"avoid to serve files from odoo and therefore to avoid to load the odoo "
"process. "
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__autovacuum_gc
msgid ""
"If checked, the autovacuum of the garbage collection will be automatically "
"executed when the storage is used to store attachments. Sometime, the "
"autovacuum is to avoid when files in the storage are referenced by other "
"systems (like a website). In such case, records in the fs.file.gc table must"
" be manually processed."
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__optimizes_directory_path
msgid ""
"If checked, the directory path will be optimized to avoid too much files "
"into the same directory. This options is used when the storage is used to "
"store attachments. Depending on the storage, this option can be ignored. "
"It's useful for storage based on real file. This way, files with similar "
"properties will be stored in the same directory, avoiding overcrowding in "
"the root directory and optimizing access times."
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__use_filename_obfuscation
msgid ""
"If checked, the filename will be obfuscated. This option is useful to avoid "
"to expose sensitive information trough the URL or in the remote storage. The"
" obfuscation is done using a hash of the filename. The original filename is "
"stored in the attachment metadata. The obfusation is to avoid if the storage"
" is used to store files that are referenced by other systems (like a "
"website) where the filename is important for SEO."
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__use_as_default_for_attachments
msgid "If checked, this storage will be used to store all the attachments "
msgstr "Ako je označeno, ovo skladište će se koristiti za pohranu svih priloga "
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_ir_model_fields__storage_id
msgid ""
"If specified, all attachments linked to this field will be stored in the "
"provided storage."
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_ir_model__storage_id
msgid ""
"If specified, all attachments linked to this model will be stored in the "
"provided storage."
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_ir_attachment__internal_url
msgid "Internal URL"
msgstr "Interni URL"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__is_directory_path_in_url
msgid "Is Directory Path In Url"
msgstr "Je putanja direktorija u URL"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc____last_update
msgid "Last Modified on"
msgstr "Zadnje mijenjano"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc__write_uid
msgid "Last Updated by"
msgstr "Zadnji ažurirao"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc__write_date
msgid "Last Updated on"
msgstr "Zadnje ažurirano"
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__field_ids
msgid ""
"List of fields such as attachments linked to one of these fields will be "
"stored in this storage. NB: If the attachment is linked to a field that is "
"in one FS storage, and the related model is in another FS storage, we will "
"store it into the storage linked to the resource field."
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__field_xmlids
msgid ""
"List of fields xml ids such as attachments linked to one of these fields "
"will be stored in this storage. NB: If the attachment is linked to a field "
"that is in one FS storage, and the related model is in another FS storage, "
"we will store it into the storage linked to the resource field."
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__model_ids
msgid ""
"List of models such as attachments linked to one of these models will be "
"stored in this storage."
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__model_xmlids
msgid ""
"List of models xml ids such as attachments linked to one of these models "
"will be stored in this storage."
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__model_ids
msgid "Model"
msgstr "Model"
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/fs_storage.py:0
#, python-format
msgid ""
"Model %(model)s already stored in another FS storage ('%(other_storage)s')"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__model_xmlids
msgid "Model Xmlids"
msgstr "XML ID-jevi modela"
#. module: fs_attachment
#: model:ir.model,name:fs_attachment.model_ir_model
msgid "Models"
msgstr "Modeli"
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__is_directory_path_in_url
msgid ""
"Normally the directory_path is for internal usage. If this flag is enabled "
"the path will be used to compute the public URL."
msgstr ""
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/ir_attachment.py:0
#, python-format
msgid "Only administrators can execute this action."
msgstr "Samo administratori mogu izvršiti ovu akciju."
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/fs_storage.py:0
#, python-format
msgid "Only one storage can be used as default for attachments"
msgstr "Samo jedno skladište može se koristiti kao zadano za priloge"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__optimizes_directory_path
msgid "Optimizes Directory Path"
msgstr "Optimizira putanju direktorija"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_ir_model__storage_id
#: model:ir.model.fields,field_description:fs_attachment.field_ir_model_fields__storage_id
msgid "Storage"
msgstr "Skladište"
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/ir_attachment.py:0
#, python-format
msgid "Storage '%s' is disabled (see environment configuration)."
msgstr "Skladište '%s' je onemogućeno (vidi konfiguraciju okruženja)."
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc__fs_storage_code
msgid "Storage Code"
msgstr "Kod skladišta"
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/ir_attachment.py:0
#, python-format
msgid "Storages are disabled (see environment configuration)."
msgstr "Skladišta su onemogućena (vidi konfiguraciju okruženja)."
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc__store_fname
msgid "Stored Filename"
msgstr "Pohranjeni naziv datoteke"
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_ir_attachment__fs_storage_code
msgid ""
"Technical code used to identify the storage backend into the code.This code "
"must be unique. This code is used for example to define the storage backend "
"to store the attachments via the configuration parameter "
"'ir_attachment.storage.force.database' when the module 'fs_attachment' is "
"installed."
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_ir_attachment__fs_url
msgid "The URL to access the file from the filesystem storage."
msgstr "URL za pristup datoteci iz filesystem skladišta."
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_ir_attachment__internal_url
msgid "The URL to access the file from the server."
msgstr "URL za pristup datoteci s servera."
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/fs_storage.py:0
#, python-format
msgid ""
"The force_db_for_default_attachment_rules can only be set if the storage is "
"used as default for attachments."
msgstr ""
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/fs_storage.py:0
#, python-format
msgid "The force_db_for_default_attachment_rules is not a valid python dict."
msgstr "Force_db_for_default_attachment_rules nije važeći python dict."
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_ir_attachment__fs_filename
msgid ""
"The name of the file in the filesystem storage.To preserve the mimetype and "
"the meaning of the filenamethe filename is computed from the name and the "
"extension"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_ir_attachment__fs_url_path
msgid "The path to access the file from the filesystem storage."
msgstr "Putanja za pristup datoteci iz filesystem skladišta."
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_ir_attachment__fs_storage_id
msgid "The storage where the file is stored."
msgstr "Skladište gdje je datoteka pohranjena."
#. module: fs_attachment
#: model:ir.model.constraint,message:fs_attachment.constraint_fs_file_gc_store_fname_uniq
msgid "The stored filename must be unique!"
msgstr "Pohranjeni naziv datoteke mora biti jedinstven!"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__use_as_default_for_attachments
msgid "Use As Default For Attachments"
msgstr "Koristi kao zadano za priloge"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__use_filename_obfuscation
msgid "Use Filename Obfuscation"
msgstr "Koristi zamućivanje naziva datoteke"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__use_x_sendfile_to_serve_internal_url
msgid "Use X-Sendfile To Serve Internal Url"
msgstr "Koristi X-Sendfile za posluživanje internog URL-a"
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__force_db_for_default_attachment_rules
msgid ""
"When storing attachments in an external storage, storage may be slow.If the storage is used to store odoo attachments by default, this could lead to a bad user experience since small images (128, 256) are used in Odoo in list / kanban views. We want them to be fast to read.This field allows to force the store of some attachments in the odoo database. The value is a dict Where the key is the beginning of the mimetype to configure and the value is the limit in size below which attachments are kept in DB. 0 means no limit.\n"
"Default configuration means:\n"
"* images mimetypes (image/png, image/jpeg, ...) below 50KB are stored in database\n"
"* application/javascript are stored in database whatever their size \n"
"* text/css are stored in database whatever their size"
msgstr ""
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/ir_attachment.py:0
#, python-format
msgid ""
"You can't write on multiple attachments with different mimetypes at the same"
" time."
msgstr ""

View file

@ -0,0 +1,506 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * fs_attachment
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2023-10-29 00:15+0000\n"
"Last-Translator: Ivorra78 <informatica@totmaterial.es>\n"
"Language-Team: none\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.17\n"
#. module: fs_attachment
#: model:ir.model,name:fs_attachment.model_ir_attachment
#: model_terms:ir.ui.view,arch_db:fs_attachment.fs_storage_form_view
msgid "Attachment"
msgstr "Archivo adjunto"
#. module: fs_attachment
#: model_terms:ir.ui.view,arch_db:fs_attachment.fs_storage_form_view
msgid "Attachment's Url"
msgstr "Url del Archivo Adjunto"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__autovacuum_gc
msgid "Autovacuum Garbage Collection"
msgstr "Recogida Automática de Basura"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__base_url
msgid "Base Url"
msgstr "Url Base"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__base_url_for_files
msgid "Base Url For Files"
msgstr "Url base Para Archivos"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc__create_uid
msgid "Created by"
msgstr "Creado por"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc__create_date
msgid "Created on"
msgstr "Creado el"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc__display_name
msgid "Display Name"
msgstr "Mostrar Nombre"
#. module: fs_attachment
#: model:ir.model,name:fs_attachment.model_fs_storage
msgid "FS Storage"
msgstr "Almacenamiento FS"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__field_ids
msgid "Field"
msgstr "Campo"
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/fs_storage.py:0
#, python-format
msgid ""
"Field %(field)s already stored in another FS storage ('%(other_storage)s')"
msgstr "Campo %(field)s ya almacenado en otro FS storage ('%(other_storage)s')"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__field_xmlids
msgid "Field Xmlids"
msgstr "Campo Xmlids"
#. module: fs_attachment
#: model:ir.model,name:fs_attachment.model_ir_model_fields
msgid "Fields"
msgstr "Campos"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_ir_attachment__fs_filename
msgid "File Name into the filesystem storage"
msgstr "Nombre de archivo en el almacenamiento del sistema de archivos"
#. module: fs_attachment
#: model:ir.model,name:fs_attachment.model_ir_binary
msgid "File streaming helper model for controllers"
msgstr "Modelo de ayuda de transmisión de archivos para controladores"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_ir_attachment__fs_storage_id
msgid "Filesystem Storage"
msgstr "Almacenamiento del sistema de Archivos"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_ir_attachment__fs_storage_code
msgid "Filesystem Storage Code"
msgstr "Código de almacenamiento del Sistema de Archivos"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_ir_attachment__fs_url
msgid "Filesystem URL"
msgstr "URL del sistema de archivos"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_ir_attachment__fs_url_path
msgid "Filesystem URL Path"
msgstr "Ruta URL del Sistema de Archivos"
#. module: fs_attachment
#: model:ir.model,name:fs_attachment.model_fs_file_gc
msgid "Filesystem storage file garbage collector"
msgstr ""
"Recolector de basura de archivos de almacenamiento del sistema de archivos"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__force_db_for_default_attachment_rules
msgid "Force Db For Default Attachment Rules"
msgstr "Forzar Db para Reglas de Adjuntos por Defecto"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc__id
msgid "ID"
msgstr "ID (identificación)"
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__use_x_sendfile_to_serve_internal_url
msgid ""
"If checked and odoo is behind a proxy that supports x-sendfile, the content "
"served by the attachment's internal URL will be servedby the proxy using the"
" fs_url if defined. If not, the file will be served by odoo that will stream"
" the content read from the filesystem storage. This option is useful to "
"avoid to serve files from odoo and therefore to avoid to load the odoo "
"process. "
msgstr ""
"Si esta marcado y odoo esta detrás de un servidor que soporta x-sendfile, el "
"contenido servido por la URL interna del adjunto será servido por el proxy "
"usando fs_url si esta definido. Si no, el archivo será servido por odoo que "
"transmitirá el contenido leído desde el almacenamiento del sistema de "
"archivos. Esta opción es útil para evitar servir archivos desde odoo y por "
"lo tanto evitar cargar el proceso odoo. "
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__autovacuum_gc
msgid ""
"If checked, the autovacuum of the garbage collection will be automatically "
"executed when the storage is used to store attachments. Sometime, the "
"autovacuum is to avoid when files in the storage are referenced by other "
"systems (like a website). In such case, records in the fs.file.gc table must"
" be manually processed."
msgstr ""
"Si está marcada, el autovacío de la recolección de basura se ejecutará "
"automáticamente cuando el almacenamiento se utilice para guardar archivos "
"adjuntos. A veces, el autovacío debe evitarse cuando los archivos del "
"almacenamiento son referenciados por otros sistemas (como un sitio web). En "
"tal caso, los registros de la tabla fs.file.gc deben procesarse manualmente."
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__optimizes_directory_path
msgid ""
"If checked, the directory path will be optimized to avoid too much files "
"into the same directory. This options is used when the storage is used to "
"store attachments. Depending on the storage, this option can be ignored. "
"It's useful for storage based on real file. This way, files with similar "
"properties will be stored in the same directory, avoiding overcrowding in "
"the root directory and optimizing access times."
msgstr ""
"Si se marca, la ruta del directorio se optimizará para evitar demasiados "
"archivos en el mismo directorio. Esta opción se utiliza cuando el "
"almacenamiento se utiliza para almacenar archivos adjuntos. Dependiendo del "
"almacenamiento, esta opción puede ser ignorada. Es útil para el "
"almacenamiento basado en archivos reales. De esta forma, los ficheros con "
"propiedades similares se almacenarán en el mismo directorio, evitando la "
"saturación del directorio raíz y optimizando los tiempos de acceso."
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__use_filename_obfuscation
msgid ""
"If checked, the filename will be obfuscated. This option is useful to avoid "
"to expose sensitive information trough the URL or in the remote storage. The"
" obfuscation is done using a hash of the filename. The original filename is "
"stored in the attachment metadata. The obfusation is to avoid if the storage"
" is used to store files that are referenced by other systems (like a "
"website) where the filename is important for SEO."
msgstr ""
"Si está marcada, el nombre del archivo será ofuscado. Esta opción es útil "
"para evitar exponer información sensible a través de la URL o en el "
"almacenamiento remoto. La ofuscación se realiza utilizando un hash del "
"nombre del archivo. El nombre original del archivo se almacena en los "
"metadatos del adjunto. La ofuscación es para evitar si el almacenamiento se "
"utiliza para almacenar archivos que son referenciados por otros sistemas ("
"como un sitio web) donde el nombre del archivo es importante para SEO."
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__use_as_default_for_attachments
msgid "If checked, this storage will be used to store all the attachments "
msgstr ""
"Si se marca, este almacén se utilizará para almacenar todos los archivos "
"adjuntos "
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_ir_model_fields__storage_id
msgid ""
"If specified, all attachments linked to this field will be stored in the "
"provided storage."
msgstr ""
"Si se especifica, todos los adjuntos vinculados a este campo se guardarán en "
"el almacenamiento proporcionado."
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_ir_model__storage_id
msgid ""
"If specified, all attachments linked to this model will be stored in the "
"provided storage."
msgstr ""
"Si se especifica, todos los archivos adjuntos vinculados a este modelo se "
"almacenarán en el almacenamiento proporcionado."
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_ir_attachment__internal_url
msgid "Internal URL"
msgstr "URL Interna"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__is_directory_path_in_url
msgid "Is Directory Path In Url"
msgstr "Está la Ruta del Directorio en la Url"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc____last_update
msgid "Last Modified on"
msgstr "Última Modificación el"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc__write_uid
msgid "Last Updated by"
msgstr "Última Actualización por"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc__write_date
msgid "Last Updated on"
msgstr "Última Actualización el"
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__field_ids
msgid ""
"List of fields such as attachments linked to one of these fields will be "
"stored in this storage. NB: If the attachment is linked to a field that is "
"in one FS storage, and the related model is in another FS storage, we will "
"store it into the storage linked to the resource field."
msgstr ""
"La lista de campos, como los anexos vinculados a uno de estos campos, se "
"almacenará en este almacén. Nota: Si el anexo está vinculado a un campo que "
"se encuentra en un almacén FS, y el modelo relacionado se encuentra en otro "
"almacén FS, lo almacenaremos en el almacén vinculado al campo de recurso."
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__field_xmlids
msgid ""
"List of fields xml ids such as attachments linked to one of these fields "
"will be stored in this storage. NB: If the attachment is linked to a field "
"that is in one FS storage, and the related model is in another FS storage, "
"we will store it into the storage linked to the resource field."
msgstr ""
"Lista de campos xml ids como los anexos vinculados a uno de estos campos se "
"almacenarán en este almacenamiento. NB: Si el anexo está vinculado a un "
"campo que se encuentra en un almacenamiento FS, y el modelo relacionado se "
"encuentra en otro almacenamiento FS, lo almacenaremos en el almacenamiento "
"vinculado al campo de recurso."
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__model_ids
msgid ""
"List of models such as attachments linked to one of these models will be "
"stored in this storage."
msgstr ""
"La lista de modelos, así como los anexos vinculados a uno de estos modelos, "
"se almacenarán en este almacén."
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__model_xmlids
msgid ""
"List of models xml ids such as attachments linked to one of these models "
"will be stored in this storage."
msgstr ""
"Lista de modelos xml ids como los archivos adjuntos vinculados a uno de "
"estos modelos se almacenarán en este almacenamiento."
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__model_ids
msgid "Model"
msgstr "Modelo"
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/fs_storage.py:0
#, python-format
msgid ""
"Model %(model)s already stored in another FS storage ('%(other_storage)s')"
msgstr ""
"El modelo %(model)s ya está almacenado en otro almacén FS "
"('%(other_storage)s')"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__model_xmlids
msgid "Model Xmlids"
msgstr "Modelo Xmlids"
#. module: fs_attachment
#: model:ir.model,name:fs_attachment.model_ir_model
msgid "Models"
msgstr "Modelos"
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__is_directory_path_in_url
msgid ""
"Normally the directory_path is for internal usage. If this flag is enabled "
"the path will be used to compute the public URL."
msgstr ""
"Normalmente directory_path es para uso interno. Si se activa esta opción, la "
"ruta se utilizará para calcular la URL pública."
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/ir_attachment.py:0
#, python-format
msgid "Only administrators can execute this action."
msgstr "Sólo los administradores pueden ejecutar esta acción."
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/fs_storage.py:0
#, python-format
msgid "Only one storage can be used as default for attachments"
msgstr ""
"Sólo se puede utilizar un almacenamiento por defecto para los archivos "
"adjuntos"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__optimizes_directory_path
msgid "Optimizes Directory Path"
msgstr "Optimiza la Ruta del Directorio"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_ir_model__storage_id
#: model:ir.model.fields,field_description:fs_attachment.field_ir_model_fields__storage_id
msgid "Storage"
msgstr "Almacenamiento"
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/ir_attachment.py:0
#, python-format
msgid "Storage '%s' is disabled (see environment configuration)."
msgstr ""
"El almacenamiento '%s' está deshabilitado (ver configuración del entorno)."
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc__fs_storage_code
msgid "Storage Code"
msgstr "Código de Almacenamiento"
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/ir_attachment.py:0
#, python-format
msgid "Storages are disabled (see environment configuration)."
msgstr ""
"Los almacenamientos están desactivados (véase la configuración del entorno)."
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc__store_fname
msgid "Stored Filename"
msgstr "Nombre del Archivo Almacenado"
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_ir_attachment__fs_storage_code
msgid ""
"Technical code used to identify the storage backend into the code.This code "
"must be unique. This code is used for example to define the storage backend "
"to store the attachments via the configuration parameter "
"'ir_attachment.storage.force.database' when the module 'fs_attachment' is "
"installed."
msgstr ""
"Código técnico utilizado para identificar el servidor de almacenamiento en "
"el código. Este código debe ser único. Este código se utiliza, por ejemplo, "
"para definir el servidor de almacenamiento para guardar los archivos "
"adjuntos mediante el parámetro de configuración \"ir_attachment.storage.force"
".database\" cuando se instala el módulo \"fs_attachment\"."
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_ir_attachment__fs_url
msgid "The URL to access the file from the filesystem storage."
msgstr ""
"La URL para acceder al archivo desde el almacenamiento del sistema de "
"archivos."
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_ir_attachment__internal_url
msgid "The URL to access the file from the server."
msgstr "La URL para acceder al archivo desde el servidor."
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/fs_storage.py:0
#, python-format
msgid ""
"The force_db_for_default_attachment_rules can only be set if the storage is "
"used as default for attachments."
msgstr ""
"La opción force_db_for_default_attachment_rules sólo puede establecerse si "
"el almacenamiento se utiliza como predeterminado para los adjuntos."
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/fs_storage.py:0
#, python-format
msgid "The force_db_for_default_attachment_rules is not a valid python dict."
msgstr "El force_db_for_default_attachment_rules no es un dict. python válido."
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_ir_attachment__fs_filename
msgid ""
"The name of the file in the filesystem storage.To preserve the mimetype and "
"the meaning of the filenamethe filename is computed from the name and the "
"extension"
msgstr ""
"El nombre del archivo en el sistema de almacenamiento de archivos. Para "
"preservar el mimetype y el significado del filenamethe nombre de archivo se "
"calcula a partir del nombre y la extensión"
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_ir_attachment__fs_url_path
msgid "The path to access the file from the filesystem storage."
msgstr ""
"La ruta para acceder al archivo desde el almacenamiento del sistema de "
"archivos."
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_ir_attachment__fs_storage_id
msgid "The storage where the file is stored."
msgstr "El almacén donde se guarda el archivo."
#. module: fs_attachment
#: model:ir.model.constraint,message:fs_attachment.constraint_fs_file_gc_store_fname_uniq
msgid "The stored filename must be unique!"
msgstr "¡El nombre de archivo almacenado debe ser único!"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__use_as_default_for_attachments
msgid "Use As Default For Attachments"
msgstr "Usar por Defecto para Archivos Adjuntos"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__use_filename_obfuscation
msgid "Use Filename Obfuscation"
msgstr "Utilizar la Ofuscación de Nombre de Archivo"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__use_x_sendfile_to_serve_internal_url
msgid "Use X-Sendfile To Serve Internal Url"
msgstr "Usar X-Sendfile Para Servir Url Internas"
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__force_db_for_default_attachment_rules
msgid ""
"When storing attachments in an external storage, storage may be slow.If the storage is used to store odoo attachments by default, this could lead to a bad user experience since small images (128, 256) are used in Odoo in list / kanban views. We want them to be fast to read.This field allows to force the store of some attachments in the odoo database. The value is a dict Where the key is the beginning of the mimetype to configure and the value is the limit in size below which attachments are kept in DB. 0 means no limit.\n"
"Default configuration means:\n"
"* images mimetypes (image/png, image/jpeg, ...) below 50KB are stored in database\n"
"* application/javascript are stored in database whatever their size \n"
"* text/css are stored in database whatever their size"
msgstr ""
"Cuando se almacenan archivos adjuntos en un almacenamiento externo, el "
"almacenamiento puede ser lento.si el almacenamiento se utiliza para "
"almacenar archivos adjuntos odoo por defecto, esto podría conducir a una "
"mala experiencia de usuario ya que las imágenes pequeñas (128, 256) se "
"utilizan en Odoo en la lista / vistas kanban. Este campo permite forzar el "
"almacenamiento de algunos archivos adjuntos en la base de datos de Odoo. El "
"valor es un dict Donde la clave es el comienzo del mimetype a configurar y "
"el valor es el límite en tamaño por debajo del cual los archivos adjuntos se "
"mantienen en DB. 0 significa sin limite.\n"
"La configuración por defecto significa:\n"
"* los mimetypes de imágenes (image/png, image/jpeg, ...) por debajo de 50KB "
"se almacenan en base de datos\n"
"* las aplicaciones/javascript se almacenan en la base de datos sea cual sea "
"su tamaño \n"
"* texto/css se almacenan en la base de datos sea cual sea su tamaño"
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/ir_attachment.py:0
#, python-format
msgid ""
"You can't write on multiple attachments with different mimetypes at the same"
" time."
msgstr ""
"No se puede escribir en varios archivos adjuntos con diferentes tipos de "
"mimo tipos al mismo tiempo."

View file

@ -0,0 +1,420 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * fs_attachment
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"
#. module: fs_attachment
#: model:ir.model,name:fs_attachment.model_ir_attachment
#: model_terms:ir.ui.view,arch_db:fs_attachment.fs_storage_form_view
msgid "Attachment"
msgstr ""
#. module: fs_attachment
#: model_terms:ir.ui.view,arch_db:fs_attachment.fs_storage_form_view
msgid "Attachment's Url"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__autovacuum_gc
msgid "Autovacuum Garbage Collection"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__base_url
msgid "Base Url"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__base_url_for_files
msgid "Base Url For Files"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc__create_uid
msgid "Created by"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc__create_date
msgid "Created on"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc__display_name
msgid "Display Name"
msgstr ""
#. module: fs_attachment
#: model:ir.model,name:fs_attachment.model_fs_storage
msgid "FS Storage"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__field_ids
msgid "Field"
msgstr ""
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/fs_storage.py:0
#, python-format
msgid ""
"Field %(field)s already stored in another FS storage ('%(other_storage)s')"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__field_xmlids
msgid "Field Xmlids"
msgstr ""
#. module: fs_attachment
#: model:ir.model,name:fs_attachment.model_ir_model_fields
msgid "Fields"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_ir_attachment__fs_filename
msgid "File Name into the filesystem storage"
msgstr ""
#. module: fs_attachment
#: model:ir.model,name:fs_attachment.model_ir_binary
msgid "File streaming helper model for controllers"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_ir_attachment__fs_storage_id
msgid "Filesystem Storage"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_ir_attachment__fs_storage_code
msgid "Filesystem Storage Code"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_ir_attachment__fs_url
msgid "Filesystem URL"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_ir_attachment__fs_url_path
msgid "Filesystem URL Path"
msgstr ""
#. module: fs_attachment
#: model:ir.model,name:fs_attachment.model_fs_file_gc
msgid "Filesystem storage file garbage collector"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__force_db_for_default_attachment_rules
msgid "Force Db For Default Attachment Rules"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc__id
msgid "ID"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__use_x_sendfile_to_serve_internal_url
msgid ""
"If checked and odoo is behind a proxy that supports x-sendfile, the content "
"served by the attachment's internal URL will be servedby the proxy using the"
" fs_url if defined. If not, the file will be served by odoo that will stream"
" the content read from the filesystem storage. This option is useful to "
"avoid to serve files from odoo and therefore to avoid to load the odoo "
"process. "
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__autovacuum_gc
msgid ""
"If checked, the autovacuum of the garbage collection will be automatically "
"executed when the storage is used to store attachments. Sometime, the "
"autovacuum is to avoid when files in the storage are referenced by other "
"systems (like a website). In such case, records in the fs.file.gc table must"
" be manually processed."
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__optimizes_directory_path
msgid ""
"If checked, the directory path will be optimized to avoid too much files "
"into the same directory. This options is used when the storage is used to "
"store attachments. Depending on the storage, this option can be ignored. "
"It's useful for storage based on real file. This way, files with similar "
"properties will be stored in the same directory, avoiding overcrowding in "
"the root directory and optimizing access times."
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__use_filename_obfuscation
msgid ""
"If checked, the filename will be obfuscated. This option is useful to avoid "
"to expose sensitive information trough the URL or in the remote storage. The"
" obfuscation is done using a hash of the filename. The original filename is "
"stored in the attachment metadata. The obfusation is to avoid if the storage"
" is used to store files that are referenced by other systems (like a "
"website) where the filename is important for SEO."
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__use_as_default_for_attachments
msgid "If checked, this storage will be used to store all the attachments "
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_ir_model_fields__storage_id
msgid ""
"If specified, all attachments linked to this field will be stored in the "
"provided storage."
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_ir_model__storage_id
msgid ""
"If specified, all attachments linked to this model will be stored in the "
"provided storage."
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_ir_attachment__internal_url
msgid "Internal URL"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__is_directory_path_in_url
msgid "Is Directory Path In Url"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc____last_update
msgid "Last Modified on"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc__write_uid
msgid "Last Updated by"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc__write_date
msgid "Last Updated on"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__field_ids
msgid ""
"List of fields such as attachments linked to one of these fields will be "
"stored in this storage. NB: If the attachment is linked to a field that is "
"in one FS storage, and the related model is in another FS storage, we will "
"store it into the storage linked to the resource field."
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__field_xmlids
msgid ""
"List of fields xml ids such as attachments linked to one of these fields "
"will be stored in this storage. NB: If the attachment is linked to a field "
"that is in one FS storage, and the related model is in another FS storage, "
"we will store it into the storage linked to the resource field."
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__model_ids
msgid ""
"List of models such as attachments linked to one of these models will be "
"stored in this storage."
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__model_xmlids
msgid ""
"List of models xml ids such as attachments linked to one of these models "
"will be stored in this storage."
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__model_ids
msgid "Model"
msgstr ""
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/fs_storage.py:0
#, python-format
msgid ""
"Model %(model)s already stored in another FS storage ('%(other_storage)s')"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__model_xmlids
msgid "Model Xmlids"
msgstr ""
#. module: fs_attachment
#: model:ir.model,name:fs_attachment.model_ir_model
msgid "Models"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__is_directory_path_in_url
msgid ""
"Normally the directory_path is for internal usage. If this flag is enabled "
"the path will be used to compute the public URL."
msgstr ""
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/ir_attachment.py:0
#, python-format
msgid "Only administrators can execute this action."
msgstr ""
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/fs_storage.py:0
#, python-format
msgid "Only one storage can be used as default for attachments"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__optimizes_directory_path
msgid "Optimizes Directory Path"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_ir_model__storage_id
#: model:ir.model.fields,field_description:fs_attachment.field_ir_model_fields__storage_id
msgid "Storage"
msgstr ""
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/ir_attachment.py:0
#, python-format
msgid "Storage '%s' is disabled (see environment configuration)."
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc__fs_storage_code
msgid "Storage Code"
msgstr ""
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/ir_attachment.py:0
#, python-format
msgid "Storages are disabled (see environment configuration)."
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc__store_fname
msgid "Stored Filename"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_ir_attachment__fs_storage_code
msgid ""
"Technical code used to identify the storage backend into the code.This code "
"must be unique. This code is used for example to define the storage backend "
"to store the attachments via the configuration parameter "
"'ir_attachment.storage.force.database' when the module 'fs_attachment' is "
"installed."
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_ir_attachment__fs_url
msgid "The URL to access the file from the filesystem storage."
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_ir_attachment__internal_url
msgid "The URL to access the file from the server."
msgstr ""
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/fs_storage.py:0
#, python-format
msgid ""
"The force_db_for_default_attachment_rules can only be set if the storage is "
"used as default for attachments."
msgstr ""
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/fs_storage.py:0
#, python-format
msgid "The force_db_for_default_attachment_rules is not a valid python dict."
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_ir_attachment__fs_filename
msgid ""
"The name of the file in the filesystem storage.To preserve the mimetype and "
"the meaning of the filenamethe filename is computed from the name and the "
"extension"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_ir_attachment__fs_url_path
msgid "The path to access the file from the filesystem storage."
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_ir_attachment__fs_storage_id
msgid "The storage where the file is stored."
msgstr ""
#. module: fs_attachment
#: model:ir.model.constraint,message:fs_attachment.constraint_fs_file_gc_store_fname_uniq
msgid "The stored filename must be unique!"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__use_as_default_for_attachments
msgid "Use As Default For Attachments"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__use_filename_obfuscation
msgid "Use Filename Obfuscation"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__use_x_sendfile_to_serve_internal_url
msgid "Use X-Sendfile To Serve Internal Url"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__force_db_for_default_attachment_rules
msgid ""
"When storing attachments in an external storage, storage may be slow.If the storage is used to store odoo attachments by default, this could lead to a bad user experience since small images (128, 256) are used in Odoo in list / kanban views. We want them to be fast to read.This field allows to force the store of some attachments in the odoo database. The value is a dict Where the key is the beginning of the mimetype to configure and the value is the limit in size below which attachments are kept in DB. 0 means no limit.\n"
"Default configuration means:\n"
"* images mimetypes (image/png, image/jpeg, ...) below 50KB are stored in database\n"
"* application/javascript are stored in database whatever their size \n"
"* text/css are stored in database whatever their size"
msgstr ""
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/ir_attachment.py:0
#, python-format
msgid ""
"You can't write on multiple attachments with different mimetypes at the same"
" time."
msgstr ""

View file

@ -0,0 +1,419 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * fs_attachment
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: fs_attachment
#: model:ir.model,name:fs_attachment.model_ir_attachment
#: model_terms:ir.ui.view,arch_db:fs_attachment.fs_storage_form_view
msgid "Attachment"
msgstr ""
#. module: fs_attachment
#: model_terms:ir.ui.view,arch_db:fs_attachment.fs_storage_form_view
msgid "Attachment's Url"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__autovacuum_gc
msgid "Autovacuum Garbage Collection"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__base_url
msgid "Base Url"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__base_url_for_files
msgid "Base Url For Files"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc__create_uid
msgid "Created by"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc__create_date
msgid "Created on"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc__display_name
msgid "Display Name"
msgstr ""
#. module: fs_attachment
#: model:ir.model,name:fs_attachment.model_fs_storage
msgid "FS Storage"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__field_ids
msgid "Field"
msgstr ""
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/fs_storage.py:0
#, python-format
msgid ""
"Field %(field)s already stored in another FS storage ('%(other_storage)s')"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__field_xmlids
msgid "Field Xmlids"
msgstr ""
#. module: fs_attachment
#: model:ir.model,name:fs_attachment.model_ir_model_fields
msgid "Fields"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_ir_attachment__fs_filename
msgid "File Name into the filesystem storage"
msgstr ""
#. module: fs_attachment
#: model:ir.model,name:fs_attachment.model_ir_binary
msgid "File streaming helper model for controllers"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_ir_attachment__fs_storage_id
msgid "Filesystem Storage"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_ir_attachment__fs_storage_code
msgid "Filesystem Storage Code"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_ir_attachment__fs_url
msgid "Filesystem URL"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_ir_attachment__fs_url_path
msgid "Filesystem URL Path"
msgstr ""
#. module: fs_attachment
#: model:ir.model,name:fs_attachment.model_fs_file_gc
msgid "Filesystem storage file garbage collector"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__force_db_for_default_attachment_rules
msgid "Force Db For Default Attachment Rules"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc__id
msgid "ID"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__use_x_sendfile_to_serve_internal_url
msgid ""
"If checked and odoo is behind a proxy that supports x-sendfile, the content "
"served by the attachment's internal URL will be servedby the proxy using the"
" fs_url if defined. If not, the file will be served by odoo that will stream"
" the content read from the filesystem storage. This option is useful to "
"avoid to serve files from odoo and therefore to avoid to load the odoo "
"process. "
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__autovacuum_gc
msgid ""
"If checked, the autovacuum of the garbage collection will be automatically "
"executed when the storage is used to store attachments. Sometime, the "
"autovacuum is to avoid when files in the storage are referenced by other "
"systems (like a website). In such case, records in the fs.file.gc table must"
" be manually processed."
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__optimizes_directory_path
msgid ""
"If checked, the directory path will be optimized to avoid too much files "
"into the same directory. This options is used when the storage is used to "
"store attachments. Depending on the storage, this option can be ignored. "
"It's useful for storage based on real file. This way, files with similar "
"properties will be stored in the same directory, avoiding overcrowding in "
"the root directory and optimizing access times."
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__use_filename_obfuscation
msgid ""
"If checked, the filename will be obfuscated. This option is useful to avoid "
"to expose sensitive information trough the URL or in the remote storage. The"
" obfuscation is done using a hash of the filename. The original filename is "
"stored in the attachment metadata. The obfusation is to avoid if the storage"
" is used to store files that are referenced by other systems (like a "
"website) where the filename is important for SEO."
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__use_as_default_for_attachments
msgid "If checked, this storage will be used to store all the attachments "
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_ir_model_fields__storage_id
msgid ""
"If specified, all attachments linked to this field will be stored in the "
"provided storage."
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_ir_model__storage_id
msgid ""
"If specified, all attachments linked to this model will be stored in the "
"provided storage."
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_ir_attachment__internal_url
msgid "Internal URL"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__is_directory_path_in_url
msgid "Is Directory Path In Url"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc____last_update
msgid "Last Modified on"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc__write_uid
msgid "Last Updated by"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc__write_date
msgid "Last Updated on"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__field_ids
msgid ""
"List of fields such as attachments linked to one of these fields will be "
"stored in this storage. NB: If the attachment is linked to a field that is "
"in one FS storage, and the related model is in another FS storage, we will "
"store it into the storage linked to the resource field."
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__field_xmlids
msgid ""
"List of fields xml ids such as attachments linked to one of these fields "
"will be stored in this storage. NB: If the attachment is linked to a field "
"that is in one FS storage, and the related model is in another FS storage, "
"we will store it into the storage linked to the resource field."
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__model_ids
msgid ""
"List of models such as attachments linked to one of these models will be "
"stored in this storage."
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__model_xmlids
msgid ""
"List of models xml ids such as attachments linked to one of these models "
"will be stored in this storage."
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__model_ids
msgid "Model"
msgstr ""
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/fs_storage.py:0
#, python-format
msgid ""
"Model %(model)s already stored in another FS storage ('%(other_storage)s')"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__model_xmlids
msgid "Model Xmlids"
msgstr ""
#. module: fs_attachment
#: model:ir.model,name:fs_attachment.model_ir_model
msgid "Models"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__is_directory_path_in_url
msgid ""
"Normally the directory_path is for internal usage. If this flag is enabled "
"the path will be used to compute the public URL."
msgstr ""
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/ir_attachment.py:0
#, python-format
msgid "Only administrators can execute this action."
msgstr ""
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/fs_storage.py:0
#, python-format
msgid "Only one storage can be used as default for attachments"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__optimizes_directory_path
msgid "Optimizes Directory Path"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_ir_model__storage_id
#: model:ir.model.fields,field_description:fs_attachment.field_ir_model_fields__storage_id
msgid "Storage"
msgstr ""
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/ir_attachment.py:0
#, python-format
msgid "Storage '%s' is disabled (see environment configuration)."
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc__fs_storage_code
msgid "Storage Code"
msgstr ""
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/ir_attachment.py:0
#, python-format
msgid "Storages are disabled (see environment configuration)."
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc__store_fname
msgid "Stored Filename"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_ir_attachment__fs_storage_code
msgid ""
"Technical code used to identify the storage backend into the code.This code "
"must be unique. This code is used for example to define the storage backend "
"to store the attachments via the configuration parameter "
"'ir_attachment.storage.force.database' when the module 'fs_attachment' is "
"installed."
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_ir_attachment__fs_url
msgid "The URL to access the file from the filesystem storage."
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_ir_attachment__internal_url
msgid "The URL to access the file from the server."
msgstr ""
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/fs_storage.py:0
#, python-format
msgid ""
"The force_db_for_default_attachment_rules can only be set if the storage is "
"used as default for attachments."
msgstr ""
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/fs_storage.py:0
#, python-format
msgid "The force_db_for_default_attachment_rules is not a valid python dict."
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_ir_attachment__fs_filename
msgid ""
"The name of the file in the filesystem storage.To preserve the mimetype and "
"the meaning of the filenamethe filename is computed from the name and the "
"extension"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_ir_attachment__fs_url_path
msgid "The path to access the file from the filesystem storage."
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_ir_attachment__fs_storage_id
msgid "The storage where the file is stored."
msgstr ""
#. module: fs_attachment
#: model:ir.model.constraint,message:fs_attachment.constraint_fs_file_gc_store_fname_uniq
msgid "The stored filename must be unique!"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__use_as_default_for_attachments
msgid "Use As Default For Attachments"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__use_filename_obfuscation
msgid "Use Filename Obfuscation"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__use_x_sendfile_to_serve_internal_url
msgid "Use X-Sendfile To Serve Internal Url"
msgstr ""
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__force_db_for_default_attachment_rules
msgid ""
"When storing attachments in an external storage, storage may be slow.If the storage is used to store odoo attachments by default, this could lead to a bad user experience since small images (128, 256) are used in Odoo in list / kanban views. We want them to be fast to read.This field allows to force the store of some attachments in the odoo database. The value is a dict Where the key is the beginning of the mimetype to configure and the value is the limit in size below which attachments are kept in DB. 0 means no limit.\n"
"Default configuration means:\n"
"* images mimetypes (image/png, image/jpeg, ...) below 50KB are stored in database\n"
"* application/javascript are stored in database whatever their size \n"
"* text/css are stored in database whatever their size"
msgstr ""
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/ir_attachment.py:0
#, python-format
msgid ""
"You can't write on multiple attachments with different mimetypes at the same"
" time."
msgstr ""

View file

@ -0,0 +1,499 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * fs_attachment
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2024-01-05 10:38+0000\n"
"Last-Translator: mymage <stefano.consolaro@mymage.it>\n"
"Language-Team: none\n"
"Language: it\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.17\n"
#. module: fs_attachment
#: model:ir.model,name:fs_attachment.model_ir_attachment
#: model_terms:ir.ui.view,arch_db:fs_attachment.fs_storage_form_view
msgid "Attachment"
msgstr "Allegato"
#. module: fs_attachment
#: model_terms:ir.ui.view,arch_db:fs_attachment.fs_storage_form_view
msgid "Attachment's Url"
msgstr "URL allegato"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__autovacuum_gc
msgid "Autovacuum Garbage Collection"
msgstr "Raccolta rifiuti con aspirazione automatica"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__base_url
msgid "Base Url"
msgstr "URL base"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__base_url_for_files
msgid "Base Url For Files"
msgstr "URL base per i file"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc__create_uid
msgid "Created by"
msgstr "Creato da"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc__create_date
msgid "Created on"
msgstr "Creato il"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc__display_name
msgid "Display Name"
msgstr "Nome visualizzato"
#. module: fs_attachment
#: model:ir.model,name:fs_attachment.model_fs_storage
msgid "FS Storage"
msgstr "Deposito FS"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__field_ids
msgid "Field"
msgstr "Campo"
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/fs_storage.py:0
#, python-format
msgid ""
"Field %(field)s already stored in another FS storage ('%(other_storage)s')"
msgstr ""
"Il campo %(field)s è già archiviato in un altro deposito FS "
"('%(other_storage)s')"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__field_xmlids
msgid "Field Xmlids"
msgstr "ID file XML"
#. module: fs_attachment
#: model:ir.model,name:fs_attachment.model_ir_model_fields
msgid "Fields"
msgstr "Campi"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_ir_attachment__fs_filename
msgid "File Name into the filesystem storage"
msgstr "Nome del file nel filesystem del deposito"
#. module: fs_attachment
#: model:ir.model,name:fs_attachment.model_ir_binary
msgid "File streaming helper model for controllers"
msgstr "Modello aiuto streaming file per controller"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_ir_attachment__fs_storage_id
msgid "Filesystem Storage"
msgstr "Deposito filesystem"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_ir_attachment__fs_storage_code
msgid "Filesystem Storage Code"
msgstr "Codice deposito filesystem"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_ir_attachment__fs_url
msgid "Filesystem URL"
msgstr "URL filesystem"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_ir_attachment__fs_url_path
msgid "Filesystem URL Path"
msgstr "Percorso URL filesystem"
#. module: fs_attachment
#: model:ir.model,name:fs_attachment.model_fs_file_gc
msgid "Filesystem storage file garbage collector"
msgstr "Cestino file deposito filesystem"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__force_db_for_default_attachment_rules
msgid "Force Db For Default Attachment Rules"
msgstr "Forza DB per regole allegati predefinite"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc__id
msgid "ID"
msgstr "ID"
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__use_x_sendfile_to_serve_internal_url
msgid ""
"If checked and odoo is behind a proxy that supports x-sendfile, the content "
"served by the attachment's internal URL will be servedby the proxy using the"
" fs_url if defined. If not, the file will be served by odoo that will stream"
" the content read from the filesystem storage. This option is useful to "
"avoid to serve files from odoo and therefore to avoid to load the odoo "
"process. "
msgstr ""
"Se selezionata e Odoo è dietro unproxy che supporta x-sendfile, il contenuto "
"fornito dall'URL interno dell'allegato verrà fornito dal proxy utilizzando "
"il fs_url se definito. Altrimenti, il file verrà fornito da Odoo che "
"trasmettarà il contenuto letto dal deposito del filesystem. Questa opzione è "
"utile per evitare di servire file da Odoo e quindi per evitare di caricare i "
"processi Odoo. "
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__autovacuum_gc
msgid ""
"If checked, the autovacuum of the garbage collection will be automatically "
"executed when the storage is used to store attachments. Sometime, the "
"autovacuum is to avoid when files in the storage are referenced by other "
"systems (like a website). In such case, records in the fs.file.gc table must"
" be manually processed."
msgstr ""
"Se selezionata, l'aspiratore automatico del cestino verrà eseguito "
"automaticamente quando il deposito è utilizzato per archiviare allegati. "
"Alcune volte, l'aspiratore automatico è da evitare quando i file nel "
"deposito sono riferiti da altri sistemi (come un sito web). In tal caso, i "
"record nella tabella fs.file.gc devono essere elaborati manualmente."
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__optimizes_directory_path
msgid ""
"If checked, the directory path will be optimized to avoid too much files "
"into the same directory. This options is used when the storage is used to "
"store attachments. Depending on the storage, this option can be ignored. "
"It's useful for storage based on real file. This way, files with similar "
"properties will be stored in the same directory, avoiding overcrowding in "
"the root directory and optimizing access times."
msgstr ""
"Se selezionata, il percorso della cartella verrà ottimizzato per evitare di "
"avere troppi file all'interno della cartella. Queste opzioni vengono "
"utilizzate per archiviare allegati. In funzione del deposito, questa opzione "
"può essere ignorata. È utile per depositi basati su file reali. In questo "
"modo, file con proprietà simili verranno archiviati nella stessa cartella, "
"evitando l'affollamento nella cartella radice e ottimizzando il tempo di "
"accesso."
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__use_filename_obfuscation
msgid ""
"If checked, the filename will be obfuscated. This option is useful to avoid "
"to expose sensitive information trough the URL or in the remote storage. The"
" obfuscation is done using a hash of the filename. The original filename is "
"stored in the attachment metadata. The obfusation is to avoid if the storage"
" is used to store files that are referenced by other systems (like a "
"website) where the filename is important for SEO."
msgstr ""
"Se selezionata, il nome del file sarà offuscato. Questa opzione è utile per "
"evitare di esporre informaZioni sensibili attravrso l'URL o su depositi "
"remoti. L'offscamento è realizzato utilizzando una hash del nome del file. "
"Il nome orginale del file è salvato nei metadati dell'allegato. "
"L'offuscamento è da evitare se il deposito è utilizzato per archiviare file "
"che sono referenziati da altri sistemi (come un sito web) dove il nome del "
"file è utile per il SEO."
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__use_as_default_for_attachments
msgid "If checked, this storage will be used to store all the attachments "
msgstr ""
"Se selezionata, questo deposito verrà utilizzato per archiviare tutti gli "
"allegati "
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_ir_model_fields__storage_id
msgid ""
"If specified, all attachments linked to this field will be stored in the "
"provided storage."
msgstr ""
"Se specificato, tutti gli allegati collegati a questo file verranno salvati "
"nel deposito indicato."
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_ir_model__storage_id
msgid ""
"If specified, all attachments linked to this model will be stored in the "
"provided storage."
msgstr ""
"Se specificato, tutti gli allegati collegati a questo modello verranno "
"archiviati nel deposito indicato."
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_ir_attachment__internal_url
msgid "Internal URL"
msgstr "URL interno"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__is_directory_path_in_url
msgid "Is Directory Path In Url"
msgstr "Il percorso cartella è nell'URL"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc____last_update
msgid "Last Modified on"
msgstr "Ultima modifica il"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc__write_uid
msgid "Last Updated by"
msgstr "Ultimo aggiornamento di"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc__write_date
msgid "Last Updated on"
msgstr "Ultimo aggiornamento il"
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__field_ids
msgid ""
"List of fields such as attachments linked to one of these fields will be "
"stored in this storage. NB: If the attachment is linked to a field that is "
"in one FS storage, and the related model is in another FS storage, we will "
"store it into the storage linked to the resource field."
msgstr ""
"Elenco dei campi come gli allegati collegati ad uno di questi campi verranno "
"archiviati in questo deposito. NB: se l'allegato è collegato ad un file che "
"è in un deposito FS, e il relativo modello è in un altro deposito FS, "
"verranno archiviati nel deposito collegato al campo risorsa."
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__field_xmlids
msgid ""
"List of fields xml ids such as attachments linked to one of these fields "
"will be stored in this storage. NB: If the attachment is linked to a field "
"that is in one FS storage, and the related model is in another FS storage, "
"we will store it into the storage linked to the resource field."
msgstr ""
"Elenco dei campi id XML come gli allegati collegati ad uno di questi campi "
"verranno archiviati in questo deposito. NB: se l'allegato è collegato ad un "
"file che è in un deposito FS, e il relativo modello è in un altro deposito "
"FS, verranno archiviati nel deposito collegato al campo risorsa."
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__model_ids
msgid ""
"List of models such as attachments linked to one of these models will be "
"stored in this storage."
msgstr ""
"Elenco di modelli come gli allegati collegati ad uno di questi modelli "
"verranno archiviati in questo deposito."
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__model_xmlids
msgid ""
"List of models xml ids such as attachments linked to one of these models "
"will be stored in this storage."
msgstr ""
"Elenco di modelli id XML come gli allegati collegati ad uno di questi "
"modelli verranno archiviati in questo deposito."
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__model_ids
msgid "Model"
msgstr "Modello"
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/fs_storage.py:0
#, python-format
msgid ""
"Model %(model)s already stored in another FS storage ('%(other_storage)s')"
msgstr ""
"Il modello %(model)s è già archiviato in un altro deposito FS "
"('%(other_storage)s')"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__model_xmlids
msgid "Model Xmlids"
msgstr "Modello Xmlids"
#. module: fs_attachment
#: model:ir.model,name:fs_attachment.model_ir_model
msgid "Models"
msgstr "Modelli"
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__is_directory_path_in_url
msgid ""
"Normally the directory_path is for internal usage. If this flag is enabled "
"the path will be used to compute the public URL."
msgstr ""
"Normalmente il directory_path è per uso interno. Se questa opzione è "
"abilitata il percorso verrà utilizzato per calcolare l'URL pubblico."
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/ir_attachment.py:0
#, python-format
msgid "Only administrators can execute this action."
msgstr "Solo gli amministratori possono eseguire questa azione."
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/fs_storage.py:0
#, python-format
msgid "Only one storage can be used as default for attachments"
msgstr "Solo un deposito può essere usato come predefinito per gli allegati"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__optimizes_directory_path
msgid "Optimizes Directory Path"
msgstr "Ottimizza percorso cartella"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_ir_model__storage_id
#: model:ir.model.fields,field_description:fs_attachment.field_ir_model_fields__storage_id
msgid "Storage"
msgstr "Deposito"
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/ir_attachment.py:0
#, python-format
msgid "Storage '%s' is disabled (see environment configuration)."
msgstr "Il deposito '%s' è disabilitato (vedere configurazion ambiente)."
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc__fs_storage_code
msgid "Storage Code"
msgstr "Codice deposito"
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/ir_attachment.py:0
#, python-format
msgid "Storages are disabled (see environment configuration)."
msgstr "I depositi sono disabilitati (vedere configurazion ambiente)."
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_file_gc__store_fname
msgid "Stored Filename"
msgstr "Nome file memorizzato"
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_ir_attachment__fs_storage_code
msgid ""
"Technical code used to identify the storage backend into the code.This code "
"must be unique. This code is used for example to define the storage backend "
"to store the attachments via the configuration parameter "
"'ir_attachment.storage.force.database' when the module 'fs_attachment' is "
"installed."
msgstr ""
"Codice tecnico usato per identificare il backend deposito nel codice. Questo "
"codice deve essere univoco. Questo codice è utilizzato per esempio per "
"definire il backend deposito dove depositare gli allegati attraverso il "
"parametro configurazione 'ir_attachment.storage.force.database' quando il "
"modulo 'fs_attachment' è installato."
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_ir_attachment__fs_url
msgid "The URL to access the file from the filesystem storage."
msgstr "L'URL per accedere al file dal deposito del filesystem."
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_ir_attachment__internal_url
msgid "The URL to access the file from the server."
msgstr "L'URL per accedere al file dal server."
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/fs_storage.py:0
#, python-format
msgid ""
"The force_db_for_default_attachment_rules can only be set if the storage is "
"used as default for attachments."
msgstr ""
"Il force_db_for_default_attachment_rules può essere impostato solo se il "
"deposito è utilizzato cone predefinito per gli allegati."
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/fs_storage.py:0
#, python-format
msgid "The force_db_for_default_attachment_rules is not a valid python dict."
msgstr ""
"Il force_db_for_default_attachment_rules non è un dizionario Python valido."
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_ir_attachment__fs_filename
msgid ""
"The name of the file in the filesystem storage.To preserve the mimetype and "
"the meaning of the filenamethe filename is computed from the name and the "
"extension"
msgstr ""
"Il nome del file nel deposito del filesystem. Per preservare i tipi MIME e "
"il significato del nome del file, il nome del file è calcolato dal nome e "
"dall'estensione"
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_ir_attachment__fs_url_path
msgid "The path to access the file from the filesystem storage."
msgstr "Il percorso per accedere al file dal deposito del filesystem."
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_ir_attachment__fs_storage_id
msgid "The storage where the file is stored."
msgstr "Il deposito dove è archiviato il file."
#. module: fs_attachment
#: model:ir.model.constraint,message:fs_attachment.constraint_fs_file_gc_store_fname_uniq
msgid "The stored filename must be unique!"
msgstr "Il nome del file archiviato deve essere univoco!"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__use_as_default_for_attachments
msgid "Use As Default For Attachments"
msgstr "Utilizzare come predefinito per gl allegati"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__use_filename_obfuscation
msgid "Use Filename Obfuscation"
msgstr "Utilizza offuscamento nome del file"
#. module: fs_attachment
#: model:ir.model.fields,field_description:fs_attachment.field_fs_storage__use_x_sendfile_to_serve_internal_url
msgid "Use X-Sendfile To Serve Internal Url"
msgstr "Utilizza X-Sendfile per fornire l'URL interno"
#. module: fs_attachment
#: model:ir.model.fields,help:fs_attachment.field_fs_storage__force_db_for_default_attachment_rules
msgid ""
"When storing attachments in an external storage, storage may be slow.If the storage is used to store odoo attachments by default, this could lead to a bad user experience since small images (128, 256) are used in Odoo in list / kanban views. We want them to be fast to read.This field allows to force the store of some attachments in the odoo database. The value is a dict Where the key is the beginning of the mimetype to configure and the value is the limit in size below which attachments are kept in DB. 0 means no limit.\n"
"Default configuration means:\n"
"* images mimetypes (image/png, image/jpeg, ...) below 50KB are stored in database\n"
"* application/javascript are stored in database whatever their size \n"
"* text/css are stored in database whatever their size"
msgstr ""
"Quando si archiviano allegati in un deposito esterno, il deposito può essere "
"lento. Se il deposito è utilizzato per archiviare allegati Odoo in modo "
"predefinito, questo può portare ad una esperienza utente negativa poichè "
"piccole immagini (128, 256) vengono utilizzate in Odoo nelle viste elenco / "
"kanban. Vogliamo che siano veloci da caricare. Questo campo consente di "
"forzare l'archiviazione degli allegati nel database Odoo. Il valore è un "
"dizionario dove la chiave è l'inizio del tipo MIME da configurare e il "
"valore è il limite in dimesine sotto il quale gl iallegati vengonotenuti nel "
"DB. 0 significa senza limite.\n"
"Configrazione predefinita significa:\n"
"* tipi MIME immagini (image/png, image/jpeg, ...) sotto i 50KB sono salvati "
"nel database\n"
"* application/javascript sono salvati nel database indipendentemente dalla "
"dimensione\n"
"* text/css sono salvati nel database indipendentemente dalla dimensione"
#. module: fs_attachment
#. odoo-python
#: code:addons/fs_attachment/models/ir_attachment.py:0
#, python-format
msgid ""
"You can't write on multiple attachments with different mimetypes at the same"
" time."
msgstr ""
"Non si può scrivere su allegati multipli con tipi MIME differenti "
"contemporaneamente."

View file

@ -0,0 +1,6 @@
from . import fs_file_gc
from . import fs_storage
from . import ir_attachment
from . import ir_binary
from . import ir_model
from . import ir_model_fields

View file

@ -0,0 +1,168 @@
# Copyright 2023 ACSONE SA/NV
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
import logging
import threading
from contextlib import closing, contextmanager
from odoo import api, fields, models
from odoo.sql_db import Cursor
_logger = logging.getLogger(__name__)
class FsFileGC(models.Model):
_name = "fs.file.gc"
_description = "Filesystem storage file garbage collector"
store_fname = fields.Char("Stored Filename")
fs_storage_code = fields.Char("Storage Code")
_sql_constraints = [
(
"store_fname_uniq",
"unique (store_fname)",
"The stored filename must be unique!",
),
]
def _is_test_mode(self) -> bool:
"""Return True if we are running the tests, so we do not mark files for
garbage collection into a separate transaction.
"""
return (
getattr(threading.current_thread(), "testing", False)
or self.env.registry.in_test_mode()
)
@contextmanager
def _in_new_cursor(self) -> Cursor:
"""Context manager to execute code in a new cursor"""
if self._is_test_mode() or not self.env.registry.ready:
yield self.env.cr
return
with closing(self.env.registry.cursor()) as cr:
try:
yield cr
except Exception:
cr.rollback()
raise
else:
# disable pylint error because this is a valid commit,
# we are in a new env
cr.commit() # pylint: disable=invalid-commit
@api.model
def _mark_for_gc(self, store_fname: str) -> None:
"""Mark a file for garbage collection"
This process is done in a separate transaction since the data must be
preserved even if the transaction is rolled back.
"""
with self._in_new_cursor() as cr:
code = store_fname.partition("://")[0]
# use plain SQL to avoid the ORM ignore conflicts errors
cr.execute(
"""
INSERT INTO
fs_file_gc (
store_fname,
fs_storage_code,
create_date,
write_date,
create_uid,
write_uid
)
VALUES (
%s,
%s,
now() at time zone 'UTC',
now() at time zone 'UTC',
%s,
%s
)
ON CONFLICT DO NOTHING
""",
(store_fname, code, self.env.uid, self.env.uid),
)
@api.autovacuum
def _gc_files(self) -> None:
"""Garbage collect files"""
# This method is mainly a copy of the method _gc_file_store_unsafe()
# from the module fs_attachment. The only difference is that the list
# of files to delete is retrieved from the table fs_file_gc instead
# of the odoo filestore.
# Continue in a new transaction. The LOCK statement below must be the
# first one in the current transaction, otherwise the database snapshot
# used by it may not contain the most recent changes made to the table
# ir_attachment! Indeed, if concurrent transactions create attachments,
# the LOCK statement will wait until those concurrent transactions end.
# But this transaction will not see the new attachements if it has done
# other requests before the LOCK (like the method _storage() above).
cr = self._cr
cr.commit() # pylint: disable=invalid-commit
# prevent all concurrent updates on ir_attachment and fs_file_gc
# while collecting, but only attempt to grab the lock for a little bit,
# otherwise it'd start blocking other transactions.
# (will be retried later anyway)
cr.execute("SET LOCAL lock_timeout TO '10s'")
cr.execute("LOCK fs_file_gc IN SHARE MODE")
cr.execute("LOCK ir_attachment IN SHARE MODE")
self._gc_files_unsafe()
# commit to release the lock
cr.commit() # pylint: disable=invalid-commit
def _gc_files_unsafe(self) -> None:
# get the list of fs.storage codes that must be autovacuumed
codes = (
self.env["fs.storage"].search([]).filtered("autovacuum_gc").mapped("code")
)
if not codes:
return
# we process by batch of storage codes.
self._cr.execute(
"""
SELECT
fs_storage_code,
array_agg(store_fname)
FROM
fs_file_gc
WHERE
fs_storage_code IN %s
AND NOT EXISTS (
SELECT 1
FROM ir_attachment
WHERE store_fname = fs_file_gc.store_fname
)
GROUP BY
fs_storage_code
""",
(tuple(codes),),
)
for code, store_fnames in self._cr.fetchall():
self.env["fs.storage"].get_by_code(code)
fs = self.env["fs.storage"].get_fs_by_code(code)
for store_fname in store_fnames:
try:
file_path = store_fname.partition("://")[2]
fs.rm(file_path)
except Exception:
_logger.debug("Failed to remove file %s", store_fname)
# delete the records from the table fs_file_gc
self._cr.execute(
"""
DELETE FROM
fs_file_gc
WHERE
fs_storage_code IN %s
""",
(tuple(codes),),
)

View file

@ -0,0 +1,503 @@
# Copyright 2023 ACSONE SA/NV
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from __future__ import annotations
from odoo import _, api, fields, models, tools
from odoo.exceptions import ValidationError
from odoo.tools.safe_eval import const_eval
from .ir_attachment import IrAttachment
class FsStorage(models.Model):
_inherit = "fs.storage"
optimizes_directory_path = fields.Boolean(
help="If checked, the directory path will be optimized to avoid "
"too much files into the same directory. This options is used when the "
"storage is used to store attachments. Depending on the storage, this "
"option can be ignored. It's useful for storage based on real file. "
"This way, files with similar properties will be stored in the same "
"directory, avoiding overcrowding in the root directory and optimizing "
"access times."
)
autovacuum_gc = fields.Boolean(
string="Autovacuum Garbage Collection",
default=True,
help="If checked, the autovacuum of the garbage collection will be "
"automatically executed when the storage is used to store attachments. "
"Sometime, the autovacuum is to avoid when files in the storage are referenced "
"by other systems (like a website). In such case, records in the fs.file.gc "
"table must be manually processed.",
)
base_url = fields.Char(default="")
is_directory_path_in_url = fields.Boolean(
default=False,
help="Normally the directory_path is for internal usage. "
"If this flag is enabled the path will be used to compute the "
"public URL.",
)
base_url_for_files = fields.Char(compute="_compute_base_url_for_files", store=True)
use_x_sendfile_to_serve_internal_url = fields.Boolean(
string="Use X-Sendfile To Serve Internal Url",
help="If checked and odoo is behind a proxy that supports x-sendfile, "
"the content served by the attachment's internal URL will be served"
"by the proxy using the fs_url if defined. If not, the file will be "
"served by odoo that will stream the content read from the filesystem "
"storage. This option is useful to avoid to serve files from odoo "
"and therefore to avoid to load the odoo process. ",
)
use_as_default_for_attachments = fields.Boolean(
help="If checked, this storage will be used to store all the attachments ",
default=False,
)
force_db_for_default_attachment_rules = fields.Text(
help="When storing attachments in an external storage, storage may be slow."
"If the storage is used to store odoo attachments by default, this could lead "
"to a bad user experience since small images (128, 256) are used in Odoo "
"in list / kanban views. We want them to be fast to read."
"This field allows to force the store of some attachments in the odoo "
"database. The value is a dict Where the key is the beginning of the "
"mimetype to configure and the value is the limit in size below which "
"attachments are kept in DB. 0 means no limit.\n"
"Default configuration means:\n"
"* images mimetypes (image/png, image/jpeg, ...) below 50KB are stored "
"in database\n"
"* application/javascript are stored in database whatever their size \n"
"* text/css are stored in database whatever their size",
default=lambda self: self._default_force_db_for_default_attachment_rules,
)
use_filename_obfuscation = fields.Boolean(
help="If checked, the filename will be obfuscated. This option is "
"useful to avoid to expose sensitive information trough the URL "
"or in the remote storage. The obfuscation is done using a hash "
"of the filename. The original filename is stored in the attachment "
"metadata. The obfusation is to avoid if the storage is used to store "
"files that are referenced by other systems (like a website) where "
"the filename is important for SEO.",
)
model_xmlids = fields.Char(
help="List of models xml ids such as attachments linked to one of "
"these models will be stored in this storage."
)
model_ids = fields.One2many(
"ir.model",
"storage_id",
help="List of models such as attachments linked to one of these "
"models will be stored in this storage.",
compute="_compute_model_ids",
inverse="_inverse_model_ids",
)
field_xmlids = fields.Char(
help="List of fields xml ids such as attachments linked to one of "
"these fields will be stored in this storage. NB: If the attachment "
"is linked to a field that is in one FS storage, and the related "
"model is in another FS storage, we will store it into"
" the storage linked to the resource field."
)
field_ids = fields.One2many(
"ir.model.fields",
"storage_id",
help="List of fields such as attachments linked to one of these "
"fields will be stored in this storage. NB: If the attachment "
"is linked to a field that is in one FS storage, and the related "
"model is in another FS storage, we will store it into"
" the storage linked to the resource field.",
compute="_compute_field_ids",
inverse="_inverse_field_ids",
)
@api.constrains("use_as_default_for_attachments")
def _check_use_as_default_for_attachments(self):
# constrains are checked in python since values can be provided by
# the server environment
defaults = self.search([]).filtered("use_as_default_for_attachments")
if len(defaults) > 1:
raise ValidationError(
_("Only one storage can be used as default for attachments")
)
@api.constrains("model_xmlids")
def _check_model_xmlid_storage_unique(self):
"""
A given model can be stored in only 1 storage.
As model_ids is a non stored field, we must implement a Python
constraint on the XML ids list.
"""
for rec in self.filtered("model_xmlids"):
xmlids = rec.model_xmlids.split(",")
for xmlid in xmlids:
other_storages = (
self.env["fs.storage"]
.search([])
.filtered_domain(
[
("id", "!=", rec.id),
("model_xmlids", "ilike", xmlid),
]
)
)
if other_storages:
raise ValidationError(
_(
"Model %(model)s already stored in another "
"FS storage ('%(other_storage)s')"
)
% {"model": xmlid, "other_storage": other_storages[0].name}
)
@api.constrains("field_xmlids")
def _check_field_xmlid_storage_unique(self):
"""
A given field can be stored in only 1 storage.
As field_ids is a non stored field, we must implement a Python
constraint on the XML ids list.
"""
for rec in self.filtered("field_xmlids"):
xmlids = rec.field_xmlids.split(",")
for xmlid in xmlids:
other_storages = (
self.env["fs.storage"]
.search([])
.filtered_domain(
[
("id", "!=", rec.id),
("field_xmlids", "ilike", xmlid),
]
)
)
if other_storages:
raise ValidationError(
_(
"Field %(field)s already stored in another "
"FS storage ('%(other_storage)s')"
)
% {"field": xmlid, "other_storage": other_storages[0].name}
)
@property
def _server_env_fields(self):
env_fields = super()._server_env_fields
env_fields.update(
{
"optimizes_directory_path": {},
"autovacuum_gc": {},
"base_url": {},
"is_directory_path_in_url": {},
"use_x_sendfile_to_serve_internal_url": {},
"use_as_default_for_attachments": {},
"force_db_for_default_attachment_rules": {},
"use_filename_obfuscation": {},
"model_xmlids": {},
"field_xmlids": {},
}
)
return env_fields
@property
def _default_force_db_for_default_attachment_rules(self) -> str:
return '{"image/": 51200, "application/javascript": 0, "text/css": 0}'
@api.onchange("use_as_default_for_attachments")
def _onchange_use_as_default_for_attachments(self):
if not self.use_as_default_for_attachments:
self.force_db_for_default_attachment_rules = ""
else:
self.force_db_for_default_attachment_rules = (
self._default_force_db_for_default_attachment_rules
)
@api.depends("model_xmlids")
def _compute_model_ids(self):
"""
Use the char field (containing all model xmlids) to fulfill the o2m field.
"""
for rec in self:
xmlids = (
rec.model_xmlids.split(",") if isinstance(rec.model_xmlids, str) else []
)
model_ids = []
for xmlid in xmlids:
# Method returns False if no model is found for this xmlid
model_id = self.env["ir.model.data"]._xmlid_to_res_id(xmlid)
if model_id:
model_ids.append(model_id)
rec.model_ids = [(6, 0, model_ids)]
def _inverse_model_ids(self):
"""
When the model_ids o2m field is updated, re-compute the char list
of model XML ids.
"""
for rec in self:
xmlids = models.Model.get_external_id(rec.model_ids).values()
rec.model_xmlids = ",".join(xmlids)
@api.depends("field_xmlids")
def _compute_field_ids(self):
"""
Use the char field (containing all field xmlids) to fulfill the o2m field.
"""
for rec in self:
xmlids = (
rec.field_xmlids.split(",") if isinstance(rec.field_xmlids, str) else []
)
field_ids = []
for xmlid in xmlids:
# Method returns False if no field is found for this xmlid
field_id = self.env["ir.model.data"]._xmlid_to_res_id(xmlid)
if field_id:
field_ids.append(field_id)
rec.field_ids = [(6, 0, field_ids)]
def _inverse_field_ids(self):
"""
When the field_ids o2m field is updated, re-compute the char list
of field XML ids.
"""
for rec in self:
xmlids = models.Model.get_external_id(rec.field_ids).values()
rec.field_xmlids = ",".join(xmlids)
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if not vals.get("use_as_default_for_attachments"):
vals["force_db_for_default_attachment_rules"] = None
res = super().create(vals_list)
res._create_write_check_constraints(vals)
return res
def write(self, vals):
if "use_as_default_for_attachments" in vals:
if not vals["use_as_default_for_attachments"]:
vals["force_db_for_default_attachment_rules"] = None
res = super().write(vals)
self._create_write_check_constraints(vals)
return res
def _create_write_check_constraints(self, vals):
"""
Container for all checks performed during creation/writing.
Args:
vals (dict): Dictionary of values being written.
This method is meant to contain checks executed during the creation
or writing of records.
"""
if (
"use_as_default_for_attachments" in vals
or "force_db_for_default_attachment_rules" in vals
):
self._check_force_db_for_default_attachment_rules()
def _check_force_db_for_default_attachment_rules(self):
"""
Validate 'force_db_for_default_attachment_rules' field.
This method doesn't work properly with a constraints() decorator because
the field use_as_default_for_attachments is a computed field, not stored
in the database. The presence of computed fields in this method is a
result of inheriting this model from "server.env.mixin".
"""
for rec in self:
if not rec.force_db_for_default_attachment_rules:
continue
if not rec.use_as_default_for_attachments:
raise ValidationError(
_(
"The force_db_for_default_attachment_rules can only be set "
"if the storage is used as default for attachments."
)
)
try:
const_eval(rec.force_db_for_default_attachment_rules)
except (SyntaxError, TypeError, ValueError) as e:
raise ValidationError(
_(
"The force_db_for_default_attachment_rules is not a valid "
"python dict."
)
) from e
@api.model
@tools.ormcache_context(keys=["attachment_res_field", "attachment_res_model"])
def get_default_storage_code_for_attachments(self):
"""Return the code of the storage to use to store the attachments.
If the resource field is linked to a particular storage, return this one.
Otherwise if the resource model is linked to a particular storage,
return it.
Finally return the code of the storage to use by default."""
res_field = self.env.context.get("attachment_res_field")
res_model = self.env.context.get("attachment_res_model")
if res_field and res_model:
field = (
self.env["ir.model.fields"]
.sudo()
.search([("model", "=", res_model), ("name", "=", res_field)], limit=1)
)
if field:
storage = (
self.env["fs.storage"]
.sudo()
.search([])
.filtered_domain([("field_ids", "in", [field.id])])
)
if storage:
return storage.code
if res_model:
model = (
self.env["ir.model"].sudo().search([("model", "=", res_model)], limit=1)
)
if model:
storage = (
self.env["fs.storage"]
.sudo()
.search([])
.filtered_domain([("model_ids", "in", [model.id])])
)
if storage:
return storage.code
storages = (
self.sudo()
.search([])
.filtered_domain([("use_as_default_for_attachments", "=", True)])
)
if storages:
return storages[0].code
return None
@api.model
@tools.ormcache("code")
def get_force_db_for_default_attachment_rules(self, code):
"""Return the rules to force the storage of some attachments in the DB
:param code: the code of the storage
:return: a dict where the key is the beginning of the mimetype to configure
and the value is the limit in size below which attachments are kept in DB.
0 means no limit.
"""
storage = self.sudo().get_by_code(code)
if (
storage
and storage.use_as_default_for_attachments
and storage.force_db_for_default_attachment_rules
):
return const_eval(storage.force_db_for_default_attachment_rules)
return {}
@api.model
@tools.ormcache("code")
def _must_optimize_directory_path(self, code):
return self.sudo().get_by_code(code).optimizes_directory_path
@api.model
@tools.ormcache("code")
def _must_autovacuum_gc(self, code):
return self.sudo().get_by_code(code).autovacuum_gc
@api.model
@tools.ormcache("code")
def _must_use_filename_obfuscation(self, code):
return self.sudo().get_by_code(code).use_filename_obfuscation
@api.depends("base_url", "is_directory_path_in_url")
def _compute_base_url_for_files(self):
for rec in self:
if not rec.base_url:
rec.base_url_for_files = ""
continue
parts = [rec.base_url]
if rec.is_directory_path_in_url and rec.directory_path:
parts.append(rec.directory_path)
rec.base_url_for_files = self._normalize_url("/".join(parts))
@api.model
def _get_url_for_attachment(
self, attachment: IrAttachment, exclude_base_url: bool = False
) -> str | None:
"""Return the URL to access the attachment
:param attachment: an attachment record
:return: the URL to access the attachment
"""
fs_storage = self.sudo().get_by_code(attachment.fs_storage_code)
if not fs_storage:
return None
base_url = fs_storage.base_url_for_files
if not base_url:
return None
if exclude_base_url:
base_url = base_url.replace(fs_storage.base_url.rstrip("/"), "") or "/"
# always remove the directory_path from the fs_filename
# only if it's at the start of the filename
fs_filename = attachment.fs_filename
if fs_storage.directory_path and fs_filename.startswith(
fs_storage.directory_path
):
fs_filename = fs_filename.replace(fs_storage.directory_path, "")
parts = [base_url, fs_filename]
if attachment.fs_storage_id:
if (
fs_storage.optimizes_directory_path
and not fs_storage.use_filename_obfuscation
):
checksum = attachment.checksum
parts = [base_url, checksum[:2], checksum[2:4], fs_filename]
return self._normalize_url("/".join(parts))
@api.model
def _normalize_url(self, url: str) -> str:
"""Normalize the URL
:param url: the URL to normalize
:return: the normalized URL
remove all the double slashes and the trailing slash except if the URL
is only a slash (in this case we return a single slash). Avoid to remove
the double slash in the protocol part of the URL.
"""
if url == "/":
return url
parts = url.split("/")
parts = [x for x in parts if x]
if not parts:
return "/"
if parts[0].endswith(":"):
parts[0] = parts[0] + "/"
else:
# we preserve the trailing slash if the URL is absolute
parts[0] = "/" + parts[0]
return "/".join(parts)
def recompute_urls(self) -> None:
"""Recompute the URL of all attachments since the base_url or the
directory_path has changed. This method must be explicitly called
by the user since we don't want to recompute the URL on each change
of the base_url or directory_path. We could also have cases where such
a recompute is not wanted. For example, when you restore a database
from production to staging, you don't want to recompute the URL of
the attachments created in production (since the directory_path use
in production is readonly for the staging database) but you change the
directory_path of the staging database to ensure that all the moditications
in staging are done in a different directory and will not impact the
production.
"""
# The weird "res_field = False OR res_field != False" domain
# is required! It's because of an override of _search in ir.attachment
# which adds ('res_field', '=', False) when the domain does not
# contain 'res_field'.
# https://github.com/odoo/odoo/blob/9032617120138848c63b3cfa5d1913c5e5ad76db/
# odoo/addons/base/ir/ir_attachment.py#L344-L347
domain = [
("fs_storage_id", "in", self.ids),
"|",
("res_field", "=", False),
("res_field", "!=", False),
]
attachments = self.env["ir.attachment"].search(domain)
attachments._compute_fs_url()
attachments._compute_fs_url_path()

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,148 @@
# Copyright 2023 ACSONE SA/NV
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
import logging
import werkzeug.http
from odoo import models
from odoo.http import request
from odoo.tools.image import image_process
from ..fs_stream import FsStream
_logger = logging.getLogger(__name__)
class IrBinary(models.AbstractModel):
_inherit = "ir.binary"
def _get_fs_attachment_for_field(self, record, field_name):
if record._name == "ir.attachment" and record.fs_filename:
return record
record.check_field_access_rights("read", [field_name])
field_def = record._fields[field_name]
if field_def.attachment and field_def.store:
fs_attachment = (
self.env["ir.attachment"]
.sudo()
.search(
domain=[
("res_model", "=", record._name),
("res_id", "=", record.id),
("res_field", "=", field_name),
],
limit=1,
)
)
if fs_attachment and fs_attachment.fs_filename:
return fs_attachment
return None
def _record_to_stream(self, record, field_name):
# Extend base implementation to support attachment stored into a
# filesystem storage
fs_attachment = self._get_fs_attachment_for_field(record, field_name)
if fs_attachment:
return FsStream.from_fs_attachment(fs_attachment)
return super()._record_to_stream(record, field_name)
def _get_stream_from(
self,
record,
field_name="raw",
filename=None,
filename_field="name",
mimetype=None,
default_mimetype="application/octet-stream",
):
stream = super()._get_stream_from(
record,
field_name=field_name,
filename=filename,
filename_field=filename_field,
mimetype=mimetype,
default_mimetype=default_mimetype,
)
if stream.type == "fs":
if mimetype:
stream.mimetype = mimetype
if filename:
stream.download_name = filename
elif record and filename_field in record:
stream.download_name = record[filename_field] or stream.download_name
return stream
def _get_image_stream_from(
self,
record,
field_name="raw",
filename=None,
filename_field="name",
mimetype=None,
default_mimetype="image/png",
placeholder=None,
width=0,
height=0,
crop=False,
quality=0,
):
# we need to override this method since if you pass a width or height or
# set crop=True, the stream data must be a bytes object, not a
# file-like object. In the base implementation, the stream data is
# passed to `image_process` method to transform it and this method
# expects a bytes object.
initial_width = width
initial_height = height
initial_crop = crop
if record._name != "ir.attachment" and field_name:
field_def = record._fields[field_name]
if field_def.type in ("fs_image", "fs_file"):
value = record[field_name]
if value:
record = value.attachment
field_name = "raw"
elif field_def.type in ("binary"):
fs_attachment = self._get_fs_attachment_for_field(record, field_name)
if fs_attachment:
record = fs_attachment
field_name = "raw"
stream = super()._get_image_stream_from(
record,
field_name=field_name,
filename=filename,
filename_field=filename_field,
mimetype=mimetype,
default_mimetype=default_mimetype,
placeholder=placeholder,
width=0,
height=0,
crop=False,
quality=quality,
)
modified = werkzeug.http.is_resource_modified(
request.httprequest.environ,
etag=stream.etag,
last_modified=stream.last_modified,
)
if modified and (initial_width or initial_height or initial_crop):
if stream.type == "path":
with open(stream.path, "rb") as file:
stream.type = "data"
stream.path = None
stream.data = file.read()
elif stream.type == "fs":
stream.data = stream.read()
stream.type = "data"
stream.data = image_process(
stream.data,
size=(initial_width, initial_height),
crop=initial_crop,
quality=quality,
)
stream.size = len(stream.data)
return stream

View file

@ -0,0 +1,15 @@
# Copyright 2023 ACSONE SA/NV
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models
class IrModel(models.Model):
_inherit = "ir.model"
storage_id = fields.Many2one(
"fs.storage",
help="If specified, all attachments linked to this model will be "
"stored in the provided storage.",
)

View file

@ -0,0 +1,15 @@
# Copyright 2023 ACSONE SA/NV
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models
class IrModelFields(models.Model):
_inherit = "ir.model.fields"
storage_id = fields.Many2one(
"fs.storage",
help="If specified, all attachments linked to this field will be "
"stored in the provided storage.",
)

View file

@ -0,0 +1,21 @@
_MAP = {
"y": True,
"yes": True,
"t": True,
"true": True,
"on": True,
"1": True,
"n": False,
"no": False,
"f": False,
"false": False,
"off": False,
"0": False,
}
def strtobool(value):
try:
return _MAP[str(value).lower()]
except KeyError as e:
raise ValueError('"{}" is not a valid bool value'.format(value)) from e

View file

@ -0,0 +1,15 @@
Thierry Ducrest <thierry.ducrest@camptocamp.com>
Guewen Baconnier <guewen.baconnier@camptocamp.com>
Julien Coux <julien.coux@camptocamp.com>
Akim Juillerat <akim.juillerat@camptocamp.com>
Thomas Nowicki <thomas.nowicki@camptocamp.com>
Vincent Renaville <vincent.renaville@camptocamp.com>
Denis Leemann <denis.leemann@camptocamp.com>
Patrick Tombez <patrick.tombez@camptocamp.com>
Don Kendall <kendall@donkendall.com>
Stephane Mangin <stephane.mangin@camptocamp.com>
Laurent Mignon <laurent.mignon@acsone.eu>
Marie Lejeune <marie.lejeune@acsone.eu>
Wolfgang Pichler <wpichler@callino.at>
Nans Lefebvre <len@lambdao.dev>
Mohamed Alkobrosli <alkobroslymohamed@gmail.com>

View file

@ -0,0 +1,45 @@
In some cases, you need to store attachment in another system that the Odoo's
filestore. For example, when your deployment is based on a multi-server
architecture to ensure redundancy and scalability, your attachments must
be stored in a way that they are accessible from all the servers. In this
way, you can use a shared storage system like NFS or a cloud storage like
S3 compliant storage, or....
This addon extend the storage mechanism of Odoo's attachments to allow
you to store them in any storage filesystem supported by the Python
library `fsspec <https://filesystem-spec.readthedocs.io/en/latest/>`_ and made
available via the `fs_storage` addon.
In contrast to Odoo, when a file is stored into an external storage, this
addon ensures that the filename keeps its meaning (In odoo the filename
into the filestore is the file content checksum). Concretely the filename
is based on the pattern:
'<name-without-extension>-<attachment-id>-<version>.<extension>'
This addon also adds on the attachments 2 new fields to use
to retrieve the file content from a URL:
* ``Internal URL``: URL to retrieve the file content from the Odoo's
filestore.
* ``Filesystem URL``: URL to retrieve the file content from the external
storage.
.. note::
The internal URL is always available, but the filesystem URL is only
available when the attachment is stored in an external storage.
Particular attention has been paid to limit as much as possible the consumption
of resources necessary to serve via Odoo the content stored in an external
filesystem. The implementation is based on an end-to-end streaming of content
between the external filesystem and the Odoo client application by default.
Nevertheless, if your content is available via a URL on the external filesystem,
you can configure the storage to use the x-sendfile mechanism to serve the
content if it's activated on your Odoo instance. In this case, the content
served by Odoo at the internal URL will be proxied to the filesystem URL
by nginx.
Last but not least, the addon adds a new method `open` on the attachment. This
method allows you to open the attachment as a file. For attachments stored into
the filestore or in an external filesystem, it allows you to directly read from
and write to the file and therefore minimize the memory consumption since data
are not kept into memory before being written into the database.

View file

@ -0,0 +1,72 @@
16.0.1.0.13 (2024-05-10)
~~~~~~~~~~~~~~~~~~~~~~~~
**Bugfixes**
- No crash o missign file.
Prior to this change, Odoo was crashing as soon as access to a file stored into
an external filesytem was not possible. This can lead to a complete system block.
This change prevents this kind of blockage by ignoring access error to files
stored into external system on read operations. These kind of errors are logged
into the log files for traceability. (`#361 <https://github.com/OCA/storage/issues/361>`_)
16.0.1.0.8 (2023-12-20)
~~~~~~~~~~~~~~~~~~~~~~~
**Bugfixes**
- Fix the error retrieving attachment files when the storage is set to optimize directory paths. (`#312 <https://github.com/OCA/storage/issues/312>`_)
16.0.1.0.6 (2023-12-02)
~~~~~~~~~~~~~~~~~~~~~~~
**Bugfixes**
- Improve performance at creation of an attachment or when the attachment is updated.
Before this change, when the fs_url was computed the computed value was always
reassigned to the fs_url attribute even if the value was the same. In a lot of
cases the value was the same and the reassignment was not necessary. Unfortunately
this reassignment has as side effect to mark the record as dirty and generate a
SQL update statement at the end of the transaction. (`#307 <https://github.com/OCA/storage/issues/307>`_)
16.0.1.0.5 (2023-11-29)
~~~~~~~~~~~~~~~~~~~~~~~
**Bugfixes**
- When manipulating the file system api through a local variable named *fs*,
we observed some strange behavior when it was wrongly redefined in an
enclosing scope as in the following example: *with fs.open(...) as fs*.
This commit fixes this issue by renaming the local variable and therefore
avoiding the name clash. (`#306 <https://github.com/OCA/storage/issues/306>`_)
16.0.1.0.4 (2023-11-22)
~~~~~~~~~~~~~~~~~~~~~~~
**Bugfixes**
- Fix error when an url is computed for an attachment in a storage configure wihtout directory path. (`#302 <https://github.com/OCA/storage/issues/302>`_)
16.0.1.0.3 (2023-10-17)
~~~~~~~~~~~~~~~~~~~~~~~
**Bugfixes**
- Fix access to technical models to be able to upload attachments for users with basic access (`#289 <https://github.com/OCA/storage/issues/289>`_)
16.0.1.0.2 (2023-10-09)
~~~~~~~~~~~~~~~~~~~~~~~
**Bugfixes**
- Ensures python 3.9 compatibility. (`#285 <https://github.com/OCA/storage/issues/285>`_)
- If a storage is not used to store all the attachments by default, the call to the
`get_force_db_for_default_attachment_rules` method must return an empty dictionary. (`#286 <https://github.com/OCA/storage/issues/286>`_)

View file

@ -0,0 +1,228 @@
Configuration
~~~~~~~~~~~~~
The configuration is done through the creation of a filesytem storage record
into odoo. To create a new storage, go to the menu
``Settings > Technical > FS Storage`` and click on ``Create``.
In addition to the common fields available to configure a storage, specifics
fields are available under the section 'Attachment' to configure the way
attachments will be stored in the filesystem.
* ``Optimizes Directory Path``: This option is useful if you need to prevent
having too many files in a single directory. It will create a directory
structure based on the attachment's checksum (with 2 levels of depth)
For example, if the checksum is ``123456789``, the file will be stored in the
directory ``/path/to/storage/12/34/my_file-1-0.txt``.
* ``Autovacuum GC``: This is used to automatically remove files from the filesystem
when it's no longer referenced in Odoo. Some storage backends (like S3) may
charge you for the storage of files, so it's important to remove them when
they're no longer needed. In some cases, this option is not desirable, for
example if you're using a storage backend to store images shared with others
systems (like your website) and you don't want to remove the files from the
storage while they're still referenced into the others systems.
This mechanism is based on a ``fs.file.gc`` model used to collect the files
to remove. This model is automatically populated by the ``ir.attachment``
model when a file is removed from the database. If you disable this option,
you'll have to manually take care of the records in the ``fs.file.gc`` for
your filesystem storage.
* ``Use As Default For Attachment``: This options allows you to declare the storage
as the default one for attachments. If you have multiple filesystem storage
configured, you can choose which one will be used by default for attachments.
Once activated, attachments created without specifying a storage will be
stored in this default storage.
* ``Force DB For Default Attachment Rules``: This option is useful if you want to
force the storage of some attachments in the database, even if you have a
default filesystem storage configured. This is specially useful when you're
using a storage backend like S3, where the latency of the network can be
high. This option is a JSON field that allows you to define the mimetypes and
the size limit below which the attachments will be stored in the database.
Small images (128, 256) are used in Odoo in list / kanban views. We
want them to be fast to read.
They are generally < 50KB (default configuration) so they don't take
that much space in database, but they'll be read much faster than from
the object storage.
The assets (application/javascript, text/css) are stored in database
as well whatever their size is:
* a database doesn't have thousands of them
* of course better for performance
* better portability of a database: when replicating a production
instance for dev, the assets are included
The default configuration is:
{"image/": 51200, "application/javascript": 0, "text/css": 0}
Where the key is the beginning of the mimetype to configure and the
value is the limit in size below which attachments are kept in DB.
0 means no limit.
Default configuration means:
* images mimetypes (image/png, image/jpeg, ...) below 50KB are
stored in database
* application/javascript are stored in database whatever their size
* text/css are stored in database whatever their size
This option is only available on the filesystem storage that is used
as default for attachments.
It is also possible to use different FS storages for attachments linked to
different resource fields/models. You can configure it either on the ``fs.storage``
directly, or in a server environment file:
* From the ``fs.storage``: Fields `model_ids` and `field_ids` will encode for which
models/fields use this storage as default storage for attachments having these resource
model/field. Note that if an attachment has both resource model and field, it will
first take the FS storage where the field is explicitely linked, then is not found,
the one where the model is explicitely linked.
* From a server environment file: In this case you just have to provide a comma-
separated list of models (under the `model_xmlids` key) or fields (under the
`field_xmlids` key). To do so, use the model/field XML ids provided by Odoo.
See the Server Environment section for a concrete example.
Another key feature of this module is the ability to get access to the attachments
from URLs.
* ``Base URL``: This is the base URL used to access the attachments from the
filesystem storage itself. If your storage doesn't provide a way to access
the files from a URL, you can leave this field empty.
* ``Is Directory Path In URL``: Normally the directory patch configured on the storage
is not included in the URL. If you want to include it, you can activate this option.
* ``Use X-Sendfile To Serve Internal Url``: If checked and odoo is behind a proxy
that supports x-sendfile, the content served by the attachment's internal URL
will be served by the proxy using the filesystem url path if defined (This field
is available on the attachment if the storage is configured with a base URL)
If not, the file will be served by odoo that will stream the content read from
the filesystem storage. This option is useful to avoid to serve files from odoo
and therefore to avoid to load the odoo process.
To be fully functional, this option requires the proxy to support x-sendfile
(apache) or x-accel-redirect (nginx). You must also configure your proxy by
adding for each storage a rule to redirect the url rooted at the 'storagge code'
to the server serving the files. For example, if you have a storage with the
code 'my_storage' and a server serving the files at the url 'http://myserver.com',
you must add the following rule in your proxy configuration:
.. code-block:: nginx
location /my_storage/ {
internal;
proxy_pass http://myserver.com;
}
With this configuration a call to '/web/content/<att.id>/<att.name><att.extension>"
for a file stored in the 'my_storage' storage will generate a response by odoo
with the URI
``/my_storage/<paht_in_storage>/<att.name>-<att.id>-<version><att.extension>``
in the headers ``X-Accel-Redirect`` and ``X-Sendfile`` and the proxy will redirect to
``http://myserver.com/<paht_in_storage>/<att.name>-<att.id>-<version><att.extension>``.
see https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/ for more
information.
* ``Use Filename Obfuscation``: If checked, the filename used to store the content
into the filesystem storage will be obfuscated. This is useful to avoid to
expose the real filename of the attachments outside of the Odoo database.
The filename will be obfuscated by using the checksum of the content. This option
is to avoid when the content of your filestore is shared with other systems
(like your website) and you want to keep a meaningful filename to ensure
SEO. This option is disabled by default.
Server Environment
~~~~~~~~~~~~~~~~~~
When you configure a storage through the use of server environment file, you can
provide values for the following keys:
* ``optimizes_directory_path``
* ``autovacuum_gc``
* ``base_url``
* ``is_directory_path_in_url``
* ``use_x_sendfile_to_serve_internal_url``
* ``use_as_default_for_attachments``
* ``force_db_for_default_attachment_rules``
* ``use_filename_obfuscation``
* ``model_xmlids``
* ``field_xmlids``
For example, the configuration of my storage with code `fsprod` used to store
the attachments by default could be:
.. code-block:: ini
[fs_storage.fsprod]
protocol=s3
options={"endpoint_url": "https://my_s3_server/", "key": "KEY", "secret": "SECRET"}
directory_path=my_bucket
use_as_default_for_attachments=True
use_filename_obfuscation=True
model_xmlids=base.model_res_lang,base.model_res_country
field_xmlids=base.field_res_partner__image_128
Advanced usage: Using attachment as a file
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The `open` method on the attachment can be used to open manipulate the attachment
as a file object. The object returned by the call to the method implements
methods from ``io.IOBase``. The method can ba called as any other python method.
In such a case, it's your responsibility to close the file at the end of your
process.
.. code-block:: python
attachment = self.env.create({"name": "test.txt"})
the_file = attachment.open("wb")
try:
the_file.write(b"content")
finally:
the_file.close()
The result of the call to `open` also works in a context ``with`` block. In such
a case, when the code exit the block, the file is automatically closed.
.. code-block:: python
attachment = self.env.create({"name": "test.txt"})
with attachment.open("wb") as the_file:
the_file.write(b"content")
It's always safer to prefer the second approach.
When your attachment is stored into the odoo filestore or into an external
filesystem storage, each time you call the open method, a new file is created.
This way of doing ensures that if the transaction is rolled back the original content
is preserved. Nevertheless you could have use cases where you would like to write
to the existing file directly. For example you could create an empty attachment
to store a csv report and then use the `open` method to write your content directly
into the new file. To support this kind a use cases, the parameter `new_version`
can be passed as `False` to avoid the creation of a new file.
.. code-block:: python
attachment = self.env.create({"name": "test.txt"})
with attachment.open("w", new_version=False) as f:
writer = csv.writer(f, delimiter=";")
....
Tips & Tricks
~~~~~~~~~~~~~
* When working in multi staging environments, the management of the attachments
can be tricky. For example, if you have a production instance and a staging
instance based on a backup of the production environment, you may want to have
the attachments shared between the two instances BUT you don't want to have
one instance removing or modifying the attachments of the other instance.
To do so, you can add on your staging instances a new storage and declare it
as the default storage to use for attachments. This way, all the new attachments
will be stored in this new storage but the attachments created on the production
instance will still be read from the production storage. Be careful to adapt the
configuration of your storage to the production environment to make it read only.
(The use of server environment files is a good way to do so).

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2023 ACSONE SA/NV
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo>
<record model="ir.model.access" id="fs_file_gc_access_name">
<field name="name">fs.file.gc access name</field>
<field name="model_id" ref="model_fs_file_gc" />
<field name="group_id" ref="base.group_system" />
<field name="perm_read" eval="1" />
<field name="perm_create" eval="1" />
<field name="perm_write" eval="1" />
<field name="perm_unlink" eval="1" />
</record>
</odoo>

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View file

@ -0,0 +1,792 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>README.rst</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: gray; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic, pre.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document">
<a class="reference external image-reference" href="https://odoo-community.org/get-involved?utm_source=readme">
<img alt="Odoo Community Association" src="https://odoo-community.org/readme-banner-image" />
</a>
<div class="section" id="base-attachment-object-store">
<h1>Base Attachment Object Store</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:03d52a1eb8acbea54afd494673cc996016973fa06cf64ae65384a78e13b6e5ac
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/license-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/storage/tree/16.0/fs_attachment"><img alt="OCA/storage" src="https://img.shields.io/badge/github-OCA%2Fstorage-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/storage-16-0/storage-16-0-fs_attachment"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/storage&amp;target_branch=16.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>In some cases, you need to store attachment in another system that the Odoos
filestore. For example, when your deployment is based on a multi-server
architecture to ensure redundancy and scalability, your attachments must
be stored in a way that they are accessible from all the servers. In this
way, you can use a shared storage system like NFS or a cloud storage like
S3 compliant storage, or….</p>
<p>This addon extend the storage mechanism of Odoos attachments to allow
you to store them in any storage filesystem supported by the Python
library <a class="reference external" href="https://filesystem-spec.readthedocs.io/en/latest/">fsspec</a> and made
available via the <cite>fs_storage</cite> addon.</p>
<p>In contrast to Odoo, when a file is stored into an external storage, this
addon ensures that the filename keeps its meaning (In odoo the filename
into the filestore is the file content checksum). Concretely the filename
is based on the pattern:
&lt;name-without-extension&gt;-&lt;attachment-id&gt;-&lt;version&gt;.&lt;extension&gt;</p>
<p>This addon also adds on the attachments 2 new fields to use
to retrieve the file content from a URL:</p>
<ul class="simple">
<li><tt class="docutils literal">Internal URL</tt>: URL to retrieve the file content from the Odoos
filestore.</li>
<li><tt class="docutils literal">Filesystem URL</tt>: URL to retrieve the file content from the external
storage.</li>
</ul>
<div class="admonition note">
<p class="first admonition-title">Note</p>
<p class="last">The internal URL is always available, but the filesystem URL is only
available when the attachment is stored in an external storage.
Particular attention has been paid to limit as much as possible the consumption
of resources necessary to serve via Odoo the content stored in an external
filesystem. The implementation is based on an end-to-end streaming of content
between the external filesystem and the Odoo client application by default.
Nevertheless, if your content is available via a URL on the external filesystem,
you can configure the storage to use the x-sendfile mechanism to serve the
content if its activated on your Odoo instance. In this case, the content
served by Odoo at the internal URL will be proxied to the filesystem URL
by nginx.</p>
</div>
<p>Last but not least, the addon adds a new method <cite>open</cite> on the attachment. This
method allows you to open the attachment as a file. For attachments stored into
the filestore or in an external filesystem, it allows you to directly read from
and write to the file and therefore minimize the memory consumption since data
are not kept into memory before being written into the database.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#usage" id="toc-entry-1">Usage</a><ul>
<li><a class="reference internal" href="#configuration" id="toc-entry-2">Configuration</a></li>
<li><a class="reference internal" href="#server-environment" id="toc-entry-3">Server Environment</a></li>
<li><a class="reference internal" href="#advanced-usage-using-attachment-as-a-file" id="toc-entry-4">Advanced usage: Using attachment as a file</a></li>
<li><a class="reference internal" href="#tips-tricks" id="toc-entry-5">Tips &amp; Tricks</a></li>
</ul>
</li>
<li><a class="reference internal" href="#changelog" id="toc-entry-6">Changelog</a><ul>
<li><a class="reference internal" href="#section-1" id="toc-entry-7">16.0.1.0.13 (2024-05-10)</a></li>
<li><a class="reference internal" href="#section-2" id="toc-entry-8">16.0.1.0.8 (2023-12-20)</a></li>
<li><a class="reference internal" href="#section-3" id="toc-entry-9">16.0.1.0.6 (2023-12-02)</a></li>
<li><a class="reference internal" href="#section-4" id="toc-entry-10">16.0.1.0.5 (2023-11-29)</a></li>
<li><a class="reference internal" href="#section-5" id="toc-entry-11">16.0.1.0.4 (2023-11-22)</a></li>
<li><a class="reference internal" href="#section-6" id="toc-entry-12">16.0.1.0.3 (2023-10-17)</a></li>
<li><a class="reference internal" href="#section-7" id="toc-entry-13">16.0.1.0.2 (2023-10-09)</a></li>
</ul>
</li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-14">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-15">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-16">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-17">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-18">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="usage">
<h2><a class="toc-backref" href="#toc-entry-1">Usage</a></h2>
<div class="section" id="configuration">
<h3><a class="toc-backref" href="#toc-entry-2">Configuration</a></h3>
<p>The configuration is done through the creation of a filesytem storage record
into odoo. To create a new storage, go to the menu
<tt class="docutils literal">Settings &gt; Technical &gt; FS Storage</tt> and click on <tt class="docutils literal">Create</tt>.</p>
<p>In addition to the common fields available to configure a storage, specifics
fields are available under the section Attachment to configure the way
attachments will be stored in the filesystem.</p>
<ul>
<li><p class="first"><tt class="docutils literal">Optimizes Directory Path</tt>: This option is useful if you need to prevent
having too many files in a single directory. It will create a directory
structure based on the attachments checksum (with 2 levels of depth)
For example, if the checksum is <tt class="docutils literal">123456789</tt>, the file will be stored in the
directory <tt class="docutils literal"><span class="pre">/path/to/storage/12/34/my_file-1-0.txt</span></tt>.</p>
</li>
<li><p class="first"><tt class="docutils literal">Autovacuum GC</tt>: This is used to automatically remove files from the filesystem
when its no longer referenced in Odoo. Some storage backends (like S3) may
charge you for the storage of files, so its important to remove them when
theyre no longer needed. In some cases, this option is not desirable, for
example if youre using a storage backend to store images shared with others
systems (like your website) and you dont want to remove the files from the
storage while theyre still referenced into the others systems.
This mechanism is based on a <tt class="docutils literal">fs.file.gc</tt> model used to collect the files
to remove. This model is automatically populated by the <tt class="docutils literal">ir.attachment</tt>
model when a file is removed from the database. If you disable this option,
youll have to manually take care of the records in the <tt class="docutils literal">fs.file.gc</tt> for
your filesystem storage.</p>
</li>
<li><p class="first"><tt class="docutils literal">Use As Default For Attachment</tt>: This options allows you to declare the storage
as the default one for attachments. If you have multiple filesystem storage
configured, you can choose which one will be used by default for attachments.
Once activated, attachments created without specifying a storage will be
stored in this default storage.</p>
</li>
<li><p class="first"><tt class="docutils literal">Force DB For Default Attachment Rules</tt>: This option is useful if you want to
force the storage of some attachments in the database, even if you have a
default filesystem storage configured. This is specially useful when youre
using a storage backend like S3, where the latency of the network can be
high. This option is a JSON field that allows you to define the mimetypes and
the size limit below which the attachments will be stored in the database.</p>
<p>Small images (128, 256) are used in Odoo in list / kanban views. We
want them to be fast to read.
They are generally &lt; 50KB (default configuration) so they dont take
that much space in database, but theyll be read much faster than from
the object storage.</p>
<p>The assets (application/javascript, text/css) are stored in database
as well whatever their size is:</p>
<ul class="simple">
<li>a database doesnt have thousands of them</li>
<li>of course better for performance</li>
<li>better portability of a database: when replicating a production
instance for dev, the assets are included</li>
</ul>
<p>The default configuration is:</p>
<blockquote>
<p>{“image/”: 51200, “application/javascript”: 0, “text/css”: 0}</p>
<p>Where the key is the beginning of the mimetype to configure and the
value is the limit in size below which attachments are kept in DB.
0 means no limit.</p>
</blockquote>
<p>Default configuration means:</p>
<ul class="simple">
<li>images mimetypes (image/png, image/jpeg, …) below 50KB are
stored in database</li>
<li>application/javascript are stored in database whatever their size</li>
<li>text/css are stored in database whatever their size</li>
</ul>
<p>This option is only available on the filesystem storage that is used
as default for attachments.</p>
</li>
</ul>
<p>It is also possible to use different FS storages for attachments linked to
different resource fields/models. You can configure it either on the <tt class="docutils literal">fs.storage</tt>
directly, or in a server environment file:</p>
<ul class="simple">
<li>From the <tt class="docutils literal">fs.storage</tt>: Fields <cite>model_ids</cite> and <cite>field_ids</cite> will encode for which
models/fields use this storage as default storage for attachments having these resource
model/field. Note that if an attachment has both resource model and field, it will
first take the FS storage where the field is explicitely linked, then is not found,
the one where the model is explicitely linked.</li>
<li>From a server environment file: In this case you just have to provide a comma-
separated list of models (under the <cite>model_xmlids</cite> key) or fields (under the
<cite>field_xmlids</cite> key). To do so, use the model/field XML ids provided by Odoo.
See the Server Environment section for a concrete example.</li>
</ul>
<p>Another key feature of this module is the ability to get access to the attachments
from URLs.</p>
<ul>
<li><p class="first"><tt class="docutils literal">Base URL</tt>: This is the base URL used to access the attachments from the
filesystem storage itself. If your storage doesnt provide a way to access
the files from a URL, you can leave this field empty.</p>
</li>
<li><p class="first"><tt class="docutils literal">Is Directory Path In URL</tt>: Normally the directory patch configured on the storage
is not included in the URL. If you want to include it, you can activate this option.</p>
</li>
<li><p class="first"><tt class="docutils literal">Use <span class="pre">X-Sendfile</span> To Serve Internal Url</tt>: If checked and odoo is behind a proxy
that supports x-sendfile, the content served by the attachments internal URL
will be served by the proxy using the filesystem url path if defined (This field
is available on the attachment if the storage is configured with a base URL)
If not, the file will be served by odoo that will stream the content read from
the filesystem storage. This option is useful to avoid to serve files from odoo
and therefore to avoid to load the odoo process.</p>
<p>To be fully functional, this option requires the proxy to support x-sendfile
(apache) or x-accel-redirect (nginx). You must also configure your proxy by
adding for each storage a rule to redirect the url rooted at the storagge code
to the server serving the files. For example, if you have a storage with the
code my_storage and a server serving the files at the url <a class="reference external" href="http://myserver.com">http://myserver.com</a>,
you must add the following rule in your proxy configuration:</p>
<pre class="code nginx literal-block">
<span class="k">location</span><span class="w"> </span><span class="s">/my_storage/</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kn">internal</span><span class="p">;</span><span class="w">
</span><span class="kn">proxy_pass</span><span class="w"> </span><span class="s">http://myserver.com</span><span class="p">;</span><span class="w">
</span><span class="p">}</span>
</pre>
<p>With this configuration a call to /web/content/&lt;att.id&gt;/&lt;att.name&gt;&lt;att.extension&gt;
for a file stored in the my_storage storage will generate a response by odoo
with the URI
<tt class="docutils literal"><span class="pre">/my_storage/&lt;paht_in_storage&gt;/&lt;att.name&gt;-&lt;att.id&gt;-&lt;version&gt;&lt;att.extension&gt;</span></tt>
in the headers <tt class="docutils literal"><span class="pre">X-Accel-Redirect</span></tt> and <tt class="docutils literal"><span class="pre">X-Sendfile</span></tt> and the proxy will redirect to
<tt class="docutils literal"><span class="pre">http://myserver.com/&lt;paht_in_storage&gt;/&lt;att.name&gt;-&lt;att.id&gt;-&lt;version&gt;&lt;att.extension&gt;</span></tt>.</p>
<p>see <a class="reference external" href="https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/">https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/</a> for more
information.</p>
</li>
<li><p class="first"><tt class="docutils literal">Use Filename Obfuscation</tt>: If checked, the filename used to store the content
into the filesystem storage will be obfuscated. This is useful to avoid to
expose the real filename of the attachments outside of the Odoo database.
The filename will be obfuscated by using the checksum of the content. This option
is to avoid when the content of your filestore is shared with other systems
(like your website) and you want to keep a meaningful filename to ensure
SEO. This option is disabled by default.</p>
</li>
</ul>
</div>
<div class="section" id="server-environment">
<h3><a class="toc-backref" href="#toc-entry-3">Server Environment</a></h3>
<p>When you configure a storage through the use of server environment file, you can
provide values for the following keys:</p>
<ul class="simple">
<li><tt class="docutils literal">optimizes_directory_path</tt></li>
<li><tt class="docutils literal">autovacuum_gc</tt></li>
<li><tt class="docutils literal">base_url</tt></li>
<li><tt class="docutils literal">is_directory_path_in_url</tt></li>
<li><tt class="docutils literal">use_x_sendfile_to_serve_internal_url</tt></li>
<li><tt class="docutils literal">use_as_default_for_attachments</tt></li>
<li><tt class="docutils literal">force_db_for_default_attachment_rules</tt></li>
<li><tt class="docutils literal">use_filename_obfuscation</tt></li>
<li><tt class="docutils literal">model_xmlids</tt></li>
<li><tt class="docutils literal">field_xmlids</tt></li>
</ul>
<p>For example, the configuration of my storage with code <cite>fsprod</cite> used to store
the attachments by default could be:</p>
<pre class="code ini literal-block">
<span class="k">[fs_storage.fsprod]</span><span class="w">
</span><span class="na">protocol</span><span class="o">=</span><span class="s">s3</span><span class="w">
</span><span class="na">options={&quot;endpoint_url&quot;</span><span class="o">:</span><span class="w"> </span><span class="s">&quot;https://my_s3_server/&quot;</span><span class="na">, &quot;key&quot;</span><span class="o">:</span><span class="w"> </span><span class="s">&quot;KEY&quot;</span><span class="na">, &quot;secret&quot;</span><span class="o">:</span><span class="w"> </span><span class="s">&quot;SECRET&quot;</span><span class="na">}</span><span class="w">
</span><span class="na">directory_path</span><span class="o">=</span><span class="s">my_bucket</span><span class="w">
</span><span class="na">use_as_default_for_attachments</span><span class="o">=</span><span class="s">True</span><span class="w">
</span><span class="na">use_filename_obfuscation</span><span class="o">=</span><span class="s">True</span><span class="w">
</span><span class="na">model_xmlids</span><span class="o">=</span><span class="s">base.model_res_lang,base.model_res_country</span><span class="w">
</span><span class="na">field_xmlids</span><span class="o">=</span><span class="s">base.field_res_partner__image_128</span>
</pre>
</div>
<div class="section" id="advanced-usage-using-attachment-as-a-file">
<h3><a class="toc-backref" href="#toc-entry-4">Advanced usage: Using attachment as a file</a></h3>
<p>The <cite>open</cite> method on the attachment can be used to open manipulate the attachment
as a file object. The object returned by the call to the method implements
methods from <tt class="docutils literal">io.IOBase</tt>. The method can ba called as any other python method.
In such a case, its your responsibility to close the file at the end of your
process.</p>
<pre class="code python literal-block">
<span class="n">attachment</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">env</span><span class="o">.</span><span class="n">create</span><span class="p">({</span><span class="s2">&quot;name&quot;</span><span class="p">:</span> <span class="s2">&quot;test.txt&quot;</span><span class="p">})</span><span class="w">
</span><span class="n">the_file</span> <span class="o">=</span> <span class="n">attachment</span><span class="o">.</span><span class="n">open</span><span class="p">(</span><span class="s2">&quot;wb&quot;</span><span class="p">)</span><span class="w">
</span><span class="k">try</span><span class="p">:</span><span class="w">
</span> <span class="n">the_file</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="sa">b</span><span class="s2">&quot;content&quot;</span><span class="p">)</span><span class="w">
</span><span class="k">finally</span><span class="p">:</span><span class="w">
</span> <span class="n">the_file</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>
</pre>
<p>The result of the call to <cite>open</cite> also works in a context <tt class="docutils literal">with</tt> block. In such
a case, when the code exit the block, the file is automatically closed.</p>
<pre class="code python literal-block">
<span class="n">attachment</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">env</span><span class="o">.</span><span class="n">create</span><span class="p">({</span><span class="s2">&quot;name&quot;</span><span class="p">:</span> <span class="s2">&quot;test.txt&quot;</span><span class="p">})</span><span class="w">
</span><span class="k">with</span> <span class="n">attachment</span><span class="o">.</span><span class="n">open</span><span class="p">(</span><span class="s2">&quot;wb&quot;</span><span class="p">)</span> <span class="k">as</span> <span class="n">the_file</span><span class="p">:</span><span class="w">
</span> <span class="n">the_file</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="sa">b</span><span class="s2">&quot;content&quot;</span><span class="p">)</span>
</pre>
<p>Its always safer to prefer the second approach.</p>
<p>When your attachment is stored into the odoo filestore or into an external
filesystem storage, each time you call the open method, a new file is created.
This way of doing ensures that if the transaction is rolled back the original content
is preserved. Nevertheless you could have use cases where you would like to write
to the existing file directly. For example you could create an empty attachment
to store a csv report and then use the <cite>open</cite> method to write your content directly
into the new file. To support this kind a use cases, the parameter <cite>new_version</cite>
can be passed as <cite>False</cite> to avoid the creation of a new file.</p>
<pre class="code python literal-block">
<span class="n">attachment</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">env</span><span class="o">.</span><span class="n">create</span><span class="p">({</span><span class="s2">&quot;name&quot;</span><span class="p">:</span> <span class="s2">&quot;test.txt&quot;</span><span class="p">})</span><span class="w">
</span><span class="k">with</span> <span class="n">attachment</span><span class="o">.</span><span class="n">open</span><span class="p">(</span><span class="s2">&quot;w&quot;</span><span class="p">,</span> <span class="n">new_version</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span><span class="w">
</span> <span class="n">writer</span> <span class="o">=</span> <span class="n">csv</span><span class="o">.</span><span class="n">writer</span><span class="p">(</span><span class="n">f</span><span class="p">,</span> <span class="n">delimiter</span><span class="o">=</span><span class="s2">&quot;;&quot;</span><span class="p">)</span><span class="w">
</span> <span class="o">....</span>
</pre>
</div>
<div class="section" id="tips-tricks">
<h3><a class="toc-backref" href="#toc-entry-5">Tips &amp; Tricks</a></h3>
<ul>
<li><p class="first">When working in multi staging environments, the management of the attachments
can be tricky. For example, if you have a production instance and a staging
instance based on a backup of the production environment, you may want to have
the attachments shared between the two instances BUT you dont want to have
one instance removing or modifying the attachments of the other instance.</p>
<p>To do so, you can add on your staging instances a new storage and declare it
as the default storage to use for attachments. This way, all the new attachments
will be stored in this new storage but the attachments created on the production
instance will still be read from the production storage. Be careful to adapt the
configuration of your storage to the production environment to make it read only.
(The use of server environment files is a good way to do so).</p>
</li>
</ul>
</div>
</div>
<div class="section" id="changelog">
<h2><a class="toc-backref" href="#toc-entry-6">Changelog</a></h2>
<div class="section" id="section-1">
<h3><a class="toc-backref" href="#toc-entry-7">16.0.1.0.13 (2024-05-10)</a></h3>
<p><strong>Bugfixes</strong></p>
<ul>
<li><p class="first">No crash o missign file.</p>
<p>Prior to this change, Odoo was crashing as soon as access to a file stored into
an external filesytem was not possible. This can lead to a complete system block.
This change prevents this kind of blockage by ignoring access error to files
stored into external system on read operations. These kind of errors are logged
into the log files for traceability. (<a class="reference external" href="https://github.com/OCA/storage/issues/361">#361</a>)</p>
</li>
</ul>
</div>
<div class="section" id="section-2">
<h3><a class="toc-backref" href="#toc-entry-8">16.0.1.0.8 (2023-12-20)</a></h3>
<p><strong>Bugfixes</strong></p>
<ul class="simple">
<li>Fix the error retrieving attachment files when the storage is set to optimize directory paths. (<a class="reference external" href="https://github.com/OCA/storage/issues/312">#312</a>)</li>
</ul>
</div>
<div class="section" id="section-3">
<h3><a class="toc-backref" href="#toc-entry-9">16.0.1.0.6 (2023-12-02)</a></h3>
<p><strong>Bugfixes</strong></p>
<ul>
<li><p class="first">Improve performance at creation of an attachment or when the attachment is updated.</p>
<p>Before this change, when the fs_url was computed the computed value was always
reassigned to the fs_url attribute even if the value was the same. In a lot of
cases the value was the same and the reassignment was not necessary. Unfortunately
this reassignment has as side effect to mark the record as dirty and generate a
SQL update statement at the end of the transaction. (<a class="reference external" href="https://github.com/OCA/storage/issues/307">#307</a>)</p>
</li>
</ul>
</div>
<div class="section" id="section-4">
<h3><a class="toc-backref" href="#toc-entry-10">16.0.1.0.5 (2023-11-29)</a></h3>
<p><strong>Bugfixes</strong></p>
<ul class="simple">
<li>When manipulating the file system api through a local variable named <em>fs</em>,
we observed some strange behavior when it was wrongly redefined in an
enclosing scope as in the following example: <em>with fs.open(…) as fs</em>.
This commit fixes this issue by renaming the local variable and therefore
avoiding the name clash. (<a class="reference external" href="https://github.com/OCA/storage/issues/306">#306</a>)</li>
</ul>
</div>
<div class="section" id="section-5">
<h3><a class="toc-backref" href="#toc-entry-11">16.0.1.0.4 (2023-11-22)</a></h3>
<p><strong>Bugfixes</strong></p>
<ul class="simple">
<li>Fix error when an url is computed for an attachment in a storage configure wihtout directory path. (<a class="reference external" href="https://github.com/OCA/storage/issues/302">#302</a>)</li>
</ul>
</div>
<div class="section" id="section-6">
<h3><a class="toc-backref" href="#toc-entry-12">16.0.1.0.3 (2023-10-17)</a></h3>
<p><strong>Bugfixes</strong></p>
<ul class="simple">
<li>Fix access to technical models to be able to upload attachments for users with basic access (<a class="reference external" href="https://github.com/OCA/storage/issues/289">#289</a>)</li>
</ul>
</div>
<div class="section" id="section-7">
<h3><a class="toc-backref" href="#toc-entry-13">16.0.1.0.2 (2023-10-09)</a></h3>
<p><strong>Bugfixes</strong></p>
<ul class="simple">
<li>Ensures python 3.9 compatibility. (<a class="reference external" href="https://github.com/OCA/storage/issues/285">#285</a>)</li>
<li>If a storage is not used to store all the attachments by default, the call to the
<cite>get_force_db_for_default_attachment_rules</cite> method must return an empty dictionary. (<a class="reference external" href="https://github.com/OCA/storage/issues/286">#286</a>)</li>
</ul>
</div>
</div>
<div class="section" id="bug-tracker">
<h2><a class="toc-backref" href="#toc-entry-14">Bug Tracker</a></h2>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/storage/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/storage/issues/new?body=module:%20fs_attachment%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h2><a class="toc-backref" href="#toc-entry-15">Credits</a></h2>
<div class="section" id="authors">
<h3><a class="toc-backref" href="#toc-entry-16">Authors</a></h3>
<ul class="simple">
<li>Camptocamp</li>
<li>ACSONE SA/NV</li>
</ul>
</div>
<div class="section" id="contributors">
<h3><a class="toc-backref" href="#toc-entry-17">Contributors</a></h3>
<p>Thierry Ducrest &lt;<a class="reference external" href="mailto:thierry.ducrest&#64;camptocamp.com">thierry.ducrest&#64;camptocamp.com</a>&gt;
Guewen Baconnier &lt;<a class="reference external" href="mailto:guewen.baconnier&#64;camptocamp.com">guewen.baconnier&#64;camptocamp.com</a>&gt;
Julien Coux &lt;<a class="reference external" href="mailto:julien.coux&#64;camptocamp.com">julien.coux&#64;camptocamp.com</a>&gt;
Akim Juillerat &lt;<a class="reference external" href="mailto:akim.juillerat&#64;camptocamp.com">akim.juillerat&#64;camptocamp.com</a>&gt;
Thomas Nowicki &lt;<a class="reference external" href="mailto:thomas.nowicki&#64;camptocamp.com">thomas.nowicki&#64;camptocamp.com</a>&gt;
Vincent Renaville &lt;<a class="reference external" href="mailto:vincent.renaville&#64;camptocamp.com">vincent.renaville&#64;camptocamp.com</a>&gt;
Denis Leemann &lt;<a class="reference external" href="mailto:denis.leemann&#64;camptocamp.com">denis.leemann&#64;camptocamp.com</a>&gt;
Patrick Tombez &lt;<a class="reference external" href="mailto:patrick.tombez&#64;camptocamp.com">patrick.tombez&#64;camptocamp.com</a>&gt;
Don Kendall &lt;<a class="reference external" href="mailto:kendall&#64;donkendall.com">kendall&#64;donkendall.com</a>&gt;
Stephane Mangin &lt;<a class="reference external" href="mailto:stephane.mangin&#64;camptocamp.com">stephane.mangin&#64;camptocamp.com</a>&gt;
Laurent Mignon &lt;<a class="reference external" href="mailto:laurent.mignon&#64;acsone.eu">laurent.mignon&#64;acsone.eu</a>&gt;
Marie Lejeune &lt;<a class="reference external" href="mailto:marie.lejeune&#64;acsone.eu">marie.lejeune&#64;acsone.eu</a>&gt;
Wolfgang Pichler &lt;<a class="reference external" href="mailto:wpichler&#64;callino.at">wpichler&#64;callino.at</a>&gt;
Nans Lefebvre &lt;<a class="reference external" href="mailto:len&#64;lambdao.dev">len&#64;lambdao.dev</a>&gt;
Mohamed Alkobrosli &lt;<a class="reference external" href="mailto:alkobroslymohamed&#64;gmail.com">alkobroslymohamed&#64;gmail.com</a>&gt;</p>
</div>
<div class="section" id="maintainers">
<h3><a class="toc-backref" href="#toc-entry-18">Maintainers</a></h3>
<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>Current <a class="reference external" href="https://odoo-community.org/page/maintainer-role">maintainer</a>:</p>
<p><a class="reference external image-reference" href="https://github.com/lmignon"><img alt="lmignon" src="https://github.com/lmignon.png?size=40px" /></a></p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/storage/tree/16.0/fs_attachment">OCA/storage</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>
</div>
</body>
</html>

View file

@ -0,0 +1,5 @@
from . import test_fs_attachment
from . import test_fs_attachment_file_like_adapter
from . import test_fs_attachment_internal_url
from . import test_fs_storage
from . import test_stream

View file

@ -0,0 +1,74 @@
# Copyright 2023 ACSONE SA/NV (http://acsone.eu).
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import os
import shutil
import tempfile
from odoo.tests.common import TransactionCase
class TestFSAttachmentCommon(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
temp_dir = tempfile.mkdtemp()
cls.temp_backend = cls.env["fs.storage"].create(
{
"name": "Temp FS Storage",
"protocol": "file",
"code": "tmp_dir",
"directory_path": temp_dir,
}
)
cls.backend_optimized = cls.env["fs.storage"].create(
{
"name": "Temp Optimized FS Storage",
"protocol": "file",
"code": "tmp_opt",
"directory_path": temp_dir,
"optimizes_directory_path": True,
}
)
cls.temp_dir = temp_dir
cls.gc_file_model = cls.env["fs.file.gc"]
cls.ir_attachment_model = cls.env["ir.attachment"]
@cls.addClassCleanup
def cleanup_tempdir():
shutil.rmtree(temp_dir)
def setUp(self):
super().setUp()
# enforce temp_backend field since it seems that they are reset on
# savepoint rollback when managed by server_environment -> TO Be investigated
self.temp_backend.write(
{
"protocol": "file",
"code": "tmp_dir",
"directory_path": self.temp_dir,
}
)
self.backend_optimized.write(
{
"protocol": "file",
"code": "tmp_opt",
"directory_path": self.temp_dir,
"optimizes_directory_path": True,
}
)
def tearDown(self) -> None:
super().tearDown()
# empty the temp dir
for f in os.listdir(self.temp_dir):
full_path = os.path.join(self.temp_dir, f)
if os.path.isfile(full_path):
os.remove(full_path)
else: # using optimizes_directory_path, we'll have a directory
shutil.rmtree(full_path)
class MyException(Exception):
"""Exception to be raised into tests ensure that we trap only this
exception and not other exceptions raised by the test"""

View file

@ -0,0 +1,487 @@
# Copyright 2023 ACSONE SA/NV (http://acsone.eu).
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import os
from unittest import mock
from odoo.tools import mute_logger
from .common import MyException, TestFSAttachmentCommon
class TestFSAttachment(TestFSAttachmentCommon):
def test_create_attachment_explicit_location(self):
content = b"This is a test attachment"
attachment = (
self.env["ir.attachment"]
.with_context(
storage_location=self.temp_backend.code,
force_storage_key="test.txt",
)
.create({"name": "test.txt", "raw": content})
)
self.assertEqual(os.listdir(self.temp_dir), [f"test-{attachment.id}-0.txt"])
self.assertEqual(attachment.raw, content)
self.assertFalse(attachment.db_datas)
self.assertEqual(attachment.mimetype, "text/plain")
with attachment.open("rb") as f:
self.assertEqual(f.read(), content)
with attachment.open("wb") as f:
f.write(b"new")
self.assertEqual(attachment.raw, b"new")
def test_create_attachment_with_meaningful_name(self):
"""In this test we use a backend with 'optimizes_directory_path',
which rewrites the filename to be a meaningful name.
We ensure that the rewritten path is consistently used,
meaning we can read the file after.
"""
content = b"This is a test attachment"
attachment = (
self.env["ir.attachment"]
.with_context(
storage_location=self.backend_optimized.code,
force_storage_key="test.txt",
)
.create({"name": "test.txt", "raw": content})
)
# the expected store_fname is made of the storage code,
# a random middle part, and the filename
# example: tmp_opt://te/st/test-198-0.txt
# The storage root is NOT part of the store_fname
self.assertFalse("tmp/" in attachment.store_fname)
# remove protocol and file name to keep the middle part
sub_path = os.path.dirname(attachment.store_fname.split("://")[1])
# the subpath is consistently 'te/st' because the file storage key is forced
# if it's arbitrary we might get a random name (3fbc5er....txt), in which case
# the middle part would also be 'random', in our example 3f/bc
self.assertEqual(sub_path, "te/st")
# we can read the file, so storage finds it correctly
with attachment.open("rb") as f:
self.assertEqual(f.read(), content)
new_content = b"new content"
with attachment.open("wb") as f:
f.write(new_content)
# the store fname should have changed, as its version number has increased
# e.g. tmp_opt://te/st/test-1766-0.txt to tmp_opt://te/st/test-1766-1.txt
# but the protocol and sub path should be the same
new_sub_path = os.path.dirname(attachment.store_fname.split("://")[1])
self.assertEqual(sub_path, new_sub_path)
with attachment.open("rb") as f:
self.assertEqual(f.read(), new_content)
def test_open_attachment_in_db(self):
self.env["ir.config_parameter"].sudo().set_param("ir_attachment.location", "db")
content = b"This is a test attachment in db"
attachment = self.ir_attachment_model.create(
{"name": "test.txt", "raw": content}
)
self.assertFalse(attachment.store_fname)
self.assertTrue(attachment.db_datas)
self.assertEqual(attachment.mimetype, "text/plain")
with attachment.open("rb") as f:
self.assertEqual(f.read(), content)
with attachment.open("wb") as f:
f.write(b"new")
self.assertEqual(attachment.raw, b"new")
def test_attachment_open_in_filestore(self):
self.env["ir.config_parameter"].sudo().set_param(
"ir_attachment.location", "file"
)
content = b"This is a test attachment in filestore"
attachment = self.ir_attachment_model.create(
{"name": "test.txt", "raw": content}
)
self.assertTrue(attachment.store_fname)
self.assertFalse(attachment.db_datas)
self.assertEqual(attachment.raw, content)
with attachment.open("rb") as f:
self.assertEqual(f.read(), content)
with attachment.open("wb") as f:
f.write(b"new")
self.assertEqual(attachment.raw, b"new")
def test_default_attachment_store_in_fs(self):
self.temp_backend.use_as_default_for_attachments = True
content = b"This is a test attachment in filestore tmp_dir"
attachment = self.ir_attachment_model.create(
{"name": "test.txt", "raw": content}
)
self.assertTrue(attachment.store_fname)
self.assertFalse(attachment.db_datas)
self.assertEqual(attachment.raw, content)
self.assertEqual(attachment.mimetype, "text/plain")
self.env.flush_all()
initial_filename = f"test-{attachment.id}-0.txt"
self.assertEqual(os.listdir(self.temp_dir), [initial_filename])
with attachment.open("rb") as f:
self.assertEqual(f.read(), content)
with open(os.path.join(self.temp_dir, initial_filename), "rb") as f:
self.assertEqual(f.read(), content)
# update the attachment
attachment.raw = b"new"
with attachment.open("rb") as f:
self.assertEqual(f.read(), b"new")
# a new file version is created
new_filename = f"test-{attachment.id}-1.txt"
with open(os.path.join(self.temp_dir, new_filename), "rb") as f:
self.assertEqual(f.read(), b"new")
self.assertEqual(attachment.raw, b"new")
self.assertEqual(attachment.store_fname, f"tmp_dir://{new_filename}")
self.assertEqual(attachment.mimetype, "text/plain")
# the original file is to to be deleted by the GC
self.assertEqual(
set(os.listdir(self.temp_dir)), {initial_filename, new_filename}
)
# run the GC
self.env.flush_all()
self.gc_file_model._gc_files_unsafe()
self.assertEqual(os.listdir(self.temp_dir), [new_filename])
attachment.unlink()
# concrete file deletion is done by the GC
self.env.flush_all()
self.assertEqual(os.listdir(self.temp_dir), [new_filename])
# run the GC
self.gc_file_model._gc_files_unsafe()
self.assertEqual(os.listdir(self.temp_dir), [])
def test_fs_update_transactionnal(self):
"""In this test we check that if a rollback is done on an update
The original content is preserved
"""
self.temp_backend.use_as_default_for_attachments = True
content = b"Transactional update"
attachment = self.ir_attachment_model.create(
{"name": "test.txt", "raw": content}
)
self.env.flush_all()
self.assertEqual(attachment.raw, content)
initial_filename = f"test-{attachment.id}-0.txt"
self.assertEqual(attachment.store_fname, f"tmp_dir://{initial_filename}")
self.assertEqual(attachment.fs_filename, initial_filename)
self.assertEqual(
os.listdir(self.temp_dir), [os.path.basename(initial_filename)]
)
orignal_store_fname = attachment.store_fname
try:
with self.env.cr.savepoint():
attachment.raw = b"updated"
new_filename = f"test-{attachment.id}-1.txt"
new_store_fname = f"tmp_dir://{new_filename}"
self.assertEqual(attachment.store_fname, new_store_fname)
self.assertEqual(attachment.fs_filename, new_filename)
# at this stage the original file and the new file are present
# in the list of files to GC
gc_files = self.gc_file_model.search([]).mapped("store_fname")
self.assertIn(orignal_store_fname, gc_files)
self.assertIn(orignal_store_fname, gc_files)
raise MyException("dummy exception")
except MyException:
...
self.assertEqual(attachment.store_fname, f"tmp_dir://{initial_filename}")
self.assertEqual(attachment.fs_filename, initial_filename)
self.assertEqual(attachment.raw, content)
self.assertEqual(attachment.mimetype, "text/plain")
self.assertEqual(
set(os.listdir(self.temp_dir)),
{os.path.basename(initial_filename), os.path.basename(new_filename)},
)
# in test mode, gc collector is not run into a separate transaction
# therefore it has been reset. We manually add our two store_fname
# to the list of files to GC
self.gc_file_model._mark_for_gc(orignal_store_fname)
self.gc_file_model._mark_for_gc(new_store_fname)
# run gc
self.gc_file_model._gc_files_unsafe()
self.assertEqual(
os.listdir(self.temp_dir), [os.path.basename(initial_filename)]
)
def test_fs_create_transactional(self):
"""In this test we check that if a rollback is done on a create
The file is removed
"""
self.temp_backend.use_as_default_for_attachments = True
content = b"Transactional create"
try:
with self.env.cr.savepoint():
attachment = self.ir_attachment_model.create(
{"name": "test.txt", "raw": content}
)
self.env.flush_all()
self.assertEqual(attachment.raw, content)
initial_filename = f"test-{attachment.id}-0.txt"
self.assertEqual(
attachment.store_fname, f"tmp_dir://{initial_filename}"
)
self.assertEqual(attachment.fs_filename, initial_filename)
self.assertEqual(
os.listdir(self.temp_dir), [os.path.basename(initial_filename)]
)
new_store_fname = attachment.store_fname
# at this stage the new file is into the list of files to GC
gc_files = self.gc_file_model.search([]).mapped("store_fname")
self.assertIn(new_store_fname, gc_files)
raise MyException("dummy exception")
except MyException:
...
self.env.flush_all()
# in test mode, gc collector is not run into a separate transaction
# therefore it has been reset. We manually add our new file to the
# list of files to GC
self.gc_file_model._mark_for_gc(new_store_fname)
# run gc
self.gc_file_model._gc_files_unsafe()
self.assertEqual(os.listdir(self.temp_dir), [])
def test_fs_no_delete_if_not_in_current_directory_path(self):
"""In this test we check that it's not possible to removes files
outside the current directory path even if they were created by the
current filesystem storage.
"""
# normal delete
self.temp_backend.use_as_default_for_attachments = True
content = b"Transactional create"
attachment = self.ir_attachment_model.create(
{"name": "test.txt", "raw": content}
)
self.env.flush_all()
initial_filename = f"test-{attachment.id}-0.txt"
self.assertEqual(
os.listdir(self.temp_dir), [os.path.basename(initial_filename)]
)
attachment.unlink()
self.gc_file_model._gc_files_unsafe()
self.assertEqual(os.listdir(self.temp_dir), [])
# delete outside the current directory path
attachment = self.ir_attachment_model.create(
{"name": "test.txt", "raw": content}
)
self.env.flush_all()
initial_filename = f"test-{attachment.id}-0.txt"
self.assertEqual(
os.listdir(self.temp_dir), [os.path.basename(initial_filename)]
)
self.temp_backend.directory_path = "/dummy"
attachment.unlink()
self.gc_file_model._gc_files_unsafe()
# unlink is not physically done since the file is outside the current
self.assertEqual(
os.listdir(self.temp_dir), [os.path.basename(initial_filename)]
)
def test_no_gc_if_disabled_on_storage(self):
store_fname = "tmp_dir://dummy-0-0.txt"
self.gc_file_model._mark_for_gc(store_fname)
self.temp_backend.autovacuum_gc = False
self.gc_file_model._gc_files_unsafe()
self.assertIn(store_fname, self.gc_file_model.search([]).mapped("store_fname"))
self.temp_backend.autovacuum_gc = False
self.gc_file_model._gc_files_unsafe()
self.assertIn(store_fname, self.gc_file_model.search([]).mapped("store_fname"))
self.temp_backend.autovacuum_gc = True
self.gc_file_model._gc_files_unsafe()
self.assertNotIn(
store_fname, self.gc_file_model.search([]).mapped("store_fname")
)
def test_attachment_fs_url(self):
self.temp_backend.base_url = "https://acsone.eu/media"
self.temp_backend.use_as_default_for_attachments = True
content = b"Transactional update"
attachment = self.ir_attachment_model.create(
{"name": "test.txt", "raw": content}
)
self.env.flush_all()
attachment_path = f"/test-{attachment.id}-0.txt"
self.assertEqual(attachment.fs_url, f"https://acsone.eu/media{attachment_path}")
self.assertEqual(attachment.fs_url_path, attachment_path)
self.temp_backend.is_directory_path_in_url = True
self.temp_backend.recompute_urls()
attachment_path = f"{self.temp_dir}/test-{attachment.id}-0.txt"
self.assertEqual(attachment.fs_url, f"https://acsone.eu/media{attachment_path}")
self.assertEqual(attachment.fs_url_path, attachment_path)
def test_force_attachment_in_db_rules(self):
self.temp_backend.use_as_default_for_attachments = True
# force storage in db for text/plain
self.temp_backend.force_db_for_default_attachment_rules = '{"text/plain": 0}'
attachment = self.ir_attachment_model.create(
{"name": "test.txt", "raw": b"content"}
)
self.env.flush_all()
self.assertFalse(attachment.store_fname)
self.assertEqual(attachment.db_datas, b"content")
self.assertEqual(attachment.mimetype, "text/plain")
def test_force_storage_to_db(self):
self.temp_backend.use_as_default_for_attachments = True
attachment = self.ir_attachment_model.create(
{"name": "test.txt", "raw": b"content"}
)
self.env.flush_all()
self.assertTrue(attachment.store_fname)
self.assertFalse(attachment.db_datas)
store_fname = attachment.store_fname
# we change the rules to force the storage in db for text/plain
self.temp_backend.force_db_for_default_attachment_rules = '{"text/plain": 0}'
attachment.force_storage_to_db_for_special_fields()
self.assertFalse(attachment.store_fname)
self.assertEqual(attachment.db_datas, b"content")
# we check that the file is marked for GC
gc_files = self.gc_file_model.search([]).mapped("store_fname")
self.assertIn(store_fname, gc_files)
@mute_logger("odoo.addons.fs_attachment.models.ir_attachment")
def test_force_storage_to_fs(self):
attachment = self.ir_attachment_model.create(
{"name": "test.txt", "raw": b"content"}
)
self.env.flush_all()
fs_path = self.ir_attachment_model._filestore() + "/" + attachment.store_fname
self.assertTrue(os.path.exists(fs_path))
self.assertEqual(os.listdir(self.temp_dir), [])
# we decide to force the storage in the filestore
self.temp_backend.use_as_default_for_attachments = True
with mock.patch.object(self.env.cr, "commit"), mock.patch(
"odoo.addons.fs_attachment.models.ir_attachment.clean_fs"
) as clean_fs:
self.ir_attachment_model.force_storage()
clean_fs.assert_called_once()
# files into the filestore must be moved to our filesystem storage
filename = f"test-{attachment.id}-0.txt"
self.assertEqual(attachment.store_fname, f"tmp_dir://{filename}")
self.assertIn(filename, os.listdir(self.temp_dir))
def test_storage_use_filename_obfuscation(self):
self.temp_backend.base_url = "https://acsone.eu/media"
self.temp_backend.use_as_default_for_attachments = True
self.temp_backend.use_filename_obfuscation = True
attachment = self.ir_attachment_model.create(
{"name": "test.txt", "raw": b"content"}
)
self.env.flush_all()
self.assertTrue(attachment.store_fname)
self.assertEqual(attachment.name, "test.txt")
self.assertEqual(attachment.checksum, attachment.store_fname.split("/")[-1])
self.assertEqual(attachment.checksum, attachment.fs_url.split("/")[-1])
self.assertEqual(attachment.mimetype, "text/plain")
def test_create_attachments_basic_user(self):
demo_user = self.env.ref("base.user_demo")
demo_partner = self.env.ref("base.partner_demo")
self.temp_backend.use_as_default_for_attachments = True
# Ensure basic access
group_user = self.env.ref("base.group_user")
group_partner_manager = self.env.ref("base.group_partner_manager")
demo_user.write(
{"groups_id": [(6, 0, [group_user.id, group_partner_manager.id])]}
)
# Create basic attachment
self.ir_attachment_model.with_user(demo_user).create(
{"name": "test.txt", "raw": b"content"}
)
# Create attachment related to model
self.ir_attachment_model.with_user(demo_user).create(
{
"name": "test.txt",
"raw": b"content",
"res_model": "res.partner",
"res_id": demo_partner.id,
}
)
# Create attachment related to field
partner_image_field = self.env["ir.model.fields"].search(
[("model", "=", "res.partner"), ("name", "=", "image1920")]
)
self.ir_attachment_model.with_user(demo_user).create(
{
"name": "test.txt",
"raw": b"content",
"res_model": "res.partner",
"res_id": demo_partner.id,
"res_field": partner_image_field.name,
}
)
def test_update_png_to_svg(self):
b64_data_png = (
b"iVBORw0KGgoAAAANSUhEUgAAADMAAAAhCAIAAAD73QTtAAAAA3NCSVQICAjb4U/gAA"
b"AAP0lEQVRYhe3OMQGAMBAAsVL/nh8FDDfxQ6Igz8ycle7fgU9mnVln1pl1Zp1ZZ9aZd"
b"WadWWfWmXVmnVln1u2dvfL/Az+TRcv4AAAAAElFTkSuQmCC"
)
attachment = self.ir_attachment_model.create(
{
"name": "test.png",
"datas": b64_data_png,
}
)
self.assertEqual(attachment.mimetype, "image/png")
b64_data_svg = (
b"PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/Pgo8IURPQ1RZUEU"
b"gc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDIwMDEwOTA0Ly9FTiIKICJodH"
b"RwOi8vd3d3LnczLm9yZy9UUi8yMDAxL1JFQy1TVkctMjAwMTA5MDQvRFREL3N2Zz"
b"EwLmR0ZCI+CjxzdmcgdmVyc2lvbj0iMS4wIiB4bWxucz0iaHR0cDovL3d3dy53My5"
b"vcmcvMjAwMC9zdmciCiB3aWR0aD0iNTEuMDAwMDAwcHQiIGhlaWdodD0iMzMuMDAw"
b"MDAwcHQiIHZpZXdCb3g9IjAgMCA1MS4wMDAwMDAgMzMuMDAwMDAwIgogcHJlc2Vydm"
b"VBc3BlY3RSYXRpbz0ieE1pZFlNaWQgbWVldCI+Cgo8ZyB0cmFuc2Zvcm09InRyYW5z"
b"bGF0ZSgwLjAwMDAwMCwzMy4wMDAwMDApIHNjYWxlKDAuMTAwMDAwLC0wLjEwMDAwMCk"
b"iCmZpbGw9IiMwMDAwMDAiIHN0cm9rZT0ibm9uZSI+CjwvZz4KPC9zdmc+Cg=="
)
attachment.write(
{
"datas": b64_data_svg,
}
)
self.assertEqual(attachment.mimetype, "image/svg+xml")
def test_write_name(self):
self.temp_backend.use_as_default_for_attachments = True
attachment = self.ir_attachment_model.create(
{"name": "file.bin", "datas": b"aGVsbG8gd29ybGQK"}
)
self.assertTrue(attachment.fs_filename.startswith("file-"))
self.assertTrue(attachment.fs_filename.endswith(".bin"))
attachment.write({"name": "file2.txt"})
self.assertTrue(attachment.fs_filename.startswith("file2-"))
self.assertTrue(attachment.fs_filename.endswith(".txt"))
def test_store_in_db_instead_of_object_storage_domain(self):
IrAttachment = self.env["ir.attachment"]
self.patch(
type(IrAttachment),
"_get_storage_force_db_config",
lambda self: {"text/plain": 0, "image/png": 100},
)
self.assertEqual(
self.env["ir.attachment"]._store_in_db_instead_of_object_storage_domain(),
[
"|",
("mimetype", "=like", "text/plain%"),
"&",
("mimetype", "=like", "image/png%"),
("file_size", "<=", 100),
],
)

View file

@ -0,0 +1,233 @@
# Copyright 2023 ACSONE SA/NV (http://acsone.eu).
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from ..models.ir_attachment import AttachmentFileLikeAdapter
from .common import MyException, TestFSAttachmentCommon
class TestFSAttachmentFileLikeAdapterMixin:
@classmethod
def _create_attachment(cls):
raise NotImplementedError
@classmethod
def prepareClass(cls):
cls.initial_content = b"This is a test attachment"
cls.new_content = b"This is a new test attachment"
def prepare(self):
self.attachment = self._create_attachment()
def open(self, attachment=None, mode="rb", new_version=False, **kwargs):
return AttachmentFileLikeAdapter(
attachment or self.attachment,
mode=mode,
new_version=new_version,
**kwargs,
)
def test_read(self):
with self.open(mode="rb") as f:
self.assertEqual(f.read(), self.initial_content)
def test_write(self):
with self.open(mode="wb") as f:
f.write(self.new_content)
self.assertEqual(self.new_content, self.attachment.raw)
def test_write_append(self):
self.assertEqual(self.initial_content, self.attachment.raw)
with self.open(mode="ab") as f:
f.write(self.new_content)
self.assertEqual(self.initial_content + self.new_content, self.attachment.raw)
def test_write_new_version(self):
initial_fname = self.attachment.store_fname
with self.open(mode="wb", new_version=True) as f:
f.write(self.new_content)
self.assertEqual(self.new_content, self.attachment.raw)
if initial_fname:
self.assertNotEqual(self.attachment.store_fname, initial_fname)
def test_write_append_new_version(self):
initial_fname = self.attachment.store_fname
with self.open(mode="ab", new_version=True) as f:
f.write(self.new_content)
self.assertEqual(self.initial_content + self.new_content, self.attachment.raw)
if initial_fname:
self.assertNotEqual(self.attachment.store_fname, initial_fname)
def test_write_transactional_new_version_only(self):
try:
initial_fname = self.attachment.store_fname
with self.env.cr.savepoint():
with self.open(mode="wb", new_version=True) as f:
f.write(self.new_content)
self.assertEqual(self.new_content, self.attachment.raw)
if initial_fname:
self.assertNotEqual(self.attachment.store_fname, initial_fname)
raise MyException("Test")
except MyException:
...
self.assertEqual(self.initial_content, self.attachment.raw)
if initial_fname:
self.assertEqual(self.attachment.store_fname, initial_fname)
class TestAttachmentInFileSystemFileLikeAdapter(
TestFSAttachmentCommon, TestFSAttachmentFileLikeAdapterMixin
):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.prepareClass()
def setUp(self):
super().setUp()
self.prepare()
@classmethod
def _create_attachment(cls):
return (
cls.env["ir.attachment"]
.with_context(
storage_location=cls.temp_backend.code,
storage_file_path="test.txt",
)
.create({"name": "test.txt", "raw": cls.initial_content})
)
class TestAttachmentInDBFileLikeAdapter(
TestFSAttachmentCommon, TestFSAttachmentFileLikeAdapterMixin
):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.prepareClass()
def setUp(self):
super().setUp()
self.env["ir.config_parameter"].sudo().set_param("ir_attachment.location", "db")
self.prepare()
def tearDown(self) -> None:
self.attachment.unlink()
super().tearDown()
@classmethod
def _create_attachment(cls):
return cls.env["ir.attachment"].create(
{"name": "test.txt", "raw": cls.initial_content}
)
class TestAttachmentInFileFileLikeAdapter(
TestFSAttachmentCommon, TestFSAttachmentFileLikeAdapterMixin
):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.prepareClass()
def setUp(self):
super().setUp()
self.env["ir.config_parameter"].sudo().set_param(
"ir_attachment.location", "file"
)
self.prepare()
def tearDown(self) -> None:
self.attachment.unlink()
self.attachment._gc_file_store_unsafe()
super().tearDown()
@classmethod
def _create_attachment(cls):
return cls.env["ir.attachment"].create(
{"name": "test.txt", "raw": cls.initial_content}
)
class TestAttachmentInFileSystemDependingModelFileLikeAdapter(
TestFSAttachmentCommon, TestFSAttachmentFileLikeAdapterMixin
):
"""
Configure the temp backend to store only attachments linked to
res.partner model.
Check that opening/updating the file does not change the storage type.
"""
@classmethod
def setUpClass(cls):
res = super().setUpClass()
cls.temp_backend.model_xmlids = "base.model_res_partner"
cls.prepareClass()
return res
def setUp(self):
super().setUp()
super().prepare()
@classmethod
def _create_attachment(cls):
return (
cls.env["ir.attachment"]
.with_context(
storage_file_path="test.txt",
)
.create(
{
"name": "test.txt",
"raw": cls.initial_content,
"res_model": "res.partner",
}
)
)
def test_storage_location(self):
self.assertEqual(self.attachment.fs_storage_id, self.temp_backend)
class TestAttachmentInFileSystemDependingFieldFileLikeAdapter(
TestFSAttachmentCommon, TestFSAttachmentFileLikeAdapterMixin
):
"""
Configure the temp backend to store only attachments linked to
res.country ID field.
Check that opening/updating the file does not change the storage type.
"""
@classmethod
def setUpClass(cls):
res = super().setUpClass()
cls.temp_backend.field_xmlids = "base.field_res_country__id"
cls.prepareClass()
return res
def setUp(self):
super().setUp()
super().prepare()
@classmethod
def _create_attachment(cls):
return (
cls.env["ir.attachment"]
.with_context(
storage_file_path="test.txt",
)
.create(
{
"name": "test.txt",
"raw": cls.initial_content,
"res_model": "res.country",
"res_field": "id",
}
)
)
def test_storage_location(self):
self.assertEqual(self.attachment.fs_storage_id, self.temp_backend)

View file

@ -0,0 +1,108 @@
# Copyright 2023 ACSONE SA/NV (http://acsone.eu).
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import os
import shutil
import tempfile
from unittest.mock import patch
from odoo.tests.common import HttpCase
from odoo.tools import config
class TestFsAttachmentInternalUrl(HttpCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
temp_dir = tempfile.mkdtemp()
cls.temp_backend = cls.env["fs.storage"].create(
{
"name": "Temp FS Storage",
"protocol": "file",
"code": "tmp_dir",
"directory_path": temp_dir,
"base_url": "http://my.public.files/",
}
)
cls.temp_dir = temp_dir
cls.gc_file_model = cls.env["fs.file.gc"]
cls.content = b"This is a test attachment"
cls.attachment = (
cls.env["ir.attachment"]
.with_context(
storage_location=cls.temp_backend.code,
storage_file_path="test.txt",
)
.create({"name": "test.txt", "raw": cls.content})
)
@cls.addClassCleanup
def cleanup_tempdir():
shutil.rmtree(temp_dir)
def setUp(self):
super().setUp()
# enforce temp_backend field since it seems that they are reset on
# savepoint rollback when managed by server_environment -> TO Be investigated
self.temp_backend.write(
{
"protocol": "file",
"code": "tmp_dir",
"directory_path": self.temp_dir,
"base_url": "http://my.public.files/",
}
)
@classmethod
def tearDownClass(cls):
super().tearDownClass()
for f in os.listdir(cls.temp_dir):
os.remove(os.path.join(cls.temp_dir, f))
def assertDownload(
self, url, headers, assert_status_code, assert_headers, assert_content=None
):
res = self.url_open(url, headers=headers)
res.raise_for_status()
self.assertEqual(res.status_code, assert_status_code)
for header_name, header_value in assert_headers.items():
self.assertEqual(
res.headers.get(header_name),
header_value,
f"Wrong value for header {header_name}",
)
if assert_content:
self.assertEqual(res.content, assert_content, "Wong content")
return res
def test_fs_attachment_internal_url(self):
self.authenticate("admin", "admin")
self.assertDownload(
self.attachment.internal_url,
headers={},
assert_status_code=200,
assert_headers={
"Content-Type": "text/plain; charset=utf-8",
"Content-Disposition": "inline; filename=test.txt",
},
assert_content=self.content,
)
def test_fs_attachment_internal_url_x_sendfile(self):
self.authenticate("admin", "admin")
self.temp_backend.write({"use_x_sendfile_to_serve_internal_url": True})
with patch.object(config, "options", {**config.options, "x_sendfile": True}):
x_accel_redirect = f"/tmp_dir/test-{self.attachment.id}-0.txt"
self.assertDownload(
self.attachment.internal_url,
headers={},
assert_status_code=200,
assert_headers={
"Content-Type": "text/plain; charset=utf-8",
"Content-Disposition": "inline; filename=test.txt",
"X-Accel-Redirect": x_accel_redirect,
"Content-Length": "0",
"X-Sendfile": x_accel_redirect,
},
assert_content=None,
)

View file

@ -0,0 +1,414 @@
# Copyright 2023 ACSONE SA/NV (http://acsone.eu).
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import base64
import os
from odoo.exceptions import ValidationError
from .common import TestFSAttachmentCommon
class TestFsStorage(TestFSAttachmentCommon):
@classmethod
def setUpClass(cls):
res = super().setUpClass()
cls.default_backend = cls.env.ref("fs_storage.default_fs_storage")
return res
def test_compute_model_ids(self):
"""
Give a list of model xmlids and check that the o2m field model_ids
is correctly fulfilled.
"""
self.temp_backend.model_xmlids = (
"base.model_res_partner,base.model_ir_attachment"
)
model_ids = self.temp_backend.model_ids
self.assertEqual(len(model_ids), 2)
model_names = model_ids.mapped("model")
self.assertEqual(set(model_names), {"res.partner", "ir.attachment"})
def test_inverse_model_ids(self):
"""
Modify backend model_ids and check the char field model_xmlids
is correctly updated
"""
model_1 = self.env["ir.model"].search([("model", "=", "res.partner")])
model_2 = self.env["ir.model"].search([("model", "=", "ir.attachment")])
self.temp_backend.model_ids = [(6, 0, [model_1.id, model_2.id])]
self.assertEqual(
self.temp_backend.model_xmlids,
"base.model_res_partner,base.model_ir_attachment",
)
def test_compute_field_ids(self):
"""
Give a list of field xmlids and check that the o2m field field_ids
is correctly fulfilled.
"""
self.temp_backend.field_xmlids = (
"base.field_res_partner__id,base.field_res_partner__create_date"
)
field_ids = self.temp_backend.field_ids
self.assertEqual(len(field_ids), 2)
field_names = field_ids.mapped("name")
self.assertEqual(set(field_names), {"id", "create_date"})
field_models = field_ids.mapped("model")
self.assertEqual(set(field_models), {"res.partner"})
def test_inverse_field_ids(self):
"""
Modify backend field_ids and check the char field field_xmlids
is correctly updated
"""
field_1 = self.env["ir.model.fields"].search(
[("model", "=", "res.partner"), ("name", "=", "id")]
)
field_2 = self.env["ir.model.fields"].search(
[("model", "=", "res.partner"), ("name", "=", "create_date")]
)
self.temp_backend.field_ids = [(6, 0, [field_1.id, field_2.id])]
self.assertEqual(
self.temp_backend.field_xmlids,
"base.field_res_partner__id,base.field_res_partner__create_date",
)
def test_constraint_unique_storage_model(self):
"""
A given model can be linked to a unique storage
"""
self.temp_backend.model_xmlids = (
"base.model_res_partner,base.model_ir_attachment"
)
self.env.ref("fs_storage.default_fs_storage")
with self.assertRaises(ValidationError):
self.default_backend.model_xmlids = "base.model_res_partner"
def test_constraint_unique_storage_field(self):
"""
A given field can be linked to a unique storage
"""
self.temp_backend.field_xmlids = (
"base.field_res_partner__id,base.field_res_partner__name"
)
with self.assertRaises(ValidationError):
self.default_backend.field_xmlids = "base.field_res_partner__name"
def test_force_model_create_attachment(self):
"""
Force 'res.partner' model to temp_backend
Use odoofs as default for attachments
* Check that only attachments linked to res.partner model are stored
in the first FS.
* Check that updating this first attachment does not change the storage
"""
self.default_backend.use_as_default_for_attachments = True
self.temp_backend.model_xmlids = "base.model_res_partner"
# 1a. First attachment linked to res.partner model
content = b"This is a test attachment linked to res.partner model"
attachment = self.ir_attachment_model.create(
{"name": "test.txt", "raw": content, "res_model": "res.partner"}
)
self.assertTrue(attachment.store_fname)
self.assertFalse(attachment.db_datas)
self.assertEqual(attachment.raw, content)
self.assertEqual(attachment.mimetype, "text/plain")
self.env.flush_all()
initial_filename = f"test-{attachment.id}-0.txt"
self.assertEqual(attachment.fs_storage_code, self.temp_backend.code)
self.assertEqual(os.listdir(self.temp_dir), [initial_filename])
with attachment.open("rb") as f:
self.assertEqual(f.read(), content)
with open(os.path.join(self.temp_dir, initial_filename), "rb") as f:
self.assertEqual(f.read(), content)
# 1b. Update the attachment
new_content = b"Update the test attachment"
attachment.raw = new_content
with attachment.open("rb") as f:
self.assertEqual(f.read(), new_content)
# a new file version is created
new_filename = f"test-{attachment.id}-1.txt"
with open(os.path.join(self.temp_dir, new_filename), "rb") as f:
self.assertEqual(f.read(), new_content)
self.assertEqual(attachment.raw, new_content)
self.assertEqual(attachment.store_fname, f"tmp_dir://{new_filename}")
# 2. Second attachment linked to res.country model
content = b"This is a test attachment linked to res.country model"
attachment = self.ir_attachment_model.create(
{"name": "test.txt", "raw": content, "res_model": "res.country"}
)
self.assertTrue(attachment.store_fname)
self.assertFalse(attachment.db_datas)
self.assertEqual(attachment.raw, content)
self.assertEqual(attachment.mimetype, "text/plain")
self.env.flush_all()
self.assertEqual(attachment.fs_storage_code, self.default_backend.code)
def test_force_field_create_attachment(self):
"""
Force 'base.field_res.partner__name' field to temp_backend
Use odoofs as default for attachments
* Check that only attachments linked to res.partner name field are stored
in the first FS.
* Check that updating this first attachment does not change the storage
"""
self.default_backend.use_as_default_for_attachments = True
self.temp_backend.field_xmlids = "base.field_res_partner__name"
# 1a. First attachment linked to res.partner name field
content = b"This is a test attachment linked to res.partner name field"
attachment = self.ir_attachment_model.create(
{
"name": "test.txt",
"raw": content,
"res_model": "res.partner",
"res_field": "name",
}
)
self.assertTrue(attachment.store_fname)
self.assertFalse(attachment.db_datas)
self.assertEqual(attachment.raw, content)
self.assertEqual(attachment.mimetype, "text/plain")
self.env.flush_all()
initial_filename = f"test-{attachment.id}-0.txt"
self.assertEqual(attachment.fs_storage_code, self.temp_backend.code)
self.assertEqual(os.listdir(self.temp_dir), [initial_filename])
with attachment.open("rb") as f:
self.assertEqual(f.read(), content)
with open(os.path.join(self.temp_dir, initial_filename), "rb") as f:
self.assertEqual(f.read(), content)
# 1b. Update the attachment
new_content = b"Update the test attachment"
attachment.raw = new_content
with attachment.open("rb") as f:
self.assertEqual(f.read(), new_content)
# a new file version is created
new_filename = f"test-{attachment.id}-1.txt"
with open(os.path.join(self.temp_dir, new_filename), "rb") as f:
self.assertEqual(f.read(), new_content)
self.assertEqual(attachment.raw, new_content)
self.assertEqual(attachment.store_fname, f"tmp_dir://{new_filename}")
# 2. Second attachment linked to res.partner but other field (website)
content = b"This is a test attachment linked to res.partner website field"
attachment = self.ir_attachment_model.create(
{
"name": "test.txt",
"raw": content,
"res_model": "res.partner",
"res_field": "website",
}
)
self.assertTrue(attachment.store_fname)
self.assertFalse(attachment.db_datas)
self.assertEqual(attachment.raw, content)
self.assertEqual(attachment.mimetype, "text/plain")
self.env.flush_all()
self.assertEqual(attachment.fs_storage_code, self.default_backend.code)
# 3. Third attachment linked to res.partner but no specific field
content = b"This is a test attachment linked to res.partner model"
attachment = self.ir_attachment_model.create(
{"name": "test.txt", "raw": content, "res_model": "res.partner"}
)
self.assertTrue(attachment.store_fname)
self.assertFalse(attachment.db_datas)
self.assertEqual(attachment.raw, content)
self.assertEqual(attachment.mimetype, "text/plain")
self.env.flush_all()
self.assertEqual(attachment.fs_storage_code, self.default_backend.code)
def test_force_field_and_model_create_attachment(self):
"""
Force res.partner model to default_backend.
But force specific res.partner name field to temp_backend.
* Check that attachments linked to res.partner name field are
stored in temp_backend, and other attachments linked to other
fields of res.partner are stored in default_backend
* Check that updating this first attachment does not change the storage
"""
self.default_backend.model_xmlids = "base.model_res_partner"
self.temp_backend.field_xmlids = "base.field_res_partner__name"
# 1a. First attachment linked to res.partner name field
content = b"This is a test attachment linked to res.partner name field"
attachment = self.ir_attachment_model.create(
{
"name": "test.txt",
"raw": content,
"res_model": "res.partner",
"res_field": "name",
}
)
self.assertTrue(attachment.store_fname)
self.assertFalse(attachment.db_datas)
self.assertEqual(attachment.raw, content)
self.assertEqual(attachment.mimetype, "text/plain")
self.env.flush_all()
initial_filename = f"test-{attachment.id}-0.txt"
self.assertEqual(attachment.fs_storage_code, self.temp_backend.code)
self.assertEqual(os.listdir(self.temp_dir), [initial_filename])
with attachment.open("rb") as f:
self.assertEqual(f.read(), content)
with open(os.path.join(self.temp_dir, initial_filename), "rb") as f:
self.assertEqual(f.read(), content)
# 1b. Update the attachment
new_content = b"Update the test attachment"
attachment.raw = new_content
with attachment.open("rb") as f:
self.assertEqual(f.read(), new_content)
# a new file version is created
new_filename = f"test-{attachment.id}-1.txt"
with open(os.path.join(self.temp_dir, new_filename), "rb") as f:
self.assertEqual(f.read(), new_content)
self.assertEqual(attachment.raw, new_content)
self.assertEqual(attachment.store_fname, f"tmp_dir://{new_filename}")
# 2. Second attachment linked to res.partner but other field (website)
content = b"This is a test attachment linked to res.partner website field"
attachment = self.ir_attachment_model.create(
{
"name": "test.txt",
"raw": content,
"res_model": "res.partner",
"res_field": "website",
}
)
self.assertTrue(attachment.store_fname)
self.assertFalse(attachment.db_datas)
self.assertEqual(attachment.raw, content)
self.assertEqual(attachment.mimetype, "text/plain")
self.env.flush_all()
self.assertEqual(attachment.fs_storage_code, self.default_backend.code)
# 3. Third attachment linked to res.partner but no specific field
content = b"This is a test attachment linked to res.partner model"
attachment = self.ir_attachment_model.create(
{"name": "test.txt", "raw": content, "res_model": "res.partner"}
)
self.assertTrue(attachment.store_fname)
self.assertFalse(attachment.db_datas)
self.assertEqual(attachment.raw, content)
self.assertEqual(attachment.mimetype, "text/plain")
self.env.flush_all()
self.assertEqual(attachment.fs_storage_code, self.default_backend.code)
# Fourth attachment linked to res.country: no storage because
# no default FS storage
content = b"This is a test attachment linked to res.country model"
attachment = self.ir_attachment_model.create(
{"name": "test.txt", "raw": content, "res_model": "res.country"}
)
self.assertTrue(attachment.store_fname)
self.assertFalse(attachment.db_datas)
self.assertEqual(attachment.raw, content)
self.assertEqual(attachment.mimetype, "text/plain")
self.env.flush_all()
self.assertFalse(attachment.fs_storage_code)
def test_recompute_urls(self):
"""
Mark temp_backend as default and set its base_url.
Create one attachment in temp_backend that is linked to a field and one that is not.
* Check that after updating the base_url for the backend, executing recompute_urls
updates fs_url for both attachments, whether they are linked to a field or not
"""
self.temp_backend.base_url = "https://acsone.eu/media"
self.temp_backend.use_as_default_for_attachments = True
self.ir_attachment_model.create(
{
"name": "field.txt",
"raw": "Attachment linked to a field",
"res_model": "res.partner",
"res_field": "name",
}
)
self.ir_attachment_model.create(
{
"name": "no_field.txt",
"raw": "Attachment not linked to a field",
}
)
self.env.flush_all()
self.env.cr.execute(
f"""
SELECT COUNT(*)
FROM ir_attachment
WHERE fs_storage_id = {self.temp_backend.id}
AND fs_url LIKE '{self.temp_backend.base_url}%'
"""
)
self.assertEqual(self.env.cr.dictfetchall()[0].get("count"), 2)
self.temp_backend.base_url = "https://forgeflow.com/media"
self.temp_backend.recompute_urls()
self.env.flush_all()
self.env.cr.execute(
f"""
SELECT COUNT(*)
FROM ir_attachment
WHERE fs_storage_id = {self.temp_backend.id}
AND fs_url LIKE '{self.temp_backend.base_url}%'
"""
)
self.assertEqual(self.env.cr.dictfetchall()[0].get("count"), 2)
def test_url_for_image_dir_optimized_and_not_obfuscated(self):
# Create a base64 encoded mock image (1x1 pixel transparent PNG)
image_data = base64.b64encode(
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08"
b"\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDAT\x08\xd7c\xf8\x0f\x00"
b"\x01\x01\x01\x00\xd1\x8d\xcd\xbf\x00\x00\x00\x00IEND\xaeB`\x82"
)
# Create a mock image filestore
fs_storage = self.env["fs.storage"].create(
{
"name": "FS Product Image Backend",
"code": "file",
"base_url": "https://localhost/images",
"optimizes_directory_path": True,
"use_filename_obfuscation": False,
}
)
# Create a mock image attachment
attachment = self.env["ir.attachment"].create(
{"name": "test_image.png", "datas": image_data, "mimetype": "image/png"}
)
# Get the url from the model
fs_url_1 = fs_storage._get_url_for_attachment(attachment)
# Generate the url that should be accessed
base_url = fs_storage.base_url_for_files
fs_filename = attachment.fs_filename
checksum = attachment.checksum
parts = [base_url, checksum[:2], checksum[2:4], fs_filename]
fs_url_2 = fs_storage._normalize_url("/".join(parts))
# Make some checks and asset if the two urls are equal
self.assertTrue(parts)
self.assertTrue(checksum)
self.assertEqual(fs_url_1, fs_url_2)

View file

@ -0,0 +1,238 @@
# Copyright 2023 ACSONE SA/NV (http://acsone.eu).
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import base64
import io
import os
import shutil
import tempfile
from PIL import Image
from odoo.tests.common import HttpCase
class TestStream(HttpCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
temp_dir = tempfile.mkdtemp()
cls.temp_backend = cls.env["fs.storage"].create(
{
"name": "Temp FS Storage",
"protocol": "file",
"code": "tmp_dir",
"directory_path": temp_dir,
"base_url": "http://my.public.files/",
}
)
cls.temp_dir = temp_dir
cls.content = b"This is a test attachment"
cls.attachment_binary = (
cls.env["ir.attachment"]
.with_context(
storage_location=cls.temp_backend.code,
storage_file_path="test.txt",
)
.create({"name": "test.txt", "raw": cls.content})
)
cls.image = cls._create_image(128, 128)
cls.attachment_image = (
cls.env["ir.attachment"]
.with_context(
storage_location=cls.temp_backend.code,
storage_file_path="test.png",
)
.create({"name": "test.png", "raw": cls.image})
)
@cls.addClassCleanup
def cleanup_tempdir():
shutil.rmtree(temp_dir)
assert cls.attachment_binary.fs_filename
assert cls.attachment_image.fs_filename
def setUp(self):
super().setUp()
# enforce temp_backend field since it seems that they are reset on
# savepoint rollback when managed by server_environment -> TO Be investigated
self.temp_backend.write(
{
"protocol": "file",
"code": "tmp_dir",
"directory_path": self.temp_dir,
"base_url": "http://my.public.files/",
}
)
@classmethod
def tearDownClass(cls):
super().tearDownClass()
for f in os.listdir(cls.temp_dir):
os.remove(os.path.join(cls.temp_dir, f))
@classmethod
def _create_image(cls, width, height, color="#4169E1", img_format="PNG"):
f = io.BytesIO()
Image.new("RGB", (width, height), color).save(f, img_format)
f.seek(0)
return f.read()
def assertDownload(
self, url, headers, assert_status_code, assert_headers, assert_content=None
):
res = self.url_open(url, headers=headers)
res.raise_for_status()
self.assertEqual(res.status_code, assert_status_code)
for header_name, header_value in assert_headers.items():
self.assertEqual(
res.headers.get(header_name),
header_value,
f"Wrong value for header {header_name}",
)
if assert_content:
self.assertEqual(res.content, assert_content, "Wong content")
return res
def test_content_url(self):
self.authenticate("admin", "admin")
url = f"/web/content/{self.attachment_binary.id}"
self.assertDownload(
url,
headers={},
assert_status_code=200,
assert_headers={
"Content-Type": "text/plain; charset=utf-8",
"Content-Disposition": "inline; filename=test.txt",
},
assert_content=self.content,
)
url = f"/web/content/{self.attachment_binary.id}/?filename=test2.txt&mimetype=text/csv"
self.assertDownload(
url,
headers={},
assert_status_code=200,
assert_headers={
"Content-Type": "text/csv; charset=utf-8",
"Content-Disposition": "inline; filename=test2.txt",
},
assert_content=self.content,
)
def test_image_url(self):
self.authenticate("admin", "admin")
url = f"/web/image/{self.attachment_image.id}"
self.assertDownload(
url,
headers={},
assert_status_code=200,
assert_headers={
"Content-Type": "image/png",
"Content-Disposition": "inline; filename=test.png",
},
assert_content=self.image,
)
def test_image_url_with_size(self):
self.authenticate("admin", "admin")
url = f"/web/image/{self.attachment_image.id}?width=64&height=64"
res = self.assertDownload(
url,
headers={},
assert_status_code=200,
assert_headers={
"Content-Type": "image/png",
"Content-Disposition": "inline; filename=test.png",
},
)
self.assertEqual(Image.open(io.BytesIO(res.content)).size, (64, 64))
def test_response_csp_header(self):
self.authenticate("admin", "admin")
url = f"/web/content/{self.attachment_binary.id}"
self.assertDownload(
url,
headers={},
assert_status_code=200,
assert_headers={
"X-Content-Type-Options": "nosniff",
"Content-Security-Policy": "default-src 'none'",
},
)
def test_serving_field_image(self):
self.authenticate("admin", "admin")
demo_partner = self.env.ref("base.partner_demo")
demo_partner.with_context(
storage_location=self.temp_backend.code,
).write({"image_128": base64.encodebytes(self._create_image(128, 128))})
url = f"/web/image/{demo_partner._name}/{demo_partner.id}/image_128"
res = self.assertDownload(
url,
headers={},
assert_status_code=200,
assert_headers={
"Content-Type": "image/png",
},
)
self.assertEqual(Image.open(io.BytesIO(res.content)).size, (128, 128))
url = f"/web/image/{demo_partner._name}/{demo_partner.id}/avatar_128"
avatar_res = self.assertDownload(
url,
headers={},
assert_status_code=200,
assert_headers={
"Content-Type": "image/png",
},
)
self.assertEqual(Image.open(io.BytesIO(avatar_res.content)).size, (128, 128))
def test_image_url_name_with_newline(self):
"""Test downloading a file with a newline in the name.
This test simulates the scenario that causes the Werkzeug error:
`ValueError: Detected newline in header value`.
It verifies that:
1. An `ir.attachment` record is created with a newline character
(`\n`) explicitly included in its `name` field ("tes\nt.png").
2. Accessing this attachment via the `/web/image/{attachment_id}` URL
succeeds with an HTTP 200 status code.
3. Crucially, the `Content-Disposition` header returned in the response
contains a *sanitized* filename ("tes_t.png"). The newline character
has been replaced (typically with an underscore by `secure_filename`).
This confirms that the filename sanitization implemented (likely in the
streaming logic, e.g., `FsStream.get_response` using `secure_filename`)
correctly processes the unsafe filename before passing it to Werkzeug,
thus preventing the original `ValueError` and ensuring safe header values.
"""
attachment_image = (
self.env["ir.attachment"]
.with_context(
storage_location=self.temp_backend.code,
storage_file_path="test.png",
)
.create(
{"name": "tes\nt.png", "raw": self.image}
) # newline in the filename
)
# Ensure the name IS stored with the newline before sanitization happens on download
self.assertIn("\n", attachment_image.name)
self.authenticate("admin", "admin")
url = f"/web/image/{attachment_image.id}"
self.assertDownload(
url,
headers={},
assert_status_code=200,
assert_headers={
"Content-Type": "image/png",
# Assert that the filename in the header IS sanitized
"Content-Disposition": "inline; filename=tes_t.png",
},
assert_content=self.image,
)

View file

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2023 ACSONE SA/NV
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo>
<record model="ir.ui.view" id="fs_storage_form_view">
<field name="name">fs.storage.form (in fs_attachment)</field>
<field name="model">fs.storage</field>
<field name="inherit_id" ref="fs_storage.fs_storage_form_view" />
<field name="arch" type="xml">
<field name="options" position="after">
<field name="model_ids" widget="many2many">
<tree>
<field name="name" />
<field name="model" />
</tree>
</field>
<field name="field_ids" widget="many2many">
<tree>
<field name="name" />
<field name="model" />
</tree>
</field>
<separator string="Attachment" />
<field name="optimizes_directory_path" />
<field name="autovacuum_gc" />
<field name="use_as_default_for_attachments" />
<field
name="force_db_for_default_attachment_rules"
widget="ace"
options="{'mode': 'python'}"
attrs="{'invisible': [('use_as_default_for_attachments', '=', False)]}"
/>
<separator string="Attachment's Url" />
<field name="base_url" />
<field name="is_directory_path_in_url" />
<field name="use_filename_obfuscation" />
<field name="use_x_sendfile_to_serve_internal_url" />
</field>
</field>
</record>
</odoo>

View file

@ -0,0 +1,42 @@
[project]
name = "odoo-bringout-oca-storage-fs_attachment"
version = "16.0.0"
description = "Base Attachment Object Store - Store attachments on external object store"
authors = [
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
]
dependencies = [
"odoo-bringout-oca-storage-fs_storage>=16.0.0",
"requests>=2.25.1"
]
readme = "README.md"
requires-python = ">= 3.11"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Office/Business",
]
[project.urls]
homepage = "https://github.com/bringout/0"
repository = "https://github.com/bringout/0"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.metadata]
allow-direct-references = true
[tool.hatch.build.targets.wheel]
packages = ["fs_attachment"]
[tool.rye]
managed = true
dev-dependencies = [
"pytest>=8.4.1",
]

View file

@ -0,0 +1,46 @@
# Fs Base Multi Image
Odoo addon: fs_base_multi_image
## Installation
```bash
pip install odoo-bringout-oca-storage-fs_base_multi_image
```
## Dependencies
This addon depends on:
- fs_image
## Manifest Information
- **Name**: Fs Base Multi Image
- **Version**: 16.0.1.1.1
- **Category**: N/A
- **License**: AGPL-3
- **Installable**: False
## Source
Based on [OCA/storage](https://github.com/OCA/storage) branch 16.0, addon `fs_base_multi_image`.
## License
This package maintains the original AGPL-3 license from the upstream Odoo project.
## Documentation
- Overview: doc/OVERVIEW.md
- Architecture: doc/ARCHITECTURE.md
- Models: doc/MODELS.md
- Controllers: doc/CONTROLLERS.md
- Wizards: doc/WIZARDS.md
- Reports: doc/REPORTS.md
- Security: doc/SECURITY.md
- Install: doc/INSTALL.md
- Usage: doc/USAGE.md
- Configuration: doc/CONFIGURATION.md
- Dependencies: doc/DEPENDENCIES.md
- Troubleshooting: doc/TROUBLESHOOTING.md
- FAQ: doc/FAQ.md

View file

@ -0,0 +1,32 @@
# Architecture
```mermaid
flowchart TD
U[Users] -->|HTTP| V[Views and QWeb Templates]
V --> C[Controllers]
V --> W[Wizards Transient Models]
C --> M[Models and ORM]
W --> M
M --> R[Reports]
DX[Data XML] --> M
S[Security ACLs and Groups] -. enforces .-> M
subgraph Fs_base_multi_image Module - fs_base_multi_image
direction LR
M:::layer
W:::layer
C:::layer
V:::layer
R:::layer
S:::layer
DX:::layer
end
classDef layer fill:#eef8ff,stroke:#6ea8fe,stroke-width:1px
```
Notes
- Views include tree/form/kanban templates and report templates.
- Controllers provide website/portal routes when present.
- Wizards are UI flows implemented with `models.TransientModel`.
- Data XML loads data/demo records; Security defines groups and access.

View file

@ -0,0 +1,3 @@
# Configuration
Refer to Odoo settings for fs_base_multi_image. Configure related models, access rights, and options as needed.

View file

@ -0,0 +1,3 @@
# Controllers
This module does not define custom HTTP controllers.

View file

@ -0,0 +1,5 @@
# Dependencies
This addon depends on:
- [fs_image](../../odoo-bringout-oca-storage-fs_image)

View file

@ -0,0 +1,4 @@
# FAQ
- Q: Which Odoo version? A: 16.0 (OCA/OCB packaged).
- Q: How to enable? A: Start server with --addon fs_base_multi_image or install in UI.

View file

@ -0,0 +1,7 @@
# Install
```bash
pip install odoo-bringout-oca-storage-fs_base_multi_image"
# or
uv pip install odoo-bringout-oca-storage-fs_base_multi_image"
```

View file

@ -0,0 +1,14 @@
# Models
Detected core models and extensions in fs_base_multi_image.
```mermaid
classDiagram
class fs_image
class fs_image_relation_mixin
class fs_image_mixin
```
Notes
- Classes show model technical names; fields omitted for brevity.
- Items listed under _inherit are extensions of existing models.

View file

@ -0,0 +1,6 @@
# Overview
Packaged Odoo addon: fs_base_multi_image. Provides features documented in upstream Odoo 16 under this addon.
- Source: OCA/OCB 16.0, addon fs_base_multi_image
- License: LGPL-3

View file

@ -0,0 +1,3 @@
# Reports
This module does not define custom reports.

View file

@ -0,0 +1,77 @@
# Security
Access control and security definitions in fs_base_multi_image.
## Access Control Lists (ACLs)
Model access permissions defined in:
- **[all_odoo_addons_repos.txt](../all_odoo_addons_repos.txt)**
- 318 model access rules
- **[bosnian_translations.json](../bosnian_translations.json)**
- 50 model access rules
- **[bosnian_translations_output.json](../bosnian_translations_output.json)**
- 444 model access rules
- **[CHANGELOG.md](../CHANGELOG.md)**
- 132 model access rules
- **[delete_all_odoo_addons.sh](../delete_all_odoo_addons.sh)**
- 50 model access rules
- **[delete_odoo_addons.sh](../delete_odoo_addons.sh)**
- 44 model access rules
- **[doc](../doc)**
- **[docker](../docker)**
- **[input](../input)**
- **[nix](../nix)**
- **[odoo.conf](../odoo.conf)**
- 58 model access rules
- **[odoo_packages_bez_l10n.txt](../odoo_packages_bez_l10n.txt)**
- 1947 model access rules
- **[odoo_packages_bringout.txt](../odoo_packages_bringout.txt)**
- 1947 model access rules
- **[odoo_packages.txt](../odoo_packages.txt)**
- 2085 model access rules
- **[output](../output)**
- **[packages](../packages)**
- **[PACKAGES.md](../PACKAGES.md)**
- 298 model access rules
- **[README.md](../README.md)**
- 338 model access rules
- **[scripts](../scripts)**
- **[temp](../temp)**
- **[TRANSLATION_BS_SUMMARY.md](../TRANSLATION_BS_SUMMARY.md)**
- 146 model access rules
- **[verify_deletions.sh](../verify_deletions.sh)**
- 55 model access rules
## Record Rules
Row-level security rules defined in:
## Security Groups & Configuration
Security groups and permissions defined in:
- **[fs_image.xml](../fs_base_multi_image/security/fs_image.xml)**
- **[res_groups.xml](../fs_base_multi_image/security/res_groups.xml)**
- 1 security groups defined
```mermaid
graph TB
subgraph "Security Layers"
A[Users] --> B[Groups]
B --> C[Access Control Lists]
C --> D[Models]
B --> E[Record Rules]
E --> F[Individual Records]
end
```
Security files overview:
- **[fs_image.xml](../fs_base_multi_image/security/fs_image.xml)**
- Security groups, categories, and XML-based rules
- **[res_groups.xml](../fs_base_multi_image/security/res_groups.xml)**
- Security groups, categories, and XML-based rules
Notes
- Access Control Lists define which groups can access which models
- Record Rules provide row-level security (filter records by user/group)
- Security groups organize users and define permission sets
- All security is enforced at the ORM level by Odoo

View file

@ -0,0 +1,5 @@
# Troubleshooting
- Ensure Python and Odoo environment matches repo guidance.
- Check database connectivity and logs if startup fails.
- Validate that dependent addons listed in DEPENDENCIES.md are installed.

View file

@ -0,0 +1,7 @@
# Usage
Start Odoo including this addon (from repo root):
```bash
python3 scripts/nix_odoo_web_server.py --db-name mydb --addon fs_base_multi_image
```

View file

@ -0,0 +1,3 @@
# Wizards
This module does not include UI wizards.

View file

@ -0,0 +1,115 @@
===================
Fs Base Multi Image
===================
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:156cdd22cfc76b0f6518758875151f309bc244f3166cc2ee4bf49026317e4348
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png
:target: https://odoo-community.org/page/development-status
:alt: Alpha
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstorage-lightgray.png?logo=github
:target: https://github.com/OCA/storage/tree/16.0/fs_base_multi_image
:alt: OCA/storage
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/storage-16-0/storage-16-0-fs_base_multi_image
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/storage&target_branch=16.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
This addon is a technical addon providing a set of models to ease the
creation of other models that need to be linked to multiple images stored
into external filesystems.
The models provided by this addon are:
* ``fs.image``: a model that stores a reference to an image stored into
an external filesystem.
* ``fs.image.relation.mixin``: an abstract model that can be used to
as base class for models created to store an image linked to a model.
This abstract model defines fields and methods to transparently handle
2 cases:
* the image is specific to the model.
* the image is shared between multiple models and therefore is a ``fs.image`` instance linked to the mixin.
.. IMPORTANT::
This is an alpha version, the data model and design can change at any time without warning.
Only for development or testing purpose, do not use in production.
`More details on development status <https://odoo-community.org/page/development-status>`_
**Table of contents**
.. contents::
:local:
Usage
=====
To be able to create and or manages shared images, you must have the ``Image Manager``
role. If you do not have this role, as an authenticated user, you can
only view the shared images.
Known issues / Roadmap
======================
* Add dedicated widget to ease the addition of new images to a model linked to
multiple images. (As it's the case in the *storage_image_product* addon)
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/storage/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/storage/issues/new?body=module:%20fs_base_multi_image%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* ACSONE SA/NV
Contributors
~~~~~~~~~~~~
* Laurent Mignon <laurent.mignon@acsone.eu>
Maintainers
~~~~~~~~~~~
This module is maintained by the OCA.
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
.. |maintainer-lmignon| image:: https://github.com/lmignon.png?size=40px
:target: https://github.com/lmignon
:alt: lmignon
Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
|maintainer-lmignon|
This module is part of the `OCA/storage <https://github.com/OCA/storage/tree/16.0/fs_base_multi_image>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View file

@ -0,0 +1 @@
from . import models

View file

@ -0,0 +1,34 @@
# Copyright 2023 ACSONE SA/NV
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
{
"name": "Fs Base Multi Image",
"summary": """
Mulitple Images from External File System""",
"version": "16.0.1.1.1",
"license": "AGPL-3",
"author": "ACSONE SA/NV,Odoo Community Association (OCA)",
"website": "https://github.com/OCA/storage",
"depends": [
"fs_image",
],
"data": [
"security/res_groups.xml",
"security/fs_image.xml",
"views/fs_image.xml",
"views/fs_image_relation_mixin.xml",
],
"assets": {
"web.assets_backend": [
"fs_base_multi_image/static/src/fields/"
"fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.esm.js",
"fs_base_multi_image/static/src/fields/"
"fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.scss",
"fs_base_multi_image/static/src/fields/"
"fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.xml",
],
},
"demo": [],
"maintainers": ["lmignon"],
"development_status": "Alpha",
}

View file

@ -0,0 +1,196 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * fs_base_multi_image
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: fs_base_multi_image
#. odoo-javascript
#: code:addons/fs_base_multi_image/static/src/fields/fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.esm.js:0
#, python-format
msgid "An error occurred during the images upload."
msgstr "Dogodila se greška tijekom uploada slika."
#. module: fs_base_multi_image
#. odoo-javascript
#: code:addons/fs_base_multi_image/static/src/fields/fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.xml:0
#, python-format
msgid "Choose how you want to store the new images:"
msgstr "Odaberite kako želite pohraniti nove slike:"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__create_uid
#: model_terms:ir.ui.view,arch_db:fs_base_multi_image.fs_image_search_view
msgid "Created by"
msgstr "Kreirao"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__create_date
msgid "Created on"
msgstr "Kreirano"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__display_name
msgid "Display Name"
msgstr "Prikazani naziv"
#. module: fs_base_multi_image
#: model:ir.actions.act_window,name:fs_base_multi_image.fs_image_act_window
msgid "Fs Image"
msgstr "Fs slika"
#. module: fs_base_multi_image
#: model:ir.ui.menu,name:fs_base_multi_image.fs_image_menu
msgid "Fs Images"
msgstr "Fs slike"
#. module: fs_base_multi_image
#: model_terms:ir.ui.view,arch_db:fs_base_multi_image.fs_image_search_view
msgid "Group By"
msgstr "Grupiši po"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__id
msgid "ID"
msgstr "ID"
#. module: fs_base_multi_image
#: model:ir.model,name:fs_base_multi_image.model_fs_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__image
#: model_terms:ir.ui.view,arch_db:fs_base_multi_image.fs_image_form_view
msgid "Image"
msgstr "Slika"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__image_medium
msgid "Image (128)"
msgstr "Slika (128)"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__image
msgid "Image (original)"
msgstr "Slika (originalna)"
#. module: fs_base_multi_image
#: model:res.groups,name:fs_base_multi_image.group_image_manager
msgid "Image Manager"
msgstr "Manager slika"
#. module: fs_base_multi_image
#: model:ir.model,name:fs_base_multi_image.model_fs_image_relation_mixin
msgid "Image Relation"
msgstr "Relacija slike"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__image_medium
msgid "Image medium"
msgstr "Srednja slika"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image____last_update
msgid "Last Modified on"
msgstr "Zadnje mijenjano"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__write_uid
msgid "Last Updated by"
msgstr "Zadnji ažurirao"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__write_date
msgid "Last Updated on"
msgstr "Zadnje ažurirano"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__link_existing
msgid "Link Existing"
msgstr "Povezivanje postojećeg"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__image_id
msgid "Linked image"
msgstr "Povezana slika"
#. module: fs_base_multi_image
#: model_terms:ir.ui.view,arch_db:fs_base_multi_image.fs_image_search_view
msgid "MimeType"
msgstr "MimeType"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__mimetype
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__mimetype
msgid "Mimetype"
msgstr "Mimetype"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__name
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__name
#: model_terms:ir.ui.view,arch_db:fs_base_multi_image.fs_image_search_view
msgid "Name"
msgstr "Naziv:"
#. module: fs_base_multi_image
#. odoo-javascript
#: code:addons/fs_base_multi_image/static/src/fields/fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.xml:0
#, python-format
msgid "Reusable images"
msgstr "Slike za ponovna uporaba"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__sequence
msgid "Sequence"
msgstr "Sekvenca"
#. module: fs_base_multi_image
#. odoo-javascript
#: code:addons/fs_base_multi_image/static/src/fields/fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.xml:0
#, python-format
msgid "Specific"
msgstr "Specifično"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__specific_image
msgid "Specific Image"
msgstr "Specifična slika"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__specific_image_medium
msgid "Specific Image (128)"
msgstr "Specifična slika (128)"
#. module: fs_base_multi_image
#. odoo-javascript
#: code:addons/fs_base_multi_image/static/src/fields/fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.xml:0
#, python-format
msgid "You can drag and drop images to create new records or"
msgstr "Možete prevući i spustiti slike za stvaranje novih zapisa ili"
#. module: fs_base_multi_image
#. odoo-python
#: code:addons/fs_base_multi_image/models/fs_image_relation_mixin.py:0
#, python-format
msgid "You must set an image"
msgstr "Morate postaviti sliku"
#. module: fs_base_multi_image
#. odoo-javascript
#: code:addons/fs_base_multi_image/static/src/fields/fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.xml:0
#, python-format
msgid "click here"
msgstr "kliknite ovdje"
#. module: fs_base_multi_image
#. odoo-javascript
#: code:addons/fs_base_multi_image/static/src/fields/fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.xml:0
#, python-format
msgid "to select image files."
msgstr "za odabir datoteka slika."

View file

@ -0,0 +1,209 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * fs_base_multi_image
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2024-01-27 14:36+0000\n"
"Last-Translator: Ivorra78 <informatica@totmaterial.es>\n"
"Language-Team: none\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.17\n"
#. module: fs_base_multi_image
#. odoo-javascript
#: code:addons/fs_base_multi_image/static/src/fields/fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.esm.js:0
#, python-format
msgid "An error occurred during the images upload."
msgstr "Se ha producido un error durante la carga de imágenes."
#. module: fs_base_multi_image
#. odoo-javascript
#: code:addons/fs_base_multi_image/static/src/fields/fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.xml:0
#, python-format
msgid "Choose how you want to store the new images:"
msgstr "Elige cómo quieres almacenar las nuevas imágenes:"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__create_uid
#: model_terms:ir.ui.view,arch_db:fs_base_multi_image.fs_image_search_view
msgid "Created by"
msgstr "Creado por"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__create_date
msgid "Created on"
msgstr "Creado el"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__display_name
msgid "Display Name"
msgstr "Mostrar Nombre"
#. module: fs_base_multi_image
#: model:ir.actions.act_window,name:fs_base_multi_image.fs_image_act_window
msgid "Fs Image"
msgstr "Imagen Fs"
#. module: fs_base_multi_image
#: model:ir.ui.menu,name:fs_base_multi_image.fs_image_menu
msgid "Fs Images"
msgstr "Imágenes Fs"
#. module: fs_base_multi_image
#: model_terms:ir.ui.view,arch_db:fs_base_multi_image.fs_image_search_view
msgid "Group By"
msgstr "Agrupar por"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__id
msgid "ID"
msgstr "ID (identificación)"
#. module: fs_base_multi_image
#: model:ir.model,name:fs_base_multi_image.model_fs_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__image
#: model_terms:ir.ui.view,arch_db:fs_base_multi_image.fs_image_form_view
msgid "Image"
msgstr "Imagen"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__image_medium
msgid "Image (128)"
msgstr "Imagen (128)"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__image
msgid "Image (original)"
msgstr "Imagen (original)"
#. module: fs_base_multi_image
#: model:res.groups,name:fs_base_multi_image.group_image_manager
msgid "Image Manager"
msgstr "Administrador de Imágenes"
#. module: fs_base_multi_image
#: model:ir.model,name:fs_base_multi_image.model_fs_image_relation_mixin
msgid "Image Relation"
msgstr "Relación de Imágenes"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__image_medium
msgid "Image medium"
msgstr "Imagen mediana"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image____last_update
msgid "Last Modified on"
msgstr "Última Modificación el"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__write_uid
msgid "Last Updated by"
msgstr "Última Actualización por"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__write_date
msgid "Last Updated on"
msgstr "Última Actualización el"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__link_existing
msgid "Link Existing"
msgstr "Enlace Existente"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__image_id
msgid "Linked image"
msgstr "Imagen vinculada"
#. module: fs_base_multi_image
#: model_terms:ir.ui.view,arch_db:fs_base_multi_image.fs_image_search_view
msgid "MimeType"
msgstr "Tipo Mimo"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__mimetype
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__mimetype
msgid "Mimetype"
msgstr "Tipo Mimo"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__name
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__name
#: model_terms:ir.ui.view,arch_db:fs_base_multi_image.fs_image_search_view
msgid "Name"
msgstr "Nombre"
#. module: fs_base_multi_image
#. odoo-javascript
#: code:addons/fs_base_multi_image/static/src/fields/fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.xml:0
#, python-format
msgid "Reusable images"
msgstr "Imágenes reutilizables"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__sequence
msgid "Sequence"
msgstr "Secuencia"
#. module: fs_base_multi_image
#. odoo-javascript
#: code:addons/fs_base_multi_image/static/src/fields/fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.xml:0
#, python-format
msgid "Specific"
msgstr "Específico"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__specific_image
msgid "Specific Image"
msgstr "Imagen Específica"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__specific_image_medium
msgid "Specific Image (128)"
msgstr "Imagen Específica (128)"
#. module: fs_base_multi_image
#. odoo-javascript
#: code:addons/fs_base_multi_image/static/src/fields/fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.xml:0
#, python-format
msgid "You can drag and drop images to create new records or"
msgstr "Puede arrastrar y soltar imágenes para crear nuevos registros o"
#. module: fs_base_multi_image
#. odoo-python
#: code:addons/fs_base_multi_image/models/fs_image_relation_mixin.py:0
#, python-format
msgid "You must set an image"
msgstr "Usted debe establecer una imagen"
#. module: fs_base_multi_image
#. odoo-javascript
#: code:addons/fs_base_multi_image/static/src/fields/fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.xml:0
#, python-format
msgid "click here"
msgstr "pulse aquí"
#. module: fs_base_multi_image
#. odoo-javascript
#: code:addons/fs_base_multi_image/static/src/fields/fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.xml:0
#, python-format
msgid "to select image files."
msgstr "para seleccionar archivos de imagen."
#, python-format
#~ msgid "Cannot set image on a linked image"
#~ msgstr "No se puede establecer la imagen en una imagen vinculada"
#~ msgid "Image 128"
#~ msgstr "Imagen 128"
#~ msgid "Specific Image 128"
#~ msgstr "Imagen Específica 128"

View file

@ -0,0 +1,201 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * fs_base_multi_image
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2024-07-12 10:58+0000\n"
"Last-Translator: \"Benjamin Willig (ACSONE)\" <benjamin.willig@acsone.eu>\n"
"Language-Team: none\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 5.6.2\n"
#. module: fs_base_multi_image
#. odoo-javascript
#: code:addons/fs_base_multi_image/static/src/fields/fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.esm.js:0
#, python-format
msgid "An error occurred during the images upload."
msgstr "Une erreur est apparue lors de l'upload des images."
#. module: fs_base_multi_image
#. odoo-javascript
#: code:addons/fs_base_multi_image/static/src/fields/fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.xml:0
#, python-format
msgid "Choose how you want to store the new images:"
msgstr "Choisissez où les images seront stockées :"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__create_uid
#: model_terms:ir.ui.view,arch_db:fs_base_multi_image.fs_image_search_view
msgid "Created by"
msgstr "Créé par"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__create_date
msgid "Created on"
msgstr "Crée le"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__display_name
msgid "Display Name"
msgstr "Nom affiché"
#. module: fs_base_multi_image
#: model:ir.actions.act_window,name:fs_base_multi_image.fs_image_act_window
msgid "Fs Image"
msgstr "Image FS"
#. module: fs_base_multi_image
#: model:ir.ui.menu,name:fs_base_multi_image.fs_image_menu
msgid "Fs Images"
msgstr "Images FS"
#. module: fs_base_multi_image
#: model_terms:ir.ui.view,arch_db:fs_base_multi_image.fs_image_search_view
msgid "Group By"
msgstr ""
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__id
msgid "ID"
msgstr ""
#. module: fs_base_multi_image
#: model:ir.model,name:fs_base_multi_image.model_fs_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__image
#: model_terms:ir.ui.view,arch_db:fs_base_multi_image.fs_image_form_view
msgid "Image"
msgstr "Image"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__image_medium
msgid "Image (128)"
msgstr "Image (128)"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__image
msgid "Image (original)"
msgstr "Image (originale)"
#. module: fs_base_multi_image
#: model:res.groups,name:fs_base_multi_image.group_image_manager
msgid "Image Manager"
msgstr "Gestionnaire des images"
#. module: fs_base_multi_image
#: model:ir.model,name:fs_base_multi_image.model_fs_image_relation_mixin
msgid "Image Relation"
msgstr ""
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__image_medium
msgid "Image medium"
msgstr "Image moyenne"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image____last_update
msgid "Last Modified on"
msgstr ""
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__write_uid
msgid "Last Updated by"
msgstr ""
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__write_date
msgid "Last Updated on"
msgstr ""
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__link_existing
msgid "Link Existing"
msgstr "Lier une image existante"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__image_id
msgid "Linked image"
msgstr "Image liée"
#. module: fs_base_multi_image
#: model_terms:ir.ui.view,arch_db:fs_base_multi_image.fs_image_search_view
msgid "MimeType"
msgstr ""
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__mimetype
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__mimetype
msgid "Mimetype"
msgstr ""
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__name
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__name
#: model_terms:ir.ui.view,arch_db:fs_base_multi_image.fs_image_search_view
msgid "Name"
msgstr "Nom"
#. module: fs_base_multi_image
#. odoo-javascript
#: code:addons/fs_base_multi_image/static/src/fields/fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.xml:0
#, python-format
msgid "Reusable images"
msgstr "Images réutilisables"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__sequence
msgid "Sequence"
msgstr "Séquence"
#. module: fs_base_multi_image
#. odoo-javascript
#: code:addons/fs_base_multi_image/static/src/fields/fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.xml:0
#, python-format
msgid "Specific"
msgstr "Spécifique"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__specific_image
msgid "Specific Image"
msgstr "Image Spécifique"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__specific_image_medium
msgid "Specific Image (128)"
msgstr "Image spécifique (128)"
#. module: fs_base_multi_image
#. odoo-javascript
#: code:addons/fs_base_multi_image/static/src/fields/fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.xml:0
#, python-format
msgid "You can drag and drop images to create new records or"
msgstr ""
"Vous pouvez glisser/déposer des images pour créer des nouveaux "
"enregistrements ou"
#. module: fs_base_multi_image
#. odoo-python
#: code:addons/fs_base_multi_image/models/fs_image_relation_mixin.py:0
#, python-format
msgid "You must set an image"
msgstr "Vous devez définir une image"
#. module: fs_base_multi_image
#. odoo-javascript
#: code:addons/fs_base_multi_image/static/src/fields/fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.xml:0
#, python-format
msgid "click here"
msgstr "cliquez ici"
#. module: fs_base_multi_image
#. odoo-javascript
#: code:addons/fs_base_multi_image/static/src/fields/fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.xml:0
#, python-format
msgid "to select image files."
msgstr "pour sélectionner des fichiers."

View file

@ -0,0 +1,196 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * fs_base_multi_image
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: fs_base_multi_image
#. odoo-javascript
#: code:addons/fs_base_multi_image/static/src/fields/fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.esm.js:0
#, python-format
msgid "An error occurred during the images upload."
msgstr ""
#. module: fs_base_multi_image
#. odoo-javascript
#: code:addons/fs_base_multi_image/static/src/fields/fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.xml:0
#, python-format
msgid "Choose how you want to store the new images:"
msgstr ""
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__create_uid
#: model_terms:ir.ui.view,arch_db:fs_base_multi_image.fs_image_search_view
msgid "Created by"
msgstr ""
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__create_date
msgid "Created on"
msgstr ""
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__display_name
msgid "Display Name"
msgstr ""
#. module: fs_base_multi_image
#: model:ir.actions.act_window,name:fs_base_multi_image.fs_image_act_window
msgid "Fs Image"
msgstr ""
#. module: fs_base_multi_image
#: model:ir.ui.menu,name:fs_base_multi_image.fs_image_menu
msgid "Fs Images"
msgstr ""
#. module: fs_base_multi_image
#: model_terms:ir.ui.view,arch_db:fs_base_multi_image.fs_image_search_view
msgid "Group By"
msgstr ""
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__id
msgid "ID"
msgstr ""
#. module: fs_base_multi_image
#: model:ir.model,name:fs_base_multi_image.model_fs_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__image
#: model_terms:ir.ui.view,arch_db:fs_base_multi_image.fs_image_form_view
msgid "Image"
msgstr ""
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__image_medium
msgid "Image (128)"
msgstr ""
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__image
msgid "Image (original)"
msgstr ""
#. module: fs_base_multi_image
#: model:res.groups,name:fs_base_multi_image.group_image_manager
msgid "Image Manager"
msgstr ""
#. module: fs_base_multi_image
#: model:ir.model,name:fs_base_multi_image.model_fs_image_relation_mixin
msgid "Image Relation"
msgstr ""
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__image_medium
msgid "Image medium"
msgstr ""
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image____last_update
msgid "Last Modified on"
msgstr ""
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__write_uid
msgid "Last Updated by"
msgstr ""
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__write_date
msgid "Last Updated on"
msgstr ""
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__link_existing
msgid "Link Existing"
msgstr ""
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__image_id
msgid "Linked image"
msgstr ""
#. module: fs_base_multi_image
#: model_terms:ir.ui.view,arch_db:fs_base_multi_image.fs_image_search_view
msgid "MimeType"
msgstr ""
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__mimetype
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__mimetype
msgid "Mimetype"
msgstr ""
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__name
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__name
#: model_terms:ir.ui.view,arch_db:fs_base_multi_image.fs_image_search_view
msgid "Name"
msgstr ""
#. module: fs_base_multi_image
#. odoo-javascript
#: code:addons/fs_base_multi_image/static/src/fields/fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.xml:0
#, python-format
msgid "Reusable images"
msgstr ""
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__sequence
msgid "Sequence"
msgstr ""
#. module: fs_base_multi_image
#. odoo-javascript
#: code:addons/fs_base_multi_image/static/src/fields/fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.xml:0
#, python-format
msgid "Specific"
msgstr ""
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__specific_image
msgid "Specific Image"
msgstr ""
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__specific_image_medium
msgid "Specific Image (128)"
msgstr ""
#. module: fs_base_multi_image
#. odoo-javascript
#: code:addons/fs_base_multi_image/static/src/fields/fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.xml:0
#, python-format
msgid "You can drag and drop images to create new records or"
msgstr ""
#. module: fs_base_multi_image
#. odoo-python
#: code:addons/fs_base_multi_image/models/fs_image_relation_mixin.py:0
#, python-format
msgid "You must set an image"
msgstr ""
#. module: fs_base_multi_image
#. odoo-javascript
#: code:addons/fs_base_multi_image/static/src/fields/fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.xml:0
#, python-format
msgid "click here"
msgstr ""
#. module: fs_base_multi_image
#. odoo-javascript
#: code:addons/fs_base_multi_image/static/src/fields/fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.xml:0
#, python-format
msgid "to select image files."
msgstr ""

View file

@ -0,0 +1,217 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * fs_base_multi_image
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2024-04-22 09:37+0000\n"
"Last-Translator: mymage <stefano.consolaro@mymage.it>\n"
"Language-Team: none\n"
"Language: it\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.17\n"
#. module: fs_base_multi_image
#. odoo-javascript
#: code:addons/fs_base_multi_image/static/src/fields/fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.esm.js:0
#, python-format
msgid "An error occurred during the images upload."
msgstr "Si è verificato un errore durante il caricamento dell'immagine."
#. module: fs_base_multi_image
#. odoo-javascript
#: code:addons/fs_base_multi_image/static/src/fields/fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.xml:0
#, python-format
msgid "Choose how you want to store the new images:"
msgstr "Scegliere come archiviare le nuove immagini:"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__create_uid
#: model_terms:ir.ui.view,arch_db:fs_base_multi_image.fs_image_search_view
msgid "Created by"
msgstr "Creato da"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__create_date
msgid "Created on"
msgstr "Creato il"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__display_name
msgid "Display Name"
msgstr "Nome visualizzato"
#. module: fs_base_multi_image
#: model:ir.actions.act_window,name:fs_base_multi_image.fs_image_act_window
msgid "Fs Image"
msgstr "Immagine FS"
#. module: fs_base_multi_image
#: model:ir.ui.menu,name:fs_base_multi_image.fs_image_menu
msgid "Fs Images"
msgstr "Immagini FS"
#. module: fs_base_multi_image
#: model_terms:ir.ui.view,arch_db:fs_base_multi_image.fs_image_search_view
msgid "Group By"
msgstr "Raggruppa per"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__id
msgid "ID"
msgstr "ID"
#. module: fs_base_multi_image
#: model:ir.model,name:fs_base_multi_image.model_fs_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__image
#: model_terms:ir.ui.view,arch_db:fs_base_multi_image.fs_image_form_view
msgid "Image"
msgstr "Immagine"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__image_medium
msgid "Image (128)"
msgstr "Immagine 128"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__image
msgid "Image (original)"
msgstr "Immagine (originale)"
#. module: fs_base_multi_image
#: model:res.groups,name:fs_base_multi_image.group_image_manager
msgid "Image Manager"
msgstr "Gestore immagine"
#. module: fs_base_multi_image
#: model:ir.model,name:fs_base_multi_image.model_fs_image_relation_mixin
msgid "Image Relation"
msgstr "Relazione immagine"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__image_medium
msgid "Image medium"
msgstr "Immagine media"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image____last_update
msgid "Last Modified on"
msgstr "Ultima modifica il"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__write_uid
msgid "Last Updated by"
msgstr "Ultimo aggiornamento di"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__write_date
msgid "Last Updated on"
msgstr "Ultimo aggiornamento il"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__link_existing
msgid "Link Existing"
msgstr "Collegamento esistente"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__image_id
msgid "Linked image"
msgstr "Immagine collegata"
#. module: fs_base_multi_image
#: model_terms:ir.ui.view,arch_db:fs_base_multi_image.fs_image_search_view
msgid "MimeType"
msgstr "Tipo MIME"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__mimetype
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__mimetype
msgid "Mimetype"
msgstr "Tipo MIME"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image__name
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__name
#: model_terms:ir.ui.view,arch_db:fs_base_multi_image.fs_image_search_view
msgid "Name"
msgstr "Nome"
#. module: fs_base_multi_image
#. odoo-javascript
#: code:addons/fs_base_multi_image/static/src/fields/fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.xml:0
#, python-format
msgid "Reusable images"
msgstr "Immagini riutilizzabili"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__sequence
msgid "Sequence"
msgstr "Sequenza"
#. module: fs_base_multi_image
#. odoo-javascript
#: code:addons/fs_base_multi_image/static/src/fields/fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.xml:0
#, python-format
msgid "Specific"
msgstr "Specifica"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__specific_image
msgid "Specific Image"
msgstr "Immagine specifica"
#. module: fs_base_multi_image
#: model:ir.model.fields,field_description:fs_base_multi_image.field_fs_image_relation_mixin__specific_image_medium
msgid "Specific Image (128)"
msgstr "Immagine specifica (128)"
#. module: fs_base_multi_image
#. odoo-javascript
#: code:addons/fs_base_multi_image/static/src/fields/fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.xml:0
#, python-format
msgid "You can drag and drop images to create new records or"
msgstr "Si possono trascinare e rilasciare immagini per creare nuovi record o"
#. module: fs_base_multi_image
#. odoo-python
#: code:addons/fs_base_multi_image/models/fs_image_relation_mixin.py:0
#, python-format
msgid "You must set an image"
msgstr "Bisogna impostare una immagine"
#. module: fs_base_multi_image
#. odoo-javascript
#: code:addons/fs_base_multi_image/static/src/fields/fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.xml:0
#, python-format
msgid "click here"
msgstr "fare clic qui"
#. module: fs_base_multi_image
#. odoo-javascript
#: code:addons/fs_base_multi_image/static/src/fields/fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.xml:0
#, python-format
msgid "to select image files."
msgstr "per selezionare file immagine."
#, python-format
#~ msgid "fs_image"
#~ msgstr "fs_image"
#, python-format
#~ msgid "specific"
#~ msgstr "specifica"
#, python-format
#~ msgid "Cannot set image on a linked image"
#~ msgstr "Non si può impostare una immagin in una immagine collegata"
#~ msgid "Image 128"
#~ msgstr "Immagine 128"
#~ msgid "Specific Image 128"
#~ msgstr "Immagine specifica 128"

View file

@ -0,0 +1,2 @@
from . import fs_image
from . import fs_image_relation_mixin

View file

@ -0,0 +1,29 @@
# Copyright 2023 ACSONE SA/NV
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
from odoo.addons.fs_image import fields as fs_fields
class FsImage(models.Model):
_name = "fs.image"
_inherit = "fs.image.mixin"
_description = "Image"
_order = "name, id"
_rec_name = "name"
image = fs_fields.FSImage(required=True) # makes field required
name = fields.Char(compute="_compute_name", store=True, index=True)
mimetype = fields.Char(compute="_compute_mimetype", store=True)
@api.depends("image")
def _compute_name(self):
for record in self:
record.name = record.image.name if record.image else None
@api.depends("image")
def _compute_mimetypes(self):
for record in self:
record.mimetype = record.image.mimetype if record.image else None

View file

@ -0,0 +1,104 @@
# Copyright 2023 ACSONE SA/NV
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from odoo.addons.fs_image import fields as fs_fields
class FsImageRelationMixin(models.AbstractModel):
_name = "fs.image.relation.mixin"
_description = "Image Relation"
_order = "sequence, name"
_rec_name = "name"
sequence = fields.Integer()
image_id = fields.Many2one(
comodel_name="fs.image",
string="Linked image",
)
specific_image = fs_fields.FSImage("Specific Image")
# resized fields stored (as attachment) for performance
specific_image_medium = fs_fields.FSImage(
"Specific Image (128)",
related="specific_image",
max_width=128,
max_height=128,
store=True,
)
link_existing = fields.Boolean(default=False)
image = fs_fields.FSImage(
"Image (original)",
compute="_compute_image",
inverse="_inverse_image",
store=False,
)
# resized fields stored (as attachment) for performance
image_medium = fs_fields.FSImage(
"Image (128)", compute="_compute_image_medium", store=False
)
name = fields.Char(compute="_compute_name", store=True, index=True)
mimetype = fields.Char(compute="_compute_mimetype", store=True)
@api.constrains("specific_image", "image_id")
def _check_image(self):
for record in self:
if not record.image_id and not record.specific_image:
raise ValidationError(_("You must set an image"))
@api.depends("image")
def _compute_name(self):
for record in self:
record.name = record.image.name if record.image else None
@api.depends("image")
def _compute_mimetypes(self):
for record in self:
record.mimetype = record.image.mimetype if record.image else None
@api.depends("image_id", "specific_image", "link_existing")
def _compute_image(self):
for record in self:
if record.link_existing:
record.image = record.image_id.image
else:
record.image = record.specific_image
@api.depends("image_id", "specific_image", "link_existing")
def _compute_image_medium(self):
for record in self:
if record.link_existing:
record.image_medium = record.image_id.image_medium
else:
record.image_medium = record.specific_image_medium
def _inverse_image(self):
for record in self:
if not record.link_existing:
record.specific_image = record.image
@api.model
def _cleanup_vals(self, vals):
link_existing = vals.get("link_existing")
if link_existing:
if "specific_image" in vals:
vals.pop("specific_image")
if "image" in vals:
# image is set when using the kanban renderer so it
# prevents the name field to be computed well
vals.pop("image")
return vals
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
self._cleanup_vals(vals)
return super().create(vals_list)
def write(self, vals):
self._cleanup_vals(vals)
return super().write(vals)

View file

@ -0,0 +1 @@
* Laurent Mignon <laurent.mignon@acsone.eu>

View file

@ -0,0 +1,14 @@
This addon is a technical addon providing a set of models to ease the
creation of other models that need to be linked to multiple images stored
into external filesystems.
The models provided by this addon are:
* ``fs.image``: a model that stores a reference to an image stored into
an external filesystem.
* ``fs.image.relation.mixin``: an abstract model that can be used to
as base class for models created to store an image linked to a model.
This abstract model defines fields and methods to transparently handle
2 cases:
* the image is specific to the model.
* the image is shared between multiple models and therefore is a ``fs.image`` instance linked to the mixin.

View file

@ -0,0 +1,2 @@
* Add dedicated widget to ease the addition of new images to a model linked to
multiple images. (As it's the case in the *storage_image_product* addon)

View file

@ -0,0 +1,3 @@
To be able to create and or manages shared images, you must have the ``Image Manager``
role. If you do not have this role, as an authenticated user, you can
only view the shared images.

View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2023 ACSONE SA/NV
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo>
<record model="ir.model.access" id="fs_image_access_read">
<field name="name">fs.image access read</field>
<field name="model_id" ref="model_fs_image" />
<field name="group_id" ref="base.group_user" />
<field name="perm_read" eval="1" />
<field name="perm_create" eval="0" />
<field name="perm_write" eval="0" />
<field name="perm_unlink" eval="0" />
</record>
<record model="ir.model.access" id="fs_image_access_manage">
<field name="name">fs.image access manage</field>
<field name="model_id" ref="model_fs_image" />
<field name="group_id" ref="group_image_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" />
</record>
</odoo>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record model="res.groups" id="group_image_manager">
<field name="name">Image Manager</field>
<field
name="users"
eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"
/>
</record>
</odoo>

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View file

@ -0,0 +1,456 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>Fs Base Multi Image</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: 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="fs-base-multi-image">
<h1 class="title">Fs Base Multi Image</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:156cdd22cfc76b0f6518758875151f309bc244f3166cc2ee4bf49026317e4348
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Alpha" src="https://img.shields.io/badge/maturity-Alpha-red.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/storage/tree/16.0/fs_base_multi_image"><img alt="OCA/storage" src="https://img.shields.io/badge/github-OCA%2Fstorage-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/storage-16-0/storage-16-0-fs_base_multi_image"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/storage&amp;target_branch=16.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This addon is a technical addon providing a set of models to ease the
creation of other models that need to be linked to multiple images stored
into external filesystems.</p>
<p>The models provided by this addon are:</p>
<ul class="simple">
<li><tt class="docutils literal">fs.image</tt>: a model that stores a reference to an image stored into
an external filesystem.</li>
<li><tt class="docutils literal">fs.image.relation.mixin</tt>: an abstract model that can be used to
as base class for models created to store an image linked to a model.
This abstract model defines fields and methods to transparently handle
2 cases:
* the image is specific to the model.
* the image is shared between multiple models and therefore is a <tt class="docutils literal">fs.image</tt> instance linked to the mixin.</li>
</ul>
<div class="admonition important">
<p class="first admonition-title">Important</p>
<p class="last">This is an alpha version, the data model and design can change at any time without warning.
Only for development or testing purpose, do not use in production.
<a class="reference external" href="https://odoo-community.org/page/development-status">More details on development status</a></p>
</div>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#usage" id="toc-entry-1">Usage</a></li>
<li><a class="reference internal" href="#known-issues-roadmap" id="toc-entry-2">Known issues / Roadmap</a></li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-3">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-4">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-5">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-6">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-7">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#toc-entry-1">Usage</a></h1>
<p>To be able to create and or manages shared images, you must have the <tt class="docutils literal">Image Manager</tt>
role. If you do not have this role, as an authenticated user, you can
only view the shared images.</p>
</div>
<div class="section" id="known-issues-roadmap">
<h1><a class="toc-backref" href="#toc-entry-2">Known issues / Roadmap</a></h1>
<ul class="simple">
<li>Add dedicated widget to ease the addition of new images to a model linked to
multiple images. (As its the case in the <em>storage_image_product</em> addon)</li>
</ul>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#toc-entry-3">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/storage/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/storage/issues/new?body=module:%20fs_base_multi_image%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#toc-entry-4">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#toc-entry-5">Authors</a></h2>
<ul class="simple">
<li>ACSONE SA/NV</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#toc-entry-6">Contributors</a></h2>
<ul class="simple">
<li>Laurent Mignon &lt;<a class="reference external" href="mailto:laurent.mignon&#64;acsone.eu">laurent.mignon&#64;acsone.eu</a>&gt;</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#toc-entry-7">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>Current <a class="reference external" href="https://odoo-community.org/page/maintainer-role">maintainer</a>:</p>
<p><a class="reference external image-reference" href="https://github.com/lmignon"><img alt="lmignon" src="https://github.com/lmignon.png?size=40px" /></a></p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/storage/tree/16.0/fs_base_multi_image">OCA/storage</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,232 @@
/** @odoo-module **/
import {blockUI, unblockUI} from "web.framework";
import {onWillRender, useRef, useState} from "@odoo/owl";
import {X2ManyField} from "@web/views/fields/x2many/x2many_field";
import {registry} from "@web/core/registry";
import {useX2ManyCrud} from "@web/views/fields/relational_utils";
export class FsImageRelationDndUploadField extends X2ManyField {
/**
* When using this widget, displayed image relation views must contains
* following fields:
* - sequence
* - image_id
* - specific_image
* - link_existing
*/
setup() {
super.setup();
this.options = this.activeField.options;
this.defaultTarget = this.options.target || "specific";
this.state = useState({
dragging: false,
target: this.defaultTarget,
});
this.fileInput = useRef("fileInput");
this.defaultSequence = 0;
this.operations = useX2ManyCrud(() => this.list, this.isMany2Many);
onWillRender(() => {
this.initDefaultSequence();
});
}
get targetImage() {
return this.state.target;
}
get relationRecordId() {
return this.props.record.data.id;
}
get displayDndZone() {
const activeActions = this.activeActions;
return (
("link" in activeActions ? activeActions.link : activeActions.create) &&
!this.props.readonly
);
}
initDefaultSequence() {
let sequence = 0;
_.each(this.props.value.records, (record) => {
sequence = record.data.sequence;
if (sequence >= this.defaultSequence) {
this.defaultSequence = sequence + 1;
}
});
}
getNewSequence() {
const sequence = this.defaultSequence;
this.defaultSequence += 1;
return sequence;
}
setDragging() {
this.state.dragging = true;
}
setNotDragging() {
this.state.dragging = false;
}
onDragEnter(ev) {
ev.preventDefault();
this.setDragging();
}
onDragLeave(ev) {
ev.preventDefault();
this.setNotDragging();
}
onClickSelectDocuments(ev) {
ev.preventDefault();
this.fileInput.el.click();
}
onDrop(ev) {
ev.preventDefault();
this.setNotDragging();
this.uploadImages(ev.dataTransfer.files);
}
onFilesSelected(ev) {
ev.preventDefault();
this.uploadImages(ev.target.files);
}
onChangeImageTarget(ev) {
this.state.target = ev.target.value;
}
async uploadFsImage(imagesDesc) {
const self = this;
self.env.model.orm
.call("fs.image", "create", [imagesDesc])
.then((fsImageIds) => {
let values = {};
_.each(fsImageIds, (fsImageId) => {
values = self.getFsImageRelationValues(fsImageId);
self.createFieldRelationRecords(values);
});
unblockUI();
})
.catch(() => {
self.displayUploadError();
});
}
displayUploadError() {
unblockUI();
this.env.services.notification.add(
this.env._t("An error occurred during the images upload."),
{
type: "danger",
sticky: true,
}
);
}
getFsImageRelationValues(fsImageId) {
let values = {
default_image_id: fsImageId,
default_link_existing: true,
};
values = {...values, ...this.getRelationCommonValues()};
return values;
}
async uploadSpecificImage(imagesDesc) {
const self = this;
_.each(imagesDesc, (imageDesc) => {
self.createFieldRelationRecords(
self.getSpecificImageRelationValues(imageDesc)
);
});
unblockUI();
}
getSpecificImageRelationValues(imageDesc) {
return {
...this.getRelationCommonValues(),
default_specific_image: imageDesc.image,
};
}
getRelationCommonValues() {
return {
default_sequence: this.getNewSequence(),
};
}
async createFieldRelationRecords(createValues) {
await this.list.addNew({
position: "bottom",
context: createValues,
mode: "readonly",
allowWarning: true,
});
}
async uploadImages(files) {
const self = this;
const promises = [];
blockUI();
_.each(files, function (file) {
if (!file.type.includes("image")) {
return;
}
const filePromise = new Promise(function (resolve) {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = function (upload) {
let data = upload.target.result;
data = data.split(",")[1];
resolve([file.name, data]);
};
});
promises.push(filePromise);
});
return Promise.all(promises).then(function (fileContents) {
const imagesDesc = [];
_.each(fileContents, function (fileContent) {
imagesDesc.push(self.getFileImageDesc(fileContent));
});
if (imagesDesc.length > 0) {
switch (self.targetImage) {
case "fs_image":
self.uploadFsImage(imagesDesc);
break;
case "specific":
self.uploadSpecificImage(imagesDesc);
break;
default:
unblockUI();
}
} else {
unblockUI();
}
});
}
getFileImageDesc(fileContent) {
return {
image: {
filename: fileContent[0],
content: fileContent[1],
},
};
}
}
FsImageRelationDndUploadField.template = "web.FsImageRelationDndUploadField";
registry
.category("fields")
.add("fs_image_relation_dnd_upload", FsImageRelationDndUploadField);

View file

@ -0,0 +1,38 @@
.o_field_x2many {
.dnd-zone {
display: table;
padding: 10px;
width: 90%;
margin: auto;
margin-top: 5px;
min-height: 100px;
border: 1px solid transparent;
border-color: var(--notebook-link-border-color, transparent);
text-align: center;
&.dragging-inside {
outline: 2px dashed #a096be;
outline-offset: -10px;
}
.row {
display: table-row;
div {
vertical-align: middle;
display: table-cell;
text-align: center;
select[name="fs_image_target"] {
display: inline-block;
width: fit-content;
color: #01666b;
}
}
}
input[name="files"] {
display: none;
}
}
}

View file

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t
t-name="web.FsImageRelationDndUploadField"
t-inherit="web.X2ManyField"
t-inherit-mode="primary"
owl="1"
>
<xpath expr="//div[hasclass('o_x2m_control_panel')]" position="before">
<t t-if="displayDndZone">
<div
t-attf-class="dnd-zone {{state.dragging ? 'dragging-inside' : ''}}"
t-on-drop="ev => this.onDrop(ev)"
t-on-dragenter="ev => this.onDragEnter(ev)"
t-on-dragover="ev => this.onDragEnter(ev)"
t-on-dragleave="ev => this.onDragLeave(ev)"
>
<div class="row">
<div>
You can drag and drop images to create new records or <a
href="#"
t-on-click="onClickSelectDocuments"
>click here</a> to select image files.
</div>
</div>
<div class="row" name="target_selection">
<div>
Choose how you want to store the new images:
<select
name="fs_image_target"
t-on-change="onChangeImageTarget"
class="o_input pe-3"
>
<option
value="fs_image"
t-att-selected="state.target == 'fs_image'"
>Reusable images</option>
<option
value="specific"
t-att-selected="state.target == 'specific'"
>Specific</option>
</select>
</div>
</div>
<input
t-ref="fileInput"
type="file"
name="files"
multiple="true"
t-on-change="onFilesSelected"
accept="image/*"
/>
</div>
</t>
</xpath>
</t>
</templates>

View file

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2023 ACSONE SA/NV
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo>
<record model="ir.ui.view" id="fs_image_form_view">
<field name="name">fs.image.form (in fs_base_multi_image)</field>
<field name="model">fs.image</field>
<field name="arch" type="xml">
<form>
<sheet>
<label for="name" />
<h1>
<field name="name" />
</h1>
<group>
<group>
<field name="image" string="Image" />
</group>
</group>
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="fs_image_search_view">
<field name="name">fs.image.search (in fs_base_multi_image)</field>
<field name="model">fs.image</field>
<field name="arch" type="xml">
<search>
<field
name="name"
filter_domain="[('name','ilike',self)]"
string="Name"
/>
<separator />
<field name="create_uid" string="Created by" />
<field name="mimetype" />
<group expand="0" string="Group By">
<filter
string="MimeType"
name="mimetype"
domain="[]"
context="{'group_by':'mimetype'}"
/>
</group>
</search>
</field>
</record>
<record model="ir.ui.view" id="fs_image_tree_view">
<field name="name">fs.image.tree (in fs_base_multi_image)</field>
<field name="model">fs.image</field>
<field name="arch" type="xml">
<tree>
<field name="name" />
<field name="create_uid" />
<field name="create_date" />
</tree>
</field>
</record>
<record model="ir.actions.act_window" id="fs_image_act_window">
<field name="name">Fs Image</field>
<field name="res_model">fs.image</field>
<field name="view_mode">tree,form</field>
<field name="domain">[]</field>
<field name="context">{}</field>
</record>
<record model="ir.ui.menu" id="fs_image_menu">
<field name="name">Fs Images</field>
<field name="parent_id" ref="fs_storage.menu_storage" />
<field name="action" ref="fs_image_act_window" />
</record>
</odoo>

View file

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2023 ACSONE SA/NV
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo>
<record model="ir.ui.view" id="fs_image_relation_mixin_form_view">
<field name="name">fs.image.relation.mixin.form</field>
<field name="model">fs.image.relation.mixin</field>
<field name="arch" type="xml">
<form>
<sheet>
<field
name="image"
class="oe_avatar"
options="{'preview_image': 'image_medium', 'zoom': true}"
readonly="1"
attrs="{'invisible': [('link_existing', '=', False)]}"
/>
<field
name="specific_image"
class="oe_avatar"
options="{'preview_image': 'image_medium', 'zoom': true}"
attrs="{'invisible': [('link_existing', '=', True)]}"
/>
<group>
<field name="link_existing" />
<field name="name" />
<field name="sequence" />
<field
name="image_id"
attrs="{'invisible': [('link_existing', '=', False)]}"
/>
</group>
<group name="extra">
<!-- Add here custom relation fields -->
</group>
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="fs_image_relation_mixin_kanban_view">
<field name="name">fs.image.relation.mixin.kanban</field>
<field name="model">fs.image.relation.mixin</field>
<field name="arch" type="xml">
<kanban>
<field name="image" />
<field name="sequence" />
<field name="image_id" />
<field name="specific_image" />
<field name="link_existing" />
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_card oe_kanban_global_click">
<a
type="delete"
style="position: absolute; right: 0; padding: 4px; display: inline-block"
>X</a>
<div class="o_kanban_image me-1" />
<div class="oe_kanban_details">
<div class="o_kanban_record_top mb-0">
<div class="o_kanban_record_headings">
<strong class="o_kanban_record_title">
<field name="name" />
</strong>
</div>
</div>
<div name="extra" class="mt-1" />
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
</odoo>

View file

@ -0,0 +1,43 @@
[project]
name = "odoo-bringout-oca-storage-fs_base_multi_image"
version = "16.0.0"
description = "Fs Base Multi Image -
Mulitple Images from External File System"
authors = [
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
]
dependencies = [
"odoo-bringout-oca-storage-fs_image>=16.0.0",
"requests>=2.25.1"
]
readme = "README.md"
requires-python = ">= 3.11"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Office/Business",
]
[project.urls]
homepage = "https://github.com/bringout/0"
repository = "https://github.com/bringout/0"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.metadata]
allow-direct-references = true
[tool.hatch.build.targets.wheel]
packages = ["fs_base_multi_image"]
[tool.rye]
managed = true
dev-dependencies = [
"pytest>=8.4.1",
]

View file

@ -0,0 +1,46 @@
# Fs Base Multi Media
Odoo addon: fs_base_multi_media
## Installation
```bash
pip install odoo-bringout-oca-storage-fs_base_multi_media
```
## Dependencies
This addon depends on:
- fs_file
## Manifest Information
- **Name**: Fs Base Multi Media
- **Version**: 16.0.1.0.1
- **Category**: N/A
- **License**: AGPL-3
- **Installable**: False
## Source
Based on [OCA/storage](https://github.com/OCA/storage) branch 16.0, addon `fs_base_multi_media`.
## License
This package maintains the original AGPL-3 license from the upstream Odoo project.
## Documentation
- Overview: doc/OVERVIEW.md
- Architecture: doc/ARCHITECTURE.md
- Models: doc/MODELS.md
- Controllers: doc/CONTROLLERS.md
- Wizards: doc/WIZARDS.md
- Reports: doc/REPORTS.md
- Security: doc/SECURITY.md
- Install: doc/INSTALL.md
- Usage: doc/USAGE.md
- Configuration: doc/CONFIGURATION.md
- Dependencies: doc/DEPENDENCIES.md
- Troubleshooting: doc/TROUBLESHOOTING.md
- FAQ: doc/FAQ.md

View file

@ -0,0 +1,32 @@
# Architecture
```mermaid
flowchart TD
U[Users] -->|HTTP| V[Views and QWeb Templates]
V --> C[Controllers]
V --> W[Wizards Transient Models]
C --> M[Models and ORM]
W --> M
M --> R[Reports]
DX[Data XML] --> M
S[Security ACLs and Groups] -. enforces .-> M
subgraph Fs_base_multi_media Module - fs_base_multi_media
direction LR
M:::layer
W:::layer
C:::layer
V:::layer
R:::layer
S:::layer
DX:::layer
end
classDef layer fill:#eef8ff,stroke:#6ea8fe,stroke-width:1px
```
Notes
- Views include tree/form/kanban templates and report templates.
- Controllers provide website/portal routes when present.
- Wizards are UI flows implemented with `models.TransientModel`.
- Data XML loads data/demo records; Security defines groups and access.

View file

@ -0,0 +1,3 @@
# Configuration
Refer to Odoo settings for fs_base_multi_media. Configure related models, access rights, and options as needed.

View file

@ -0,0 +1,3 @@
# Controllers
This module does not define custom HTTP controllers.

View file

@ -0,0 +1,5 @@
# Dependencies
This addon depends on:
- [fs_file](../../odoo-bringout-oca-storage-fs_file)

View file

@ -0,0 +1,4 @@
# FAQ
- Q: Which Odoo version? A: 16.0 (OCA/OCB packaged).
- Q: How to enable? A: Start server with --addon fs_base_multi_media or install in UI.

View file

@ -0,0 +1,7 @@
# Install
```bash
pip install odoo-bringout-oca-storage-fs_base_multi_media"
# or
uv pip install odoo-bringout-oca-storage-fs_base_multi_media"
```

View file

@ -0,0 +1,14 @@
# Models
Detected core models and extensions in fs_base_multi_media.
```mermaid
classDiagram
class fs_media
class fs_media_relation_mixin
class fs_media_type
```
Notes
- Classes show model technical names; fields omitted for brevity.
- Items listed under _inherit are extensions of existing models.

View file

@ -0,0 +1,6 @@
# Overview
Packaged Odoo addon: fs_base_multi_media. Provides features documented in upstream Odoo 16 under this addon.
- Source: OCA/OCB 16.0, addon fs_base_multi_media
- License: LGPL-3

View file

@ -0,0 +1,3 @@
# Reports
This module does not define custom reports.

View file

@ -0,0 +1,80 @@
# Security
Access control and security definitions in fs_base_multi_media.
## Access Control Lists (ACLs)
Model access permissions defined in:
- **[all_odoo_addons_repos.txt](../all_odoo_addons_repos.txt)**
- 318 model access rules
- **[bosnian_translations.json](../bosnian_translations.json)**
- 50 model access rules
- **[bosnian_translations_output.json](../bosnian_translations_output.json)**
- 444 model access rules
- **[CHANGELOG.md](../CHANGELOG.md)**
- 132 model access rules
- **[delete_all_odoo_addons.sh](../delete_all_odoo_addons.sh)**
- 50 model access rules
- **[delete_odoo_addons.sh](../delete_odoo_addons.sh)**
- 44 model access rules
- **[doc](../doc)**
- **[docker](../docker)**
- **[input](../input)**
- **[nix](../nix)**
- **[odoo.conf](../odoo.conf)**
- 58 model access rules
- **[odoo_packages_bez_l10n.txt](../odoo_packages_bez_l10n.txt)**
- 1947 model access rules
- **[odoo_packages_bringout.txt](../odoo_packages_bringout.txt)**
- 1947 model access rules
- **[odoo_packages.txt](../odoo_packages.txt)**
- 2085 model access rules
- **[output](../output)**
- **[packages](../packages)**
- **[PACKAGES.md](../PACKAGES.md)**
- 298 model access rules
- **[README.md](../README.md)**
- 338 model access rules
- **[scripts](../scripts)**
- **[temp](../temp)**
- **[TRANSLATION_BS_SUMMARY.md](../TRANSLATION_BS_SUMMARY.md)**
- 146 model access rules
- **[verify_deletions.sh](../verify_deletions.sh)**
- 55 model access rules
## Record Rules
Row-level security rules defined in:
## Security Groups & Configuration
Security groups and permissions defined in:
- **[fs_media_type.xml](../fs_base_multi_media/security/fs_media_type.xml)**
- **[fs_media.xml](../fs_base_multi_media/security/fs_media.xml)**
- **[res_groups.xml](../fs_base_multi_media/security/res_groups.xml)**
- 1 security groups defined
```mermaid
graph TB
subgraph "Security Layers"
A[Users] --> B[Groups]
B --> C[Access Control Lists]
C --> D[Models]
B --> E[Record Rules]
E --> F[Individual Records]
end
```
Security files overview:
- **[fs_media_type.xml](../fs_base_multi_media/security/fs_media_type.xml)**
- Security groups, categories, and XML-based rules
- **[fs_media.xml](../fs_base_multi_media/security/fs_media.xml)**
- Security groups, categories, and XML-based rules
- **[res_groups.xml](../fs_base_multi_media/security/res_groups.xml)**
- Security groups, categories, and XML-based rules
Notes
- Access Control Lists define which groups can access which models
- Record Rules provide row-level security (filter records by user/group)
- Security groups organize users and define permission sets
- All security is enforced at the ORM level by Odoo

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