mirror of
https://github.com/bringout/oca-storage.git
synced 2026-04-18 05:32:01 +02:00
Initial commit: OCA Storage packages (17 packages)
This commit is contained in:
commit
7a380f05d3
659 changed files with 41828 additions and 0 deletions
63
README.md
Normal file
63
README.md
Normal 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.
|
||||||
46
odoo-bringout-oca-storage-fs_attachment/README.md
Normal file
46
odoo-bringout-oca-storage-fs_attachment/README.md
Normal 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
|
||||||
32
odoo-bringout-oca-storage-fs_attachment/doc/ARCHITECTURE.md
Normal file
32
odoo-bringout-oca-storage-fs_attachment/doc/ARCHITECTURE.md
Normal 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.
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Configuration
|
||||||
|
|
||||||
|
Refer to Odoo settings for fs_attachment. Configure related models, access rights, and options as needed.
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Controllers
|
||||||
|
|
||||||
|
This module does not define custom HTTP controllers.
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
# Dependencies
|
||||||
|
|
||||||
|
This addon depends on:
|
||||||
|
|
||||||
|
- [fs_storage](../../odoo-bringout-oca-storage-fs_storage)
|
||||||
4
odoo-bringout-oca-storage-fs_attachment/doc/FAQ.md
Normal file
4
odoo-bringout-oca-storage-fs_attachment/doc/FAQ.md
Normal 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.
|
||||||
7
odoo-bringout-oca-storage-fs_attachment/doc/INSTALL.md
Normal file
7
odoo-bringout-oca-storage-fs_attachment/doc/INSTALL.md
Normal 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"
|
||||||
|
```
|
||||||
17
odoo-bringout-oca-storage-fs_attachment/doc/MODELS.md
Normal file
17
odoo-bringout-oca-storage-fs_attachment/doc/MODELS.md
Normal 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.
|
||||||
6
odoo-bringout-oca-storage-fs_attachment/doc/OVERVIEW.md
Normal file
6
odoo-bringout-oca-storage-fs_attachment/doc/OVERVIEW.md
Normal 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
|
||||||
3
odoo-bringout-oca-storage-fs_attachment/doc/REPORTS.md
Normal file
3
odoo-bringout-oca-storage-fs_attachment/doc/REPORTS.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Reports
|
||||||
|
|
||||||
|
This module does not define custom reports.
|
||||||
73
odoo-bringout-oca-storage-fs_attachment/doc/SECURITY.md
Normal file
73
odoo-bringout-oca-storage-fs_attachment/doc/SECURITY.md
Normal 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
|
||||||
|
|
@ -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.
|
||||||
7
odoo-bringout-oca-storage-fs_attachment/doc/USAGE.md
Normal file
7
odoo-bringout-oca-storage-fs_attachment/doc/USAGE.md
Normal 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
|
||||||
|
```
|
||||||
3
odoo-bringout-oca-storage-fs_attachment/doc/WIZARDS.md
Normal file
3
odoo-bringout-oca-storage-fs_attachment/doc/WIZARDS.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Wizards
|
||||||
|
|
||||||
|
This module does not include UI wizards.
|
||||||
455
odoo-bringout-oca-storage-fs_attachment/fs_attachment/README.rst
Normal file
455
odoo-bringout-oca-storage-fs_attachment/fs_attachment/README.rst
Normal 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.
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
from . import models
|
||||||
|
from .hooks import pre_init_hook
|
||||||
|
|
@ -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",
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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")
|
||||||
419
odoo-bringout-oca-storage-fs_attachment/fs_attachment/i18n/bs.po
Normal file
419
odoo-bringout-oca-storage-fs_attachment/fs_attachment/i18n/bs.po
Normal 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 ""
|
||||||
506
odoo-bringout-oca-storage-fs_attachment/fs_attachment/i18n/es.po
Normal file
506
odoo-bringout-oca-storage-fs_attachment/fs_attachment/i18n/es.po
Normal 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."
|
||||||
420
odoo-bringout-oca-storage-fs_attachment/fs_attachment/i18n/fr.po
Normal file
420
odoo-bringout-oca-storage-fs_attachment/fs_attachment/i18n/fr.po
Normal 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 ""
|
||||||
|
|
@ -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 ""
|
||||||
499
odoo-bringout-oca-storage-fs_attachment/fs_attachment/i18n/it.po
Normal file
499
odoo-bringout-oca-storage-fs_attachment/fs_attachment/i18n/it.po
Normal 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."
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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),),
|
||||||
|
)
|
||||||
|
|
@ -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
|
|
@ -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
|
||||||
|
|
@ -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.",
|
||||||
|
)
|
||||||
|
|
@ -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.",
|
||||||
|
)
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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>`_)
|
||||||
|
|
@ -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).
|
||||||
0
odoo-bringout-oca-storage-fs_attachment/fs_attachment/readme/newsfragments/.gitignore
vendored
Normal file
0
odoo-bringout-oca-storage-fs_attachment/fs_attachment/readme/newsfragments/.gitignore
vendored
Normal 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 |
|
|
@ -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&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 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….</p>
|
||||||
|
<p>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 <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:
|
||||||
|
‘<name-without-extension>-<attachment-id>-<version>.<extension>’</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 Odoo’s
|
||||||
|
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 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.</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 & 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 > Technical > 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 attachment’s 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 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 <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,
|
||||||
|
you’ll 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 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.</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 < 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.</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 doesn’t 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 doesn’t 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 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.</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/<att.id>/<att.name><att.extension>”
|
||||||
|
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/<paht_in_storage>/<att.name>-<att.id>-<version><att.extension></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/<paht_in_storage>/<att.name>-<att.id>-<version><att.extension></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={"endpoint_url"</span><span class="o">:</span><span class="w"> </span><span class="s">"https://my_s3_server/"</span><span class="na">, "key"</span><span class="o">:</span><span class="w"> </span><span class="s">"KEY"</span><span class="na">, "secret"</span><span class="o">:</span><span class="w"> </span><span class="s">"SECRET"</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, it’s 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">"name"</span><span class="p">:</span> <span class="s2">"test.txt"</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">"wb"</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">"content"</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">"name"</span><span class="p">:</span> <span class="s2">"test.txt"</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">"wb"</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">"content"</span><span class="p">)</span>
|
||||||
|
</pre>
|
||||||
|
<p>It’s 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">"name"</span><span class="p">:</span> <span class="s2">"test.txt"</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">"w"</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">";"</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 & 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 don’t 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 <<a class="reference external" href="mailto:thierry.ducrest@camptocamp.com">thierry.ducrest@camptocamp.com</a>>
|
||||||
|
Guewen Baconnier <<a class="reference external" href="mailto:guewen.baconnier@camptocamp.com">guewen.baconnier@camptocamp.com</a>>
|
||||||
|
Julien Coux <<a class="reference external" href="mailto:julien.coux@camptocamp.com">julien.coux@camptocamp.com</a>>
|
||||||
|
Akim Juillerat <<a class="reference external" href="mailto:akim.juillerat@camptocamp.com">akim.juillerat@camptocamp.com</a>>
|
||||||
|
Thomas Nowicki <<a class="reference external" href="mailto:thomas.nowicki@camptocamp.com">thomas.nowicki@camptocamp.com</a>>
|
||||||
|
Vincent Renaville <<a class="reference external" href="mailto:vincent.renaville@camptocamp.com">vincent.renaville@camptocamp.com</a>>
|
||||||
|
Denis Leemann <<a class="reference external" href="mailto:denis.leemann@camptocamp.com">denis.leemann@camptocamp.com</a>>
|
||||||
|
Patrick Tombez <<a class="reference external" href="mailto:patrick.tombez@camptocamp.com">patrick.tombez@camptocamp.com</a>>
|
||||||
|
Don Kendall <<a class="reference external" href="mailto:kendall@donkendall.com">kendall@donkendall.com</a>>
|
||||||
|
Stephane Mangin <<a class="reference external" href="mailto:stephane.mangin@camptocamp.com">stephane.mangin@camptocamp.com</a>>
|
||||||
|
Laurent Mignon <<a class="reference external" href="mailto:laurent.mignon@acsone.eu">laurent.mignon@acsone.eu</a>>
|
||||||
|
Marie Lejeune <<a class="reference external" href="mailto:marie.lejeune@acsone.eu">marie.lejeune@acsone.eu</a>>
|
||||||
|
Wolfgang Pichler <<a class="reference external" href="mailto:wpichler@callino.at">wpichler@callino.at</a>>
|
||||||
|
Nans Lefebvre <<a class="reference external" href="mailto:len@lambdao.dev">len@lambdao.dev</a>>
|
||||||
|
Mohamed Alkobrosli <<a class="reference external" href="mailto:alkobroslymohamed@gmail.com">alkobroslymohamed@gmail.com</a>></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>
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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"""
|
||||||
|
|
@ -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),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
|
@ -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>
|
||||||
42
odoo-bringout-oca-storage-fs_attachment/pyproject.toml
Normal file
42
odoo-bringout-oca-storage-fs_attachment/pyproject.toml
Normal 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",
|
||||||
|
]
|
||||||
46
odoo-bringout-oca-storage-fs_base_multi_image/README.md
Normal file
46
odoo-bringout-oca-storage-fs_base_multi_image/README.md
Normal 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
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Configuration
|
||||||
|
|
||||||
|
Refer to Odoo settings for fs_base_multi_image. Configure related models, access rights, and options as needed.
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Controllers
|
||||||
|
|
||||||
|
This module does not define custom HTTP controllers.
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
# Dependencies
|
||||||
|
|
||||||
|
This addon depends on:
|
||||||
|
|
||||||
|
- [fs_image](../../odoo-bringout-oca-storage-fs_image)
|
||||||
4
odoo-bringout-oca-storage-fs_base_multi_image/doc/FAQ.md
Normal file
4
odoo-bringout-oca-storage-fs_base_multi_image/doc/FAQ.md
Normal 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.
|
||||||
|
|
@ -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"
|
||||||
|
```
|
||||||
14
odoo-bringout-oca-storage-fs_base_multi_image/doc/MODELS.md
Normal file
14
odoo-bringout-oca-storage-fs_base_multi_image/doc/MODELS.md
Normal 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.
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Reports
|
||||||
|
|
||||||
|
This module does not define custom reports.
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Wizards
|
||||||
|
|
||||||
|
This module does not include UI wizards.
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
from . import models
|
||||||
|
|
@ -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",
|
||||||
|
}
|
||||||
|
|
@ -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."
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -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."
|
||||||
|
|
@ -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 ""
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
from . import fs_image
|
||||||
|
from . import fs_image_relation_mixin
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
* Laurent Mignon <laurent.mignon@acsone.eu>
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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 |
|
|
@ -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&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 it’s 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 <<a class="reference external" href="mailto:laurent.mignon@acsone.eu">laurent.mignon@acsone.eu</a>></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>
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
43
odoo-bringout-oca-storage-fs_base_multi_image/pyproject.toml
Normal file
43
odoo-bringout-oca-storage-fs_base_multi_image/pyproject.toml
Normal 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",
|
||||||
|
]
|
||||||
46
odoo-bringout-oca-storage-fs_base_multi_media/README.md
Normal file
46
odoo-bringout-oca-storage-fs_base_multi_media/README.md
Normal 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
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Configuration
|
||||||
|
|
||||||
|
Refer to Odoo settings for fs_base_multi_media. Configure related models, access rights, and options as needed.
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Controllers
|
||||||
|
|
||||||
|
This module does not define custom HTTP controllers.
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
# Dependencies
|
||||||
|
|
||||||
|
This addon depends on:
|
||||||
|
|
||||||
|
- [fs_file](../../odoo-bringout-oca-storage-fs_file)
|
||||||
4
odoo-bringout-oca-storage-fs_base_multi_media/doc/FAQ.md
Normal file
4
odoo-bringout-oca-storage-fs_base_multi_media/doc/FAQ.md
Normal 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.
|
||||||
|
|
@ -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"
|
||||||
|
```
|
||||||
14
odoo-bringout-oca-storage-fs_base_multi_media/doc/MODELS.md
Normal file
14
odoo-bringout-oca-storage-fs_base_multi_media/doc/MODELS.md
Normal 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.
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Reports
|
||||||
|
|
||||||
|
This module does not define custom reports.
|
||||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue