commit 7a380f05d3d92f3a95a54eed7bf6e1ff7f96eed2 Author: Ernad Husremovic Date: Fri Aug 29 15:43:06 2025 +0200 Initial commit: OCA Storage packages (17 packages) diff --git a/README.md b/README.md new file mode 100644 index 0000000..0eea9f2 --- /dev/null +++ b/README.md @@ -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. diff --git a/odoo-bringout-oca-storage-fs_attachment/README.md b/odoo-bringout-oca-storage-fs_attachment/README.md new file mode 100644 index 0000000..cb42b3e --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/README.md @@ -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 diff --git a/odoo-bringout-oca-storage-fs_attachment/doc/ARCHITECTURE.md b/odoo-bringout-oca-storage-fs_attachment/doc/ARCHITECTURE.md new file mode 100644 index 0000000..d67a367 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/doc/ARCHITECTURE.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_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. diff --git a/odoo-bringout-oca-storage-fs_attachment/doc/CONFIGURATION.md b/odoo-bringout-oca-storage-fs_attachment/doc/CONFIGURATION.md new file mode 100644 index 0000000..7d557b3 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/doc/CONFIGURATION.md @@ -0,0 +1,3 @@ +# Configuration + +Refer to Odoo settings for fs_attachment. Configure related models, access rights, and options as needed. diff --git a/odoo-bringout-oca-storage-fs_attachment/doc/CONTROLLERS.md b/odoo-bringout-oca-storage-fs_attachment/doc/CONTROLLERS.md new file mode 100644 index 0000000..f628e77 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/doc/CONTROLLERS.md @@ -0,0 +1,3 @@ +# Controllers + +This module does not define custom HTTP controllers. diff --git a/odoo-bringout-oca-storage-fs_attachment/doc/DEPENDENCIES.md b/odoo-bringout-oca-storage-fs_attachment/doc/DEPENDENCIES.md new file mode 100644 index 0000000..7830e40 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/doc/DEPENDENCIES.md @@ -0,0 +1,5 @@ +# Dependencies + +This addon depends on: + +- [fs_storage](../../odoo-bringout-oca-storage-fs_storage) diff --git a/odoo-bringout-oca-storage-fs_attachment/doc/FAQ.md b/odoo-bringout-oca-storage-fs_attachment/doc/FAQ.md new file mode 100644 index 0000000..f91634c --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/doc/FAQ.md @@ -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. diff --git a/odoo-bringout-oca-storage-fs_attachment/doc/INSTALL.md b/odoo-bringout-oca-storage-fs_attachment/doc/INSTALL.md new file mode 100644 index 0000000..f86b4a4 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/doc/INSTALL.md @@ -0,0 +1,7 @@ +# Install + +```bash +pip install odoo-bringout-oca-storage-fs_attachment" +# or +uv pip install odoo-bringout-oca-storage-fs_attachment" +``` diff --git a/odoo-bringout-oca-storage-fs_attachment/doc/MODELS.md b/odoo-bringout-oca-storage-fs_attachment/doc/MODELS.md new file mode 100644 index 0000000..0c9ae5f --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/doc/MODELS.md @@ -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. diff --git a/odoo-bringout-oca-storage-fs_attachment/doc/OVERVIEW.md b/odoo-bringout-oca-storage-fs_attachment/doc/OVERVIEW.md new file mode 100644 index 0000000..fc5e06e --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/doc/OVERVIEW.md @@ -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 diff --git a/odoo-bringout-oca-storage-fs_attachment/doc/REPORTS.md b/odoo-bringout-oca-storage-fs_attachment/doc/REPORTS.md new file mode 100644 index 0000000..e0ea35f --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/doc/REPORTS.md @@ -0,0 +1,3 @@ +# Reports + +This module does not define custom reports. diff --git a/odoo-bringout-oca-storage-fs_attachment/doc/SECURITY.md b/odoo-bringout-oca-storage-fs_attachment/doc/SECURITY.md new file mode 100644 index 0000000..e757c48 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/doc/SECURITY.md @@ -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 diff --git a/odoo-bringout-oca-storage-fs_attachment/doc/TROUBLESHOOTING.md b/odoo-bringout-oca-storage-fs_attachment/doc/TROUBLESHOOTING.md new file mode 100644 index 0000000..56853cb --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/doc/TROUBLESHOOTING.md @@ -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. diff --git a/odoo-bringout-oca-storage-fs_attachment/doc/USAGE.md b/odoo-bringout-oca-storage-fs_attachment/doc/USAGE.md new file mode 100644 index 0000000..0b7fc87 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/doc/USAGE.md @@ -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 +``` diff --git a/odoo-bringout-oca-storage-fs_attachment/doc/WIZARDS.md b/odoo-bringout-oca-storage-fs_attachment/doc/WIZARDS.md new file mode 100644 index 0000000..48e790d --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/doc/WIZARDS.md @@ -0,0 +1,3 @@ +# Wizards + +This module does not include UI wizards. diff --git a/odoo-bringout-oca-storage-fs_attachment/fs_attachment/README.rst b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/README.rst new file mode 100644 index 0000000..fe6cf74 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/README.rst @@ -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 `_ 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: +'--.' + +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//" + for a file stored in the 'my_storage' storage will generate a response by odoo + with the URI + ``/my_storage//--`` + in the headers ``X-Accel-Redirect`` and ``X-Sendfile`` and the proxy will redirect to + ``http://myserver.com//--``. + + 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 `_) + + +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 `_) + + +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 `_) + + +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 `_) + + +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 `_) + + +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 `_) + + +16.0.1.0.2 (2023-10-09) +~~~~~~~~~~~~~~~~~~~~~~~ + +**Bugfixes** + +- Ensures python 3.9 compatibility. (`#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 `_) + +Bug Tracker +=========== + +Bugs are tracked on `GitHub 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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Camptocamp +* ACSONE SA/NV + +Contributors +~~~~~~~~~~~~ + +Thierry Ducrest +Guewen Baconnier +Julien Coux +Akim Juillerat +Thomas Nowicki +Vincent Renaville +Denis Leemann +Patrick Tombez +Don Kendall +Stephane Mangin +Laurent Mignon +Marie Lejeune +Wolfgang Pichler +Nans Lefebvre +Mohamed Alkobrosli + +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 `__: + +|maintainer-lmignon| + +This module is part of the `OCA/storage `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/odoo-bringout-oca-storage-fs_attachment/fs_attachment/__init__.py b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/__init__.py new file mode 100644 index 0000000..6d58305 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/__init__.py @@ -0,0 +1,2 @@ +from . import models +from .hooks import pre_init_hook diff --git a/odoo-bringout-oca-storage-fs_attachment/fs_attachment/__manifest__.py b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/__manifest__.py new file mode 100644 index 0000000..4037530 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/__manifest__.py @@ -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", +} diff --git a/odoo-bringout-oca-storage-fs_attachment/fs_attachment/fs_stream.py b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/fs_stream.py new file mode 100644 index 0000000..6f9c9b1 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/fs_stream.py @@ -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) diff --git a/odoo-bringout-oca-storage-fs_attachment/fs_attachment/hooks.py b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/hooks.py new file mode 100644 index 0000000..8fca70d --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/hooks.py @@ -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") diff --git a/odoo-bringout-oca-storage-fs_attachment/fs_attachment/i18n/bs.po b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/i18n/bs.po new file mode 100644 index 0000000..6caabc1 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/i18n/bs.po @@ -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 "" diff --git a/odoo-bringout-oca-storage-fs_attachment/fs_attachment/i18n/es.po b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/i18n/es.po new file mode 100644 index 0000000..4c360f5 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/i18n/es.po @@ -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 \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." diff --git a/odoo-bringout-oca-storage-fs_attachment/fs_attachment/i18n/fr.po b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/i18n/fr.po new file mode 100644 index 0000000..447d0fc --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/i18n/fr.po @@ -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 "" diff --git a/odoo-bringout-oca-storage-fs_attachment/fs_attachment/i18n/fs_attachment.pot b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/i18n/fs_attachment.pot new file mode 100644 index 0000000..911b604 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/i18n/fs_attachment.pot @@ -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 "" diff --git a/odoo-bringout-oca-storage-fs_attachment/fs_attachment/i18n/it.po b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/i18n/it.po new file mode 100644 index 0000000..d523516 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/i18n/it.po @@ -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 \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." diff --git a/odoo-bringout-oca-storage-fs_attachment/fs_attachment/models/__init__.py b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/models/__init__.py new file mode 100644 index 0000000..124d8ac --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/models/__init__.py @@ -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 diff --git a/odoo-bringout-oca-storage-fs_attachment/fs_attachment/models/fs_file_gc.py b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/models/fs_file_gc.py new file mode 100644 index 0000000..6ab70ec --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/models/fs_file_gc.py @@ -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),), + ) diff --git a/odoo-bringout-oca-storage-fs_attachment/fs_attachment/models/fs_storage.py b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/models/fs_storage.py new file mode 100644 index 0000000..732d801 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/models/fs_storage.py @@ -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() diff --git a/odoo-bringout-oca-storage-fs_attachment/fs_attachment/models/ir_attachment.py b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/models/ir_attachment.py new file mode 100644 index 0000000..223c416 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/models/ir_attachment.py @@ -0,0 +1,1153 @@ +# Copyright 2017-2013 Camptocamp SA +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from __future__ import annotations + +import io +import logging +import mimetypes +import os +import re +import time +from contextlib import closing, contextmanager + +import fsspec # pylint: disable=missing-manifest-dependency +import psycopg2 +from slugify import slugify # pylint: disable=missing-manifest-dependency + +import odoo +from odoo import _, api, fields, models +from odoo.exceptions import AccessError, UserError +from odoo.osv.expression import AND, OR, normalize_domain + +from .strtobool import strtobool + +_logger = logging.getLogger(__name__) + + +REGEX_SLUGIFY = r"[^-a-z0-9_]+" + +FS_FILENAME_RE_PARSER = re.compile( + r"^(?P.+)-(?P\d+)-(?P\d+)(?P\..+)$" +) + + +def is_true(strval): + return bool(strtobool(strval or "0")) + + +def clean_fs(files): + _logger.info("cleaning old files from filestore") + for full_path in files: + if os.path.exists(full_path): + try: + os.unlink(full_path) + except OSError: + _logger.info( + "_file_delete could not unlink %s", full_path, exc_info=True + ) + except IOError: + # Harmless and needed for race conditions + _logger.info( + "_file_delete could not unlink %s", full_path, exc_info=True + ) + + +class IrAttachment(models.Model): + _inherit = "ir.attachment" + + fs_filename = fields.Char( + "File Name into the filesystem storage", + help="The name of the file in the filesystem storage." + "To preserve the mimetype and the meaning of the filename" + "the filename is computed from the name and the extension", + readonly=True, + ) + + internal_url = fields.Char( + "Internal URL", + compute="_compute_internal_url", + help="The URL to access the file from the server.", + ) + + fs_url = fields.Char( + "Filesystem URL", + compute="_compute_fs_url", + help="The URL to access the file from the filesystem storage.", + store=True, + ) + fs_url_path = fields.Char( + "Filesystem URL Path", + compute="_compute_fs_url_path", + help="The path to access the file from the filesystem storage.", + ) + fs_storage_code = fields.Char( + "Filesystem Storage Code", + related="fs_storage_id.code", + store=True, + ) + fs_storage_id = fields.Many2one( + "fs.storage", + "Filesystem Storage", + compute="_compute_fs_storage_id", + help="The storage where the file is stored.", + store=True, + ondelete="restrict", + ) + + @api.depends("name") + def _compute_internal_url(self) -> None: + for rec in self: + filename, extension = os.path.splitext(rec.name) + # determine if the file is an image + pfx = "/web/content" + if rec.mimetype and rec.mimetype.startswith("image/"): + pfx = "/web/image" + + if not extension: + extension = mimetypes.guess_extension(rec.mimetype) + rec.internal_url = f"{pfx}/{rec.id}/{filename}{extension}" + + @api.depends("fs_filename") + def _compute_fs_url(self) -> None: + for rec in self: + new_url = None + actual_url = rec.fs_url or None + if rec.fs_filename: + new_url = self.env["fs.storage"]._get_url_for_attachment(rec) + # ensure we compare value of same type and not None with False + new_url = new_url or None + if new_url != actual_url: + rec.fs_url = new_url + + @api.depends("fs_filename") + def _compute_fs_url_path(self) -> None: + for rec in self: + rec.fs_url_path = None + if rec.fs_filename: + rec.fs_url_path = self.env["fs.storage"]._get_url_for_attachment( + rec, exclude_base_url=True + ) + + @api.depends("fs_filename") + def _compute_fs_storage_id(self): + for rec in self: + if rec.store_fname: + code = rec.store_fname.partition("://")[0] + fs_storage = self.env["fs.storage"].sudo().get_by_code(code) + if fs_storage != rec.fs_storage_id: + rec.fs_storage_id = fs_storage + elif rec.fs_storage_id: + rec.fs_storage_id = None + + @staticmethod + def _is_storage_disabled(storage=None, log=True): + msg = _("Storages are disabled (see environment configuration).") + if storage: + msg = _("Storage '%s' is disabled (see environment configuration).") % ( + storage, + ) + is_disabled = is_true(os.environ.get("DISABLE_ATTACHMENT_STORAGE")) + if is_disabled and log: + _logger.warning(msg) + return is_disabled + + def _get_storage_force_db_config(self): + return self.env["fs.storage"].get_force_db_for_default_attachment_rules( + self._storage() + ) + + def _store_in_db_instead_of_object_storage_domain(self): + """Return a domain for attachments that must be forced to DB + + Read the docstring of ``_store_in_db_instead_of_object_storage`` for + more details. + + Used in ``force_storage_to_db_for_special_fields`` to find records + to move from the object storage to the database. + + The domain must be inline with the conditions in + ``_store_in_db_instead_of_object_storage``. + """ + domain = [] + storage_config = self._get_storage_force_db_config() + for mimetype_key, limit in storage_config.items(): + part = [("mimetype", "=like", "{}%".format(mimetype_key))] + if limit: + part = AND([part, [("file_size", "<=", limit)]]) + # OR simplifies to [(1, '=', 1)] if a domain being OR'ed is empty + domain = OR([domain, part]) if domain else part + return domain + + def _store_in_db_instead_of_object_storage(self, data, mimetype): + """Return whether an attachment must be stored in db + + When we are using an Object Storage. This is sometimes required + because the object storage is slower than the database/filesystem. + + 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 configuration can be modified on the fs.storage record, in the + field ``force_db_for_default_attachment_rules``, as a dictionary, for + instance:: + + {"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. + + These limits are applied only if the storage is the default one for + attachments (see ``_storage``). + + The conditions are also applied into the domain of the method + ``_store_in_db_instead_of_object_storage_domain`` used to move records + from a filesystem storage to the database. + + """ + if self._is_storage_disabled(): + return True + storage_config = self._get_storage_force_db_config() + for mimetype_key, limit in storage_config.items(): + if mimetype.startswith(mimetype_key): + if not limit: + return True + bin_data = data + return len(bin_data) <= limit + return False + + def _get_datas_related_values(self, data, mimetype): + storage = self.env.context.get("storage_location") or self._storage() + if data and storage in self._get_storage_codes(): + if self._store_in_db_instead_of_object_storage(data, mimetype): + # compute the fields that depend on datas + bin_data = data + values = { + "file_size": len(bin_data), + "checksum": self._compute_checksum(bin_data), + "index_content": self._index(bin_data, mimetype), + "store_fname": False, + "db_datas": data, + } + return values + return super( + IrAttachment, self.with_context(mimetype=mimetype) + )._get_datas_related_values(data, mimetype) + + ########################################################### + # Odoo methods that we override to use the object storage # + ########################################################### + @api.model + def _storage(self): + # We check if a filesystem storage is configured for attachments + storage = self.env["fs.storage"].get_default_storage_code_for_attachments() + if not storage: + # If not, we use the default storage configured into odoo + storage = super()._storage() + return storage + + @api.model_create_multi + def create(self, vals_list): + """ + Storage may depend on resource field, but the method calling _storage + (_get_datas_related_values) does not take all vals, just the mimetype. + The only way to give res_field and res_model to _storage method + is to pass them into the context, and perform 1 create call per record + to create. + """ + vals_list_no_model = [] + attachments = self.env["ir.attachment"] + for vals in vals_list: + if vals.get("res_model"): + attachment = super( + IrAttachment, + self.with_context( + attachment_res_model=vals.get("res_model"), + attachment_res_field=vals.get("res_field"), + ), + ).create(vals) + attachments += attachment + else: + vals_list_no_model.append(vals) + atts = super().create(vals_list_no_model) + attachments |= atts + attachments._enforce_meaningful_storage_filename() + return attachments + + def write(self, vals): + if not self: + return super().write(vals) + if ("datas" in vals or "raw" in vals) and not ( + "name" in vals or "mimetype" in vals + ): + mimetype = self._compute_mimetype(vals) + if mimetype and mimetype != "application/octet-stream": + vals["mimetype"] = mimetype + else: + # When we write on an attachment, if the mimetype is not provided, it + # will be computed from the name. The problem is that if you assign a + # value to the field ``datas`` or ``raw``, the name is not provided + # nor the mimetype, so the mimetype will be set to ``application/octet- + # stream``. + # We want to avoid this, so we take the mimetype of the first attachment + # and we set it on all the attachments if they all have the same mimetype. + # If they don't have the same mimetype, we raise an error. + # OPW-3277070 + mimetypes = self.mapped("mimetype") + if len(set(mimetypes)) == 1: + vals["mimetype"] = mimetypes[0] + else: + raise UserError( + _( + "You can't write on multiple attachments with different " + "mimetypes at the same time." + ) + ) + for rec in self: + # As when creating a new attachment, we must pass the res_field + # and res_model into the context hence sadly we must perform 1 call + # for each attachment + super( + IrAttachment, + rec.with_context( + attachment_res_model=vals.get("res_model") or rec.res_model, + attachment_res_field=vals.get("res_field") or rec.res_field, + ), + ).write(vals) + + if "name" in vals: + self._enforce_meaningful_storage_filename() + + return True + + @api.model + def _file_read(self, fname): + if self._is_file_from_a_storage(fname): + return self._storage_file_read(fname) + else: + return super()._file_read(fname) + + @api.model + def _file_write(self, bin_data, checksum): + location = self.env.context.get("storage_location") or self._storage() + if location in self._get_storage_codes(): + filename = self._storage_file_write(bin_data) + else: + filename = super()._file_write(bin_data, checksum) + return filename + + @api.model + def _file_delete(self, fname) -> None: # pylint: disable=missing-return + if self._is_file_from_a_storage(fname): + cr = self.env.cr + # using SQL to include files hidden through unlink or due to record + # rules + cr.execute( + "SELECT COUNT(*) FROM ir_attachment WHERE store_fname = %s", (fname,) + ) + count = cr.fetchone()[0] + if not count: + self._storage_file_delete(fname) + else: + super()._file_delete(fname) + + def _set_attachment_data(self, asbytes) -> None: # pylint: disable=missing-return + super()._set_attachment_data(asbytes) + self._enforce_meaningful_storage_filename() + + ############################################## + # Internal methods to use the object storage # + ############################################## + @api.model + def _storage_file_read(self, fname: str) -> bytes | None: + """Read the file from the filesystem storage""" + fs, _storage, fname = self._fs_parse_store_fname(fname) + try: + with fs.open(fname, "rb") as f: + return f.read() + except IOError: + _logger.info( + "Error reading %s on storage %s", fname, _storage, exc_info=True + ) + return b"" + + def _storage_write_option(self, fs): + mimetype = self.env.context.get("mimetype") + if mimetype: + root_fs = self.env["fs.storage"]._get_root_filesystem(fs) + if hasattr(root_fs, "s3"): + return {"ContentType": mimetype} + return {} + + @api.model + def _storage_file_write(self, bin_data: bytes) -> str: + """Write the file to the filesystem storage""" + storage = self.env.context.get("storage_location") or self._storage() + fs = self._get_fs_storage_for_code(storage) + path = self._get_fs_path(storage, bin_data) + dirname = os.path.dirname(path) + if not fs.exists(dirname): + fs.makedirs(dirname) + fname = f"{storage}://{path}" + kwargs = self._storage_write_option(fs) + with fs.open(path, "wb", **kwargs) as f: + f.write(bin_data) + self._fs_mark_for_gc(fname) + return fname + + @api.model + def _storage_file_delete(self, fname): + """Delete the file from the filesystem storage + + It's safe to use the fname (the store_fname) to delete the file because + even if it's the full path to the file, the gc will only delete the file + if they belong to the configured storage directory path. + """ + self._fs_mark_for_gc(fname) + + @api.model + def _get_fs_path(self, storage_code: str, bin_data: bytes) -> str: + """Compute the path to store the file in the filesystem storage""" + key = self.env.context.get("force_storage_key") + if not key: + key = self._compute_checksum(bin_data) + if self.env["fs.storage"]._must_optimize_directory_path(storage_code): + # Generate a unique directory path based on the file's hash + key = os.path.join(key[:2], key[2:4], key) + # Generate a unique directory path based on the file's hash + return key + + def _build_fs_filename(self): + """Build the filename to store in the filesystem storage + + The filename is computed from the name, the extension and a version + number. The version number is incremented each time we build a new + filename. To know if a filename has already been build, we check if + the fs_filename field is set. If it is set, we increment the version + number. The version number is taken from the computed filename. + + The format of the filename is: + --. + """ + self.ensure_one() + filename, extension = os.path.splitext(self.name) + if not extension: + extension = mimetypes.guess_extension(self.mimetype) + version = 0 + if self.fs_filename: + parsed = self._parse_fs_filename(self.fs_filename) + if parsed: + version = parsed[2] + 1 + return "{}{}".format( + slugify( + "{}-{}-{}".format(filename, self.id, version), + regex_pattern=REGEX_SLUGIFY, + ), + extension, + ) + + def _enforce_meaningful_storage_filename(self) -> None: + """Enforce meaningful filename for files stored in the filesystem storage + + The filename of the file in the filesystem storage is computed from + the mimetype and the name of the attachment. This method is called + when an attachment is created to ensure that the filename of the file + in the filesystem keeps the same meaning as the name of the attachment. + + Keeping the same meaning and mimetype is important to also ease to provide + a meaningful and SEO friendly URL to the file in the filesystem storage. + """ + for attachment in self: + if not self._is_file_from_a_storage(attachment.store_fname): + continue + fs, storage, filename = attachment._get_fs_parts() + + if self.env["fs.storage"]._must_use_filename_obfuscation(storage): + attachment.fs_filename = filename + continue + new_filename = attachment._build_fs_filename() + # we must keep the same full path as the original filename + new_filename_with_path = os.path.join( + os.path.dirname(filename), new_filename + ) + fs.rename(filename, new_filename_with_path) + attachment.fs_filename = new_filename + # we need to update the store_fname with the new filename by + # calling the write method of the field since the write method + # of ir_attachment prevent normal write on store_fname + attachment._force_write_store_fname(f"{storage}://{new_filename_with_path}") + self._fs_mark_for_gc(attachment.store_fname) + + def _force_write_store_fname(self, store_fname): + """Force the write of the store_fname field + + The base implementation of the store_fname field prevent the write + of the store_fname field. This method bypass this limitation by + calling the write method of the field directly. + """ + self._fields["store_fname"].write(self, store_fname) + + @api.model + def _get_fs_storage_for_code( + self, + code: str, + ) -> fsspec.AbstractFileSystem | None: + """Return the filesystem for the given storage code""" + fs = self.env["fs.storage"].get_fs_by_code(code) + if not fs: + raise SystemError(f"No Filesystem storage for code {code}") + return fs + + @api.model + def _fs_parse_store_fname( + self, fname: str + ) -> tuple[fsspec.AbstractFileSystem, str, str]: + """Return the filesystem, the storage code and the path for the given fname + + :param fname: the fname to parse + :param base: if True, return the base filesystem + """ + partition = fname.partition("://") + storage_code = partition[0] + fs = self._get_fs_storage_for_code(storage_code) + fname = partition[2] + return fs, storage_code, fname + + @api.model + def _parse_fs_filename(self, filename: str) -> tuple[str, int, int, str] | None: + """Parse the filename and return the name, id, version and extension + --. + """ + if not filename: + return None + filename = os.path.basename(filename) + match = FS_FILENAME_RE_PARSER.match(filename) + if not match: + return None + name, res_id, version, extension = match.groups() + return name, int(res_id), int(version), extension + + @api.model + def _is_file_from_a_storage(self, fname): + if not fname: + return False + for storage_code in self._get_storage_codes(): + if self._is_storage_disabled(storage_code): + continue + uri = "{}://".format(storage_code) + if fname.startswith(uri): + return True + return False + + @api.model + def _fs_mark_for_gc(self, fname): + """Mark the file for deletion + + The file will be deleted by the garbage collector if it's no more + referenced by any attachment. We use a garbage collector to enforce + the transaction mechanism between Odoo and the filesystem storage. + Files are added to the garbage collector when: + - each time a file is created in the filesystem storage + - an attachment is deleted + + Whatever the result of the current transaction, the information of files + marked for deletion is stored in the database. + + When the garbage collector is called, it will check if the file is still + referenced by an attachment. If not, the file is physically deleted from + the filesystem storage. + + If the creation of the attachment fails, since the file is marked for + deletion when it's written into the filesystem storage, it will be + deleted by the garbage collector. + + If the content of the attachment is updated, we always create a new file. + This new file is marked for deletion and the old one too. If the transaction + succeeds, the old file is deleted by the garbage collector since it's no + more referenced by any attachment. If the transaction fails, the old file + is not deleted since it's still referenced by the attachment but the new + file is deleted since it's marked for deletion and not referenced. + """ + self.env["fs.file.gc"]._mark_for_gc(fname) + + def _get_fs_parts( + self, + ) -> tuple[fsspec.AbstractFileSystem, str, str] | tuple[None, None, None]: + """Return the filesystem, the storage code and the path for the current attachment""" + if not self.store_fname: + return None, None, None + return self._fs_parse_store_fname(self.store_fname) + + def open( + self, + mode="rb", + block_size=None, + cache_options=None, + compression=None, + new_version=True, + **kwargs, + ) -> io.IOBase: + """ + Return a file-like object from the filesystem storage where the attachment + content is stored. + + In read mode, this method works for all attachments, even if the content + is stored in the database or into the odoo filestore or a filesystem storage. + + The resultant instance must function correctly in a context ``with`` + block. + + (parameters are ignored in the case of the database storage). + + Parameters + ---------- + path: str + Target file + mode: str like 'rb', 'w' + See builtin ``open()`` + block_size: int + Some indication of buffering - this is a value in bytes + cache_options : dict, optional + Extra arguments to pass through to the cache. + compression: string or None + If given, open file using compression codec. Can either be a compression + name (a key in ``fsspec.compression.compr``) or "infer" to guess the + compression from the filename suffix. + new_version: bool + If True, and mode is 'w', create a new version of the file. + If False, and mode is 'w', overwrite the current version of the file. + This flag is True by default to avoid data loss and ensure transaction + mechanism between Odoo and the filesystem storage. + encoding, errors, newline: passed on to TextIOWrapper for text mode + + Returns + ------- + A file-like object + + TODO if open with 'w' in mode, we could use a buffered IO detecting that + the content is modified and invalidating the attachment cache... + """ + self.ensure_one() + return AttachmentFileLikeAdapter( + self, + mode=mode, + block_size=block_size, + cache_options=cache_options, + compression=compression, + new_version=new_version, + **kwargs, + ) + + @contextmanager + def _do_in_new_env(self, new_cr=False): + """Context manager that yields a new environment + + Using a new Odoo Environment thus a new PG transaction. + """ + if new_cr: + registry = odoo.modules.registry.Registry.new(self.env.cr.dbname) + with closing(registry.cursor()) as cr: + try: + yield self.env(cr=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 + else: + # make a copy + yield self.env() + + def _get_storage_codes(self): + """Get the list of filesystem storage active in the system""" + return self.env["fs.storage"].sudo().get_storage_codes() + + ################################ + # useful methods for migration # + ################################ + + def _move_attachment_to_store(self): + self.ensure_one() + _logger.info("inspecting attachment %s (%d)", self.name, self.id) + fname = self.store_fname + storage = fname.partition("://")[0] + if self._is_storage_disabled(storage): + fname = False + if fname: + # migrating from filesystem filestore + # or from the old 'store_fname' without the bucket name + _logger.info("moving %s on the object storage", fname) + self.write( + { + "datas": self.datas, + # this is required otherwise the + # mimetype gets overriden with + # 'application/octet-stream' + # on assets + "mimetype": self.mimetype, + } + ) + _logger.info("moved %s on the object storage", fname) + return self._full_path(fname) + elif self.db_datas: + _logger.info("moving on the object storage from database") + self.write({"datas": self.datas}) + + @api.model + def force_storage(self): + if not self.env["res.users"].browse(self.env.uid)._is_admin(): + raise AccessError(_("Only administrators can execute this action.")) + location = self.env.context.get("storage_location") or self._storage() + if location not in self._get_storage_codes(): + return super().force_storage() + self._force_storage_to_object_storage() + + @api.model + def force_storage_to_db_for_special_fields( + self, new_cr=False, storage: str | None = None + ): + """Migrate special attachments from Object Storage back to database + + The access to a file stored on the objects storage is slower + than a local disk or database access. For attachments like + image_small that are accessed in batch for kanban views, this + is too slow. We store this type of attachment in the database. + + This method can be used when migrating a filestore where all the files, + including the special files (assets, image_small, ...) have been pushed + to the Object Storage and we want to write them back in the database. + + It is not called anywhere, but can be called by RPC or scripts. + """ + if not storage: + storage = self._storage() + if self._is_storage_disabled(storage): + _logger.warning( + "Storage '%s' is disabled, skipping migration of attachments to DB", + storage, + ) + return + if storage not in self._get_storage_codes(): + _logger.warning( + "Storage '%s' is not configured, " + "skipping migration of attachments to DB", + storage, + ) + return + + domain = AND( + ( + normalize_domain( + [ + ("store_fname", "=like", "{}://%".format(storage)), + # for res_field, see comment in + # _force_storage_to_object_storage + "|", + ("res_field", "=", False), + ("res_field", "!=", False), + ] + ), + normalize_domain(self._store_in_db_instead_of_object_storage_domain()), + ) + ) + + with self._do_in_new_env(new_cr=new_cr) as new_env: + model_env = new_env["ir.attachment"].with_context(prefetch_fields=False) + attachment_ids = model_env.search(domain).ids + if not attachment_ids: + return + total = len(attachment_ids) + start_time = time.time() + _logger.info( + "Moving %d attachments from %s to DB for fast access", total, storage + ) + current = 0 + for attachment_id in attachment_ids: + current += 1 + # if we browse attachments outside of the loop, the first + # access to 'datas' will compute all the 'datas' fields at + # once, which means reading hundreds or thousands of files at + # once, exhausting memory + attachment = model_env.browse(attachment_id) + # this write will read the datas from the Object Storage and + # write them back in the DB (the logic for location to write is + # in the 'datas' inverse computed field) + # we need to write the mimetype too, otherwise it will be + # overwritten with 'application/octet-stream' on assets. On each + # write, the mimetype is recomputed if not given. If we don't + # pass it nor the name, the mimetype will be set to the default + # value 'application/octet-stream' on assets. + attachment.write({"datas": attachment.datas}) + if current % 100 == 0 or total - current == 0: + _logger.info( + "attachment %s/%s after %.2fs", + current, + total, + time.time() - start_time, + ) + + @api.model + def _force_storage_to_object_storage(self, new_cr=False): + _logger.info("migrating files to the object storage") + storage = self.env.context.get("storage_location") or self._storage() + if self._is_storage_disabled(storage): + return + # 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 = [ + "!", + ("store_fname", "=like", "{}://%".format(storage)), + "|", + ("res_field", "=", False), + ("res_field", "!=", False), + ] + # We do a copy of the environment so we can workaround the cache issue + # below. We do not create a new cursor by default because it causes + # serialization issues due to concurrent updates on attachments during + # the installation + with self._do_in_new_env(new_cr=new_cr) as new_env: + model_env = new_env["ir.attachment"] + ids = model_env.search(domain).ids + files_to_clean = [] + for attachment_id in ids: + try: + with new_env.cr.savepoint(): + # check that no other transaction has + # locked the row, don't send a file to storage + # in that case + self.env.cr.execute( + "SELECT id " + "FROM ir_attachment " + "WHERE id = %s " + "FOR UPDATE NOWAIT", + (attachment_id,), + log_exceptions=False, + ) + + # This is a trick to avoid having the 'datas' + # function fields computed for every attachment on + # each iteration of the loop. The former issue + # being that it reads the content of the file of + # ALL the attachments on each loop. + new_env.clear() + attachment = model_env.browse(attachment_id) + path = attachment._move_attachment_to_store() + if path: + files_to_clean.append(path) + except psycopg2.OperationalError: + _logger.error( + "Could not migrate attachment %s to S3", attachment_id + ) + + # delete the files from the filesystem once we know the changes + # have been committed in ir.attachment + if files_to_clean: + new_env.cr.commit() + clean_fs(files_to_clean) + + +class AttachmentFileLikeAdapter(object): + """ + This class is a wrapper class around the ir.attachment model. It is used to + open the ir.attachment as a file and to read/write data to it. + + When the content of the file is stored into the odoo filestore or in a + filesystem storage, this object allows you to read/write the content from + the file in a direct way without having to read/write the whole file into + memory. When the content of the file is stored into database, this content + is read/written from/into a buffer in memory. + + Parameters + ---------- + attachment : ir.attachment + The attachment to open as a file. + mode: str like 'rb', 'w' + See builtin ``open()`` + block_size: int + Some indication of buffering - this is a value in bytes + cache_options : dict, optional + Extra arguments to pass through to the cache. + compression: string or None + If given, open file using compression codec. Can either be a compression + name (a key in ``fsspec.compression.compr``) or "infer" to guess the + compression from the filename suffix. + new_version: bool + If True, and mode is 'w', create a new version of the file. + If False, and mode is 'w', overwrite the current version of the file. + This flag is True by default to avoid data loss and ensure transaction + mechanism between Odoo and the filesystem storage. + encoding, errors, newline: passed on to TextIOWrapper for text mode + + You can use this class to adapt an attachment object as a file in 2 ways: + * as a context manager wrapping the attachment object as a file + * or as a nomral utility class + + Examples + + >>> with AttachmentFileLikeAdapter(attachment, mode="rb") as f: + ... f.read() + b'Hello World' + # at the end of the context manager, the file is closed + >>> f = AttachmentFileLikeAdapter(attachment, mode="rb") + >>> f.read() + b'Hello World' + # you have to close the file manually + >>> f.close() + + """ + + def __init__( + self, + attachment: IrAttachment, + mode: str = "rb", + block_size: int | None = None, + cache_options: dict | None = None, + compression: str | None = None, + new_version: bool = False, + **kwargs, + ): + self._attachment = attachment + self._mode = mode + self._block_size = block_size + self._cache_options = cache_options + self._compression = compression + self._new_version = new_version + self._kwargs = kwargs + + # state attributes + self._file: io.IOBase | None = None + self._filesystem: fsspec.AbstractFileSystem | None = None + self._new_store_fname: str | None = None + + @property + def attachment(self) -> IrAttachment: + """The attachment object the file is related to""" + return self._attachment + + @property + def mode(self) -> str: + """The mode used to open the file""" + return self._mode + + @property + def block_size(self) -> int | None: + """The block size used to open the file""" + return self._block_size + + @property + def cache_options(self) -> dict | None: + """The cache options used to open the file""" + return self._cache_options + + @property + def compression(self) -> str | None: + """The compression used to open the file""" + return self._compression + + @property + def new_version(self) -> bool: + """Is the file open for a new version""" + return self._new_version + + @property + def kwargs(self) -> dict: + """The kwargs passed when opening the file on the""" + return self._kwargs + + @property + def _is_open_for_modify(self) -> bool: + """Is the file open for modification + A file is open for modification if it is open for writing or appending + """ + return "w" in self.mode or "a" in self.mode + + @property + def _is_open_for_read(self) -> bool: + """Is the file open for reading""" + return "r" in self.mode + + @property + def _is_stored_in_db(self) -> bool: + """Is the file stored in database""" + return self.attachment._storage() == "db" + + def __enter__(self) -> io.IOBase: + """Called when entering the context manager + + Create the file object and return it. + """ + # we call the attachment instance to get the file object + self._file_open() + return self._file + + def _file_open(self) -> io.IOBase: + """Open the attachment content as a file-like object + + This method will initialize the following attributes: + + * _file: the file-like object. + * _filesystem: filesystem object. + * _new_store_fname: the new store_fname if the file is + opened for a new version. + """ + new_store_fname = None + if ( + self._is_open_for_read + or (self._is_open_for_modify and not self.new_version) + or self._is_stored_in_db + ): + if self.attachment._is_file_from_a_storage(self.attachment.store_fname): + fs, _storage, fname = self.attachment._get_fs_parts() + filepath = fname + filesystem = fs + elif self.attachment.store_fname: + filepath = self.attachment._full_path(self.attachment.store_fname) + filesystem = fsspec.filesystem("file") + else: + filepath = f"{self.attachment.id}" + filesystem = fsspec.filesystem("memory") + if "a" in self.mode or self._is_open_for_read: + filesystem.pipe_file(filepath, self.attachment.db_datas) + the_file = filesystem.open( + filepath, + mode=self.mode, + block_size=self.block_size, + cache_options=self.cache_options, + compression=self.compression, + **self.kwargs, + ) + else: + # mode='w' and new_version=True and storage != 'db' + # We must create a new file with a new name. If we are in an + # append mode, we must copy the content of the old file (or create + # the new one by copy of the old one). + # to not break the storage plugin mechanism, we'll use the + # _file_write method to create the new empty file with a random + # content and checksum to avoid collision. + content = self._gen_random_content() + checksum = self.attachment._compute_checksum(content) + new_store_fname = self.attachment.with_context( + attachment_res_model=self.attachment.res_model, + attachment_res_field=self.attachment.res_field, + )._file_write(content, checksum) + if self.attachment._is_file_from_a_storage(new_store_fname): + ( + filesystem, + _storage, + new_filepath, + ) = self.attachment._fs_parse_store_fname(new_store_fname) + _fs, _storage, old_filepath = self.attachment._get_fs_parts() + else: + new_filepath = self.attachment._full_path(new_store_fname) + old_filepath = self.attachment._full_path(self.attachment.store_fname) + filesystem = fsspec.filesystem("file") + if "a" in self.mode: + filesystem.cp_file(old_filepath, new_filepath) + the_file = filesystem.open( + new_filepath, + mode=self.mode, + block_size=self.block_size, + cache_options=self.cache_options, + compression=self.compression, + **self.kwargs, + ) + self._filesystem = filesystem + self._new_store_fname = new_store_fname + self._file = the_file + + def _gen_random_content(self, size=256): + """Generate a random content of size bytes""" + return os.urandom(size) + + def _file_close(self): + """Close the file-like object opened by _file_open""" + if not self._file: + return + if not self._file.closed: + self._file.flush() + self._file.close() + if self._is_open_for_modify: + attachment_data = self._get_attachment_data() + if ( + not (self.new_version and self._new_store_fname) + and self._is_stored_in_db + ): + attachment_data["raw"] = self._file.getvalue() + self.attachment.write(attachment_data) + if self.new_version and self._new_store_fname: + self.attachment._force_write_store_fname(self._new_store_fname) + self.attachment._enforce_meaningful_storage_filename() + self._ensure_cache_consistency() + + def _get_attachment_data(self) -> dict: + ret = {} + if self._file: + file_path = self._file.path + if hasattr(self._filesystem, "path"): + file_path = file_path.replace(self._filesystem.path, "") + file_path = file_path.lstrip("/") + ret["checksum"] = self._filesystem.checksum(file_path) + ret["file_size"] = self._filesystem.size(file_path) + # TODO index_content is too expensive to compute here or should be configurable + # data = self._file.read() + # ret["index_content"] = self.attachment._index_content(data, + # self.attachment.mimetype, ret["checksum"]) + ret["index_content"] = b"" + + return ret + + def _ensure_cache_consistency(self): + """Ensure the cache consistency once the file is closed""" + if self._is_open_for_modify and not self._is_stored_in_db: + self.attachment.invalidate_recordset(fnames=["raw", "datas", "db_datas"]) + if ( + self.attachment.res_model + and self.attachment.res_id + and self.attachment.res_field + ): + self.attachment.env[self.attachment.res_model].browse( + self.attachment.res_id + ).invalidate_recordset(fnames=[self.attachment.res_field]) + + def __exit__(self, *args): + """Called when exiting the context manager. + + Close the file if it is not already closed. + """ + self._file_close() + + def __getattr__(self, attr): + """ + Forward all other attributes to the underlying file object. + + This method is required to make the object behave like a file object + when the AttachmentFileLikeAdapter is used outside a context manager. + + .. code-block:: python + + f = AttachmentFileLikeAdapter(attachment) + f.read() + + """ + if not self._file: + self.__enter__() + return getattr(self._file, attr) diff --git a/odoo-bringout-oca-storage-fs_attachment/fs_attachment/models/ir_binary.py b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/models/ir_binary.py new file mode 100644 index 0000000..57f6b2d --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/models/ir_binary.py @@ -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 diff --git a/odoo-bringout-oca-storage-fs_attachment/fs_attachment/models/ir_model.py b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/models/ir_model.py new file mode 100644 index 0000000..cb0be1b --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/models/ir_model.py @@ -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.", + ) diff --git a/odoo-bringout-oca-storage-fs_attachment/fs_attachment/models/ir_model_fields.py b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/models/ir_model_fields.py new file mode 100644 index 0000000..7660e0b --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/models/ir_model_fields.py @@ -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.", + ) diff --git a/odoo-bringout-oca-storage-fs_attachment/fs_attachment/models/strtobool.py b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/models/strtobool.py new file mode 100644 index 0000000..b1a849f --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/models/strtobool.py @@ -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 diff --git a/odoo-bringout-oca-storage-fs_attachment/fs_attachment/readme/CONTRIBUTORS.rst b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000..260fad9 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/readme/CONTRIBUTORS.rst @@ -0,0 +1,15 @@ +Thierry Ducrest +Guewen Baconnier +Julien Coux +Akim Juillerat +Thomas Nowicki +Vincent Renaville +Denis Leemann +Patrick Tombez +Don Kendall +Stephane Mangin +Laurent Mignon +Marie Lejeune +Wolfgang Pichler +Nans Lefebvre +Mohamed Alkobrosli diff --git a/odoo-bringout-oca-storage-fs_attachment/fs_attachment/readme/DESCRIPTION.rst b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/readme/DESCRIPTION.rst new file mode 100644 index 0000000..f0ca6ff --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/readme/DESCRIPTION.rst @@ -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 `_ 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: +'--.' + +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. diff --git a/odoo-bringout-oca-storage-fs_attachment/fs_attachment/readme/HISTORY.rst b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/readme/HISTORY.rst new file mode 100644 index 0000000..0d96aa5 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/readme/HISTORY.rst @@ -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 `_) + + +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 `_) + + +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 `_) + + +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 `_) + + +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 `_) + + +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 `_) + + +16.0.1.0.2 (2023-10-09) +~~~~~~~~~~~~~~~~~~~~~~~ + +**Bugfixes** + +- Ensures python 3.9 compatibility. (`#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 `_) diff --git a/odoo-bringout-oca-storage-fs_attachment/fs_attachment/readme/USAGE.rst b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/readme/USAGE.rst new file mode 100644 index 0000000..927bfda --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/readme/USAGE.rst @@ -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//" + for a file stored in the 'my_storage' storage will generate a response by odoo + with the URI + ``/my_storage//--`` + in the headers ``X-Accel-Redirect`` and ``X-Sendfile`` and the proxy will redirect to + ``http://myserver.com//--``. + + 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). diff --git a/odoo-bringout-oca-storage-fs_attachment/fs_attachment/readme/newsfragments/.gitignore b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/readme/newsfragments/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/odoo-bringout-oca-storage-fs_attachment/fs_attachment/security/fs_file_gc.xml b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/security/fs_file_gc.xml new file mode 100644 index 0000000..077c38c --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/security/fs_file_gc.xml @@ -0,0 +1,16 @@ + + + + + + fs.file.gc access name + + + + + + + + + diff --git a/odoo-bringout-oca-storage-fs_attachment/fs_attachment/static/description/icon.png b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/static/description/icon.png new file mode 100644 index 0000000..3a0328b Binary files /dev/null and b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/static/description/icon.png differ diff --git a/odoo-bringout-oca-storage-fs_attachment/fs_attachment/static/description/index.html b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/static/description/index.html new file mode 100644 index 0000000..a5ad6a6 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/static/description/index.html @@ -0,0 +1,792 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Base Attachment Object Store

+ +

Beta License: AGPL-3 OCA/storage Translate me on Weblate Try me on Runboat

+

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 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

+ +
+

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:

    +
    +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:

+
+[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.

+
+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.

+
+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.

+
+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)

    +
  • +
+
+
+

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)
  • +
+
+
+

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)

    +
  • +
+
+
+

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)
  • +
+
+
+

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)
  • +
+
+
+

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)
  • +
+
+
+

16.0.1.0.2 (2023-10-09)

+

Bugfixes

+
    +
  • Ensures python 3.9 compatibility. (#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)
  • +
+
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub 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.

+

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.

+ +Odoo Community Association + +

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.

+

Current maintainer:

+

lmignon

+

This module is part of the OCA/storage project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+
+ + diff --git a/odoo-bringout-oca-storage-fs_attachment/fs_attachment/tests/__init__.py b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/tests/__init__.py new file mode 100644 index 0000000..75bdb80 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/tests/__init__.py @@ -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 diff --git a/odoo-bringout-oca-storage-fs_attachment/fs_attachment/tests/common.py b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/tests/common.py new file mode 100644 index 0000000..076717a --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/tests/common.py @@ -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""" diff --git a/odoo-bringout-oca-storage-fs_attachment/fs_attachment/tests/test_fs_attachment.py b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/tests/test_fs_attachment.py new file mode 100644 index 0000000..e9b6083 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/tests/test_fs_attachment.py @@ -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), + ], + ) diff --git a/odoo-bringout-oca-storage-fs_attachment/fs_attachment/tests/test_fs_attachment_file_like_adapter.py b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/tests/test_fs_attachment_file_like_adapter.py new file mode 100644 index 0000000..bac729c --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/tests/test_fs_attachment_file_like_adapter.py @@ -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) diff --git a/odoo-bringout-oca-storage-fs_attachment/fs_attachment/tests/test_fs_attachment_internal_url.py b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/tests/test_fs_attachment_internal_url.py new file mode 100644 index 0000000..0dac94c --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/tests/test_fs_attachment_internal_url.py @@ -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, + ) diff --git a/odoo-bringout-oca-storage-fs_attachment/fs_attachment/tests/test_fs_storage.py b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/tests/test_fs_storage.py new file mode 100644 index 0000000..2dc0b5a --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/tests/test_fs_storage.py @@ -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) diff --git a/odoo-bringout-oca-storage-fs_attachment/fs_attachment/tests/test_stream.py b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/tests/test_stream.py new file mode 100644 index 0000000..5f5c322 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/tests/test_stream.py @@ -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, + ) diff --git a/odoo-bringout-oca-storage-fs_attachment/fs_attachment/views/fs_storage.xml b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/views/fs_storage.xml new file mode 100644 index 0000000..114cbdd --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/fs_attachment/views/fs_storage.xml @@ -0,0 +1,43 @@ + + + + + + fs.storage.form (in fs_attachment) + fs.storage + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/odoo-bringout-oca-storage-fs_attachment/pyproject.toml b/odoo-bringout-oca-storage-fs_attachment/pyproject.toml new file mode 100644 index 0000000..6521ba1 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_attachment/pyproject.toml @@ -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", +] diff --git a/odoo-bringout-oca-storage-fs_base_multi_image/README.md b/odoo-bringout-oca-storage-fs_base_multi_image/README.md new file mode 100644 index 0000000..0b590df --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_image/README.md @@ -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 diff --git a/odoo-bringout-oca-storage-fs_base_multi_image/doc/ARCHITECTURE.md b/odoo-bringout-oca-storage-fs_base_multi_image/doc/ARCHITECTURE.md new file mode 100644 index 0000000..0add412 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_image/doc/ARCHITECTURE.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. diff --git a/odoo-bringout-oca-storage-fs_base_multi_image/doc/CONFIGURATION.md b/odoo-bringout-oca-storage-fs_base_multi_image/doc/CONFIGURATION.md new file mode 100644 index 0000000..1a14aa1 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_image/doc/CONFIGURATION.md @@ -0,0 +1,3 @@ +# Configuration + +Refer to Odoo settings for fs_base_multi_image. Configure related models, access rights, and options as needed. diff --git a/odoo-bringout-oca-storage-fs_base_multi_image/doc/CONTROLLERS.md b/odoo-bringout-oca-storage-fs_base_multi_image/doc/CONTROLLERS.md new file mode 100644 index 0000000..f628e77 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_image/doc/CONTROLLERS.md @@ -0,0 +1,3 @@ +# Controllers + +This module does not define custom HTTP controllers. diff --git a/odoo-bringout-oca-storage-fs_base_multi_image/doc/DEPENDENCIES.md b/odoo-bringout-oca-storage-fs_base_multi_image/doc/DEPENDENCIES.md new file mode 100644 index 0000000..e9e2255 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_image/doc/DEPENDENCIES.md @@ -0,0 +1,5 @@ +# Dependencies + +This addon depends on: + +- [fs_image](../../odoo-bringout-oca-storage-fs_image) diff --git a/odoo-bringout-oca-storage-fs_base_multi_image/doc/FAQ.md b/odoo-bringout-oca-storage-fs_base_multi_image/doc/FAQ.md new file mode 100644 index 0000000..4508bf0 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_image/doc/FAQ.md @@ -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. diff --git a/odoo-bringout-oca-storage-fs_base_multi_image/doc/INSTALL.md b/odoo-bringout-oca-storage-fs_base_multi_image/doc/INSTALL.md new file mode 100644 index 0000000..6a24e4e --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_image/doc/INSTALL.md @@ -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" +``` diff --git a/odoo-bringout-oca-storage-fs_base_multi_image/doc/MODELS.md b/odoo-bringout-oca-storage-fs_base_multi_image/doc/MODELS.md new file mode 100644 index 0000000..278a376 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_image/doc/MODELS.md @@ -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. diff --git a/odoo-bringout-oca-storage-fs_base_multi_image/doc/OVERVIEW.md b/odoo-bringout-oca-storage-fs_base_multi_image/doc/OVERVIEW.md new file mode 100644 index 0000000..9b6cd0a --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_image/doc/OVERVIEW.md @@ -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 diff --git a/odoo-bringout-oca-storage-fs_base_multi_image/doc/REPORTS.md b/odoo-bringout-oca-storage-fs_base_multi_image/doc/REPORTS.md new file mode 100644 index 0000000..e0ea35f --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_image/doc/REPORTS.md @@ -0,0 +1,3 @@ +# Reports + +This module does not define custom reports. diff --git a/odoo-bringout-oca-storage-fs_base_multi_image/doc/SECURITY.md b/odoo-bringout-oca-storage-fs_base_multi_image/doc/SECURITY.md new file mode 100644 index 0000000..dade230 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_image/doc/SECURITY.md @@ -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 diff --git a/odoo-bringout-oca-storage-fs_base_multi_image/doc/TROUBLESHOOTING.md b/odoo-bringout-oca-storage-fs_base_multi_image/doc/TROUBLESHOOTING.md new file mode 100644 index 0000000..56853cb --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_image/doc/TROUBLESHOOTING.md @@ -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. diff --git a/odoo-bringout-oca-storage-fs_base_multi_image/doc/USAGE.md b/odoo-bringout-oca-storage-fs_base_multi_image/doc/USAGE.md new file mode 100644 index 0000000..d6d7e67 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_image/doc/USAGE.md @@ -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 +``` diff --git a/odoo-bringout-oca-storage-fs_base_multi_image/doc/WIZARDS.md b/odoo-bringout-oca-storage-fs_base_multi_image/doc/WIZARDS.md new file mode 100644 index 0000000..48e790d --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_image/doc/WIZARDS.md @@ -0,0 +1,3 @@ +# Wizards + +This module does not include UI wizards. diff --git a/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/README.rst b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/README.rst new file mode 100644 index 0000000..53b1645 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/README.rst @@ -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 `_ + +**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 `_. +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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* ACSONE SA/NV + +Contributors +~~~~~~~~~~~~ + +* Laurent Mignon + +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 `__: + +|maintainer-lmignon| + +This module is part of the `OCA/storage `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/__init__.py b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/__manifest__.py b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/__manifest__.py new file mode 100644 index 0000000..1782ca6 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/__manifest__.py @@ -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", +} diff --git a/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/i18n/bs.po b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/i18n/bs.po new file mode 100644 index 0000000..06b9a3a --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/i18n/bs.po @@ -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." diff --git a/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/i18n/es.po b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/i18n/es.po new file mode 100644 index 0000000..8ec4742 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/i18n/es.po @@ -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 \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" diff --git a/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/i18n/fr.po b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/i18n/fr.po new file mode 100644 index 0000000..67c27b1 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/i18n/fr.po @@ -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)\" \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." diff --git a/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/i18n/fs_base_multi_image.pot b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/i18n/fs_base_multi_image.pot new file mode 100644 index 0000000..d19608b --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/i18n/fs_base_multi_image.pot @@ -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 "" diff --git a/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/i18n/it.po b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/i18n/it.po new file mode 100644 index 0000000..4470410 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/i18n/it.po @@ -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 \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" diff --git a/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/models/__init__.py b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/models/__init__.py new file mode 100644 index 0000000..bada9cc --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/models/__init__.py @@ -0,0 +1,2 @@ +from . import fs_image +from . import fs_image_relation_mixin diff --git a/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/models/fs_image.py b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/models/fs_image.py new file mode 100644 index 0000000..285995a --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/models/fs_image.py @@ -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 diff --git a/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/models/fs_image_relation_mixin.py b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/models/fs_image_relation_mixin.py new file mode 100644 index 0000000..d38fd87 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/models/fs_image_relation_mixin.py @@ -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) diff --git a/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/readme/CONTRIBUTORS.rst b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000..172b2d2 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Laurent Mignon diff --git a/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/readme/DESCRIPTION.rst b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/readme/DESCRIPTION.rst new file mode 100644 index 0000000..3690804 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/readme/DESCRIPTION.rst @@ -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. diff --git a/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/readme/ROADMAP.rst b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/readme/ROADMAP.rst new file mode 100644 index 0000000..776f992 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/readme/ROADMAP.rst @@ -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) diff --git a/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/readme/USAGE.rst b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/readme/USAGE.rst new file mode 100644 index 0000000..7a4d226 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/readme/USAGE.rst @@ -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. diff --git a/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/security/fs_image.xml b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/security/fs_image.xml new file mode 100644 index 0000000..b976e34 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/security/fs_image.xml @@ -0,0 +1,26 @@ + + + + + + fs.image access read + + + + + + + + + + fs.image access manage + + + + + + + + + diff --git a/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/security/res_groups.xml b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/security/res_groups.xml new file mode 100644 index 0000000..0986498 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/security/res_groups.xml @@ -0,0 +1,10 @@ + + + + Image Manager + + + diff --git a/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/static/description/icon.png b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/static/description/icon.png new file mode 100644 index 0000000..3a0328b Binary files /dev/null and b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/static/description/icon.png differ diff --git a/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/static/description/index.html b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/static/description/index.html new file mode 100644 index 0000000..f6fe836 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/static/description/index.html @@ -0,0 +1,456 @@ + + + + + +Fs Base Multi Image + + + +
+

Fs Base Multi Image

+ + +

Alpha License: AGPL-3 OCA/storage Translate me on Weblate Try me on Runboat

+

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

+
+

Table of contents

+ +
+

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. +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.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • ACSONE SA/NV
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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.

+

Current maintainer:

+

lmignon

+

This module is part of the OCA/storage project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/static/src/fields/fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.esm.js b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/static/src/fields/fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.esm.js new file mode 100644 index 0000000..e62788d --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/static/src/fields/fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.esm.js @@ -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); diff --git a/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/static/src/fields/fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.scss b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/static/src/fields/fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.scss new file mode 100644 index 0000000..4848b7d --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/static/src/fields/fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.scss @@ -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; + } + } +} diff --git a/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/static/src/fields/fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.xml b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/static/src/fields/fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.xml new file mode 100644 index 0000000..6522721 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/static/src/fields/fs_image_relation_dnd_upload/fs_image_relation_dnd_upload.xml @@ -0,0 +1,60 @@ + + + + + + +
+
+
+ You can drag and drop images to create new records or click here to select image files. +
+
+ +
+
+ Choose how you want to store the new images: + +
+
+ +
+
+
+
+ +
diff --git a/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/views/fs_image.xml b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/views/fs_image.xml new file mode 100644 index 0000000..0ecddb2 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/views/fs_image.xml @@ -0,0 +1,77 @@ + + + + + + fs.image.form (in fs_base_multi_image) + fs.image + +
+ + +
+
+
+ + + fs.image.search (in fs_base_multi_image) + fs.image + + + + + + + + + + + + + + + fs.image.tree (in fs_base_multi_image) + fs.image + + + + + + + + + + + Fs Image + fs.image + tree,form + [] + {} + + + + Fs Images + + + + +
diff --git a/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/views/fs_image_relation_mixin.xml b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/views/fs_image_relation_mixin.xml new file mode 100644 index 0000000..b42ecdb --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_image/fs_base_multi_image/views/fs_image_relation_mixin.xml @@ -0,0 +1,77 @@ + + + + + + fs.image.relation.mixin.form + fs.image.relation.mixin + +
+ + + + + + + + + + + + + +
+
+
+ + + fs.image.relation.mixin.kanban + fs.image.relation.mixin + + + + + + + + + +
+ X +
+
+
+
+ + + +
+
+
+
+
+ + + + + + + diff --git a/odoo-bringout-oca-storage-fs_base_multi_image/pyproject.toml b/odoo-bringout-oca-storage-fs_base_multi_image/pyproject.toml new file mode 100644 index 0000000..e7c68d9 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_image/pyproject.toml @@ -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", +] diff --git a/odoo-bringout-oca-storage-fs_base_multi_media/README.md b/odoo-bringout-oca-storage-fs_base_multi_media/README.md new file mode 100644 index 0000000..9da2584 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_media/README.md @@ -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 diff --git a/odoo-bringout-oca-storage-fs_base_multi_media/doc/ARCHITECTURE.md b/odoo-bringout-oca-storage-fs_base_multi_media/doc/ARCHITECTURE.md new file mode 100644 index 0000000..c422ff8 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_media/doc/ARCHITECTURE.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. diff --git a/odoo-bringout-oca-storage-fs_base_multi_media/doc/CONFIGURATION.md b/odoo-bringout-oca-storage-fs_base_multi_media/doc/CONFIGURATION.md new file mode 100644 index 0000000..576c12c --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_media/doc/CONFIGURATION.md @@ -0,0 +1,3 @@ +# Configuration + +Refer to Odoo settings for fs_base_multi_media. Configure related models, access rights, and options as needed. diff --git a/odoo-bringout-oca-storage-fs_base_multi_media/doc/CONTROLLERS.md b/odoo-bringout-oca-storage-fs_base_multi_media/doc/CONTROLLERS.md new file mode 100644 index 0000000..f628e77 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_media/doc/CONTROLLERS.md @@ -0,0 +1,3 @@ +# Controllers + +This module does not define custom HTTP controllers. diff --git a/odoo-bringout-oca-storage-fs_base_multi_media/doc/DEPENDENCIES.md b/odoo-bringout-oca-storage-fs_base_multi_media/doc/DEPENDENCIES.md new file mode 100644 index 0000000..8e32263 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_media/doc/DEPENDENCIES.md @@ -0,0 +1,5 @@ +# Dependencies + +This addon depends on: + +- [fs_file](../../odoo-bringout-oca-storage-fs_file) diff --git a/odoo-bringout-oca-storage-fs_base_multi_media/doc/FAQ.md b/odoo-bringout-oca-storage-fs_base_multi_media/doc/FAQ.md new file mode 100644 index 0000000..08f6094 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_media/doc/FAQ.md @@ -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. diff --git a/odoo-bringout-oca-storage-fs_base_multi_media/doc/INSTALL.md b/odoo-bringout-oca-storage-fs_base_multi_media/doc/INSTALL.md new file mode 100644 index 0000000..83d222f --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_media/doc/INSTALL.md @@ -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" +``` diff --git a/odoo-bringout-oca-storage-fs_base_multi_media/doc/MODELS.md b/odoo-bringout-oca-storage-fs_base_multi_media/doc/MODELS.md new file mode 100644 index 0000000..ed363a9 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_media/doc/MODELS.md @@ -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. diff --git a/odoo-bringout-oca-storage-fs_base_multi_media/doc/OVERVIEW.md b/odoo-bringout-oca-storage-fs_base_multi_media/doc/OVERVIEW.md new file mode 100644 index 0000000..2be8427 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_media/doc/OVERVIEW.md @@ -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 diff --git a/odoo-bringout-oca-storage-fs_base_multi_media/doc/REPORTS.md b/odoo-bringout-oca-storage-fs_base_multi_media/doc/REPORTS.md new file mode 100644 index 0000000..e0ea35f --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_media/doc/REPORTS.md @@ -0,0 +1,3 @@ +# Reports + +This module does not define custom reports. diff --git a/odoo-bringout-oca-storage-fs_base_multi_media/doc/SECURITY.md b/odoo-bringout-oca-storage-fs_base_multi_media/doc/SECURITY.md new file mode 100644 index 0000000..244d8ac --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_media/doc/SECURITY.md @@ -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 diff --git a/odoo-bringout-oca-storage-fs_base_multi_media/doc/TROUBLESHOOTING.md b/odoo-bringout-oca-storage-fs_base_multi_media/doc/TROUBLESHOOTING.md new file mode 100644 index 0000000..56853cb --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_media/doc/TROUBLESHOOTING.md @@ -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. diff --git a/odoo-bringout-oca-storage-fs_base_multi_media/doc/USAGE.md b/odoo-bringout-oca-storage-fs_base_multi_media/doc/USAGE.md new file mode 100644 index 0000000..1b583e6 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_media/doc/USAGE.md @@ -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_media +``` diff --git a/odoo-bringout-oca-storage-fs_base_multi_media/doc/WIZARDS.md b/odoo-bringout-oca-storage-fs_base_multi_media/doc/WIZARDS.md new file mode 100644 index 0000000..48e790d --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_media/doc/WIZARDS.md @@ -0,0 +1,3 @@ +# Wizards + +This module does not include UI wizards. diff --git a/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/README.rst b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/README.rst new file mode 100644 index 0000000..289c646 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/README.rst @@ -0,0 +1,117 @@ +=================== +Fs Base Multi Media +=================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:854289aaf0e0ead7af13579fc0fb6eda2958f5386b77f5fbe056915dcc514666 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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_media + :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_media + :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 allows you to store media file into external filesystem from odoo. +It also provides is a technical mixin model to ease the creation of other models +that need to be linked to multiple medias stored into external filesystems. + +The models provided by this addon are: + +* ``fs.media``: a model that stores a reference to an media stored into + an external filesystem. +* ``fs.media.relation.mixin``: an abstract model that can be used to + as base class for models created to store an media linked to a model. + This abstract model defines fields and methods to transparently handle + 2 cases: + * the media is specific to the model. + * the media is shared between multiple models and therefore is a ``fs.media`` 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 `_ + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +To be able to create and or manages shared images, you must have the ``Media 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 media to a model linked to + multiple medias. (As it's the case in the *storage_image_product* addon) + +Bug Tracker +=========== + +Bugs are tracked on `GitHub 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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* ACSONE SA/NV +* Akretion + +Contributors +~~~~~~~~~~~~ + +* Sebastien Beau +* Laurent Mignon (https://www.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 `__: + +|maintainer-lmignon| + +This module is part of the `OCA/storage `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/__init__.py b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/__manifest__.py b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/__manifest__.py new file mode 100644 index 0000000..1af9e4c --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/__manifest__.py @@ -0,0 +1,26 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Fs Base Multi Media", + "summary": """ + Give the possibility to store media data in external filesystem from odoo""", + "version": "16.0.1.0.1", + "license": "AGPL-3", + "author": "ACSONE SA/NV,Akretion,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/storage", + "depends": [ + "fs_file", + ], + "data": [ + "security/res_groups.xml", + "security/fs_media_type.xml", + "security/fs_media.xml", + "views/fs_media.xml", + "views/fs_media_relation_mixin.xml", + "views/fs_media_type.xml", + ], + "demo": [], + "maintainers": ["lmignon"], + "development_status": "Alpha", +} diff --git a/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/i18n/bs.po b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/i18n/bs.po new file mode 100644 index 0000000..75016b4 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/i18n/bs.po @@ -0,0 +1,156 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_base_multi_media +# +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_media +#: model_terms:ir.ui.view,arch_db:fs_base_multi_media.fs_media_search_view +msgid "Attachment" +msgstr "Prilog" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_type__code +msgid "Code" +msgstr "Šifra" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media__create_uid +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_type__create_uid +#: model_terms:ir.ui.view,arch_db:fs_base_multi_media.fs_media_search_view +msgid "Created by" +msgstr "Kreirao" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media__create_date +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_type__create_date +msgid "Created on" +msgstr "Kreirano" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media__display_name +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_type__display_name +msgid "Display Name" +msgstr "Prikazani naziv" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media__file +msgid "File" +msgstr "Datoteka" + +#. module: fs_base_multi_media +#: model:ir.actions.act_window,name:fs_base_multi_media.fs_media_action +#: model:ir.ui.menu,name:fs_base_multi_media.fs_media_menu +msgid "Fs Media" +msgstr "Fs medij" + +#. module: fs_base_multi_media +#: model_terms:ir.ui.view,arch_db:fs_base_multi_media.fs_media_search_view +msgid "Group By" +msgstr "Grupiši po" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media__id +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_type__id +msgid "ID" +msgstr "ID" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media____last_update +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_type____last_update +msgid "Last Modified on" +msgstr "Zadnje mijenjano" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media__write_uid +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_type__write_uid +msgid "Last Updated by" +msgstr "Zadnji ažurirao" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media__write_date +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_type__write_date +msgid "Last Updated on" +msgstr "Zadnje ažurirano" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_relation_mixin__link_existing +msgid "Link existing media" +msgstr "Poveži postojeći medij" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_relation_mixin__media_id +msgid "Linked media" +msgstr "Povezani medij" + +#. module: fs_base_multi_media +#: model:ir.model,name:fs_base_multi_media.model_fs_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_relation_mixin__file +msgid "Media" +msgstr "Datoteka" + +#. module: fs_base_multi_media +#: model:ir.model,name:fs_base_multi_media.model_fs_media_relation_mixin +msgid "Media Relation" +msgstr "Relacija medija" + +#. module: fs_base_multi_media +#: model:ir.actions.act_window,name:fs_base_multi_media.act_open_fs_media_type_view +#: model:ir.model,name:fs_base_multi_media.model_fs_media_type +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media__media_type_id +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_relation_mixin__media_type_id +#: model:ir.ui.menu,name:fs_base_multi_media.menu_fs_media_type +msgid "Media Type" +msgstr "Tip medija" + +#. module: fs_base_multi_media +#: model_terms:ir.ui.view,arch_db:fs_base_multi_media.fs_media_search_view +msgid "MimeType" +msgstr "MimeType" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media__mimetype +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_relation_mixin__mimetype +msgid "Mimetype" +msgstr "Mimetype" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media__name +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_relation_mixin__name +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_type__name +msgid "Name" +msgstr "Naziv:" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_relation_mixin__sequence +msgid "Sequence" +msgstr "Sekvenca" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_relation_mixin__specific_file +msgid "Specific Media" +msgstr "Specifični medij" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_relation_mixin__specific_media_type_id +msgid "Specific Media Type" +msgstr "Specifični tip medija" + +#. module: fs_base_multi_media +#: model:res.groups,name:fs_base_multi_media.group_media_manager +msgid "Storage Media Manager" +msgstr "Storage media manager" + +#. module: fs_base_multi_media +#: model_terms:ir.ui.view,arch_db:fs_base_multi_media.fs_media_search_view +msgid "Type" +msgstr "Tip" diff --git a/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/i18n/es.po b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/i18n/es.po new file mode 100644 index 0000000..a8117de --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/i18n/es.po @@ -0,0 +1,159 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_base_multi_media +# +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 \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_media +#: model_terms:ir.ui.view,arch_db:fs_base_multi_media.fs_media_search_view +msgid "Attachment" +msgstr "Archivo Adjunto" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_type__code +msgid "Code" +msgstr "Código" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media__create_uid +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_type__create_uid +#: model_terms:ir.ui.view,arch_db:fs_base_multi_media.fs_media_search_view +msgid "Created by" +msgstr "Creado por" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media__create_date +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_type__create_date +msgid "Created on" +msgstr "Creado el" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media__display_name +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_type__display_name +msgid "Display Name" +msgstr "Mostrar Nombre" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media__file +msgid "File" +msgstr "Archivo" + +#. module: fs_base_multi_media +#: model:ir.actions.act_window,name:fs_base_multi_media.fs_media_action +#: model:ir.ui.menu,name:fs_base_multi_media.fs_media_menu +msgid "Fs Media" +msgstr "Medio Fs" + +#. module: fs_base_multi_media +#: model_terms:ir.ui.view,arch_db:fs_base_multi_media.fs_media_search_view +msgid "Group By" +msgstr "Agrupar Por" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media__id +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_type__id +msgid "ID" +msgstr "ID (identificación)" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media____last_update +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_type____last_update +msgid "Last Modified on" +msgstr "Última Modificación el" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media__write_uid +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_type__write_uid +msgid "Last Updated by" +msgstr "Última Actualización por" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media__write_date +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_type__write_date +msgid "Last Updated on" +msgstr "Última Actualización el" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_relation_mixin__link_existing +msgid "Link existing media" +msgstr "Vincular los medios existentes" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_relation_mixin__media_id +msgid "Linked media" +msgstr "Medios vinculados" + +#. module: fs_base_multi_media +#: model:ir.model,name:fs_base_multi_media.model_fs_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_relation_mixin__file +msgid "Media" +msgstr "Medios" + +#. module: fs_base_multi_media +#: model:ir.model,name:fs_base_multi_media.model_fs_media_relation_mixin +msgid "Media Relation" +msgstr "Relación con los medios" + +#. module: fs_base_multi_media +#: model:ir.actions.act_window,name:fs_base_multi_media.act_open_fs_media_type_view +#: model:ir.model,name:fs_base_multi_media.model_fs_media_type +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media__media_type_id +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_relation_mixin__media_type_id +#: model:ir.ui.menu,name:fs_base_multi_media.menu_fs_media_type +msgid "Media Type" +msgstr "Tipo de Medio" + +#. module: fs_base_multi_media +#: model_terms:ir.ui.view,arch_db:fs_base_multi_media.fs_media_search_view +msgid "MimeType" +msgstr "Tipo Mimo" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media__mimetype +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_relation_mixin__mimetype +msgid "Mimetype" +msgstr "Tipo Mimo" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media__name +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_relation_mixin__name +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_type__name +msgid "Name" +msgstr "Nombre" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_relation_mixin__sequence +msgid "Sequence" +msgstr "Secuencia" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_relation_mixin__specific_file +msgid "Specific Media" +msgstr "Medio Específico" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_relation_mixin__specific_media_type_id +msgid "Specific Media Type" +msgstr "Tipo de Medio Específico" + +#. module: fs_base_multi_media +#: model:res.groups,name:fs_base_multi_media.group_media_manager +msgid "Storage Media Manager" +msgstr "Gerente de Medios de Almacenamiento" + +#. module: fs_base_multi_media +#: model_terms:ir.ui.view,arch_db:fs_base_multi_media.fs_media_search_view +msgid "Type" +msgstr "Tipo" diff --git a/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/i18n/fs_base_multi_media.pot b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/i18n/fs_base_multi_media.pot new file mode 100644 index 0000000..9947a89 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/i18n/fs_base_multi_media.pot @@ -0,0 +1,156 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_base_multi_media +# +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_media +#: model_terms:ir.ui.view,arch_db:fs_base_multi_media.fs_media_search_view +msgid "Attachment" +msgstr "" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_type__code +msgid "Code" +msgstr "" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media__create_uid +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_type__create_uid +#: model_terms:ir.ui.view,arch_db:fs_base_multi_media.fs_media_search_view +msgid "Created by" +msgstr "" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media__create_date +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_type__create_date +msgid "Created on" +msgstr "" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media__display_name +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_type__display_name +msgid "Display Name" +msgstr "" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media__file +msgid "File" +msgstr "" + +#. module: fs_base_multi_media +#: model:ir.actions.act_window,name:fs_base_multi_media.fs_media_action +#: model:ir.ui.menu,name:fs_base_multi_media.fs_media_menu +msgid "Fs Media" +msgstr "" + +#. module: fs_base_multi_media +#: model_terms:ir.ui.view,arch_db:fs_base_multi_media.fs_media_search_view +msgid "Group By" +msgstr "" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media__id +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_type__id +msgid "ID" +msgstr "" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media____last_update +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_type____last_update +msgid "Last Modified on" +msgstr "" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media__write_uid +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_type__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media__write_date +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_type__write_date +msgid "Last Updated on" +msgstr "" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_relation_mixin__link_existing +msgid "Link existing media" +msgstr "" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_relation_mixin__media_id +msgid "Linked media" +msgstr "" + +#. module: fs_base_multi_media +#: model:ir.model,name:fs_base_multi_media.model_fs_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_relation_mixin__file +msgid "Media" +msgstr "" + +#. module: fs_base_multi_media +#: model:ir.model,name:fs_base_multi_media.model_fs_media_relation_mixin +msgid "Media Relation" +msgstr "" + +#. module: fs_base_multi_media +#: model:ir.actions.act_window,name:fs_base_multi_media.act_open_fs_media_type_view +#: model:ir.model,name:fs_base_multi_media.model_fs_media_type +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media__media_type_id +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_relation_mixin__media_type_id +#: model:ir.ui.menu,name:fs_base_multi_media.menu_fs_media_type +msgid "Media Type" +msgstr "" + +#. module: fs_base_multi_media +#: model_terms:ir.ui.view,arch_db:fs_base_multi_media.fs_media_search_view +msgid "MimeType" +msgstr "" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media__mimetype +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_relation_mixin__mimetype +msgid "Mimetype" +msgstr "" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media__name +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_relation_mixin__name +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_type__name +msgid "Name" +msgstr "" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_relation_mixin__sequence +msgid "Sequence" +msgstr "" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_relation_mixin__specific_file +msgid "Specific Media" +msgstr "" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_relation_mixin__specific_media_type_id +msgid "Specific Media Type" +msgstr "" + +#. module: fs_base_multi_media +#: model:res.groups,name:fs_base_multi_media.group_media_manager +msgid "Storage Media Manager" +msgstr "" + +#. module: fs_base_multi_media +#: model_terms:ir.ui.view,arch_db:fs_base_multi_media.fs_media_search_view +msgid "Type" +msgstr "" diff --git a/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/i18n/it.po b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/i18n/it.po new file mode 100644 index 0000000..b280d49 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/i18n/it.po @@ -0,0 +1,159 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_base_multi_media +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-11-28 14:33+0000\n" +"Last-Translator: mymage \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_media +#: model_terms:ir.ui.view,arch_db:fs_base_multi_media.fs_media_search_view +msgid "Attachment" +msgstr "Allegato" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_type__code +msgid "Code" +msgstr "Codice" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media__create_uid +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_type__create_uid +#: model_terms:ir.ui.view,arch_db:fs_base_multi_media.fs_media_search_view +msgid "Created by" +msgstr "Creato da" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media__create_date +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_type__create_date +msgid "Created on" +msgstr "Creato il" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media__display_name +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_type__display_name +msgid "Display Name" +msgstr "Nome visualizzato" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media__file +msgid "File" +msgstr "File" + +#. module: fs_base_multi_media +#: model:ir.actions.act_window,name:fs_base_multi_media.fs_media_action +#: model:ir.ui.menu,name:fs_base_multi_media.fs_media_menu +msgid "Fs Media" +msgstr "Media FS" + +#. module: fs_base_multi_media +#: model_terms:ir.ui.view,arch_db:fs_base_multi_media.fs_media_search_view +msgid "Group By" +msgstr "Raggruppa per" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media__id +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_type__id +msgid "ID" +msgstr "ID" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media____last_update +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_type____last_update +msgid "Last Modified on" +msgstr "Ultima modifica il" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media__write_uid +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_type__write_uid +msgid "Last Updated by" +msgstr "Ultimo aggiornamento di" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media__write_date +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_type__write_date +msgid "Last Updated on" +msgstr "Ultimo aggiornamento il" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_relation_mixin__link_existing +msgid "Link existing media" +msgstr "Collega media esistente" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_relation_mixin__media_id +msgid "Linked media" +msgstr "Media collegato" + +#. module: fs_base_multi_media +#: model:ir.model,name:fs_base_multi_media.model_fs_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_relation_mixin__file +msgid "Media" +msgstr "Media" + +#. module: fs_base_multi_media +#: model:ir.model,name:fs_base_multi_media.model_fs_media_relation_mixin +msgid "Media Relation" +msgstr "Relazione media" + +#. module: fs_base_multi_media +#: model:ir.actions.act_window,name:fs_base_multi_media.act_open_fs_media_type_view +#: model:ir.model,name:fs_base_multi_media.model_fs_media_type +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media__media_type_id +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_relation_mixin__media_type_id +#: model:ir.ui.menu,name:fs_base_multi_media.menu_fs_media_type +msgid "Media Type" +msgstr "Tipo media" + +#. module: fs_base_multi_media +#: model_terms:ir.ui.view,arch_db:fs_base_multi_media.fs_media_search_view +msgid "MimeType" +msgstr "Tipo MIME" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media__mimetype +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_relation_mixin__mimetype +msgid "Mimetype" +msgstr "Tipo MIME" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media__name +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_relation_mixin__name +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_type__name +msgid "Name" +msgstr "Nome" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_relation_mixin__sequence +msgid "Sequence" +msgstr "Sequenza" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_relation_mixin__specific_file +msgid "Specific Media" +msgstr "Media specifico" + +#. module: fs_base_multi_media +#: model:ir.model.fields,field_description:fs_base_multi_media.field_fs_media_relation_mixin__specific_media_type_id +msgid "Specific Media Type" +msgstr "Tipo media specifico" + +#. module: fs_base_multi_media +#: model:res.groups,name:fs_base_multi_media.group_media_manager +msgid "Storage Media Manager" +msgstr "Gestore deposito media" + +#. module: fs_base_multi_media +#: model_terms:ir.ui.view,arch_db:fs_base_multi_media.fs_media_search_view +msgid "Type" +msgstr "Tipo" diff --git a/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/models/__init__.py b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/models/__init__.py new file mode 100644 index 0000000..cb3a160 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/models/__init__.py @@ -0,0 +1,3 @@ +from . import fs_media +from . import fs_media_type +from . import fs_media_relation_mixin diff --git a/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/models/fs_media.py b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/models/fs_media.py new file mode 100644 index 0000000..8c4b14a --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/models/fs_media.py @@ -0,0 +1,28 @@ +# Copyright 2017 Akretion (http://www.akretion.com). +# Copyright 2023 ACSONE SA/NV +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + + +from odoo import api, fields, models + +from odoo.addons.fs_file.fields import FSFile + + +class FsMedia(models.Model): + _name = "fs.media" + _description = "Media" + + file = FSFile(required=True) + name = fields.Char(compute="_compute_name", store=True, index=True) + mimetype = fields.Char(compute="_compute_mimetype", store=True) + media_type_id = fields.Many2one("fs.media.type", "Media Type") + + @api.depends("file") + def _compute_name(self): + for record in self: + record.name = record.file.name if record.file else None + + @api.depends("file") + def _compute_mimetype(self): + for record in self: + record.mimetype = record.file.mimetype if record.file else None diff --git a/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/models/fs_media_relation_mixin.py b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/models/fs_media_relation_mixin.py new file mode 100644 index 0000000..cabc5c9 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/models/fs_media_relation_mixin.py @@ -0,0 +1,74 @@ +# 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_file import fields as fs_fields + + +class FsMediaRelationMixin(models.AbstractModel): + + _name = "fs.media.relation.mixin" + _description = "Media Relation" + _order = "sequence, name" + _rec_name = "name" + + sequence = fields.Integer() + link_existing = fields.Boolean( + string="Link existing media", + default=False, + ) + media_id = fields.Many2one( + comodel_name="fs.media", + string="Linked media", + ) + specific_file = fs_fields.FSFile("Specific Media") + specific_media_type_id = fields.Many2one( + "fs.media.type", + ) + file = fs_fields.FSFile("Media", compute="_compute_media", store=False) + media_type_id = fields.Many2one( + "fs.media.type", compute="_compute_media", store=False + ) + name = fields.Char(compute="_compute_name", store=True, index=True) + mimetype = fields.Char(compute="_compute_mimetype", store=True) + + @api.depends("file") + def _compute_name(self): + for record in self: + record.name = record.file.name if record.file else None + + @api.depends("file") + def _compute_mimetypes(self): + for record in self: + record.mimetype = record.file.mimetype if record.file else None + + @api.depends("media_id", "specific_file", "link_existing") + def _compute_media(self): + for record in self: + if record.link_existing: + record.file = record.media_id.file + record.media_type_id = record.media_id.media_type_id + else: + record.file = record.specific_file + record.media_type_id = record.specific_media_type_id + + @api.model + def _cleanup_vals(self, vals): + if ( + "link_existing" in vals + and vals["link_existing"] + and "specific_file" in vals + ): + vals["specific_file"] = False + 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) diff --git a/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/models/fs_media_type.py b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/models/fs_media_type.py new file mode 100644 index 0000000..014533e --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/models/fs_media_type.py @@ -0,0 +1,13 @@ +# Copyright 2017 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import fields, models + + +class FsMediaType(models.Model): + _name = "fs.media.type" + _description = "Media Type" + + name = fields.Char(translate=True, required=True) + code = fields.Char() diff --git a/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/readme/CONTRIBUTORS.rst b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000..dba4d6f --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Sebastien Beau +* Laurent Mignon (https://www.acsone.eu/) diff --git a/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/readme/DESCRIPTION.rst b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/readme/DESCRIPTION.rst new file mode 100644 index 0000000..d8fe9d5 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/readme/DESCRIPTION.rst @@ -0,0 +1,14 @@ +This addon allows you to store media file into external filesystem from odoo. +It also provides is a technical mixin model to ease the creation of other models +that need to be linked to multiple medias stored into external filesystems. + +The models provided by this addon are: + +* ``fs.media``: a model that stores a reference to an media stored into + an external filesystem. +* ``fs.media.relation.mixin``: an abstract model that can be used to + as base class for models created to store an media linked to a model. + This abstract model defines fields and methods to transparently handle + 2 cases: + * the media is specific to the model. + * the media is shared between multiple models and therefore is a ``fs.media`` instance linked to the mixin. diff --git a/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/readme/ROADMAP.rst b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/readme/ROADMAP.rst new file mode 100644 index 0000000..6b2d593 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/readme/ROADMAP.rst @@ -0,0 +1,2 @@ +* Add dedicated widget to ease the addition of new media to a model linked to + multiple medias. (As it's the case in the *storage_image_product* addon) diff --git a/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/readme/USAGE.rst b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/readme/USAGE.rst new file mode 100644 index 0000000..65f8ffc --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/readme/USAGE.rst @@ -0,0 +1,3 @@ +To be able to create and or manages shared images, you must have the ``Media Manager`` +role. If you do not have this role, as an authenticated user, you can +only view the shared images. diff --git a/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/security/fs_media.xml b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/security/fs_media.xml new file mode 100644 index 0000000..96539f9 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/security/fs_media.xml @@ -0,0 +1,25 @@ + + + + + + fs.media access read + + + + + + + + + + fs.media access read + + + + + + + + diff --git a/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/security/fs_media_type.xml b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/security/fs_media_type.xml new file mode 100644 index 0000000..266b693 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/security/fs_media_type.xml @@ -0,0 +1,25 @@ + + + + + + fs.media.type access read + + + + + + + + + + fs.media.type access read + + + + + + + + diff --git a/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/security/res_groups.xml b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/security/res_groups.xml new file mode 100644 index 0000000..0973e4d --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/security/res_groups.xml @@ -0,0 +1,12 @@ + + + + + Storage Media Manager + + + + diff --git a/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/static/description/icon.png b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/static/description/icon.png new file mode 100644 index 0000000..3a0328b Binary files /dev/null and b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/static/description/icon.png differ diff --git a/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/static/description/index.html b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/static/description/index.html new file mode 100644 index 0000000..699c5cd --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/static/description/index.html @@ -0,0 +1,459 @@ + + + + + + +Fs Base Multi Media + + + +
+

Fs Base Multi Media

+ + +

Alpha License: AGPL-3 OCA/storage Translate me on Weblate Try me on Runboat

+

This addon allows you to store media file into external filesystem from odoo. +It also provides is a technical mixin model to ease the creation of other models +that need to be linked to multiple medias stored into external filesystems.

+

The models provided by this addon are:

+
    +
  • fs.media: a model that stores a reference to an media stored into +an external filesystem.
  • +
  • fs.media.relation.mixin: an abstract model that can be used to +as base class for models created to store an media linked to a model. +This abstract model defines fields and methods to transparently handle +2 cases: +* the media is specific to the model. +* the media is shared between multiple models and therefore is a fs.media 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

+
+

Table of contents

+ +
+

Usage

+

To be able to create and or manages shared images, you must have the Media 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 media to a model linked to +multiple medias. (As it’s the case in the storage_image_product addon)
  • +
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub 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.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • ACSONE SA/NV
  • +
  • Akretion
  • +
+
+ +
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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.

+

Current maintainer:

+

lmignon

+

This module is part of the OCA/storage project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/views/fs_media.xml b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/views/fs_media.xml new file mode 100644 index 0000000..7be4187 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/views/fs_media.xml @@ -0,0 +1,88 @@ + + + + + + fs.media.form + fs.media + +
+ + +
+
+
+ + + fs.media.search + fs.media + + + + + + + + + + + + + + + + + fs.media.tree + fs.media + + + + + + + + + + + + Fs Media + fs.media + tree,form + [] + {} + + + + + +
diff --git a/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/views/fs_media_relation_mixin.xml b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/views/fs_media_relation_mixin.xml new file mode 100644 index 0000000..308e57c --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/views/fs_media_relation_mixin.xml @@ -0,0 +1,55 @@ + + + + + + fs.media.relation.mixin.form + fs.media.relation.mixin + +
+ + + + + + + + + + + + + + +
+
+
+ + + fs.media.relation.mixin.tree + fs.media.relation.mixin + + + + + + + + +
diff --git a/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/views/fs_media_type.xml b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/views/fs_media_type.xml new file mode 100644 index 0000000..498fab6 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_media/fs_base_multi_media/views/fs_media_type.xml @@ -0,0 +1,69 @@ + + + + + fs.media.type + + + + + + + + + + fs.media.type + +
+ + + + +
+
+
+ + + fs.media.type + + + + + + + + + + Media Type + ir.actions.act_window + fs.media.type + tree,form + + [] + {} + + + + + + form + + + + + + + tree + + + + + + +
diff --git a/odoo-bringout-oca-storage-fs_base_multi_media/pyproject.toml b/odoo-bringout-oca-storage-fs_base_multi_media/pyproject.toml new file mode 100644 index 0000000..6e1b356 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_base_multi_media/pyproject.toml @@ -0,0 +1,43 @@ +[project] +name = "odoo-bringout-oca-storage-fs_base_multi_media" +version = "16.0.0" +description = "Fs Base Multi Media - + Give the possibility to store media data in external filesystem from odoo" +authors = [ + { name = "Ernad Husremovic", email = "hernad@bring.out.ba" } +] +dependencies = [ + "odoo-bringout-oca-storage-fs_file>=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_media"] + +[tool.rye] +managed = true +dev-dependencies = [ + "pytest>=8.4.1", +] diff --git a/odoo-bringout-oca-storage-fs_file/README.md b/odoo-bringout-oca-storage-fs_file/README.md new file mode 100644 index 0000000..3059756 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file/README.md @@ -0,0 +1,46 @@ +# Fs File + +Odoo addon: fs_file + +## Installation + +```bash +pip install odoo-bringout-oca-storage-fs_file +``` + +## Dependencies + +This addon depends on: +- fs_attachment + +## Manifest Information + +- **Name**: Fs File +- **Version**: 16.0.1.0.6 +- **Category**: N/A +- **License**: AGPL-3 +- **Installable**: False + +## Source + +Based on [OCA/storage](https://github.com/OCA/storage) branch 16.0, addon `fs_file`. + +## 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 diff --git a/odoo-bringout-oca-storage-fs_file/doc/ARCHITECTURE.md b/odoo-bringout-oca-storage-fs_file/doc/ARCHITECTURE.md new file mode 100644 index 0000000..4719c97 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file/doc/ARCHITECTURE.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_file Module - fs_file + 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. diff --git a/odoo-bringout-oca-storage-fs_file/doc/CONFIGURATION.md b/odoo-bringout-oca-storage-fs_file/doc/CONFIGURATION.md new file mode 100644 index 0000000..987a53f --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file/doc/CONFIGURATION.md @@ -0,0 +1,3 @@ +# Configuration + +Refer to Odoo settings for fs_file. Configure related models, access rights, and options as needed. diff --git a/odoo-bringout-oca-storage-fs_file/doc/CONTROLLERS.md b/odoo-bringout-oca-storage-fs_file/doc/CONTROLLERS.md new file mode 100644 index 0000000..f628e77 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file/doc/CONTROLLERS.md @@ -0,0 +1,3 @@ +# Controllers + +This module does not define custom HTTP controllers. diff --git a/odoo-bringout-oca-storage-fs_file/doc/DEPENDENCIES.md b/odoo-bringout-oca-storage-fs_file/doc/DEPENDENCIES.md new file mode 100644 index 0000000..5d19fba --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file/doc/DEPENDENCIES.md @@ -0,0 +1,5 @@ +# Dependencies + +This addon depends on: + +- [fs_attachment](../../odoo-bringout-oca-storage-fs_attachment) diff --git a/odoo-bringout-oca-storage-fs_file/doc/FAQ.md b/odoo-bringout-oca-storage-fs_file/doc/FAQ.md new file mode 100644 index 0000000..3ef5e9e --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file/doc/FAQ.md @@ -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_file or install in UI. diff --git a/odoo-bringout-oca-storage-fs_file/doc/INSTALL.md b/odoo-bringout-oca-storage-fs_file/doc/INSTALL.md new file mode 100644 index 0000000..506ae75 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file/doc/INSTALL.md @@ -0,0 +1,7 @@ +# Install + +```bash +pip install odoo-bringout-oca-storage-fs_file" +# or +uv pip install odoo-bringout-oca-storage-fs_file" +``` diff --git a/odoo-bringout-oca-storage-fs_file/doc/MODELS.md b/odoo-bringout-oca-storage-fs_file/doc/MODELS.md new file mode 100644 index 0000000..f5c3a14 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file/doc/MODELS.md @@ -0,0 +1,11 @@ +# Models + +Detected core models and extensions in fs_file. + +```mermaid +classDiagram +``` + +Notes +- Classes show model technical names; fields omitted for brevity. +- Items listed under _inherit are extensions of existing models. diff --git a/odoo-bringout-oca-storage-fs_file/doc/OVERVIEW.md b/odoo-bringout-oca-storage-fs_file/doc/OVERVIEW.md new file mode 100644 index 0000000..34dacd1 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file/doc/OVERVIEW.md @@ -0,0 +1,6 @@ +# Overview + +Packaged Odoo addon: fs_file. Provides features documented in upstream Odoo 16 under this addon. + +- Source: OCA/OCB 16.0, addon fs_file +- License: LGPL-3 diff --git a/odoo-bringout-oca-storage-fs_file/doc/REPORTS.md b/odoo-bringout-oca-storage-fs_file/doc/REPORTS.md new file mode 100644 index 0000000..e0ea35f --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file/doc/REPORTS.md @@ -0,0 +1,3 @@ +# Reports + +This module does not define custom reports. diff --git a/odoo-bringout-oca-storage-fs_file/doc/SECURITY.md b/odoo-bringout-oca-storage-fs_file/doc/SECURITY.md new file mode 100644 index 0000000..e07da9d --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file/doc/SECURITY.md @@ -0,0 +1,8 @@ +# Security + +This module does not define custom security rules or access controls beyond Odoo defaults. + +Default Odoo security applies: +- Base user access through standard groups +- Model access inherited from dependencies +- No custom row-level security rules diff --git a/odoo-bringout-oca-storage-fs_file/doc/TROUBLESHOOTING.md b/odoo-bringout-oca-storage-fs_file/doc/TROUBLESHOOTING.md new file mode 100644 index 0000000..56853cb --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file/doc/TROUBLESHOOTING.md @@ -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. diff --git a/odoo-bringout-oca-storage-fs_file/doc/USAGE.md b/odoo-bringout-oca-storage-fs_file/doc/USAGE.md new file mode 100644 index 0000000..483b308 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file/doc/USAGE.md @@ -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_file +``` diff --git a/odoo-bringout-oca-storage-fs_file/doc/WIZARDS.md b/odoo-bringout-oca-storage-fs_file/doc/WIZARDS.md new file mode 100644 index 0000000..48e790d --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file/doc/WIZARDS.md @@ -0,0 +1,3 @@ +# Wizards + +This module does not include UI wizards. diff --git a/odoo-bringout-oca-storage-fs_file/fs_file/README.rst b/odoo-bringout-oca-storage-fs_file/fs_file/README.rst new file mode 100644 index 0000000..5dfe7d4 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file/fs_file/README.rst @@ -0,0 +1,273 @@ +======= +Fs File +======= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:cec7431f1becb99516793e51833fe9606ccd7459d148d15df61b03c14de1f6e4 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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_file + :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_file + :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 defines a new field type `FSFile` which is a file field that stores +a file in an external filesystem instead of the odoo's filestore. This is useful for +large files that you don't want to store in the filestore. Moreover, the field +value provides you an interface to access the file's contents and metadata. + +.. 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 `_ + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +The new field **FSFile** has been developed to allows you to store files +in an external filesystem storage. Its design is based on the following +principles: + +* The content of the file must be read from the filesystem only when + needed. +* It must be possible to manipulate the file content as a stream by default. +* Unlike Odoo's Binary field, the content is the raw file content by default + (no base64 encoding). +* To allows to exchange the file content with other systems, writing the + content as base64 is possible. The read operation will return a json + structure with the filename, the mimetype, the size and a url to download the file. + +This design allows to minimize the memory consumption of the server when +manipulating large files and exchanging them with other systems through +the default jsonrpc interface. + +Concretely, this design allows you to write code like this: + +.. code-block:: python + + from IO import BytesIO + from odoo import models, fields + from odoo.addons.fs_file.fields import FSFile + + class MyModel(models.Model): + _name = 'my.model' + + name = fields.Char() + file = FSFile() + + # Create a new record with a raw content + my_model = MyModel.create({ + 'name': 'My File', + 'file': BytesIO(b"content"), + }) + + assert(my_model.file.read() == b"content") + + # Create a new record with a base64 encoded content + my_model = MyModel.create({ + 'name': 'My File', + 'file': b"content".encode('base64'), + }) + assert(my_model.file.read() == b"content") + + # Create a new record with a file content + my_model = MyModel.create({ + 'name': 'My File', + 'file': open('my_file.txt', 'rb'), + }) + assert(my_model.file.read() == b"content") + assert(my_model.file.name == "my_file.txt") + + # create a record with a file content as base64 encoded and a filename + # This method is useful to create a record from a file uploaded + # through the web interface. + my_model = MyModel.create({ + 'name': 'My File', + 'file': { + 'filename': 'my_file.txt', + 'content': base64.b64encode(b"content"), + }, + }) + assert(my_model.file.read() == b"content") + assert(my_model.file.name == "my_file.txt") + + # write the content of the file as base64 encoded and a filename + # This method is useful to update a record from a file uploaded + # through the web interface. + my_model.write({ + 'file': { + 'name': 'my_file.txt', + 'file': base64.b64encode(b"content"), + }, + }) + + # the call to read() will return a json structure with the filename, + # the mimetype, the size and a url to download the file. + info = my_model.file.read() + assert(info["file"] == { + "filename": "my_file.txt", + "mimetype": "text/plain", + "size": 7, + "url": "/web/content/1234/my_file.txt", + }) + + # use the field as a file stream + # In such a case, the content is read from the filesystem without being + # stored in memory. + with my_model.file.open("rb) as f: + assert(f.read() == b"content") + + # use the field as a file stream to write the content + # In such a case, the content is written to the filesystem without being + # stored in memory. This kind of approach is useful to manipulate large + # files and to avoid to use too much memory. + # Transactional behaviour is ensured by the implementation! + with my_model.file.open("wb") as f: + f.write(b"content") + +Changelog +========= + +16.0.1.0.6 (2024-02-23) +~~~~~~~~~~~~~~~~~~~~~~~ + +**Bugfixes** + +- Fixes the creation of empty files. + + Before this change, the creation of empty files resulted in a constraint + violation error. This was due to the fact that even if a name was given + to the file it was not preserved into the FSFileValue object if no content + was given. As result, when the corresponding ir.attachment was created in + the database, the name was not set and the 'required' constraint was violated. (`#341 `_) + + +16.0.1.0.5 (2023-11-30) +~~~~~~~~~~~~~~~~~~~~~~~ + +**Bugfixes** + +- Ensure the cache is properly set when a new value is assigned to a FSFile field. + If the field is stored the value to the cache must be a FSFileValue object + linked to the attachment record used to store the file. Otherwise the value + must be one given since it could be the result of a compute method. (`#290 `_) + + +16.0.1.0.4 (2023-10-17) +~~~~~~~~~~~~~~~~~~~~~~~ + +**Bugfixes** + +- Browse attachment with sudo() to avoid read access errors + + In models that have a multi fs image relation, a new line + in form will trigger onchanges and will call the fs.file model + 'convert_to_cache()' method that will try to browse the attachment + with user profile that could have no read rights on attachment model. (`#288 `_) + + +16.0.1.0.3 (2023-10-05) +~~~~~~~~~~~~~~~~~~~~~~~ + +**Bugfixes** + +- Fix the *mimetype* property on *FSFileValue* objects. + + The *mimetype* value is computed as follow: + + * If an attachment is set, the mimetype is taken from the attachment. + * If no attachment is set, the mimetype is guessed from the name of the file. + * If the mimetype cannot be guessed from the name, the mimetype is guessed from + the content of the file. (`#284 `_) + + +16.0.1.0.1 (2023-09-29) +~~~~~~~~~~~~~~~~~~~~~~~ + +**Features** + +- Add a *url_path* property on the *FSFileValue* object. This property + allows you to easily get access to the relative path of the file on + the filesystem. This value is only available if the filesystem storage + is configured with a *Base URL* value. (`#281 `__) + + +**Bugfixes** + +- The *url_path*, *url* and *internal_url* properties on the *FSFileValue* + object return *None* if the information is not available (instead of *False*). + + The *url* property on the *FSFileValue* object returns the filesystem url nor + the url field of the attachment. (`#281 `__) + +Bug Tracker +=========== + +Bugs are tracked on `GitHub 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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* ACSONE SA/NV + +Contributors +~~~~~~~~~~~~ + +Laurent Mignon +Marie Lejeune +Hugues Damry + +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 `__: + +|maintainer-lmignon| + +This module is part of the `OCA/storage `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/odoo-bringout-oca-storage-fs_file/fs_file/__init__.py b/odoo-bringout-oca-storage-fs_file/fs_file/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/odoo-bringout-oca-storage-fs_file/fs_file/__manifest__.py b/odoo-bringout-oca-storage-fs_file/fs_file/__manifest__.py new file mode 100644 index 0000000..520ca6e --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file/fs_file/__manifest__.py @@ -0,0 +1,22 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Fs File", + "summary": """ + Field to store files into filesystem storages""", + "version": "16.0.1.0.6", + "license": "AGPL-3", + "author": "ACSONE SA/NV,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/storage", + "depends": ["fs_attachment"], + "data": [], + "demo": [], + "maintainers": ["lmignon"], + "development_status": "Alpha", + "assets": { + "web.assets_backend": [ + "fs_file/static/src/**/*", + ], + }, +} diff --git a/odoo-bringout-oca-storage-fs_file/fs_file/fields.py b/odoo-bringout-oca-storage-fs_file/fs_file/fields.py new file mode 100644 index 0000000..b11ba93 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file/fs_file/fields.py @@ -0,0 +1,447 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +# pylint: disable=method-required-super +import base64 +import itertools +import mimetypes +import os.path +from io import BytesIO, IOBase + +from odoo import fields +from odoo.tools.mimetypes import guess_mimetype + +from odoo.addons.fs_attachment.models.ir_attachment import IrAttachment + + +class FSFileValue: + def __init__( + self, + attachment: IrAttachment = None, + name: str = None, + value: bytes | IOBase = None, + ) -> None: + """ + This class holds the information related to FSFile field. It can be + used to assign a value to a FSFile field. In such a case, you can pass + the name and the file content as parameters. + + When + + :param attachment: the attachment to use to store the file. + :param name: the name of the file. If not provided, the name will be + taken from the attachment or the io.IOBase. + :param value: the content of the file. It can be bytes or an io.IOBase. + """ + self._is_new: bool = attachment is None + self._buffer: IOBase = None + self._attachment: IrAttachment = attachment + if name and attachment: + raise ValueError("Cannot set name and attachment at the same time") + if value: + if isinstance(value, IOBase): + self._buffer = value + if not hasattr(value, "name"): + if name: + self._buffer.name = name + else: + raise ValueError( + "name must be set when value is an io.IOBase " + "and is not provided by the io.IOBase" + ) + elif isinstance(value, bytes): + self._buffer = BytesIO(value) + if not name: + raise ValueError("name must be set when value is bytes") + self._buffer.name = name + else: + raise ValueError("value must be bytes or io.BytesIO") + elif name: + self._buffer = BytesIO(b"") + self._buffer.name = name + + @property + def write_buffer(self) -> BytesIO: + if self._buffer is None: + name = self._attachment.name if self._attachment else None + self._buffer = BytesIO() + self._buffer.name = name + return self._buffer + + @property + def name(self) -> str | None: + name = ( + self._attachment.name + if self._attachment + else self._buffer.name + if self._buffer + else None + ) + if name: + return os.path.basename(name) + return None + + @name.setter + def name(self, value: str) -> None: + # the name should only be updatable while the file is not yet stored + # TODO, we could also allow to update the name of the file and rename + # the file in the external file system + if self._is_new: + self.write_buffer.name = value + else: + raise ValueError( + "The name of the file can only be updated while the file is not " + "yet stored" + ) + + @property + def is_new(self) -> bool: + return self._is_new + + @property + def mimetype(self) -> str | None: + """Return the mimetype of the file. + + If an attachment is set, the mimetype is taken from the attachment. + If no attachment is set, the mimetype is guessed from the name of the + file. + If no name is set or if the mimetype cannot be guessed from the name, + the mimetype is guessed from the content of the file. + """ + mimetype = None + if self._attachment: + mimetype = self._attachment.mimetype + elif self.name: + mimetype = mimetypes.guess_type(self.name)[0] + # at last, try to guess the mimetype from the content + return mimetype or guess_mimetype(self.getvalue()) + + @property + def size(self) -> int: + if self._attachment: + return self._attachment.file_size + # check if the object supports len + try: + return len(self._buffer) + except TypeError: # pylint: disable=except-pass + # the object does not support len + pass + # if we are on a BytesIO, we can get the size from the buffer + if isinstance(self._buffer, BytesIO): + return self._buffer.getbuffer().nbytes + # we cannot get the size + return 0 + + @property + def url(self) -> str | None: + return self._attachment.fs_url or None if self._attachment else None + + @property + def internal_url(self) -> str | None: + return self._attachment.internal_url or None if self._attachment else None + + @property + def url_path(self) -> str | None: + return self._attachment.fs_url_path or None if self._attachment else None + + @property + def attachment(self) -> IrAttachment | None: + return self._attachment + + @attachment.setter + def attachment(self, value: IrAttachment) -> None: + self._attachment = value + self._buffer = None + + @property + def extension(self) -> str | None: + # get extension from mimetype + ext = os.path.splitext(self.name)[1] + if not ext: + ext = mimetypes.guess_extension(self.mimetype) + ext = ext and ext[1:] + return ext + + @property + def read_buffer(self) -> BytesIO: + if self._buffer is None: + content = b"" + name = None + if self._attachment: + content = self._attachment.raw or b"" + name = self._attachment.name + self._buffer = BytesIO(content) + self._buffer.name = name + return self._buffer + + def getvalue(self) -> bytes: + buffer = self.read_buffer + current_pos = buffer.tell() + buffer.seek(0) + value = buffer.read() + buffer.seek(current_pos) + return value + + def open( + self, + mode="rb", + block_size=None, + cache_options=None, + compression=None, + new_version=True, + **kwargs + ) -> IOBase: + """ + Return a file-like object that can be used to read and write the file content. + See the documentation of open() into the ir.attachment model from the + fs_attachment module for more information. + """ + if not self._attachment: + raise ValueError("Cannot open a file that is not stored") + return self._attachment.open( + mode=mode, + block_size=block_size, + cache_options=cache_options, + compression=compression, + new_version=new_version, + **kwargs, + ) + + +class FSFile(fields.Binary): + """ + This field is a binary field that stores the file content in an external + filesystem storage referenced by a storage code. + + A major difference with the standard Odoo binary field is that the value + is not encoded in base64 but is a bytes object. + + Moreover, the field is designed to always return an instance of + :class:`FSFileValue` when reading the value. This class is a file-like + object that can be used to read the file content and to get information + about the file (filename, mimetype, url, ...). + + To update the value of the field, the following values are accepted: + + - a bytes object (e.g. ``b"..."``) + - a dict with the following keys: + - ``filename``: the filename of the file + - ``content``: the content of the file encoded in base64 + - a FSFileValue instance + - a file-like object (e.g. an instance of :class:`io.BytesIO`) + + When the value is provided is a bytes object the filename is set to the + name of the field. You can override this behavior by providing specifying + a fs_filename key in the context. For example: + + .. code-block:: python + + record.with_context(fs_filename='my_file.txt').write({ + 'field': b'...', + }) + + The same applies when the value is provided as a file-like object but the + filename is set to the name of the file-like object or not a property of + the file-like object. (e.g. ``io.BytesIO(b'...')``). + + + When the value is converted to the read format, it's always an instance of + dict with the following keys: + + - ``filename``: the filename of the file + - ``mimetype``: the mimetype of the file + - ``size``: the size of the file + - ``url``: the url to access the file + + """ + + type = "fs_file" + + attachment: bool = True + + def __init__(self, *args, **kwargs): + kwargs["attachment"] = True + super().__init__(*args, **kwargs) + + def read(self, records): + domain = [ + ("res_model", "=", records._name), + ("res_field", "=", self.name), + ("res_id", "in", records.ids), + ] + data = { + att.res_id: self._convert_attachment_to_cache(att) + for att in records.env["ir.attachment"].sudo().search(domain) + } + records.env.cache.insert_missing(records, self, map(data.get, records._ids)) + + def create(self, record_values): + if not record_values: + return + env = record_values[0][0].env + with env.norecompute(): + for record, value in record_values: + if value: + cache_value = self.convert_to_cache(value, record) + attachment = self._create_attachment(record, cache_value) + cache_value = self._convert_attachment_to_cache(attachment) + record.env.cache.update( + record, + self, + [cache_value], + dirty=False, + ) + + def _create_attachment(self, record, cache_value: FSFileValue): + ir_attachment = ( + record.env["ir.attachment"] + .sudo() + .with_context( + binary_field_real_user=record.env.user, + ) + ) + create_value = self._prepare_attachment_create_values(record, cache_value) + return ir_attachment.create(create_value) + + def _prepare_attachment_create_values(self, record, cache_value: FSFileValue): + return { + "name": cache_value.name, + "raw": cache_value.getvalue(), + "res_model": record._name, + "res_field": self.name, + "res_id": record.id, + "type": "binary", + } + + def write(self, records, value): + # the code is copied from the standard Odoo Binary field + # with the following changes: + # - the value is not encoded in base64 and we therefore write on + # ir.attachment.raw instead of ir.attachment.datas + + # discard recomputation of self on records + records.env.remove_to_compute(self, records) + # update the cache, and discard the records that are not modified + cache = records.env.cache + cache_value = self.convert_to_cache(value, records) + records = cache.get_records_different_from(records, self, cache_value) + if not records: + return records + if self.store: + # determine records that are known to be not null + not_null = cache.get_records_different_from(records, self, None) + + if self.store: + # Be sure to invalidate the cache for the modified records since + # the value of the field has changed and the new value will be linked + # to the attachment record used to store the file in the storage. + cache.remove(records, self) + else: + # if the field is not stored and a value is set, we need to + # set the value in the cache since the value (the case for computed + # fields) + cache.update(records, self, itertools.repeat(cache_value)) + # retrieve the attachments that store the values, and adapt them + if self.store and any(records._ids): + real_records = records.filtered("id") + atts = ( + records.env["ir.attachment"] + .sudo() + .with_context( + binary_field_real_user=records.env.user, + ) + ) + if not_null: + atts = atts.search( + [ + ("res_model", "=", self.model_name), + ("res_field", "=", self.name), + ("res_id", "in", real_records.ids), + ] + ) + if value: + filename = cache_value.name + content = cache_value.getvalue() + # update the existing attachments + atts.write({"raw": content, "name": filename}) + atts_records = records.browse(atts.mapped("res_id")) + # set new value in the cache since we have the reference to the + # attachment record and a new access to the field will nomore + # require to load the attachment record + for record in atts_records: + new_cache_value = self._convert_attachment_to_cache( + atts.filtered(lambda att: att.res_id == record.id) + ) + cache.update(record, self, [new_cache_value], dirty=False) + # create the missing attachments + missing = real_records - atts_records + if missing: + created = atts.browse() + for record in missing: + created |= self._create_attachment(record, cache_value) + for att in created: + record = records.browse(att.res_id) + new_cache_value = self._convert_attachment_to_cache(att) + record.env.cache.update( + record, self, [new_cache_value], dirty=False + ) + else: + atts.unlink() + + return records + + def _convert_attachment_to_cache(self, attachment: IrAttachment) -> FSFileValue: + return FSFileValue(attachment=attachment) + + def _get_filename(self, record): + return record.env.context.get("fs_filename", self.name) + + def convert_to_cache(self, value, record, validate=True): + if value is None or value is False: + return None + if isinstance(value, FSFileValue): + return value + if isinstance(value, dict): + if "content" not in value and value.get("url"): + # we come from an onchange + # The id is the third element of the url + att_id = value["url"].split("/")[3] + attachment = record.env["ir.attachment"].sudo().browse(int(att_id)) + return self._convert_attachment_to_cache(attachment) + return FSFileValue( + name=value["filename"], value=base64.b64decode(value["content"]) + ) + if isinstance(value, IOBase): + name = getattr(value, "name", None) + if name is None: + name = self._get_filename(record) + return FSFileValue(name=name, value=value) + if isinstance(value, bytes): + return FSFileValue( + name=self._get_filename(record), value=base64.b64decode(value) + ) + raise ValueError( + "Invalid value for %s: %r\n" + "Should be base64 encoded bytes or a file-like object" % (self, value) + ) + + def convert_to_write(self, value, record): + return self.convert_to_cache(value, record) + + def convert_to_read(self, value, record, use_name_get=True): + if value is None or value is False: + return None + if isinstance(value, FSFileValue): + res = { + "filename": value.name, + "size": value.size, + "mimetype": value.mimetype, + } + if value.attachment: + res["url"] = value.internal_url + else: + res["content"] = base64.b64encode(value.getvalue()).decode("ascii") + return res + raise ValueError( + "Invalid value for %s: %r\n" + "Should be base64 encoded bytes or a file-like object" % (self, value) + ) diff --git a/odoo-bringout-oca-storage-fs_file/fs_file/i18n/bs.po b/odoo-bringout-oca-storage-fs_file/fs_file/i18n/bs.po new file mode 100644 index 0000000..9cab870 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file/fs_file/i18n/bs.po @@ -0,0 +1,37 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_file +# +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_file +#. odoo-javascript +#: code:addons/fs_file/static/src/views/fields/fsfile_field.xml:0 +#: code:addons/fs_file/static/src/views/fields/fsfile_field.xml:0 +#, python-format +msgid "Clear" +msgstr "Očisti" + +#. module: fs_file +#. odoo-javascript +#: code:addons/fs_file/static/src/views/fields/fsfile_field.esm.js:0 +#, python-format +msgid "Could not display the selected image" +msgstr "Odabranu sliku nije moguće prikazati." + +#. module: fs_file +#. odoo-javascript +#: code:addons/fs_file/static/src/views/fields/fsfile_field.xml:0 +#: code:addons/fs_file/static/src/views/fields/fsfile_field.xml:0 +#, python-format +msgid "Edit" +msgstr "Uredi" diff --git a/odoo-bringout-oca-storage-fs_file/fs_file/i18n/es.po b/odoo-bringout-oca-storage-fs_file/fs_file/i18n/es.po new file mode 100644 index 0000000..eb2986d --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file/fs_file/i18n/es.po @@ -0,0 +1,38 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_file +# +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 \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_file +#. odoo-javascript +#: code:addons/fs_file/static/src/views/fields/fsfile_field.xml:0 +#, python-format +msgid "Clear" +msgstr "Limpiar" + +#. module: fs_file +#. odoo-javascript +#: code:addons/fs_file/static/src/views/fields/fsfile_field.esm.js:0 +#, python-format +msgid "Could not display the selected image" +msgstr "No se ha podido mostrar la imagen seleccionada" + +#. module: fs_file +#. odoo-javascript +#: code:addons/fs_file/static/src/views/fields/fsfile_field.xml:0 +#, python-format +msgid "Edit" +msgstr "Editar" diff --git a/odoo-bringout-oca-storage-fs_file/fs_file/i18n/fs_file.pot b/odoo-bringout-oca-storage-fs_file/fs_file/i18n/fs_file.pot new file mode 100644 index 0000000..4db035f --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file/fs_file/i18n/fs_file.pot @@ -0,0 +1,37 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_file +# +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_file +#. odoo-javascript +#: code:addons/fs_file/static/src/views/fields/fsfile_field.xml:0 +#: code:addons/fs_file/static/src/views/fields/fsfile_field.xml:0 +#, python-format +msgid "Clear" +msgstr "" + +#. module: fs_file +#. odoo-javascript +#: code:addons/fs_file/static/src/views/fields/fsfile_field.esm.js:0 +#, python-format +msgid "Could not display the selected image" +msgstr "" + +#. module: fs_file +#. odoo-javascript +#: code:addons/fs_file/static/src/views/fields/fsfile_field.xml:0 +#: code:addons/fs_file/static/src/views/fields/fsfile_field.xml:0 +#, python-format +msgid "Edit" +msgstr "" diff --git a/odoo-bringout-oca-storage-fs_file/fs_file/i18n/it.po b/odoo-bringout-oca-storage-fs_file/fs_file/i18n/it.po new file mode 100644 index 0000000..1d43d8e --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file/fs_file/i18n/it.po @@ -0,0 +1,38 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_file +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-11-29 20:33+0000\n" +"Last-Translator: mymage \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_file +#. odoo-javascript +#: code:addons/fs_file/static/src/views/fields/fsfile_field.xml:0 +#, python-format +msgid "Clear" +msgstr "Pulisci" + +#. module: fs_file +#. odoo-javascript +#: code:addons/fs_file/static/src/views/fields/fsfile_field.esm.js:0 +#, python-format +msgid "Could not display the selected image" +msgstr "Impossibile visualizzare l'immagine selezionata" + +#. module: fs_file +#. odoo-javascript +#: code:addons/fs_file/static/src/views/fields/fsfile_field.xml:0 +#, python-format +msgid "Edit" +msgstr "Modifica" diff --git a/odoo-bringout-oca-storage-fs_file/fs_file/readme/CONTRIBUTORS.rst b/odoo-bringout-oca-storage-fs_file/fs_file/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000..9f71f7b --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file/fs_file/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +Laurent Mignon +Marie Lejeune +Hugues Damry diff --git a/odoo-bringout-oca-storage-fs_file/fs_file/readme/DESCRIPTION.rst b/odoo-bringout-oca-storage-fs_file/fs_file/readme/DESCRIPTION.rst new file mode 100644 index 0000000..b48e44c --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file/fs_file/readme/DESCRIPTION.rst @@ -0,0 +1,4 @@ +This addon defines a new field type `FSFile` which is a file field that stores +a file in an external filesystem instead of the odoo's filestore. This is useful for +large files that you don't want to store in the filestore. Moreover, the field +value provides you an interface to access the file's contents and metadata. diff --git a/odoo-bringout-oca-storage-fs_file/fs_file/readme/HISTORY.rst b/odoo-bringout-oca-storage-fs_file/fs_file/readme/HISTORY.rst new file mode 100644 index 0000000..db41172 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file/fs_file/readme/HISTORY.rst @@ -0,0 +1,71 @@ +16.0.1.0.6 (2024-02-23) +~~~~~~~~~~~~~~~~~~~~~~~ + +**Bugfixes** + +- Fixes the creation of empty files. + + Before this change, the creation of empty files resulted in a constraint + violation error. This was due to the fact that even if a name was given + to the file it was not preserved into the FSFileValue object if no content + was given. As result, when the corresponding ir.attachment was created in + the database, the name was not set and the 'required' constraint was violated. (`#341 `_) + + +16.0.1.0.5 (2023-11-30) +~~~~~~~~~~~~~~~~~~~~~~~ + +**Bugfixes** + +- Ensure the cache is properly set when a new value is assigned to a FSFile field. + If the field is stored the value to the cache must be a FSFileValue object + linked to the attachment record used to store the file. Otherwise the value + must be one given since it could be the result of a compute method. (`#290 `_) + + +16.0.1.0.4 (2023-10-17) +~~~~~~~~~~~~~~~~~~~~~~~ + +**Bugfixes** + +- Browse attachment with sudo() to avoid read access errors + + In models that have a multi fs image relation, a new line + in form will trigger onchanges and will call the fs.file model + 'convert_to_cache()' method that will try to browse the attachment + with user profile that could have no read rights on attachment model. (`#288 `_) + + +16.0.1.0.3 (2023-10-05) +~~~~~~~~~~~~~~~~~~~~~~~ + +**Bugfixes** + +- Fix the *mimetype* property on *FSFileValue* objects. + + The *mimetype* value is computed as follow: + + * If an attachment is set, the mimetype is taken from the attachment. + * If no attachment is set, the mimetype is guessed from the name of the file. + * If the mimetype cannot be guessed from the name, the mimetype is guessed from + the content of the file. (`#284 `_) + + +16.0.1.0.1 (2023-09-29) +~~~~~~~~~~~~~~~~~~~~~~~ + +**Features** + +- Add a *url_path* property on the *FSFileValue* object. This property + allows you to easily get access to the relative path of the file on + the filesystem. This value is only available if the filesystem storage + is configured with a *Base URL* value. (`#281 `__) + + +**Bugfixes** + +- The *url_path*, *url* and *internal_url* properties on the *FSFileValue* + object return *None* if the information is not available (instead of *False*). + + The *url* property on the *FSFileValue* object returns the filesystem url nor + the url field of the attachment. (`#281 `__) diff --git a/odoo-bringout-oca-storage-fs_file/fs_file/readme/USAGE.rst b/odoo-bringout-oca-storage-fs_file/fs_file/readme/USAGE.rst new file mode 100644 index 0000000..affe202 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file/fs_file/readme/USAGE.rst @@ -0,0 +1,100 @@ +The new field **FSFile** has been developed to allows you to store files +in an external filesystem storage. Its design is based on the following +principles: + +* The content of the file must be read from the filesystem only when + needed. +* It must be possible to manipulate the file content as a stream by default. +* Unlike Odoo's Binary field, the content is the raw file content by default + (no base64 encoding). +* To allows to exchange the file content with other systems, writing the + content as base64 is possible. The read operation will return a json + structure with the filename, the mimetype, the size and a url to download the file. + +This design allows to minimize the memory consumption of the server when +manipulating large files and exchanging them with other systems through +the default jsonrpc interface. + +Concretely, this design allows you to write code like this: + +.. code-block:: python + + from IO import BytesIO + from odoo import models, fields + from odoo.addons.fs_file.fields import FSFile + + class MyModel(models.Model): + _name = 'my.model' + + name = fields.Char() + file = FSFile() + + # Create a new record with a raw content + my_model = MyModel.create({ + 'name': 'My File', + 'file': BytesIO(b"content"), + }) + + assert(my_model.file.read() == b"content") + + # Create a new record with a base64 encoded content + my_model = MyModel.create({ + 'name': 'My File', + 'file': b"content".encode('base64'), + }) + assert(my_model.file.read() == b"content") + + # Create a new record with a file content + my_model = MyModel.create({ + 'name': 'My File', + 'file': open('my_file.txt', 'rb'), + }) + assert(my_model.file.read() == b"content") + assert(my_model.file.name == "my_file.txt") + + # create a record with a file content as base64 encoded and a filename + # This method is useful to create a record from a file uploaded + # through the web interface. + my_model = MyModel.create({ + 'name': 'My File', + 'file': { + 'filename': 'my_file.txt', + 'content': base64.b64encode(b"content"), + }, + }) + assert(my_model.file.read() == b"content") + assert(my_model.file.name == "my_file.txt") + + # write the content of the file as base64 encoded and a filename + # This method is useful to update a record from a file uploaded + # through the web interface. + my_model.write({ + 'file': { + 'name': 'my_file.txt', + 'file': base64.b64encode(b"content"), + }, + }) + + # the call to read() will return a json structure with the filename, + # the mimetype, the size and a url to download the file. + info = my_model.file.read() + assert(info["file"] == { + "filename": "my_file.txt", + "mimetype": "text/plain", + "size": 7, + "url": "/web/content/1234/my_file.txt", + }) + + # use the field as a file stream + # In such a case, the content is read from the filesystem without being + # stored in memory. + with my_model.file.open("rb) as f: + assert(f.read() == b"content") + + # use the field as a file stream to write the content + # In such a case, the content is written to the filesystem without being + # stored in memory. This kind of approach is useful to manipulate large + # files and to avoid to use too much memory. + # Transactional behaviour is ensured by the implementation! + with my_model.file.open("wb") as f: + f.write(b"content") diff --git a/odoo-bringout-oca-storage-fs_file/fs_file/readme/newsfragments/.gitignore b/odoo-bringout-oca-storage-fs_file/fs_file/readme/newsfragments/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/odoo-bringout-oca-storage-fs_file/fs_file/static/description/icon.png b/odoo-bringout-oca-storage-fs_file/fs_file/static/description/icon.png new file mode 100644 index 0000000..3a0328b Binary files /dev/null and b/odoo-bringout-oca-storage-fs_file/fs_file/static/description/icon.png differ diff --git a/odoo-bringout-oca-storage-fs_file/fs_file/static/description/index.html b/odoo-bringout-oca-storage-fs_file/fs_file/static/description/index.html new file mode 100644 index 0000000..8255234 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file/fs_file/static/description/index.html @@ -0,0 +1,612 @@ + + + + + +Fs File + + + +
+

Fs File

+ + +

Alpha License: AGPL-3 OCA/storage Translate me on Weblate Try me on Runboat

+

This addon defines a new field type FSFile which is a file field that stores +a file in an external filesystem instead of the odoo’s filestore. This is useful for +large files that you don’t want to store in the filestore. Moreover, the field +value provides you an interface to access the file’s contents and metadata.

+
+

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

+
+

Table of contents

+ +
+

Usage

+

The new field FSFile has been developed to allows you to store files +in an external filesystem storage. Its design is based on the following +principles:

+
    +
  • The content of the file must be read from the filesystem only when +needed.
  • +
  • It must be possible to manipulate the file content as a stream by default.
  • +
  • Unlike Odoo’s Binary field, the content is the raw file content by default +(no base64 encoding).
  • +
  • To allows to exchange the file content with other systems, writing the +content as base64 is possible. The read operation will return a json +structure with the filename, the mimetype, the size and a url to download the file.
  • +
+

This design allows to minimize the memory consumption of the server when +manipulating large files and exchanging them with other systems through +the default jsonrpc interface.

+

Concretely, this design allows you to write code like this:

+
+from IO import BytesIO
+from odoo import models, fields
+from odoo.addons.fs_file.fields import FSFile
+
+class MyModel(models.Model):
+    _name = 'my.model'
+
+    name = fields.Char()
+    file = FSFile()
+
+# Create a new record with a raw content
+my_model = MyModel.create({
+    'name': 'My File',
+    'file': BytesIO(b"content"),
+})
+
+assert(my_model.file.read() == b"content")
+
+# Create a new record with a base64 encoded content
+my_model = MyModel.create({
+    'name': 'My File',
+    'file': b"content".encode('base64'),
+})
+assert(my_model.file.read() == b"content")
+
+# Create a new record with a file content
+my_model = MyModel.create({
+    'name': 'My File',
+    'file': open('my_file.txt', 'rb'),
+})
+assert(my_model.file.read() == b"content")
+assert(my_model.file.name == "my_file.txt")
+
+# create a record with a file content as base64 encoded and a filename
+# This method is useful to create a record from a file uploaded
+# through the web interface.
+my_model = MyModel.create({
+    'name': 'My File',
+    'file': {
+        'filename': 'my_file.txt',
+        'content': base64.b64encode(b"content"),
+    },
+})
+assert(my_model.file.read() == b"content")
+assert(my_model.file.name == "my_file.txt")
+
+# write the content of the file as base64 encoded and a filename
+# This method is useful to update a record from a file uploaded
+# through the web interface.
+my_model.write({
+    'file': {
+        'name': 'my_file.txt',
+        'file': base64.b64encode(b"content"),
+    },
+})
+
+# the call to read() will return a json structure with the filename,
+# the mimetype, the size and a url to download the file.
+info = my_model.file.read()
+assert(info["file"] == {
+    "filename": "my_file.txt",
+    "mimetype": "text/plain",
+    "size": 7,
+    "url": "/web/content/1234/my_file.txt",
+})
+
+# use the field as a file stream
+# In such a case, the content is read from the filesystem without being
+# stored in memory.
+with my_model.file.open("rb) as f:
+  assert(f.read() == b"content")
+
+# use the field as a file stream to write the content
+# In such a case, the content is written to the filesystem without being
+# stored in memory. This kind of approach is useful to manipulate large
+# files and to avoid to use too much memory.
+# Transactional behaviour is ensured by the implementation!
+with my_model.file.open("wb") as f:
+    f.write(b"content")
+
+
+
+

Changelog

+
+

16.0.1.0.6 (2024-02-23)

+

Bugfixes

+
    +
  • Fixes the creation of empty files.

    +

    Before this change, the creation of empty files resulted in a constraint +violation error. This was due to the fact that even if a name was given +to the file it was not preserved into the FSFileValue object if no content +was given. As result, when the corresponding ir.attachment was created in +the database, the name was not set and the ‘required’ constraint was violated. (#341)

    +
  • +
+
+
+

16.0.1.0.5 (2023-11-30)

+

Bugfixes

+
    +
  • Ensure the cache is properly set when a new value is assigned to a FSFile field. +If the field is stored the value to the cache must be a FSFileValue object +linked to the attachment record used to store the file. Otherwise the value +must be one given since it could be the result of a compute method. (#290)
  • +
+
+
+

16.0.1.0.4 (2023-10-17)

+

Bugfixes

+
    +
  • Browse attachment with sudo() to avoid read access errors

    +

    In models that have a multi fs image relation, a new line +in form will trigger onchanges and will call the fs.file model +‘convert_to_cache()’ method that will try to browse the attachment +with user profile that could have no read rights on attachment model. (#288)

    +
  • +
+
+
+

16.0.1.0.3 (2023-10-05)

+

Bugfixes

+
    +
  • Fix the mimetype property on FSFileValue objects.

    +

    The mimetype value is computed as follow:

    +
      +
    • If an attachment is set, the mimetype is taken from the attachment.
    • +
    • If no attachment is set, the mimetype is guessed from the name of the file.
    • +
    • If the mimetype cannot be guessed from the name, the mimetype is guessed from +the content of the file. (#284)
    • +
    +
  • +
+
+
+

16.0.1.0.1 (2023-09-29)

+

Features

+
    +
  • Add a url_path property on the FSFileValue object. This property +allows you to easily get access to the relative path of the file on +the filesystem. This value is only available if the filesystem storage +is configured with a Base URL value. (#281)
  • +
+

Bugfixes

+
    +
  • The url_path, url and internal_url properties on the FSFileValue +object return None if the information is not available (instead of False).

    +

    The url property on the FSFileValue object returns the filesystem url nor +the url field of the attachment. (#281)

    +
  • +
+
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub 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.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • ACSONE SA/NV
  • +
+
+
+

Contributors

+

Laurent Mignon <laurent.mignon@acsone.eu> +Marie Lejeune <marie.lejeune@acsone.eu> +Hugues Damry <hughes.damry@acsone.eu>

+
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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.

+

Current maintainer:

+

lmignon

+

This module is part of the OCA/storage project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/odoo-bringout-oca-storage-fs_file/fs_file/static/src/scss/fsfile_field.scss b/odoo-bringout-oca-storage-fs_file/fs_file/static/src/scss/fsfile_field.scss new file mode 100644 index 0000000..e69de29 diff --git a/odoo-bringout-oca-storage-fs_file/fs_file/static/src/views/fields/fsfile_field.esm.js b/odoo-bringout-oca-storage-fs_file/fs_file/static/src/views/fields/fsfile_field.esm.js new file mode 100644 index 0000000..7ae3d13 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file/fs_file/static/src/views/fields/fsfile_field.esm.js @@ -0,0 +1,76 @@ +/** @odoo-module */ + +/** + * Copyright 2023 ACSONE SA/NV + */ +import {Component, onWillUpdateProps, useState} from "@odoo/owl"; + +import {FileUploader} from "@web/views/fields/file_handler"; +import {getDataURLFromFile} from "@web/core/utils/urls"; +import {registry} from "@web/core/registry"; +import {standardFieldProps} from "@web/views/fields/standard_field_props"; +import {useService} from "@web/core/utils/hooks"; + +export class FSFileField extends Component { + setup() { + this.notification = useService("notification"); + this.state = useState({ + ...this.props.value, + isValid: true, + }); + onWillUpdateProps((nextProps) => { + this.state.isUploading = false; + const {filename, mimetype, url} = nextProps.value || {}; + this.state.filename = filename; + this.state.mimetype = mimetype; + this.state.url = url; + }); + } + + async uploadFile(file) { + this.state.isUploading = true; + const data = await getDataURLFromFile(file); + this.props.record.update({ + [this.props.name]: { + filename: file.name, + content: data.split(",")[1], + }, + }); + this.state.isUploading = false; + } + + clear() { + this.props.record.update({[this.props.name]: false}); + } + + onFileRemove() { + this.state.isValid = true; + this.props.update(false); + } + onFileUploaded(info) { + this.state.isValid = true; + this.props.update({ + filename: info.name, + content: info.data, + }); + } + onLoadFailed() { + this.state.isValid = false; + this.notification.add(this.env._t("Could not display the selected image"), { + type: "danger", + }); + } +} + +FSFileField.template = "fs_file.FSFileField"; +FSFileField.components = { + FileUploader, +}; +FSFileField.props = { + ...standardFieldProps, + acceptedFileExtensions: {type: String, optional: true}, +}; +FSFileField.defaultProps = { + acceptedFileExtensions: "*", +}; +registry.category("fields").add("fs_file", FSFileField); diff --git a/odoo-bringout-oca-storage-fs_file/fs_file/static/src/views/fields/fsfile_field.xml b/odoo-bringout-oca-storage-fs_file/fs_file/static/src/views/fields/fsfile_field.xml new file mode 100644 index 0000000..e52454d --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file/fs_file/static/src/views/fields/fsfile_field.xml @@ -0,0 +1,50 @@ + + + + + +
+ + + + + + + + + + + + + +
+
+ + + + + + +
+ +
diff --git a/odoo-bringout-oca-storage-fs_file/fs_file/tests/__init__.py b/odoo-bringout-oca-storage-fs_file/fs_file/tests/__init__.py new file mode 100644 index 0000000..9c12d36 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file/fs_file/tests/__init__.py @@ -0,0 +1 @@ +from . import test_fs_file diff --git a/odoo-bringout-oca-storage-fs_file/fs_file/tests/models.py b/odoo-bringout-oca-storage-fs_file/fs_file/tests/models.py new file mode 100644 index 0000000..145c85c --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file/fs_file/tests/models.py @@ -0,0 +1,15 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models + +from ..fields import FSFile + + +class TestModel(models.Model): + + _name = "test.model" + _description = "Test Model" + _log_access = False + + fs_file = FSFile() diff --git a/odoo-bringout-oca-storage-fs_file/fs_file/tests/test_fs_file.py b/odoo-bringout-oca-storage-fs_file/fs_file/tests/test_fs_file.py new file mode 100644 index 0000000..60111f6 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file/fs_file/tests/test_fs_file.py @@ -0,0 +1,206 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import base64 +import io +import os +import tempfile +from io import BytesIO + +from odoo_test_helper import FakeModelLoader +from PIL import Image + +from odoo.tests.common import TransactionCase + +from odoo.addons.fs_storage.models.fs_storage import FSStorage + +from ..fields import FSFileValue + + +class TestFsFile(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.loader = FakeModelLoader(cls.env, cls.__module__) + cls.loader.backup_registry() + from .models import TestModel + + cls.loader.update_registry((TestModel,)) + + cls.create_content = b"content" + cls.write_content = b"new content" + cls.tmpfile_path = tempfile.mkstemp(suffix=".txt")[1] + with open(cls.tmpfile_path, "wb") as f: + f.write(cls.create_content) + cls.filename = os.path.basename(cls.tmpfile_path) + f = BytesIO() + Image.new("RGB", (1, 1), color="red").save(f, "PNG") + f.seek(0) + cls.png_content = f + + def setUp(self): + super().setUp() + self.temp_dir: FSStorage = self.env["fs.storage"].create( + { + "name": "Temp FS Storage", + "protocol": "memory", + "code": "mem_dir", + "directory_path": "/tmp/", + "model_xmlids": "fs_file.model_test_model", + } + ) + + @classmethod + def tearDownClass(cls): + if os.path.exists(cls.tmpfile_path): + os.remove(cls.tmpfile_path) + cls.loader.restore_registry() + return super().tearDownClass() + + def _test_create(self, fs_file_value): + model = self.env["test.model"] + instance = model.create({"fs_file": fs_file_value}) + self.assertTrue(isinstance(instance.fs_file, FSFileValue)) + self.assertEqual(instance.fs_file.getvalue(), self.create_content) + self.assertEqual(instance.fs_file.name, self.filename) + self.assertEqual(instance.fs_file.url_path, None) + self.assertEqual(instance.fs_file.url, None) + + def _test_write(self, fs_file_value, **ctx): + instance = self.env["test.model"].create({}) + if ctx: + instance = instance.with_context(**ctx) + instance.fs_file = fs_file_value + self.assertEqual(instance.fs_file.getvalue(), self.write_content) + self.assertEqual(instance.fs_file.name, self.filename) + + def test_read(self): + instance = self.env["test.model"].create( + {"fs_file": FSFileValue(name=self.filename, value=self.create_content)} + ) + info = instance.read(["fs_file"])[0] + self.assertDictEqual( + info["fs_file"], + { + "filename": self.filename, + "mimetype": "text/plain", + "size": 7, + "url": instance.fs_file.internal_url, + }, + ) + + def test_create_with_fsfilebytesio(self): + self._test_create(FSFileValue(name=self.filename, value=self.create_content)) + + def test_create_with_dict(self): + self._test_create( + { + "filename": self.filename, + "content": base64.b64encode(self.create_content), + } + ) + + def test_write_with_dict(self): + self._test_write( + { + "filename": self.filename, + "content": base64.b64encode(self.write_content), + } + ) + + def test_create_with_file_like(self): + with open(self.tmpfile_path, "rb") as f: + self._test_create(f) + + def test_create_in_b64(self): + instance = self.env["test.model"].create( + {"fs_file": base64.b64encode(self.create_content)} + ) + self.assertTrue(isinstance(instance.fs_file, FSFileValue)) + self.assertEqual(instance.fs_file.getvalue(), self.create_content) + + def test_write_in_b64(self): + instance = self.env["test.model"].create({"fs_file": b"test"}) + instance.write({"fs_file": base64.b64encode(self.create_content)}) + self.assertTrue(isinstance(instance.fs_file, FSFileValue)) + self.assertEqual(instance.fs_file.getvalue(), self.create_content) + + def test_write_in_b64_with_specified_filename(self): + self._test_write( + base64.b64encode(self.write_content), fs_filename=self.filename + ) + + def test_create_with_io(self): + instance = self.env["test.model"].create( + {"fs_file": io.BytesIO(self.create_content)} + ) + self.assertTrue(isinstance(instance.fs_file, FSFileValue)) + self.assertEqual(instance.fs_file.getvalue(), self.create_content) + + def test_write_with_io(self): + instance = self.env["test.model"].create( + {"fs_file": io.BytesIO(self.create_content)} + ) + instance.write({"fs_file": io.BytesIO(b"test3")}) + self.assertTrue(isinstance(instance.fs_file, FSFileValue)) + self.assertEqual(instance.fs_file.getvalue(), b"test3") + + def test_create_with_empty_value(self): + instance = self.env["test.model"].create( + {"fs_file": FSFileValue(name=self.filename, value=b"")} + ) + self.assertEqual(instance.fs_file.getvalue(), b"") + self.assertEqual(instance.fs_file.name, self.filename) + + def test_write_with_empty_value(self): + instance = self.env["test.model"].create( + {"fs_file": FSFileValue(name=self.filename, value=self.create_content)} + ) + instance.write({"fs_file": FSFileValue(name=self.filename, value=b"")}) + self.assertEqual(instance.fs_file.getvalue(), b"") + self.assertEqual(instance.fs_file.name, self.filename) + + def test_modify_fsfilebytesio(self): + """If you modify the content of the FSFileValue, + the changes will be directly applied + and a new file in the storage must be created for the new content. + """ + instance = self.env["test.model"].create( + {"fs_file": FSFileValue(name=self.filename, value=self.create_content)} + ) + initial_store_fname = instance.fs_file.attachment.store_fname + with instance.fs_file.open(mode="wb") as f: + f.write(b"new_content") + self.assertNotEqual( + instance.fs_file.attachment.store_fname, initial_store_fname + ) + self.assertEqual(instance.fs_file.getvalue(), b"new_content") + + def test_fs_value_mimetype(self): + """Test that the mimetype is correctly computed on a FSFileValue""" + value = FSFileValue(name="test.png", value=self.create_content) + # in this case, the mimetype is not computed from the filename + self.assertEqual(value.mimetype, "image/png") + + value = FSFileValue(value=open(self.tmpfile_path, "rb")) + # in this case, the mimetype is not computed from the content + self.assertEqual(value.mimetype, "text/plain") + + # if the mimetype is not found into the name, it should be computed + # from the content + value = FSFileValue(name="test", value=self.png_content) + self.assertEqual(value.mimetype, "image/png") + + def test_cache_invalidation(self): + """Test that the cache is invalidated when the FSFileValue is modified + When we assign a FSFileValue to a field, the value in the cache must + be invalidated and the new value must be computed. This is required + because the FSFileValue from the cache should always be linked to the + attachment record used to store the file in the storage. + """ + value = FSFileValue(name="test.png", value=self.create_content) + instance = self.env["test.model"].create({"fs_file": value}) + self.assertNotEqual(instance.fs_file, value) + value = FSFileValue(name="test.png", value=self.write_content) + instance.write({"fs_file": value}) + self.assertNotEqual(instance.fs_file, value) diff --git a/odoo-bringout-oca-storage-fs_file/pyproject.toml b/odoo-bringout-oca-storage-fs_file/pyproject.toml new file mode 100644 index 0000000..3503520 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file/pyproject.toml @@ -0,0 +1,43 @@ +[project] +name = "odoo-bringout-oca-storage-fs_file" +version = "16.0.0" +description = "Fs File - + Field to store files into filesystem storages" +authors = [ + { name = "Ernad Husremovic", email = "hernad@bring.out.ba" } +] +dependencies = [ + "odoo-bringout-oca-storage-fs_attachment>=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_file"] + +[tool.rye] +managed = true +dev-dependencies = [ + "pytest>=8.4.1", +] diff --git a/odoo-bringout-oca-storage-fs_file_demo/README.md b/odoo-bringout-oca-storage-fs_file_demo/README.md new file mode 100644 index 0000000..d13b486 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file_demo/README.md @@ -0,0 +1,47 @@ +# Fs File Demo + +Odoo addon: fs_file_demo + +## Installation + +```bash +pip install odoo-bringout-oca-storage-fs_file_demo +``` + +## Dependencies + +This addon depends on: +- fs_file +- fs_image + +## Manifest Information + +- **Name**: Fs File Demo +- **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_file_demo`. + +## 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 diff --git a/odoo-bringout-oca-storage-fs_file_demo/doc/ARCHITECTURE.md b/odoo-bringout-oca-storage-fs_file_demo/doc/ARCHITECTURE.md new file mode 100644 index 0000000..52b1e56 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file_demo/doc/ARCHITECTURE.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_file_demo Module - fs_file_demo + 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. diff --git a/odoo-bringout-oca-storage-fs_file_demo/doc/CONFIGURATION.md b/odoo-bringout-oca-storage-fs_file_demo/doc/CONFIGURATION.md new file mode 100644 index 0000000..1d6fca4 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file_demo/doc/CONFIGURATION.md @@ -0,0 +1,3 @@ +# Configuration + +Refer to Odoo settings for fs_file_demo. Configure related models, access rights, and options as needed. diff --git a/odoo-bringout-oca-storage-fs_file_demo/doc/CONTROLLERS.md b/odoo-bringout-oca-storage-fs_file_demo/doc/CONTROLLERS.md new file mode 100644 index 0000000..f628e77 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file_demo/doc/CONTROLLERS.md @@ -0,0 +1,3 @@ +# Controllers + +This module does not define custom HTTP controllers. diff --git a/odoo-bringout-oca-storage-fs_file_demo/doc/DEPENDENCIES.md b/odoo-bringout-oca-storage-fs_file_demo/doc/DEPENDENCIES.md new file mode 100644 index 0000000..908da7d --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file_demo/doc/DEPENDENCIES.md @@ -0,0 +1,6 @@ +# Dependencies + +This addon depends on: + +- [fs_file](../../odoo-bringout-oca-storage-fs_file) +- [fs_image](../../odoo-bringout-oca-storage-fs_image) diff --git a/odoo-bringout-oca-storage-fs_file_demo/doc/FAQ.md b/odoo-bringout-oca-storage-fs_file_demo/doc/FAQ.md new file mode 100644 index 0000000..f0431e8 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file_demo/doc/FAQ.md @@ -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_file_demo or install in UI. diff --git a/odoo-bringout-oca-storage-fs_file_demo/doc/INSTALL.md b/odoo-bringout-oca-storage-fs_file_demo/doc/INSTALL.md new file mode 100644 index 0000000..b1afc14 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file_demo/doc/INSTALL.md @@ -0,0 +1,7 @@ +# Install + +```bash +pip install odoo-bringout-oca-storage-fs_file_demo" +# or +uv pip install odoo-bringout-oca-storage-fs_file_demo" +``` diff --git a/odoo-bringout-oca-storage-fs_file_demo/doc/MODELS.md b/odoo-bringout-oca-storage-fs_file_demo/doc/MODELS.md new file mode 100644 index 0000000..13fe25a --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file_demo/doc/MODELS.md @@ -0,0 +1,12 @@ +# Models + +Detected core models and extensions in fs_file_demo. + +```mermaid +classDiagram + class fs_file +``` + +Notes +- Classes show model technical names; fields omitted for brevity. +- Items listed under _inherit are extensions of existing models. diff --git a/odoo-bringout-oca-storage-fs_file_demo/doc/OVERVIEW.md b/odoo-bringout-oca-storage-fs_file_demo/doc/OVERVIEW.md new file mode 100644 index 0000000..b876b41 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file_demo/doc/OVERVIEW.md @@ -0,0 +1,6 @@ +# Overview + +Packaged Odoo addon: fs_file_demo. Provides features documented in upstream Odoo 16 under this addon. + +- Source: OCA/OCB 16.0, addon fs_file_demo +- License: LGPL-3 diff --git a/odoo-bringout-oca-storage-fs_file_demo/doc/REPORTS.md b/odoo-bringout-oca-storage-fs_file_demo/doc/REPORTS.md new file mode 100644 index 0000000..e0ea35f --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file_demo/doc/REPORTS.md @@ -0,0 +1,3 @@ +# Reports + +This module does not define custom reports. diff --git a/odoo-bringout-oca-storage-fs_file_demo/doc/SECURITY.md b/odoo-bringout-oca-storage-fs_file_demo/doc/SECURITY.md new file mode 100644 index 0000000..f80d85c --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file_demo/doc/SECURITY.md @@ -0,0 +1,73 @@ +# Security + +Access control and security definitions in fs_file_demo. + +## 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.xml](../fs_file_demo/security/fs_file.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.xml](../fs_file_demo/security/fs_file.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 diff --git a/odoo-bringout-oca-storage-fs_file_demo/doc/TROUBLESHOOTING.md b/odoo-bringout-oca-storage-fs_file_demo/doc/TROUBLESHOOTING.md new file mode 100644 index 0000000..56853cb --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file_demo/doc/TROUBLESHOOTING.md @@ -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. diff --git a/odoo-bringout-oca-storage-fs_file_demo/doc/USAGE.md b/odoo-bringout-oca-storage-fs_file_demo/doc/USAGE.md new file mode 100644 index 0000000..f80d0f4 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file_demo/doc/USAGE.md @@ -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_file_demo +``` diff --git a/odoo-bringout-oca-storage-fs_file_demo/doc/WIZARDS.md b/odoo-bringout-oca-storage-fs_file_demo/doc/WIZARDS.md new file mode 100644 index 0000000..48e790d --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file_demo/doc/WIZARDS.md @@ -0,0 +1,3 @@ +# Wizards + +This module does not include UI wizards. diff --git a/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/README.rst b/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/README.rst new file mode 100644 index 0000000..0c726a5 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/README.rst @@ -0,0 +1,88 @@ +============ +Fs File Demo +============ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:e72aaafa8774c693512cf0e14375af0a467af9b22b55c3ff738967dffd617aee + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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_file_demo + :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_file_demo + :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 demo addon for ``fs_file``. + +.. 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 `_ + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +Go into Settings > Technical > Fs File to create a new record of model +``fs.file``, which contains a FSFile record. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub 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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* ACSONE SA/NV + +Contributors +~~~~~~~~~~~~ + +Laurent Mignon +Marie Lejeune + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/storage `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/__init__.py b/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/__manifest__.py b/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/__manifest__.py new file mode 100644 index 0000000..217c8e8 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/__manifest__.py @@ -0,0 +1,21 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Fs File Demo", + "summary": """Demo addon for fs_file and fs_image""", + "version": "16.0.1.0.1", + "license": "AGPL-3", + "author": "ACSONE SA/NV,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/storage", + "depends": [ + "fs_file", + "fs_image", + ], + "data": [ + "security/fs_file.xml", + "views/fs_file.xml", + ], + "demo": [], + "development_status": "Alpha", +} diff --git a/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/i18n/bs.po b/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/i18n/bs.po new file mode 100644 index 0000000..61f8034 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/i18n/bs.po @@ -0,0 +1,76 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_file_demo +# +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_file_demo +#: model:ir.model.fields,field_description:fs_file_demo.field_fs_file__create_uid +msgid "Created by" +msgstr "Kreirao" + +#. module: fs_file_demo +#: model:ir.model.fields,field_description:fs_file_demo.field_fs_file__create_date +msgid "Created on" +msgstr "Kreirano" + +#. module: fs_file_demo +#: model:ir.model.fields,field_description:fs_file_demo.field_fs_file__display_name +msgid "Display Name" +msgstr "Prikazani naziv" + +#. module: fs_file_demo +#: model:ir.model.fields,field_description:fs_file_demo.field_fs_file__file +msgid "File" +msgstr "Datoteka" + +#. module: fs_file_demo +#: model:ir.actions.act_window,name:fs_file_demo.fs_file_act_window +#: model:ir.model,name:fs_file_demo.model_fs_file +#: model:ir.ui.menu,name:fs_file_demo.fs_file_menu +msgid "Fs File" +msgstr "Fs datoteka" + +#. module: fs_file_demo +#: model:ir.model.fields,field_description:fs_file_demo.field_fs_file__id +msgid "ID" +msgstr "ID" + +#. module: fs_file_demo +#: model:ir.model.fields,field_description:fs_file_demo.field_fs_file__fs_image_128 +msgid "Image (128)" +msgstr "Slika (128)" + +#. module: fs_file_demo +#: model:ir.model.fields,field_description:fs_file_demo.field_fs_file__fs_image_1920 +msgid "Image (1920)" +msgstr "Slika (1920)" + +#. module: fs_file_demo +#: model:ir.model.fields,field_description:fs_file_demo.field_fs_file____last_update +msgid "Last Modified on" +msgstr "Zadnje mijenjano" + +#. module: fs_file_demo +#: model:ir.model.fields,field_description:fs_file_demo.field_fs_file__write_uid +msgid "Last Updated by" +msgstr "Zadnji ažurirao" + +#. module: fs_file_demo +#: model:ir.model.fields,field_description:fs_file_demo.field_fs_file__write_date +msgid "Last Updated on" +msgstr "Zadnje ažurirano" + +#. module: fs_file_demo +#: model:ir.model.fields,field_description:fs_file_demo.field_fs_file__name +msgid "Name" +msgstr "Naziv:" diff --git a/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/i18n/es.po b/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/i18n/es.po new file mode 100644 index 0000000..36f105e --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/i18n/es.po @@ -0,0 +1,82 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_file_demo +# +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 \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_file_demo +#: model:ir.model.fields,field_description:fs_file_demo.field_fs_file__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: fs_file_demo +#: model:ir.model.fields,field_description:fs_file_demo.field_fs_file__create_date +msgid "Created on" +msgstr "Creado el" + +#. module: fs_file_demo +#: model:ir.model.fields,field_description:fs_file_demo.field_fs_file__display_name +msgid "Display Name" +msgstr "Mostrar Nombre" + +#. module: fs_file_demo +#: model:ir.model.fields,field_description:fs_file_demo.field_fs_file__file +msgid "File" +msgstr "Archivo" + +#. module: fs_file_demo +#: model:ir.actions.act_window,name:fs_file_demo.fs_file_act_window +#: model:ir.model,name:fs_file_demo.model_fs_file +#: model:ir.ui.menu,name:fs_file_demo.fs_file_menu +msgid "Fs File" +msgstr "Archivo Fs" + +#. module: fs_file_demo +#: model:ir.model.fields,field_description:fs_file_demo.field_fs_file__id +msgid "ID" +msgstr "ID (identificación)" + +#. module: fs_file_demo +#: model:ir.model.fields,field_description:fs_file_demo.field_fs_file__fs_image_128 +msgid "Image (128)" +msgstr "" + +#. module: fs_file_demo +#: model:ir.model.fields,field_description:fs_file_demo.field_fs_file__fs_image_1920 +msgid "Image (1920)" +msgstr "" + +#. module: fs_file_demo +#: model:ir.model.fields,field_description:fs_file_demo.field_fs_file____last_update +msgid "Last Modified on" +msgstr "Última Modificación el" + +#. module: fs_file_demo +#: model:ir.model.fields,field_description:fs_file_demo.field_fs_file__write_uid +msgid "Last Updated by" +msgstr "Última Actualización por" + +#. module: fs_file_demo +#: model:ir.model.fields,field_description:fs_file_demo.field_fs_file__write_date +msgid "Last Updated on" +msgstr "Última Actualización el" + +#. module: fs_file_demo +#: model:ir.model.fields,field_description:fs_file_demo.field_fs_file__name +msgid "Name" +msgstr "Nombre" + +#~ msgid "Image" +#~ msgstr "Imagen" diff --git a/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/i18n/fs_file_demo.pot b/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/i18n/fs_file_demo.pot new file mode 100644 index 0000000..7475b66 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/i18n/fs_file_demo.pot @@ -0,0 +1,76 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_file_demo +# +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_file_demo +#: model:ir.model.fields,field_description:fs_file_demo.field_fs_file__create_uid +msgid "Created by" +msgstr "" + +#. module: fs_file_demo +#: model:ir.model.fields,field_description:fs_file_demo.field_fs_file__create_date +msgid "Created on" +msgstr "" + +#. module: fs_file_demo +#: model:ir.model.fields,field_description:fs_file_demo.field_fs_file__display_name +msgid "Display Name" +msgstr "" + +#. module: fs_file_demo +#: model:ir.model.fields,field_description:fs_file_demo.field_fs_file__file +msgid "File" +msgstr "" + +#. module: fs_file_demo +#: model:ir.actions.act_window,name:fs_file_demo.fs_file_act_window +#: model:ir.model,name:fs_file_demo.model_fs_file +#: model:ir.ui.menu,name:fs_file_demo.fs_file_menu +msgid "Fs File" +msgstr "" + +#. module: fs_file_demo +#: model:ir.model.fields,field_description:fs_file_demo.field_fs_file__id +msgid "ID" +msgstr "" + +#. module: fs_file_demo +#: model:ir.model.fields,field_description:fs_file_demo.field_fs_file__fs_image_128 +msgid "Image (128)" +msgstr "" + +#. module: fs_file_demo +#: model:ir.model.fields,field_description:fs_file_demo.field_fs_file__fs_image_1920 +msgid "Image (1920)" +msgstr "" + +#. module: fs_file_demo +#: model:ir.model.fields,field_description:fs_file_demo.field_fs_file____last_update +msgid "Last Modified on" +msgstr "" + +#. module: fs_file_demo +#: model:ir.model.fields,field_description:fs_file_demo.field_fs_file__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: fs_file_demo +#: model:ir.model.fields,field_description:fs_file_demo.field_fs_file__write_date +msgid "Last Updated on" +msgstr "" + +#. module: fs_file_demo +#: model:ir.model.fields,field_description:fs_file_demo.field_fs_file__name +msgid "Name" +msgstr "" diff --git a/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/i18n/it.po b/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/i18n/it.po new file mode 100644 index 0000000..b55cc02 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/i18n/it.po @@ -0,0 +1,82 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_file_demo +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-07-08 08:59+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.6.2\n" + +#. module: fs_file_demo +#: model:ir.model.fields,field_description:fs_file_demo.field_fs_file__create_uid +msgid "Created by" +msgstr "Creato da" + +#. module: fs_file_demo +#: model:ir.model.fields,field_description:fs_file_demo.field_fs_file__create_date +msgid "Created on" +msgstr "Creato il" + +#. module: fs_file_demo +#: model:ir.model.fields,field_description:fs_file_demo.field_fs_file__display_name +msgid "Display Name" +msgstr "Nome visualizzato" + +#. module: fs_file_demo +#: model:ir.model.fields,field_description:fs_file_demo.field_fs_file__file +msgid "File" +msgstr "File" + +#. module: fs_file_demo +#: model:ir.actions.act_window,name:fs_file_demo.fs_file_act_window +#: model:ir.model,name:fs_file_demo.model_fs_file +#: model:ir.ui.menu,name:fs_file_demo.fs_file_menu +msgid "Fs File" +msgstr "File FS" + +#. module: fs_file_demo +#: model:ir.model.fields,field_description:fs_file_demo.field_fs_file__id +msgid "ID" +msgstr "ID" + +#. module: fs_file_demo +#: model:ir.model.fields,field_description:fs_file_demo.field_fs_file__fs_image_128 +msgid "Image (128)" +msgstr "Immagine 128" + +#. module: fs_file_demo +#: model:ir.model.fields,field_description:fs_file_demo.field_fs_file__fs_image_1920 +msgid "Image (1920)" +msgstr "Immagine (1920)" + +#. module: fs_file_demo +#: model:ir.model.fields,field_description:fs_file_demo.field_fs_file____last_update +msgid "Last Modified on" +msgstr "Ultima modifica il" + +#. module: fs_file_demo +#: model:ir.model.fields,field_description:fs_file_demo.field_fs_file__write_uid +msgid "Last Updated by" +msgstr "Ultimo aggiornamento di" + +#. module: fs_file_demo +#: model:ir.model.fields,field_description:fs_file_demo.field_fs_file__write_date +msgid "Last Updated on" +msgstr "Ultimo aggiornamento il" + +#. module: fs_file_demo +#: model:ir.model.fields,field_description:fs_file_demo.field_fs_file__name +msgid "Name" +msgstr "Nome" + +#~ msgid "Image" +#~ msgstr "Immagine" diff --git a/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/models/__init__.py b/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/models/__init__.py new file mode 100644 index 0000000..e5b23f8 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/models/__init__.py @@ -0,0 +1 @@ +from . import fs_file diff --git a/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/models/fs_file.py b/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/models/fs_file.py new file mode 100644 index 0000000..9e7867b --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/models/fs_file.py @@ -0,0 +1,27 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + +from odoo.addons.fs_file import fields as fs_fields +from odoo.addons.fs_image import fields as fs_image_fields + + +class FsFile(models.Model): + + _name = "fs.file" + _description = "Fs File" + + name = fields.Char() + file = fs_fields.FSFile(string="File") + + fs_image_1920 = fs_image_fields.FSImage( + string="Image (1920)", max_width=1920, max_height=1920 + ) + fs_image_128 = fs_image_fields.FSImage( + string="Image (128)", + max_width=128, + max_height=128, + related="fs_image_1920", + store=True, + ) diff --git a/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/readme/CONTRIBUTORS.rst b/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000..ce84680 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +Laurent Mignon +Marie Lejeune diff --git a/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/readme/DESCRIPTION.rst b/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/readme/DESCRIPTION.rst new file mode 100644 index 0000000..b5321ed --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This addon is a demo addon for ``fs_file``. diff --git a/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/readme/USAGE.rst b/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/readme/USAGE.rst new file mode 100644 index 0000000..43a9027 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/readme/USAGE.rst @@ -0,0 +1,2 @@ +Go into Settings > Technical > Fs File to create a new record of model +``fs.file``, which contains a FSFile record. diff --git a/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/security/fs_file.xml b/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/security/fs_file.xml new file mode 100644 index 0000000..70db6f0 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/security/fs_file.xml @@ -0,0 +1,16 @@ + + + + + + fs.file default access + + + + + + + + + diff --git a/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/static/description/icon.png b/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/static/description/icon.png new file mode 100644 index 0000000..3a0328b Binary files /dev/null and b/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/static/description/icon.png differ diff --git a/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/static/description/index.html b/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/static/description/index.html new file mode 100644 index 0000000..47658d1 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/static/description/index.html @@ -0,0 +1,431 @@ + + + + + +Fs File Demo + + + +
+

Fs File Demo

+ + +

Alpha License: AGPL-3 OCA/storage Translate me on Weblate Try me on Runboat

+

This addon is a demo addon for fs_file.

+
+

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

+
+

Table of contents

+ +
+

Usage

+

Go into Settings > Technical > Fs File to create a new record of model +fs.file, which contains a FSFile record.

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub 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.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • ACSONE SA/NV
  • +
+
+
+

Contributors

+

Laurent Mignon <laurent.mignon@acsone.eu> +Marie Lejeune <marie.lejeune@acsone.eu>

+
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/storage project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/views/fs_file.xml b/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/views/fs_file.xml new file mode 100644 index 0000000..3efd6a7 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file_demo/fs_file_demo/views/fs_file.xml @@ -0,0 +1,53 @@ + + + + + + fs.file.form (in fs_file_demo) + fs.file + +
+
+ + + + + + + + + + + + + + fs.file.tree (in fs_file_demo) + fs.file + + + + + + + + + Fs File + fs.file + tree,form + [] + {} + + + + Fs File + + + + + + diff --git a/odoo-bringout-oca-storage-fs_file_demo/pyproject.toml b/odoo-bringout-oca-storage-fs_file_demo/pyproject.toml new file mode 100644 index 0000000..521ece9 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_file_demo/pyproject.toml @@ -0,0 +1,43 @@ +[project] +name = "odoo-bringout-oca-storage-fs_file_demo" +version = "16.0.0" +description = "Fs File Demo - Demo addon for fs_file and fs_image" +authors = [ + { name = "Ernad Husremovic", email = "hernad@bring.out.ba" } +] +dependencies = [ + "odoo-bringout-oca-storage-fs_file>=16.0.0", + "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_file_demo"] + +[tool.rye] +managed = true +dev-dependencies = [ + "pytest>=8.4.1", +] diff --git a/odoo-bringout-oca-storage-fs_image/README.md b/odoo-bringout-oca-storage-fs_image/README.md new file mode 100644 index 0000000..7efd8cd --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image/README.md @@ -0,0 +1,46 @@ +# Fs Image + +Odoo addon: fs_image + +## Installation + +```bash +pip install odoo-bringout-oca-storage-fs_image +``` + +## Dependencies + +This addon depends on: +- fs_file + +## Manifest Information + +- **Name**: Fs Image +- **Version**: 16.0.1.0.4 +- **Category**: N/A +- **License**: AGPL-3 +- **Installable**: False + +## Source + +Based on [OCA/storage](https://github.com/OCA/storage) branch 16.0, addon `fs_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 diff --git a/odoo-bringout-oca-storage-fs_image/doc/ARCHITECTURE.md b/odoo-bringout-oca-storage-fs_image/doc/ARCHITECTURE.md new file mode 100644 index 0000000..0275b84 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image/doc/ARCHITECTURE.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_image Module - fs_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. diff --git a/odoo-bringout-oca-storage-fs_image/doc/CONFIGURATION.md b/odoo-bringout-oca-storage-fs_image/doc/CONFIGURATION.md new file mode 100644 index 0000000..a1229e6 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image/doc/CONFIGURATION.md @@ -0,0 +1,3 @@ +# Configuration + +Refer to Odoo settings for fs_image. Configure related models, access rights, and options as needed. diff --git a/odoo-bringout-oca-storage-fs_image/doc/CONTROLLERS.md b/odoo-bringout-oca-storage-fs_image/doc/CONTROLLERS.md new file mode 100644 index 0000000..f628e77 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image/doc/CONTROLLERS.md @@ -0,0 +1,3 @@ +# Controllers + +This module does not define custom HTTP controllers. diff --git a/odoo-bringout-oca-storage-fs_image/doc/DEPENDENCIES.md b/odoo-bringout-oca-storage-fs_image/doc/DEPENDENCIES.md new file mode 100644 index 0000000..8e32263 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image/doc/DEPENDENCIES.md @@ -0,0 +1,5 @@ +# Dependencies + +This addon depends on: + +- [fs_file](../../odoo-bringout-oca-storage-fs_file) diff --git a/odoo-bringout-oca-storage-fs_image/doc/FAQ.md b/odoo-bringout-oca-storage-fs_image/doc/FAQ.md new file mode 100644 index 0000000..992db19 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image/doc/FAQ.md @@ -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_image or install in UI. diff --git a/odoo-bringout-oca-storage-fs_image/doc/INSTALL.md b/odoo-bringout-oca-storage-fs_image/doc/INSTALL.md new file mode 100644 index 0000000..dbf7863 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image/doc/INSTALL.md @@ -0,0 +1,7 @@ +# Install + +```bash +pip install odoo-bringout-oca-storage-fs_image" +# or +uv pip install odoo-bringout-oca-storage-fs_image" +``` diff --git a/odoo-bringout-oca-storage-fs_image/doc/MODELS.md b/odoo-bringout-oca-storage-fs_image/doc/MODELS.md new file mode 100644 index 0000000..c88b492 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image/doc/MODELS.md @@ -0,0 +1,13 @@ +# Models + +Detected core models and extensions in fs_image. + +```mermaid +classDiagram + class fs_image_mixin + class ir_attachment +``` + +Notes +- Classes show model technical names; fields omitted for brevity. +- Items listed under _inherit are extensions of existing models. diff --git a/odoo-bringout-oca-storage-fs_image/doc/OVERVIEW.md b/odoo-bringout-oca-storage-fs_image/doc/OVERVIEW.md new file mode 100644 index 0000000..d9c6dfc --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image/doc/OVERVIEW.md @@ -0,0 +1,6 @@ +# Overview + +Packaged Odoo addon: fs_image. Provides features documented in upstream Odoo 16 under this addon. + +- Source: OCA/OCB 16.0, addon fs_image +- License: LGPL-3 diff --git a/odoo-bringout-oca-storage-fs_image/doc/REPORTS.md b/odoo-bringout-oca-storage-fs_image/doc/REPORTS.md new file mode 100644 index 0000000..e0ea35f --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image/doc/REPORTS.md @@ -0,0 +1,3 @@ +# Reports + +This module does not define custom reports. diff --git a/odoo-bringout-oca-storage-fs_image/doc/SECURITY.md b/odoo-bringout-oca-storage-fs_image/doc/SECURITY.md new file mode 100644 index 0000000..e07da9d --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image/doc/SECURITY.md @@ -0,0 +1,8 @@ +# Security + +This module does not define custom security rules or access controls beyond Odoo defaults. + +Default Odoo security applies: +- Base user access through standard groups +- Model access inherited from dependencies +- No custom row-level security rules diff --git a/odoo-bringout-oca-storage-fs_image/doc/TROUBLESHOOTING.md b/odoo-bringout-oca-storage-fs_image/doc/TROUBLESHOOTING.md new file mode 100644 index 0000000..56853cb --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image/doc/TROUBLESHOOTING.md @@ -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. diff --git a/odoo-bringout-oca-storage-fs_image/doc/USAGE.md b/odoo-bringout-oca-storage-fs_image/doc/USAGE.md new file mode 100644 index 0000000..ab8f8ac --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image/doc/USAGE.md @@ -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_image +``` diff --git a/odoo-bringout-oca-storage-fs_image/doc/WIZARDS.md b/odoo-bringout-oca-storage-fs_image/doc/WIZARDS.md new file mode 100644 index 0000000..48e790d --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image/doc/WIZARDS.md @@ -0,0 +1,3 @@ +# Wizards + +This module does not include UI wizards. diff --git a/odoo-bringout-oca-storage-fs_image/fs_image/README.rst b/odoo-bringout-oca-storage-fs_image/fs_image/README.rst new file mode 100644 index 0000000..e1f8502 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image/fs_image/README.rst @@ -0,0 +1,260 @@ +======== +Fs Image +======== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:720789db007b07811c46c77857a24c41551a6f2554c9517630613347c8447f80 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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_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_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 defines a new field **FSImage** to use in your models. It is a +subclass of the **FSFile** field and comes with the same features. It extends +the **FSFile** field with specific properties dedicated to images. On the field +definition, the following additional properties are available: + +* **max_width** (int): maximum width of the image in pixels (default: ``0``, no limit) +* **max_height** (int): maximum height of the image in pixels (default: ``0``, no limit) +* **verify_resolution** (bool):whether the image resolution should be verified + to ensure it doesn't go over the maximum image resolution (default: ``True``). + See `odoo.tools.image.ImageProcess` for maximum image resolution (default: ``50e6``). + +On the field's value side, the value is an instance of a subclass of +`odoo.addons.fs_file.fields.FSFileValue`. It extends the class to allows +you to manage an alt_text for the image. The alt_text is a text that will be +displayed when the image cannot be displayed. + +.. 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 `_ + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +This new field type can be used in the same way as the odoo 'Image' field type. + +.. code-block:: python + + from odoo import models + from odoo.addons.fs_image.fields import FSImage + + class MyModel(models.Model): + _name = 'my.model' + + image = FSImage('Image', max_width=1920, max_height=1920) + + +.. code-block:: xml + + + my.model.form + my.model + +
+ + + + + +
+
+
+ + +In the example above, the image will be resized to 1920x1920px if it is larger than that. +The widget used in the form view will also allow the user set an 'alt' text for the image. + + +A mode advanced and useful example is the following: + +.. code-block:: python + + from odoo import models + from odoo.addons.fs_image.fields import FSImage + + class MyModel(models.Model): + _name = 'my.model' + + image_1920 = FSImage('Image', max_width=1920, max_height=1920) + image_128 = FSImage('Image', max_width=128, max_height=128, related='image_1920', store=True) + + +.. code-block:: xml + + + my.model.form + my.model + +
+ + + + + +
+
+
+ +In the example above we have two fields, one for the original image and one for a thumbnail. +As the thumbnail is defined as a related stored field it's automatically generated +from the original image, resized at the given size and stored in the database. +The thumbnail is then used as a preview image for the original image in the form view. +The main advantage of this approach is that the original image is not loaded in the form view +and the thumbnail is used instead, which is much smaller in size and faster to load. +The 'zoom' option allows the user to see the original image in a popup when clicking on the thumbnail. + +For convenience, the 'fs_image' module also provides a 'FSImageMixin' mixin class +that can be used to add the 'image' and 'image_medium' fields to a model. It only +define the medium thumbnail as a 128x128px image since it's the most common use case. +When using an image field in a model, it's recommended to use this mixin class +in order ensure that the 'image_medium' field is always defined. A good practice +is to use the `image_medium` field as a preview image for the `image` field in +the form view to avoid to overload the form view with a large image and consume +too much bandwidth. + +.. code-block:: python + + from odoo import models + + class MyModel(models.Model): + _name = 'my.model' + _inherit = ['fs_image.mixin'] + + +.. code-block:: xml + + + my.model.form + my.model + +
+ + + + + +
+
+
+ +Changelog +========= + +16.0.1.0.3 (2024-02-23) +~~~~~~~~~~~~~~~~~~~~~~~ + +**Bugfixes** + +- (`#305 `_) + + +16.0.1.0.2 (2023-12-02) +~~~~~~~~~~~~~~~~~~~~~~~ + +**Bugfixes** + +- Fix view crash when uploading an image + + The rawCacheKey is appropriately managed by the base class and reflects the + record's last update datetime (write_date). + Since it lacks a setter, attempting to invalidate its value results in a view crash. + Nevertheless, the value will automatically be updated upon saving the record. (`#305 `_) + + +16.0.1.0.1 (2023-12-02) +~~~~~~~~~~~~~~~~~~~~~~~ + +**Bugfixes** + +- Avoid to generate an SQL update query when an image field is read. + + Fix a bug in the initialization of the image field value object when the field + is read. Before this fix, every time the value object was initialized with + an attachment, an assignment of the alt text was done into the constructor. + This assignment triggered the mark of the field as modified and an SQL update + query was generated at the end of the request. The alt text in the constructor + of the FSImageValue class must only be used when the class is initialized without + an attachment. We now check if an attachment and an alt text are provided at + the same time and throw an exception if this is the case. (`#307 `_) + +Bug Tracker +=========== + +Bugs are tracked on `GitHub 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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* ACSONE SA/NV + +Contributors +~~~~~~~~~~~~ + +* Laurent Mignon + +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 `__: + +|maintainer-lmignon| + +This module is part of the `OCA/storage `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/odoo-bringout-oca-storage-fs_image/fs_image/__init__.py b/odoo-bringout-oca-storage-fs_image/fs_image/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image/fs_image/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/odoo-bringout-oca-storage-fs_image/fs_image/__manifest__.py b/odoo-bringout-oca-storage-fs_image/fs_image/__manifest__.py new file mode 100644 index 0000000..c174fd0 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image/fs_image/__manifest__.py @@ -0,0 +1,22 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Fs Image", + "summary": """ + Field to store images into filesystem storages""", + "version": "16.0.1.0.4", + "license": "AGPL-3", + "author": "ACSONE SA/NV,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/storage", + "depends": ["fs_file"], + "data": [], + "demo": [], + "maintainers": ["lmignon"], + "development_status": "Alpha", + "assets": { + "web.assets_backend": [ + "fs_image/static/src/**/*", + ], + }, +} diff --git a/odoo-bringout-oca-storage-fs_image/fs_image/fields.py b/odoo-bringout-oca-storage-fs_image/fs_image/fields.py new file mode 100644 index 0000000..9d50e26 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image/fs_image/fields.py @@ -0,0 +1,228 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +# pylint: disable=method-required-super +from contextlib import contextmanager +from io import BytesIO, IOBase + +from odoo import _ +from odoo.exceptions import UserError +from odoo.tools.image import image_process + +from odoo.addons.fs_attachment.models.ir_attachment import IrAttachment +from odoo.addons.fs_file.fields import FSFile, FSFileValue + + +class FSImageValue(FSFileValue): + """Value for the FSImage field""" + + def __init__( + self, + attachment: IrAttachment = None, + name: str = None, + value: bytes | IOBase = None, + alt_text: str = None, + ) -> None: + super().__init__(attachment, name, value) + if self._attachment and alt_text is not None: + raise ValueError( + "FSImageValue cannot be initialized with an attachment and an" + " alt_text at the same time. When initializing with an attachment," + " you can't pass any other argument." + ) + self._alt_text = alt_text + + @property + def alt_text(self) -> str: + alt_text = self._attachment.alt_text if self._attachment else self._alt_text + return alt_text + + @alt_text.setter + def alt_text(self, value: str) -> None: + if self._attachment: + self._attachment.alt_text = value + else: + self._alt_text = value + + @classmethod + def from_fs_file_value(cls, fs_file_value: FSFileValue) -> "FSImageValue": + if isinstance(fs_file_value, FSImageValue): + return fs_file_value + return cls( + attachment=fs_file_value.attachment, + name=fs_file_value.name if not fs_file_value.attachment else None, + value=fs_file_value._buffer + if not fs_file_value.attachment + else fs_file_value._buffer, + ) + + def image_process( + self, + size=(0, 0), + verify_resolution=False, + quality=0, + crop=None, + colorize=False, + output_format="", + ): + """ + Process the image to adapt it to the given parameters. + :param size: a tuple (max_width, max_height) containing the maximum + width and height of the processed image. + If one of the value is 0, it will be calculated to keep the aspect + ratio. + If both values are 0, the image will not be resized. + :param verify_resolution: if True, make sure the original image size is not + excessive before starting to process it. The max allowed resolution is + defined by `IMAGE_MAX_RESOLUTION` in :class:`odoo.tools.image.ImageProcess`. + :param int quality: quality setting to apply. Default to 0. + + - for JPEG: 1 is worse, 95 is best. Values above 95 should be + avoided. Falsy values will fallback to 95, but only if the image + was changed, otherwise the original image is returned. + - for PNG: set falsy to prevent conversion to a WEB palette. + - for other formats: no effect. + :param crop: (True | 'top' | 'bottom'): + * True, the image will be cropped to the given size. + * 'top', the image will be cropped at the top to the given size. + * 'bottom', the image will be cropped at the bottom to the given size. + Otherwise, it will be resized to fit the given size. + :param colorize: if True, the transparent background of the image + will be colorized in a random color. + :param str output_format: the output format. Can be PNG, JPEG, GIF, or ICO. + Default to the format of the original image. BMP is converted to + PNG, other formats than those mentioned above are converted to JPEG. + :return: the processed image as bytes + """ + return image_process( + self.getvalue(), + size=size, + crop=crop, + quality=quality, + verify_resolution=verify_resolution, + colorize=colorize, + output_format=output_format, + ) + + +class FSImage(FSFile): + """ + This field is a FSFile field with an alt_text attribute used to encapsulate + an image file stored in a filesystem storage. + + It's inspired by the 'image' field of odoo :class:`odoo.fields.Binary` but + is designed to store the image in a filesystem storage instead of the + database. + + If image size is greater than the ``max_width``/``max_height`` limit of pixels, + the image will be resized to the limit by keeping aspect ratio. + + :param int max_width: the maximum width of the image (default: ``0``, no limit) + :param int max_height: the maximum height of the image (default: ``0``, no limit) + :param bool verify_resolution: whether the image resolution should be verified + to ensure it doesn't go over the maximum image resolution + (default: ``True``). + See :class:`odoo.tools.image.ImageProcess` for maximum image resolution + (default: ``50e6``). + """ + + type = "fs_image" + + max_width = 0 + max_height = 0 + verify_resolution = True + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._image_process_mode = False + + def create(self, record_values): + with self._set_image_process_mode(): + return super().create(record_values) + + def write(self, records, value): + if isinstance(value, dict) and "content" not in value: + # we are writing on the alt_text field only + return self._update_alt_text(records, value) + with self._set_image_process_mode(): + return super().write(records, value) + + def convert_to_cache(self, value, record, validate=True): + if not value: + return None + if isinstance(value, FSImageValue): + cache_value = value + else: + cache_value = super().convert_to_cache(value, record, validate) + if not isinstance(cache_value, FSImageValue): + cache_value = FSImageValue.from_fs_file_value(cache_value) + if isinstance(value, dict) and "alt_text" in value: + cache_value.alt_text = value["alt_text"] + if self._image_process_mode and cache_value.is_new: + name = cache_value.name + new_value = BytesIO(self._image_process(cache_value)) + cache_value._buffer = new_value + cache_value.name = name + return cache_value + + def _create_attachment(self, record, cache_value): + attachment = super()._create_attachment(record, cache_value) + # odoo filter out additional fields in create method on ir.attachment + # so we need to write the alt_text after the creation + if cache_value.alt_text: + attachment.alt_text = cache_value.alt_text + return attachment + + def _convert_attachment_to_cache(self, attachment: IrAttachment) -> FSImageValue: + cache_value = super()._convert_attachment_to_cache(attachment) + return FSImageValue.from_fs_file_value(cache_value) + + def _image_process(self, cache_value: FSImageValue) -> bytes | None: + if self.readonly and not self.max_width and not self.max_height: + # no need to process images for computed fields, or related fields + return cache_value.getvalue() + return ( + cache_value.image_process( + size=(self.max_width, self.max_height), + verify_resolution=self.verify_resolution, + ) + or None + ) + + def convert_to_read(self, value, record, use_name_get=True) -> dict | None: + vals = super().convert_to_read(value, record, use_name_get) + if isinstance(value, FSImageValue): + vals["alt_text"] = value.alt_text or None + return vals + + @contextmanager + def _set_image_process_mode(self): + self._image_process_mode = True + try: + yield + finally: + self._image_process_mode = False + + def _process_related(self, value: FSImageValue): + """Override to resize the related value before saving it on self.""" + if not value: + return None + if self.readonly and not self.max_width and not self.max_height: + # no need to process images for computed fields, or related fields + # without max_width/max_height + return value + value = super()._process_related(value) + new_value = BytesIO(self._image_process(value)) + return FSImageValue(value=new_value, alt_text=value.alt_text, name=value.name) + + def _update_alt_text(self, records, value: dict): + for record in records: + if not record[self.name]: + raise UserError( + _( + "Cannot set alt_text on empty image (record %(record)s.%(field_name)s)", + record=record, + field_name=self.name, + ) + ) + record[self.name].alt_text = value["alt_text"] + return True diff --git a/odoo-bringout-oca-storage-fs_image/fs_image/i18n/bs.po b/odoo-bringout-oca-storage-fs_image/fs_image/i18n/bs.po new file mode 100644 index 0000000..92577b6 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image/fs_image/i18n/bs.po @@ -0,0 +1,111 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_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_image +#. odoo-javascript +#: code:addons/fs_image/static/src/views/fields/fsimage_field.esm.js:0 +#: code:addons/fs_image/static/src/views/fields/fsimage_field.xml:0 +#, python-format +msgid "Alt Text" +msgstr "Alt tekst" + +#. module: fs_image +#: model:ir.model.fields,field_description:fs_image.field_ir_attachment__alt_text +msgid "Alternative Text" +msgstr "Alternativni tekst" + +#. module: fs_image +#: model:ir.model.fields,help:fs_image.field_ir_attachment__alt_text +msgid "Alternative text for the image. Only used for images on a website." +msgstr "Alternativni tekst za sliku. Koristi se samo za slike na web stranici." + +#. module: fs_image +#: model:ir.model,name:fs_image.model_ir_attachment +msgid "Attachment" +msgstr "Prilog" + +#. module: fs_image +#. odoo-javascript +#: code:addons/fs_image/static/src/views/fields/fsimage_field.xml:0 +#, python-format +msgid "Binary file" +msgstr "Binarna datoteka" + +#. module: fs_image +#. odoo-javascript +#: code:addons/fs_image/static/src/views/dialogs/alttext_dialog.xml:0 +#, python-format +msgid "Cancel" +msgstr "Otkaži" + +#. module: fs_image +#. odoo-python +#: code:addons/fs_image/fields.py:0 +#, python-format +msgid "Cannot set alt_text on empty image (record %(record)s.%(field_name)s)" +msgstr "Nije moguće postaviti alt_text na praznu sliku (zapis %(record)s.%(field_name)s)" + +#. module: fs_image +#. odoo-javascript +#: code:addons/fs_image/static/src/views/fields/fsimage_field.xml:0 +#: code:addons/fs_image/static/src/views/fields/fsimage_field.xml:0 +#, python-format +msgid "Clear" +msgstr "Očisti" + +#. module: fs_image +#. odoo-javascript +#: code:addons/fs_image/static/src/views/fields/fsimage_field.xml:0 +#: code:addons/fs_image/static/src/views/fields/fsimage_field.xml:0 +#, python-format +msgid "Download" +msgstr "Preuzimanje" + +#. module: fs_image +#. odoo-javascript +#: code:addons/fs_image/static/src/views/fields/fsimage_field.xml:0 +#: code:addons/fs_image/static/src/views/fields/fsimage_field.xml:0 +#, python-format +msgid "Edit" +msgstr "Uredi" + +#. module: fs_image +#: model:ir.model.fields,field_description:fs_image.field_fs_image_mixin__image +msgid "Image" +msgstr "Slika" + +#. module: fs_image +#: model:ir.model,name:fs_image.model_fs_image_mixin +msgid "Image Mixin" +msgstr "Image Mixin" + +#. module: fs_image +#: model:ir.model.fields,field_description:fs_image.field_fs_image_mixin__image_medium +msgid "Image medium" +msgstr "Srednja slika" + +#. module: fs_image +#. odoo-javascript +#: code:addons/fs_image/static/src/views/dialogs/alttext_dialog.xml:0 +#, python-format +msgid "Save changes" +msgstr "Snimanje promjena" + +#. module: fs_image +#. odoo-javascript +#: code:addons/fs_image/static/src/views/fields/fsimage_field.xml:0 +#, python-format +msgid "Set Alt Text" +msgstr "Postavi Alt tekst" diff --git a/odoo-bringout-oca-storage-fs_image/fs_image/i18n/es.po b/odoo-bringout-oca-storage-fs_image/fs_image/i18n/es.po new file mode 100644 index 0000000..80c9233 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image/fs_image/i18n/es.po @@ -0,0 +1,115 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_image +# +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 \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_image +#. odoo-javascript +#: code:addons/fs_image/static/src/views/fields/fsimage_field.esm.js:0 +#: code:addons/fs_image/static/src/views/fields/fsimage_field.xml:0 +#, python-format +msgid "Alt Text" +msgstr "Texto Alt" + +#. module: fs_image +#: model:ir.model.fields,field_description:fs_image.field_ir_attachment__alt_text +msgid "Alternative Text" +msgstr "Texto Alternativo" + +#. module: fs_image +#: model:ir.model.fields,help:fs_image.field_ir_attachment__alt_text +msgid "Alternative text for the image. Only used for images on a website." +msgstr "" +"Texto alternativo para la imagen. Solo se utiliza para imágenes de un sitio " +"web." + +#. module: fs_image +#: model:ir.model,name:fs_image.model_ir_attachment +msgid "Attachment" +msgstr "Archivo Adjunto" + +#. module: fs_image +#. odoo-javascript +#: code:addons/fs_image/static/src/views/fields/fsimage_field.xml:0 +#, python-format +msgid "Binary file" +msgstr "Archivo binario" + +#. module: fs_image +#. odoo-javascript +#: code:addons/fs_image/static/src/views/dialogs/alttext_dialog.xml:0 +#, python-format +msgid "Cancel" +msgstr "Cancelar" + +#. module: fs_image +#. odoo-python +#: code:addons/fs_image/fields.py:0 +#, python-format +msgid "Cannot set alt_text on empty image (record %(record)s.%(field_name)s)" +msgstr "" +"No se puede establecer alt_text en una imagen vacía (record %(record)s." +"%(field_name)s)" + +#. module: fs_image +#. odoo-javascript +#: code:addons/fs_image/static/src/views/fields/fsimage_field.xml:0 +#, python-format +msgid "Clear" +msgstr "Limpiar" + +#. module: fs_image +#. odoo-javascript +#: code:addons/fs_image/static/src/views/fields/fsimage_field.xml:0 +#, python-format +msgid "Download" +msgstr "" + +#. module: fs_image +#. odoo-javascript +#: code:addons/fs_image/static/src/views/fields/fsimage_field.xml:0 +#, python-format +msgid "Edit" +msgstr "Editar" + +#. module: fs_image +#: model:ir.model.fields,field_description:fs_image.field_fs_image_mixin__image +msgid "Image" +msgstr "Imagen" + +#. module: fs_image +#: model:ir.model,name:fs_image.model_fs_image_mixin +msgid "Image Mixin" +msgstr "Mezcla de Imágenes" + +#. module: fs_image +#: model:ir.model.fields,field_description:fs_image.field_fs_image_mixin__image_medium +msgid "Image medium" +msgstr "Imagen mediana" + +#. module: fs_image +#. odoo-javascript +#: code:addons/fs_image/static/src/views/dialogs/alttext_dialog.xml:0 +#, python-format +msgid "Save changes" +msgstr "Guardar cambios" + +#. module: fs_image +#. odoo-javascript +#: code:addons/fs_image/static/src/views/fields/fsimage_field.xml:0 +#, python-format +msgid "Set Alt Text" +msgstr "Establecer Texto Alt" diff --git a/odoo-bringout-oca-storage-fs_image/fs_image/i18n/fr.po b/odoo-bringout-oca-storage-fs_image/fs_image/i18n/fr.po new file mode 100644 index 0000000..40e7d2b --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image/fs_image/i18n/fr.po @@ -0,0 +1,115 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_image +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-02-15 17:37+0000\n" +"Last-Translator: \"Benjamin Willig (ACSONE)\" \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 4.17\n" + +#. module: fs_image +#. odoo-javascript +#: code:addons/fs_image/static/src/views/fields/fsimage_field.esm.js:0 +#: code:addons/fs_image/static/src/views/fields/fsimage_field.xml:0 +#, python-format +msgid "Alt Text" +msgstr "" + +#. module: fs_image +#: model:ir.model.fields,field_description:fs_image.field_ir_attachment__alt_text +msgid "Alternative Text" +msgstr "Texte alternatif" + +#. module: fs_image +#: model:ir.model.fields,help:fs_image.field_ir_attachment__alt_text +msgid "Alternative text for the image. Only used for images on a website." +msgstr "" +"Texte alternatif pour une image. Utilisé seulement pour les images sur un " +"site web." + +#. module: fs_image +#: model:ir.model,name:fs_image.model_ir_attachment +msgid "Attachment" +msgstr "Pièce jointe" + +#. module: fs_image +#. odoo-javascript +#: code:addons/fs_image/static/src/views/fields/fsimage_field.xml:0 +#, python-format +msgid "Binary file" +msgstr "Fichier binaire" + +#. module: fs_image +#. odoo-javascript +#: code:addons/fs_image/static/src/views/dialogs/alttext_dialog.xml:0 +#, python-format +msgid "Cancel" +msgstr "Annuler" + +#. module: fs_image +#. odoo-python +#: code:addons/fs_image/fields.py:0 +#, python-format +msgid "Cannot set alt_text on empty image (record %(record)s.%(field_name)s)" +msgstr "" +"Impossible d'appliquer un texte alternatif sur une image vide " +"(enregistrement %(record)s.%(field_name)s)" + +#. module: fs_image +#. odoo-javascript +#: code:addons/fs_image/static/src/views/fields/fsimage_field.xml:0 +#, python-format +msgid "Clear" +msgstr "Effacer" + +#. module: fs_image +#. odoo-javascript +#: code:addons/fs_image/static/src/views/fields/fsimage_field.xml:0 +#, python-format +msgid "Download" +msgstr "" + +#. module: fs_image +#. odoo-javascript +#: code:addons/fs_image/static/src/views/fields/fsimage_field.xml:0 +#, python-format +msgid "Edit" +msgstr "Modifier" + +#. module: fs_image +#: model:ir.model.fields,field_description:fs_image.field_fs_image_mixin__image +msgid "Image" +msgstr "Image" + +#. module: fs_image +#: model:ir.model,name:fs_image.model_fs_image_mixin +msgid "Image Mixin" +msgstr "" + +#. module: fs_image +#: model:ir.model.fields,field_description:fs_image.field_fs_image_mixin__image_medium +msgid "Image medium" +msgstr "Image moyenne" + +#. module: fs_image +#. odoo-javascript +#: code:addons/fs_image/static/src/views/dialogs/alttext_dialog.xml:0 +#, python-format +msgid "Save changes" +msgstr "Sauvegarder les changements" + +#. module: fs_image +#. odoo-javascript +#: code:addons/fs_image/static/src/views/fields/fsimage_field.xml:0 +#, python-format +msgid "Set Alt Text" +msgstr "" diff --git a/odoo-bringout-oca-storage-fs_image/fs_image/i18n/fs_image.pot b/odoo-bringout-oca-storage-fs_image/fs_image/i18n/fs_image.pot new file mode 100644 index 0000000..e2a9611 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image/fs_image/i18n/fs_image.pot @@ -0,0 +1,111 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_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_image +#. odoo-javascript +#: code:addons/fs_image/static/src/views/fields/fsimage_field.esm.js:0 +#: code:addons/fs_image/static/src/views/fields/fsimage_field.xml:0 +#, python-format +msgid "Alt Text" +msgstr "" + +#. module: fs_image +#: model:ir.model.fields,field_description:fs_image.field_ir_attachment__alt_text +msgid "Alternative Text" +msgstr "" + +#. module: fs_image +#: model:ir.model.fields,help:fs_image.field_ir_attachment__alt_text +msgid "Alternative text for the image. Only used for images on a website." +msgstr "" + +#. module: fs_image +#: model:ir.model,name:fs_image.model_ir_attachment +msgid "Attachment" +msgstr "" + +#. module: fs_image +#. odoo-javascript +#: code:addons/fs_image/static/src/views/fields/fsimage_field.xml:0 +#, python-format +msgid "Binary file" +msgstr "" + +#. module: fs_image +#. odoo-javascript +#: code:addons/fs_image/static/src/views/dialogs/alttext_dialog.xml:0 +#, python-format +msgid "Cancel" +msgstr "" + +#. module: fs_image +#. odoo-python +#: code:addons/fs_image/fields.py:0 +#, python-format +msgid "Cannot set alt_text on empty image (record %(record)s.%(field_name)s)" +msgstr "" + +#. module: fs_image +#. odoo-javascript +#: code:addons/fs_image/static/src/views/fields/fsimage_field.xml:0 +#: code:addons/fs_image/static/src/views/fields/fsimage_field.xml:0 +#, python-format +msgid "Clear" +msgstr "" + +#. module: fs_image +#. odoo-javascript +#: code:addons/fs_image/static/src/views/fields/fsimage_field.xml:0 +#: code:addons/fs_image/static/src/views/fields/fsimage_field.xml:0 +#, python-format +msgid "Download" +msgstr "" + +#. module: fs_image +#. odoo-javascript +#: code:addons/fs_image/static/src/views/fields/fsimage_field.xml:0 +#: code:addons/fs_image/static/src/views/fields/fsimage_field.xml:0 +#, python-format +msgid "Edit" +msgstr "" + +#. module: fs_image +#: model:ir.model.fields,field_description:fs_image.field_fs_image_mixin__image +msgid "Image" +msgstr "" + +#. module: fs_image +#: model:ir.model,name:fs_image.model_fs_image_mixin +msgid "Image Mixin" +msgstr "" + +#. module: fs_image +#: model:ir.model.fields,field_description:fs_image.field_fs_image_mixin__image_medium +msgid "Image medium" +msgstr "" + +#. module: fs_image +#. odoo-javascript +#: code:addons/fs_image/static/src/views/dialogs/alttext_dialog.xml:0 +#, python-format +msgid "Save changes" +msgstr "" + +#. module: fs_image +#. odoo-javascript +#: code:addons/fs_image/static/src/views/fields/fsimage_field.xml:0 +#, python-format +msgid "Set Alt Text" +msgstr "" diff --git a/odoo-bringout-oca-storage-fs_image/fs_image/i18n/it.po b/odoo-bringout-oca-storage-fs_image/fs_image/i18n/it.po new file mode 100644 index 0000000..54e687e --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image/fs_image/i18n/it.po @@ -0,0 +1,115 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_image +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-11-22 14:06+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.6.2\n" + +#. module: fs_image +#. odoo-javascript +#: code:addons/fs_image/static/src/views/fields/fsimage_field.esm.js:0 +#: code:addons/fs_image/static/src/views/fields/fsimage_field.xml:0 +#, python-format +msgid "Alt Text" +msgstr "Testo alternativo" + +#. module: fs_image +#: model:ir.model.fields,field_description:fs_image.field_ir_attachment__alt_text +msgid "Alternative Text" +msgstr "Testo alternativo" + +#. module: fs_image +#: model:ir.model.fields,help:fs_image.field_ir_attachment__alt_text +msgid "Alternative text for the image. Only used for images on a website." +msgstr "" +"Testo alternativo per l'immagine. Utilizzato solo per le immagini nel sito " +"web." + +#. module: fs_image +#: model:ir.model,name:fs_image.model_ir_attachment +msgid "Attachment" +msgstr "Allegato" + +#. module: fs_image +#. odoo-javascript +#: code:addons/fs_image/static/src/views/fields/fsimage_field.xml:0 +#, python-format +msgid "Binary file" +msgstr "file binario" + +#. module: fs_image +#. odoo-javascript +#: code:addons/fs_image/static/src/views/dialogs/alttext_dialog.xml:0 +#, python-format +msgid "Cancel" +msgstr "Annulla" + +#. module: fs_image +#. odoo-python +#: code:addons/fs_image/fields.py:0 +#, python-format +msgid "Cannot set alt_text on empty image (record %(record)s.%(field_name)s)" +msgstr "" +"Non si può impostare il campo alt_text nelle immagini vuote (record " +"%(record)s.%(field_name)s)" + +#. module: fs_image +#. odoo-javascript +#: code:addons/fs_image/static/src/views/fields/fsimage_field.xml:0 +#, python-format +msgid "Clear" +msgstr "Pulisci" + +#. module: fs_image +#. odoo-javascript +#: code:addons/fs_image/static/src/views/fields/fsimage_field.xml:0 +#, python-format +msgid "Download" +msgstr "Scarica" + +#. module: fs_image +#. odoo-javascript +#: code:addons/fs_image/static/src/views/fields/fsimage_field.xml:0 +#, python-format +msgid "Edit" +msgstr "Modifica" + +#. module: fs_image +#: model:ir.model.fields,field_description:fs_image.field_fs_image_mixin__image +msgid "Image" +msgstr "Immagine" + +#. module: fs_image +#: model:ir.model,name:fs_image.model_fs_image_mixin +msgid "Image Mixin" +msgstr "Mixin immagine" + +#. module: fs_image +#: model:ir.model.fields,field_description:fs_image.field_fs_image_mixin__image_medium +msgid "Image medium" +msgstr "Immagine media" + +#. module: fs_image +#. odoo-javascript +#: code:addons/fs_image/static/src/views/dialogs/alttext_dialog.xml:0 +#, python-format +msgid "Save changes" +msgstr "Salva modifiche" + +#. module: fs_image +#. odoo-javascript +#: code:addons/fs_image/static/src/views/fields/fsimage_field.xml:0 +#, python-format +msgid "Set Alt Text" +msgstr "Imposta testo alternativo" diff --git a/odoo-bringout-oca-storage-fs_image/fs_image/models/__init__.py b/odoo-bringout-oca-storage-fs_image/fs_image/models/__init__.py new file mode 100644 index 0000000..17f08cd --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image/fs_image/models/__init__.py @@ -0,0 +1,2 @@ +from . import ir_attachment +from . import fs_image_mixin diff --git a/odoo-bringout-oca-storage-fs_image/fs_image/models/fs_image_mixin.py b/odoo-bringout-oca-storage-fs_image/fs_image/models/fs_image_mixin.py new file mode 100644 index 0000000..eb56840 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image/fs_image/models/fs_image_mixin.py @@ -0,0 +1,17 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models + +from ..fields import FSImage + + +class FSImageMixin(models.AbstractModel): + _name = "fs.image.mixin" + _description = "Image Mixin" + + image = FSImage("Image") + # resized fields stored (as attachment) for performance + image_medium = FSImage( + "Image medium", related="image", max_width=128, max_height=128, store=True + ) diff --git a/odoo-bringout-oca-storage-fs_image/fs_image/models/ir_attachment.py b/odoo-bringout-oca-storage-fs_image/fs_image/models/ir_attachment.py new file mode 100644 index 0000000..2856e98 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image/fs_image/models/ir_attachment.py @@ -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 IrAttachment(models.Model): + + _inherit = "ir.attachment" + + alt_text = fields.Char( + "Alternative Text", + help="Alternative text for the image. Only used for images on a website.", + translate=False, + ) diff --git a/odoo-bringout-oca-storage-fs_image/fs_image/readme/CONTRIBUTORS.rst b/odoo-bringout-oca-storage-fs_image/fs_image/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000..172b2d2 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image/fs_image/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Laurent Mignon diff --git a/odoo-bringout-oca-storage-fs_image/fs_image/readme/DESCRIPTION.rst b/odoo-bringout-oca-storage-fs_image/fs_image/readme/DESCRIPTION.rst new file mode 100644 index 0000000..9315e90 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image/fs_image/readme/DESCRIPTION.rst @@ -0,0 +1,15 @@ +This addon defines a new field **FSImage** to use in your models. It is a +subclass of the **FSFile** field and comes with the same features. It extends +the **FSFile** field with specific properties dedicated to images. On the field +definition, the following additional properties are available: + +* **max_width** (int): maximum width of the image in pixels (default: ``0``, no limit) +* **max_height** (int): maximum height of the image in pixels (default: ``0``, no limit) +* **verify_resolution** (bool):whether the image resolution should be verified + to ensure it doesn't go over the maximum image resolution (default: ``True``). + See `odoo.tools.image.ImageProcess` for maximum image resolution (default: ``50e6``). + +On the field's value side, the value is an instance of a subclass of +`odoo.addons.fs_file.fields.FSFileValue`. It extends the class to allows +you to manage an alt_text for the image. The alt_text is a text that will be +displayed when the image cannot be displayed. diff --git a/odoo-bringout-oca-storage-fs_image/fs_image/readme/HISTORY.rst b/odoo-bringout-oca-storage-fs_image/fs_image/readme/HISTORY.rst new file mode 100644 index 0000000..10acfdc --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image/fs_image/readme/HISTORY.rst @@ -0,0 +1,36 @@ +16.0.1.0.3 (2024-02-23) +~~~~~~~~~~~~~~~~~~~~~~~ + +**Bugfixes** + +- (`#305 `_) + + +16.0.1.0.2 (2023-12-02) +~~~~~~~~~~~~~~~~~~~~~~~ + +**Bugfixes** + +- Fix view crash when uploading an image + + The rawCacheKey is appropriately managed by the base class and reflects the + record's last update datetime (write_date). + Since it lacks a setter, attempting to invalidate its value results in a view crash. + Nevertheless, the value will automatically be updated upon saving the record. (`#305 `_) + + +16.0.1.0.1 (2023-12-02) +~~~~~~~~~~~~~~~~~~~~~~~ + +**Bugfixes** + +- Avoid to generate an SQL update query when an image field is read. + + Fix a bug in the initialization of the image field value object when the field + is read. Before this fix, every time the value object was initialized with + an attachment, an assignment of the alt text was done into the constructor. + This assignment triggered the mark of the field as modified and an SQL update + query was generated at the end of the request. The alt text in the constructor + of the FSImageValue class must only be used when the class is initialized without + an attachment. We now check if an attachment and an alt text are provided at + the same time and throw an exception if this is the case. (`#307 `_) diff --git a/odoo-bringout-oca-storage-fs_image/fs_image/readme/USAGE.rst b/odoo-bringout-oca-storage-fs_image/fs_image/readme/USAGE.rst new file mode 100644 index 0000000..1db6920 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image/fs_image/readme/USAGE.rst @@ -0,0 +1,113 @@ +This new field type can be used in the same way as the odoo 'Image' field type. + +.. code-block:: python + + from odoo import models + from odoo.addons.fs_image.fields import FSImage + + class MyModel(models.Model): + _name = 'my.model' + + image = FSImage('Image', max_width=1920, max_height=1920) + + +.. code-block:: xml + + + my.model.form + my.model + +
+ + + + + +
+
+
+ + +In the example above, the image will be resized to 1920x1920px if it is larger than that. +The widget used in the form view will also allow the user set an 'alt' text for the image. + + +A mode advanced and useful example is the following: + +.. code-block:: python + + from odoo import models + from odoo.addons.fs_image.fields import FSImage + + class MyModel(models.Model): + _name = 'my.model' + + image_1920 = FSImage('Image', max_width=1920, max_height=1920) + image_128 = FSImage('Image', max_width=128, max_height=128, related='image_1920', store=True) + + +.. code-block:: xml + + + my.model.form + my.model + +
+ + + + + +
+
+
+ +In the example above we have two fields, one for the original image and one for a thumbnail. +As the thumbnail is defined as a related stored field it's automatically generated +from the original image, resized at the given size and stored in the database. +The thumbnail is then used as a preview image for the original image in the form view. +The main advantage of this approach is that the original image is not loaded in the form view +and the thumbnail is used instead, which is much smaller in size and faster to load. +The 'zoom' option allows the user to see the original image in a popup when clicking on the thumbnail. + +For convenience, the 'fs_image' module also provides a 'FSImageMixin' mixin class +that can be used to add the 'image' and 'image_medium' fields to a model. It only +define the medium thumbnail as a 128x128px image since it's the most common use case. +When using an image field in a model, it's recommended to use this mixin class +in order ensure that the 'image_medium' field is always defined. A good practice +is to use the `image_medium` field as a preview image for the `image` field in +the form view to avoid to overload the form view with a large image and consume +too much bandwidth. + +.. code-block:: python + + from odoo import models + + class MyModel(models.Model): + _name = 'my.model' + _inherit = ['fs_image.mixin'] + + +.. code-block:: xml + + + my.model.form + my.model + +
+ + + + + +
+
+
diff --git a/odoo-bringout-oca-storage-fs_image/fs_image/readme/newsfragments/.gitignore b/odoo-bringout-oca-storage-fs_image/fs_image/readme/newsfragments/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/odoo-bringout-oca-storage-fs_image/fs_image/static/description/icon.png b/odoo-bringout-oca-storage-fs_image/fs_image/static/description/icon.png new file mode 100644 index 0000000..3a0328b Binary files /dev/null and b/odoo-bringout-oca-storage-fs_image/fs_image/static/description/icon.png differ diff --git a/odoo-bringout-oca-storage-fs_image/fs_image/static/description/index.html b/odoo-bringout-oca-storage-fs_image/fs_image/static/description/index.html new file mode 100644 index 0000000..0be2aea --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image/fs_image/static/description/index.html @@ -0,0 +1,591 @@ + + + + + +Fs Image + + + +
+

Fs Image

+ + +

Alpha License: AGPL-3 OCA/storage Translate me on Weblate Try me on Runboat

+

This addon defines a new field FSImage to use in your models. It is a +subclass of the FSFile field and comes with the same features. It extends +the FSFile field with specific properties dedicated to images. On the field +definition, the following additional properties are available:

+
    +
  • max_width (int): maximum width of the image in pixels (default: 0, no limit)
  • +
  • max_height (int): maximum height of the image in pixels (default: 0, no limit)
  • +
  • verify_resolution (bool):whether the image resolution should be verified +to ensure it doesn’t go over the maximum image resolution (default: True). +See odoo.tools.image.ImageProcess for maximum image resolution (default: 50e6).
  • +
+

On the field’s value side, the value is an instance of a subclass of +odoo.addons.fs_file.fields.FSFileValue. It extends the class to allows +you to manage an alt_text for the image. The alt_text is a text that will be +displayed when the image cannot be displayed.

+
+

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

+
+

Table of contents

+ +
+

Usage

+

This new field type can be used in the same way as the odoo ‘Image’ field type.

+
+from odoo import models
+from odoo.addons.fs_image.fields import FSImage
+
+class MyModel(models.Model):
+    _name = 'my.model'
+
+    image = FSImage('Image', max_width=1920, max_height=1920)
+
+
+<record id="my_model_form" model="ir.ui.view">
+    <field name="name">my.model.form</field>
+    <field name="model">my.model</field>
+    <field name="arch" type="xml">
+        <form>
+            <sheet>
+                <group>
+                    <field name="image" class="oe_avatar"/>
+                </group>
+            </sheet>
+        </form>
+    </field>
+</record>
+
+

In the example above, the image will be resized to 1920x1920px if it is larger than that. +The widget used in the form view will also allow the user set an ‘alt’ text for the image.

+

A mode advanced and useful example is the following:

+
+from odoo import models
+from odoo.addons.fs_image.fields import FSImage
+
+class MyModel(models.Model):
+    _name = 'my.model'
+
+    image_1920 = FSImage('Image', max_width=1920, max_height=1920)
+    image_128 = FSImage('Image', max_width=128, max_height=128, related='image_1920', store=True)
+
+
+<record id="my_model_form" model="ir.ui.view">
+    <field name="name">my.model.form</field>
+    <field name="model">my.model</field>
+    <field name="arch" type="xml">
+        <form>
+            <sheet>
+                <group>
+                    <field
+                        name="image_1920"
+                        class="oe_avatar"
+                         options="{'preview_image': 'image_128', 'zoom': true}"
+                     />
+                </group>
+            </sheet>
+        </form>
+    </field>
+</record>
+
+

In the example above we have two fields, one for the original image and one for a thumbnail. +As the thumbnail is defined as a related stored field it’s automatically generated +from the original image, resized at the given size and stored in the database. +The thumbnail is then used as a preview image for the original image in the form view. +The main advantage of this approach is that the original image is not loaded in the form view +and the thumbnail is used instead, which is much smaller in size and faster to load. +The ‘zoom’ option allows the user to see the original image in a popup when clicking on the thumbnail.

+

For convenience, the ‘fs_image’ module also provides a ‘FSImageMixin’ mixin class +that can be used to add the ‘image’ and ‘image_medium’ fields to a model. It only +define the medium thumbnail as a 128x128px image since it’s the most common use case. +When using an image field in a model, it’s recommended to use this mixin class +in order ensure that the ‘image_medium’ field is always defined. A good practice +is to use the image_medium field as a preview image for the image field in +the form view to avoid to overload the form view with a large image and consume +too much bandwidth.

+
+from odoo import models
+
+class MyModel(models.Model):
+    _name = 'my.model'
+    _inherit = ['fs_image.mixin']
+
+
+<record id="my_model_form" model="ir.ui.view">
+    <field name="name">my.model.form</field>
+    <field name="model">my.model</field>
+    <field name="arch" type="xml">
+        <form>
+            <sheet>
+                <group>
+                    <field
+                        name="image"
+                        class="oe_avatar"
+                        options="{'preview_image': 'image_medium', 'zoom': true}"
+                    />
+                </group>
+            </sheet>
+        </form>
+    </field>
+</record>
+
+
+
+

Changelog

+
+

16.0.1.0.3 (2024-02-23)

+

Bugfixes

+ +
+
+

16.0.1.0.2 (2023-12-02)

+

Bugfixes

+
    +
  • Fix view crash when uploading an image

    +

    The rawCacheKey is appropriately managed by the base class and reflects the +record’s last update datetime (write_date). +Since it lacks a setter, attempting to invalidate its value results in a view crash. +Nevertheless, the value will automatically be updated upon saving the record. (#305)

    +
  • +
+
+
+

16.0.1.0.1 (2023-12-02)

+

Bugfixes

+
    +
  • Avoid to generate an SQL update query when an image field is read.

    +

    Fix a bug in the initialization of the image field value object when the field +is read. Before this fix, every time the value object was initialized with +an attachment, an assignment of the alt text was done into the constructor. +This assignment triggered the mark of the field as modified and an SQL update +query was generated at the end of the request. The alt text in the constructor +of the FSImageValue class must only be used when the class is initialized without +an attachment. We now check if an attachment and an alt text are provided at +the same time and throw an exception if this is the case. (#307)

    +
  • +
+
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub 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.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • ACSONE SA/NV
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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.

+

Current maintainer:

+

lmignon

+

This module is part of the OCA/storage project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/odoo-bringout-oca-storage-fs_image/fs_image/static/src/scss/fsimage_field.scss b/odoo-bringout-oca-storage-fs_image/fs_image/static/src/scss/fsimage_field.scss new file mode 100644 index 0000000..053fd35 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image/fs_image/static/src/scss/fsimage_field.scss @@ -0,0 +1,5 @@ +.fs_file_download_button { + top: 10% !important; + left: 50% !important; + position: absolute !important; +} diff --git a/odoo-bringout-oca-storage-fs_image/fs_image/static/src/views/dialogs/alttext_dialog.esm.js b/odoo-bringout-oca-storage-fs_image/fs_image/static/src/views/dialogs/alttext_dialog.esm.js new file mode 100644 index 0000000..b648fb1 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image/fs_image/static/src/views/dialogs/alttext_dialog.esm.js @@ -0,0 +1,40 @@ +/** @odoo-module */ + +/** + * Copyright 2023 ACSONE SA/NV + */ + +import {Dialog} from "@web/core/dialog/dialog"; + +const {Component, useRef} = owl; + +export class AltTextDialog extends Component { + setup() { + this.altText = useRef("altText"); + } + + async onClose() { + if (this.props.close) { + this.props.close(); + } + } + + async onConfirm() { + try { + await this.props.confirm(this.altText.el.value); + } catch (e) { + this.props.close(); + throw e; + } + this.onClose(); + } +} + +AltTextDialog.components = {Dialog}; +AltTextDialog.template = "fs_image.AltTextDialog"; +AltTextDialog.props = { + title: String, + altText: String, + confirm: Function, + close: {type: Function, optional: true}, +}; diff --git a/odoo-bringout-oca-storage-fs_image/fs_image/static/src/views/dialogs/alttext_dialog.xml b/odoo-bringout-oca-storage-fs_image/fs_image/static/src/views/dialogs/alttext_dialog.xml new file mode 100644 index 0000000..afdd22c --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image/fs_image/static/src/views/dialogs/alttext_dialog.xml @@ -0,0 +1,33 @@ + + + + +
+ + + +
+ +
+
+ + + + +
+
+
diff --git a/odoo-bringout-oca-storage-fs_image/fs_image/static/src/views/fields/fsimage_field.esm.js b/odoo-bringout-oca-storage-fs_image/fs_image/static/src/views/fields/fsimage_field.esm.js new file mode 100644 index 0000000..086072d --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image/fs_image/static/src/views/fields/fsimage_field.esm.js @@ -0,0 +1,117 @@ +/** @odoo-module */ + +/** + * Copyright 2023 ACSONE SA/NV + */ +import { + ImageField, + fileTypeMagicWordMap, + imageCacheKey, +} from "@web/views/fields/image/image_field"; +import {onWillUpdateProps, useState} from "@odoo/owl"; + +import {AltTextDialog} from "../dialogs/alttext_dialog.esm"; +import {download, downloadFile} from "@web/core/network/download"; +import {registry} from "@web/core/registry"; +import {url} from "@web/core/utils/urls"; +import {useService} from "@web/core/utils/hooks"; + +const placeholder = "/web/static/img/placeholder.png"; + +export class FSImageField extends ImageField { + setup() { + // Call super.setup() to initialize the state + super.setup(); + this.state = useState({ + ...this.props.value, + ...this.state, + }); + onWillUpdateProps((nextProps) => { + this.state.isUploading = false; + const {filename, mimetype, alt_text, url} = nextProps.value || {}; + this.state.filename = filename; + this.state.mimetype = mimetype; + this.state.url = url; + this.state.alt_text = alt_text; + }); + this.dialogService = useService("dialog"); + } + + getUrl(previewFieldName) { + if ( + this.state.isValid && + this.props.value && + typeof this.props.value === "object" + ) { + // Check if value is a dict + if (this.props.value.content) { + // We use the binary content of the value + // Use magic-word technique for detecting image type + const magic = + fileTypeMagicWordMap[this.props.value.content[0]] || "png"; + return `data:image/${magic};base64,${this.props.value.content}`; + } + const model = this.props.record.resModel; + const id = this.props.record.resId; + let base_url = this.props.value.url; + if (id !== undefined && id !== null && id !== false) { + const field = previewFieldName; + const filename = this.props.value.filename; + base_url = `/web/image/${model}/${id}/${field}/${filename}`; + } + return url(base_url, {unique: imageCacheKey(this.rawCacheKey)}); + } + return placeholder; + } + + get hasTooltip() { + return this.props.enableZoom && !this.props.isDebugMode && this.props.value; + } + + onFileUploaded(info) { + this.state.isValid = true; + this.props.update({ + filename: info.name, + content: info.data, + }); + } + onAltTextEdit() { + const self = this; + const altText = this.props.value.alt_text || ""; + const dialogProps = { + title: this.env._t("Alt Text"), + altText: altText, + confirm: (value) => { + self.props.update({ + ...self.props.value, + alt_text: value, + }); + }, + }; + this.dialogService.add(AltTextDialog, dialogProps); + } + async onFileDownload() { + if (this.props.value.content) { + const magic = fileTypeMagicWordMap[this.props.value.content[0]] || "png"; + await downloadFile( + `data:image/${magic};base64,${this.props.value.content}`, + this.state.filename, + `image/${magic}` + ); + } else { + await download({ + data: { + model: this.props.record.resModel, + id: this.props.record.resId, + field: this.props.name, + filename: this.state.filename || "download", + download: true, + }, + url: "/web/image", + }); + } + } +} + +FSImageField.template = "fs_image.FSImageField"; +registry.category("fields").add("fs_image", FSImageField); diff --git a/odoo-bringout-oca-storage-fs_image/fs_image/static/src/views/fields/fsimage_field.xml b/odoo-bringout-oca-storage-fs_image/fs_image/static/src/views/fields/fsimage_field.xml new file mode 100644 index 0000000..ae2aa02 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image/fs_image/static/src/views/fields/fsimage_field.xml @@ -0,0 +1,75 @@ + + + + +
+
+ + + + + + + + + + + +
+ Binary file + + +
+
+ +
diff --git a/odoo-bringout-oca-storage-fs_image/fs_image/tests/__init__.py b/odoo-bringout-oca-storage-fs_image/fs_image/tests/__init__.py new file mode 100644 index 0000000..fec1f1d --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image/fs_image/tests/__init__.py @@ -0,0 +1 @@ +from . import test_fs_image diff --git a/odoo-bringout-oca-storage-fs_image/fs_image/tests/models.py b/odoo-bringout-oca-storage-fs_image/fs_image/tests/models.py new file mode 100644 index 0000000..c480202 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image/fs_image/tests/models.py @@ -0,0 +1,32 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models + +from ..fields import FSImage + + +class TestImageModel(models.Model): + + _name = "test.image.model" + _description = "Test Model" + _log_access = False + + fs_image = FSImage(verify_resolution=False) + fs_image_1024 = FSImage("Image 1024", max_width=1024, max_height=1024) + + +class TestRelatedImageModel(models.Model): + + _name = "test.related.image.model" + _description = "Test Related Image Model" + _log_access = False + + fs_image = FSImage(verify_resolution=False) + # resized fields stored (as attachment) for performance + fs_image_1024 = FSImage( + "Image 1024", related="fs_image", max_width=1024, max_height=1024, store=True + ) + fs_image_512 = FSImage( + "Image 512", related="fs_image", max_width=512, max_height=512, store=True + ) diff --git a/odoo-bringout-oca-storage-fs_image/fs_image/tests/test_fs_image.py b/odoo-bringout-oca-storage-fs_image/fs_image/tests/test_fs_image.py new file mode 100644 index 0000000..fb9a9b8 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image/fs_image/tests/test_fs_image.py @@ -0,0 +1,239 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import base64 +import io +import os +import tempfile + +from odoo_test_helper import FakeModelLoader +from PIL import Image + +from odoo.exceptions import UserError +from odoo.tests.common import TransactionCase, users, warmup + +from odoo.addons.fs_storage.models.fs_storage import FSStorage + +from ..fields import FSImageValue + + +class TestFsImage(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.env["ir.config_parameter"].set_param( + "base.image_autoresize_max_px", "10000x10000" + ) + cls.loader = FakeModelLoader(cls.env, cls.__module__) + cls.loader.backup_registry() + from .models import TestImageModel, TestRelatedImageModel + + cls.loader.update_registry((TestImageModel, TestRelatedImageModel)) + + cls.image_w = cls._create_image(4000, 2000) + cls.image_h = cls._create_image(2000, 4000) + + cls.create_content = cls.image_w + cls.write_content = cls.image_h + cls.tmpfile_path = tempfile.mkstemp(suffix=".png")[1] + with open(cls.tmpfile_path, "wb") as f: + f.write(cls.create_content) + cls.filename = os.path.basename(cls.tmpfile_path) + + def setUp(self): + super().setUp() + self.temp_dir: FSStorage = self.env["fs.storage"].create( + { + "name": "Temp FS Storage", + "protocol": "memory", + "code": "mem_dir", + "directory_path": "/tmp/", + "model_xmlids": "fs_file.model_test_model", + } + ) + + @classmethod + def tearDownClass(cls): + if os.path.exists(cls.tmpfile_path): + os.remove(cls.tmpfile_path) + cls.loader.restore_registry() + return super().tearDownClass() + + @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 _test_create(self, fs_image_value): + model = self.env["test.image.model"] + instance = model.create({"fs_image": fs_image_value}) + self.assertTrue(isinstance(instance.fs_image, FSImageValue)) + self.assertEqual(instance.fs_image.getvalue(), self.create_content) + self.assertEqual(instance.fs_image.name, self.filename) + return instance + + def _test_write(self, fs_image_value, **ctx): + instance = self.env["test.image.model"].create({}) + if ctx: + instance = instance.with_context(**ctx) + instance.fs_image = fs_image_value + self.assertEqual(instance.fs_image.getvalue(), self.write_content) + self.assertEqual(instance.fs_image.name, self.filename) + return instance + + def assert_image_size(self, value: bytes, width, height): + self.assertEqual(Image.open(io.BytesIO(value)).size, (width, height)) + + def test_read(self): + instance = self.env["test.image.model"].create( + {"fs_image": FSImageValue(name=self.filename, value=self.create_content)} + ) + info = instance.read(["fs_image"])[0] + self.assertDictEqual( + info["fs_image"], + { + "alt_text": None, + "filename": self.filename, + "mimetype": "image/png", + "size": len(self.create_content), + "url": instance.fs_image.internal_url, + }, + ) + + def test_create_with_FsImagebytesio(self): + self._test_create(FSImageValue(name=self.filename, value=self.create_content)) + + def test_create_with_dict(self): + instance = self._test_create( + { + "filename": self.filename, + "content": base64.b64encode(self.create_content), + "alt_text": "test", + } + ) + self.assertEqual(instance.fs_image.alt_text, "test") + + def test_write_with_dict(self): + instance = self._test_write( + { + "filename": self.filename, + "content": base64.b64encode(self.write_content), + "alt_text": "test_bis", + } + ) + self.assertEqual(instance.fs_image.alt_text, "test_bis") + + def test_create_with_file_like(self): + with open(self.tmpfile_path, "rb") as f: + self._test_create(f) + + def test_create_in_b64(self): + instance = self.env["test.image.model"].create( + {"fs_image": base64.b64encode(self.create_content)} + ) + self.assertTrue(isinstance(instance.fs_image, FSImageValue)) + self.assertEqual(instance.fs_image.getvalue(), self.create_content) + + def test_write_in_b64(self): + instance = self.env["test.image.model"].create({"fs_image": b"test"}) + instance.write({"fs_image": base64.b64encode(self.create_content)}) + self.assertTrue(isinstance(instance.fs_image, FSImageValue)) + self.assertEqual(instance.fs_image.getvalue(), self.create_content) + + def test_write_in_b64_with_specified_filename(self): + self._test_write( + base64.b64encode(self.write_content), fs_filename=self.filename + ) + + def test_create_with_io(self): + instance = self.env["test.image.model"].create( + {"fs_image": io.BytesIO(self.create_content)} + ) + self.assertTrue(isinstance(instance.fs_image, FSImageValue)) + self.assertEqual(instance.fs_image.getvalue(), self.create_content) + + def test_write_with_io(self): + instance = self.env["test.image.model"].create( + {"fs_image": io.BytesIO(self.create_content)} + ) + instance.write({"fs_image": io.BytesIO(b"test3")}) + self.assertTrue(isinstance(instance.fs_image, FSImageValue)) + self.assertEqual(instance.fs_image.getvalue(), b"test3") + + def test_modify_FsImagebytesio(self): + """If you modify the content of the FSImageValue, + the changes will be directly applied + and a new file in the storage must be created for the new content. + """ + instance = self.env["test.image.model"].create( + {"fs_image": FSImageValue(name=self.filename, value=self.create_content)} + ) + initial_store_fname = instance.fs_image.attachment.store_fname + with instance.fs_image.open(mode="wb") as f: + f.write(b"new_content") + self.assertNotEqual( + instance.fs_image.attachment.store_fname, initial_store_fname + ) + self.assertEqual(instance.fs_image.getvalue(), b"new_content") + + def test_image_resize(self): + instance = self.env["test.image.model"].create( + {"fs_image_1024": FSImageValue(name=self.filename, value=self.image_w)} + ) + # the image is resized to 1024x512 even if the field is 1024x1024 since + # we keep the ratio + self.assert_image_size(instance.fs_image_1024.getvalue(), 1024, 512) + + def test_image_resize_related(self): + instance = self.env["test.related.image.model"].create( + {"fs_image": FSImageValue(name=self.filename, value=self.image_w)} + ) + self.assert_image_size(instance.fs_image.getvalue(), 4000, 2000) + self.assert_image_size(instance.fs_image_1024.getvalue(), 1024, 512) + self.assert_image_size(instance.fs_image_512.getvalue(), 512, 256) + + def test_related_with_b64(self): + instance = self.env["test.related.image.model"].create( + {"fs_image": base64.b64encode(self.create_content)} + ) + self.assert_image_size(instance.fs_image.getvalue(), 4000, 2000) + self.assert_image_size(instance.fs_image_1024.getvalue(), 1024, 512) + self.assert_image_size(instance.fs_image_512.getvalue(), 512, 256) + + def test_write_alt_text(self): + instance = self.env["test.image.model"].create( + {"fs_image": FSImageValue(name=self.filename, value=self.image_w)} + ) + instance.fs_image.alt_text = "test" + self.assertEqual(instance.fs_image.alt_text, "test") + + def test_write_alt_text_with_dict(self): + instance = self.env["test.image.model"].create( + {"fs_image": FSImageValue(name=self.filename, value=self.image_w)} + ) + instance.write({"fs_image": {"alt_text": "test"}}) + self.assertEqual(instance.fs_image.alt_text, "test") + + def test_write_alt_text_on_empty_with_dict(self): + instance = self.env["test.image.model"].create({}) + with self.assertRaisesRegex(UserError, "Cannot set alt_text on empty image"): + instance.write({"fs_image": {"alt_text": "test"}}) + + @users("__system__") + @warmup + def test_generated_sql_commands(self): + # The following tests will never fail, but they will output a warning + # if the number of SQL queries changes into the logs. They + # are to help us keep track of the number of SQL queries generated + # by the module. + with self.assertQueryCount(__system__=3): + instance = self.env["test.image.model"].create( + {"fs_image": FSImageValue(name=self.filename, value=self.image_w)} + ) + + instance.invalidate_recordset() + with self.assertQueryCount(__system__=1): + self.assertEqual(instance.fs_image.getvalue(), self.image_w) + self.env.flush_all() diff --git a/odoo-bringout-oca-storage-fs_image/pyproject.toml b/odoo-bringout-oca-storage-fs_image/pyproject.toml new file mode 100644 index 0000000..c18eb7a --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image/pyproject.toml @@ -0,0 +1,43 @@ +[project] +name = "odoo-bringout-oca-storage-fs_image" +version = "16.0.0" +description = "Fs Image - + Field to store images into filesystem storages" +authors = [ + { name = "Ernad Husremovic", email = "hernad@bring.out.ba" } +] +dependencies = [ + "odoo-bringout-oca-storage-fs_file>=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_image"] + +[tool.rye] +managed = true +dev-dependencies = [ + "pytest>=8.4.1", +] diff --git a/odoo-bringout-oca-storage-fs_image_thumbnail/README.md b/odoo-bringout-oca-storage-fs_image_thumbnail/README.md new file mode 100644 index 0000000..9f431fa --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image_thumbnail/README.md @@ -0,0 +1,47 @@ +# Fs Image Thumbnail + +Odoo addon: fs_image_thumbnail + +## Installation + +```bash +pip install odoo-bringout-oca-storage-fs_image_thumbnail +``` + +## Dependencies + +This addon depends on: +- fs_image +- base_partition + +## Manifest Information + +- **Name**: Fs Image Thumbnail +- **Version**: 16.0.1.0.2 +- **Category**: N/A +- **License**: AGPL-3 +- **Installable**: False + +## Source + +Based on [OCA/storage](https://github.com/OCA/storage) branch 16.0, addon `fs_image_thumbnail`. + +## 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 diff --git a/odoo-bringout-oca-storage-fs_image_thumbnail/doc/ARCHITECTURE.md b/odoo-bringout-oca-storage-fs_image_thumbnail/doc/ARCHITECTURE.md new file mode 100644 index 0000000..6a3a0d4 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image_thumbnail/doc/ARCHITECTURE.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_image_thumbnail Module - fs_image_thumbnail + 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. diff --git a/odoo-bringout-oca-storage-fs_image_thumbnail/doc/CONFIGURATION.md b/odoo-bringout-oca-storage-fs_image_thumbnail/doc/CONFIGURATION.md new file mode 100644 index 0000000..143a5cb --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image_thumbnail/doc/CONFIGURATION.md @@ -0,0 +1,3 @@ +# Configuration + +Refer to Odoo settings for fs_image_thumbnail. Configure related models, access rights, and options as needed. diff --git a/odoo-bringout-oca-storage-fs_image_thumbnail/doc/CONTROLLERS.md b/odoo-bringout-oca-storage-fs_image_thumbnail/doc/CONTROLLERS.md new file mode 100644 index 0000000..f628e77 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image_thumbnail/doc/CONTROLLERS.md @@ -0,0 +1,3 @@ +# Controllers + +This module does not define custom HTTP controllers. diff --git a/odoo-bringout-oca-storage-fs_image_thumbnail/doc/DEPENDENCIES.md b/odoo-bringout-oca-storage-fs_image_thumbnail/doc/DEPENDENCIES.md new file mode 100644 index 0000000..4e60989 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image_thumbnail/doc/DEPENDENCIES.md @@ -0,0 +1,6 @@ +# Dependencies + +This addon depends on: + +- [fs_image](../../odoo-bringout-oca-storage-fs_image) +- [base_partition](../../odoo-bringout-oca-server-tools-base_partition) diff --git a/odoo-bringout-oca-storage-fs_image_thumbnail/doc/FAQ.md b/odoo-bringout-oca-storage-fs_image_thumbnail/doc/FAQ.md new file mode 100644 index 0000000..3800b7f --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image_thumbnail/doc/FAQ.md @@ -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_image_thumbnail or install in UI. diff --git a/odoo-bringout-oca-storage-fs_image_thumbnail/doc/INSTALL.md b/odoo-bringout-oca-storage-fs_image_thumbnail/doc/INSTALL.md new file mode 100644 index 0000000..20e9348 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image_thumbnail/doc/INSTALL.md @@ -0,0 +1,7 @@ +# Install + +```bash +pip install odoo-bringout-oca-storage-fs_image_thumbnail" +# or +uv pip install odoo-bringout-oca-storage-fs_image_thumbnail" +``` diff --git a/odoo-bringout-oca-storage-fs_image_thumbnail/doc/MODELS.md b/odoo-bringout-oca-storage-fs_image_thumbnail/doc/MODELS.md new file mode 100644 index 0000000..4dba010 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image_thumbnail/doc/MODELS.md @@ -0,0 +1,15 @@ +# Models + +Detected core models and extensions in fs_image_thumbnail. + +```mermaid +classDiagram + class fs_image_thumbnail_mixin + class fs_thumbnail + class fs_image_thumbnail_mixin + class ir_attachment +``` + +Notes +- Classes show model technical names; fields omitted for brevity. +- Items listed under _inherit are extensions of existing models. diff --git a/odoo-bringout-oca-storage-fs_image_thumbnail/doc/OVERVIEW.md b/odoo-bringout-oca-storage-fs_image_thumbnail/doc/OVERVIEW.md new file mode 100644 index 0000000..20cbc0c --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image_thumbnail/doc/OVERVIEW.md @@ -0,0 +1,6 @@ +# Overview + +Packaged Odoo addon: fs_image_thumbnail. Provides features documented in upstream Odoo 16 under this addon. + +- Source: OCA/OCB 16.0, addon fs_image_thumbnail +- License: LGPL-3 diff --git a/odoo-bringout-oca-storage-fs_image_thumbnail/doc/REPORTS.md b/odoo-bringout-oca-storage-fs_image_thumbnail/doc/REPORTS.md new file mode 100644 index 0000000..e0ea35f --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image_thumbnail/doc/REPORTS.md @@ -0,0 +1,3 @@ +# Reports + +This module does not define custom reports. diff --git a/odoo-bringout-oca-storage-fs_image_thumbnail/doc/SECURITY.md b/odoo-bringout-oca-storage-fs_image_thumbnail/doc/SECURITY.md new file mode 100644 index 0000000..2a49109 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image_thumbnail/doc/SECURITY.md @@ -0,0 +1,73 @@ +# Security + +Access control and security definitions in fs_image_thumbnail. + +## 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_thumbnail.xml](../fs_image_thumbnail/security/fs_thumbnail.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_thumbnail.xml](../fs_image_thumbnail/security/fs_thumbnail.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 diff --git a/odoo-bringout-oca-storage-fs_image_thumbnail/doc/TROUBLESHOOTING.md b/odoo-bringout-oca-storage-fs_image_thumbnail/doc/TROUBLESHOOTING.md new file mode 100644 index 0000000..56853cb --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image_thumbnail/doc/TROUBLESHOOTING.md @@ -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. diff --git a/odoo-bringout-oca-storage-fs_image_thumbnail/doc/USAGE.md b/odoo-bringout-oca-storage-fs_image_thumbnail/doc/USAGE.md new file mode 100644 index 0000000..265d16c --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image_thumbnail/doc/USAGE.md @@ -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_image_thumbnail +``` diff --git a/odoo-bringout-oca-storage-fs_image_thumbnail/doc/WIZARDS.md b/odoo-bringout-oca-storage-fs_image_thumbnail/doc/WIZARDS.md new file mode 100644 index 0000000..48e790d --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image_thumbnail/doc/WIZARDS.md @@ -0,0 +1,3 @@ +# Wizards + +This module does not include UI wizards. diff --git a/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/README.rst b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/README.rst new file mode 100644 index 0000000..157c1fa --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/README.rst @@ -0,0 +1,196 @@ +================== +Fs Image Thumbnail +================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:ae84af058fd490c7c8916156dc7db31813b6d5f7535e722740b152d6955e0d57 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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_image_thumbnail + :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_image_thumbnail + :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 module extends the **fs_image** addon to support the creation and the storage of +thumbnails for images. This module is a **technical module** and is not +meant to be installed by end-users. It only provides a mixin to be used +by other modules and a model to store the thumbnails. + +.. 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 `_ + +**Table of contents** + +.. contents:: + :local: + +Use Cases / Context +=================== + +In some specific cases you may need to generate and store thumbnails of images in Odoo. +This is the case for example when you want to provide image in specific sizes for a website +or a mobile application. + +This module provides a generic way to generate thumbnails of images and store them in a +specific filesystem storage. Indeed, you could need to store the thumbnails in a different +storage than the original image (eg: store the thumbnails in a CDN) to make sure the +thumbnails are served quickly when requested by an external application and to +avoid to expose the original image storage. + +This module uses the `fs_image `_ +module to store the thumbnails in a filesystem storage. + +The `shopinvader_product_image `_ addon uses this module to generate and +store the thumbnails of the images of the products and categories to be accessible +by the website. + +Usage +===== + +This addon provides a convenient way to get and create if not exists image +thumbnails. All the logic is implemented by the abstract model +`fs.image.thumbnail.mixin`. The main method is `get_or_create_thumbnails` which +accepts a *FSImageValue* instance, a list of thumbnail sizes and a base name. + +When the method is called, it will check if the thumbnail exists for the given +sizes and base name. If not, it will create it. + +The `fs.thumbnail` model provided by this addon is a concrete implementation of +the abstract model `fs.image.thumbnail.mixin`. The motivation to implement all the +logic in an abstract model is to allow developers to create their own thumbnail +models. This could be useful if you want to store the thumbnails in a different +storage since you can specify the storage to use by model on the `fs.storage` +form view. + +Creating / retrieving thumbnails is as simple as: + +.. code-block:: python + + from odoo.addons.fs_image.fields import FSImageValue + + # create an attachment with a image file + attachment = self.env['ir.attachment'].create({ + 'name': 'test', + 'datas': base64.b64encode(open('test.png', 'rb').read()), + 'datas_fname': 'test.png', + }) + + # create a FSImageValue instance for the attachment + image_value = FSImageValue(attachment) + + # get or create the thumbnails + thumbnails = self.env['fs.thumbnail'].get_or_create_thumbnails( + image_value, [(800,600), (400, 200)], 'my base name') + + + +If you've a model with a *FSImage* field, the call to `get_or_create_thumbnails` +is even simpler: + +.. code-block:: python + + from odoo import models + from odoo.addons.fs_image.fields import FSImage + + class MyModel(models.Model): + _name = 'my.model' + + image = FSImage('Image') + + my_record = cls.env['my.model'].create({ + 'image': open('test.png', 'rb'), + }) + + # get or create the thumbnails + thumbnails = record.image.get_or_create_thumbnails(my_record.image, + [(800,600), (400, 200)], 'my base name') + +Changelog +========= + +16.0.1.0.1 (2023-10-04) +~~~~~~~~~~~~~~~~~~~~~~~ + +**Bugfixes** + +- The call to the method *get_or_create_thumbnails* on the *fs.image.thumbnail.mixin* + class returns now an ordered dictionary where the key is the original image and + the value is a recordset of thumbnail images. The order of the dict is the order + of the images passed to the method. This ensures that when you process the result + of the method you can be sure that the order of the images is the same as the + order of the images passed to the method. (`#282 `_) + +Bug Tracker +=========== + +Bugs are tracked on `GitHub 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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* ACSONE SA/NV + +Contributors +~~~~~~~~~~~~ + +* Laurent Mignon (https://acsone.eu) + +Other credits +~~~~~~~~~~~~~ + +The development of this module has been financially supported by: + +* `Alcyon Belux `_ + +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 `__: + +|maintainer-lmignon| + +This module is part of the `OCA/storage `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/__init__.py b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/__manifest__.py b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/__manifest__.py new file mode 100644 index 0000000..6f0b67c --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/__manifest__.py @@ -0,0 +1,23 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Fs Image Thumbnail", + "summary": """ + Generate and store thumbnail for images""", + "version": "16.0.1.0.2", + "license": "AGPL-3", + "author": "ACSONE SA/NV,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/storage", + "depends": ["fs_image", "base_partition"], + "data": [ + "views/ir_attachment.xml", + "security/fs_thumbnail.xml", + "views/fs_image_thumbnail_mixin.xml", + "views/fs_thumbnail.xml", + ], + "demo": [], + "maintainers": ["lmignon"], + "development_status": "Alpha", + "external_dependencies": {"python": ["python_slugify"]}, +} diff --git a/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/i18n/bs.po b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/i18n/bs.po new file mode 100644 index 0000000..bed410a --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/i18n/bs.po @@ -0,0 +1,170 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_image_thumbnail +# +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_image_thumbnail +#: model:ir.model,name:fs_image_thumbnail.model_ir_attachment +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__attachment_id +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__attachment_id +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.fs_image_thumbnail_mixin_search_view +msgid "Attachment" +msgstr "Prilog" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,help:fs_image_thumbnail.field_fs_image_thumbnail_mixin__attachment_id +#: model:ir.model.fields,help:fs_image_thumbnail.field_fs_thumbnail__attachment_id +msgid "Attachment containing the original image" +msgstr "Prilog koji sadrži originalnu sliku" + +#. module: fs_image_thumbnail +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.fs_image_thumbnail_mixin_search_view +msgid "Base Name" +msgstr "Osnovno ime" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__create_uid +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.fs_thumbnail_search_view +msgid "Created by" +msgstr "Kreirao" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__create_date +msgid "Created on" +msgstr "Kreirano" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__display_name +msgid "Display Name" +msgstr "Prikazani naziv" + +#. module: fs_image_thumbnail +#: model:ir.model,name:fs_image_thumbnail.model_fs_image_thumbnail_mixin +msgid "Fs Image Thumbnail Mixin" +msgstr "Fs Image Thumbnail Mixin" + +#. module: fs_image_thumbnail +#: model:ir.ui.menu,name:fs_image_thumbnail.fs_thumbnail_menu +msgid "Fs Image Thumbnails" +msgstr "Fs slike minijatura" + +#. module: fs_image_thumbnail +#: model:ir.actions.act_window,name:fs_image_thumbnail.fs_thumbnail_act_window +msgid "Fs Thumbnail" +msgstr "Fs minijatura" + +#. module: fs_image_thumbnail +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.fs_image_thumbnail_mixin_search_view +msgid "Group By" +msgstr "Grupiši po" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__id +msgid "ID" +msgstr "ID" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__image +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__image +msgid "Image" +msgstr "Slika" + +#. module: fs_image_thumbnail +#: model:ir.model,name:fs_image_thumbnail.model_fs_thumbnail +msgid "Image Thumbnail" +msgstr "Minijatura slike" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail____last_update +msgid "Last Modified on" +msgstr "Zadnje mijenjano" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__write_uid +msgid "Last Updated by" +msgstr "Zadnji ažurirao" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__write_date +msgid "Last Updated on" +msgstr "Zadnje ažurirano" + +#. module: fs_image_thumbnail +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.fs_image_thumbnail_mixin_search_view +msgid "MimeType" +msgstr "MimeType" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__mimetype +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__mimetype +msgid "Mimetype" +msgstr "Mimetype" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__name +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__name +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.fs_image_thumbnail_mixin_search_view +msgid "Name" +msgstr "Naziv:" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__original_image +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__original_image +msgid "Original Image" +msgstr "Originalna slika" + +#. module: fs_image_thumbnail +#. odoo-python +#: code:addons/fs_image_thumbnail/models/fs_image_thumbnail_mixin.py:0 +#, python-format +msgid "The base name must be set when multiple images are given" +msgstr "Osnovno ime mora biti postavljen kad je dano više slika" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__base_name +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__base_name +msgid "The base name of the thumbnail image (without extension)" +msgstr "Osnovno ime minijature slike (bez ekstenzije)" + +#. module: fs_image_thumbnail +#. odoo-python +#: code:addons/fs_image_thumbnail/models/fs_image_thumbnail_mixin.py:0 +#, python-format +msgid "The image %(name)s must be attached to an attachment" +msgstr "Slika %(name)s mora biti priložena priloga" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,help:fs_image_thumbnail.field_fs_image_thumbnail_mixin__base_name +#: model:ir.model.fields,help:fs_image_thumbnail.field_fs_thumbnail__base_name +msgid "" +"The thumbnail image will be named as base_name + _ + size_x + _ + size_y + . + extension.\n" +"If not set, the base name will be the name of the original image.This base name is used to find all existing thumbnail of an image generated for the same base name." +msgstr "" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_ir_attachment__thumbnail_ids +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.ir_attachment_form_view +msgid "Thumbnails" +msgstr "Sličice" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__size_x +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__size_x +msgid "X size" +msgstr "X veličina" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__size_y +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__size_y +msgid "Y size" +msgstr "Y veličina" diff --git a/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/i18n/es.po b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/i18n/es.po new file mode 100644 index 0000000..bad8caa --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/i18n/es.po @@ -0,0 +1,178 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_image_thumbnail +# +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 \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_image_thumbnail +#: model:ir.model,name:fs_image_thumbnail.model_ir_attachment +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__attachment_id +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__attachment_id +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.fs_image_thumbnail_mixin_search_view +msgid "Attachment" +msgstr "Archivo Adjunto" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,help:fs_image_thumbnail.field_fs_image_thumbnail_mixin__attachment_id +#: model:ir.model.fields,help:fs_image_thumbnail.field_fs_thumbnail__attachment_id +msgid "Attachment containing the original image" +msgstr "Archivo adjunto con la imagen original" + +#. module: fs_image_thumbnail +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.fs_image_thumbnail_mixin_search_view +msgid "Base Name" +msgstr "Nombre de la Base" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__create_uid +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.fs_thumbnail_search_view +msgid "Created by" +msgstr "Creado por" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__create_date +msgid "Created on" +msgstr "Creado el" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__display_name +msgid "Display Name" +msgstr "Mostrar Nombre" + +#. module: fs_image_thumbnail +#: model:ir.model,name:fs_image_thumbnail.model_fs_image_thumbnail_mixin +msgid "Fs Image Thumbnail Mixin" +msgstr "Mezcla de Miniaturas de imágenes Fs" + +#. module: fs_image_thumbnail +#: model:ir.ui.menu,name:fs_image_thumbnail.fs_thumbnail_menu +msgid "Fs Image Thumbnails" +msgstr "Miniaturas de imágenes Fs" + +#. module: fs_image_thumbnail +#: model:ir.actions.act_window,name:fs_image_thumbnail.fs_thumbnail_act_window +msgid "Fs Thumbnail" +msgstr "Miniatura Fs" + +#. module: fs_image_thumbnail +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.fs_image_thumbnail_mixin_search_view +msgid "Group By" +msgstr "Agrupado Por" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__id +msgid "ID" +msgstr "ID (identificación)" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__image +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__image +msgid "Image" +msgstr "Imagen" + +#. module: fs_image_thumbnail +#: model:ir.model,name:fs_image_thumbnail.model_fs_thumbnail +msgid "Image Thumbnail" +msgstr "Imagen en Miniatura" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail____last_update +msgid "Last Modified on" +msgstr "Última Modificación el" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__write_uid +msgid "Last Updated by" +msgstr "Última Actualización por" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__write_date +msgid "Last Updated on" +msgstr "Última Actualización el" + +#. module: fs_image_thumbnail +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.fs_image_thumbnail_mixin_search_view +msgid "MimeType" +msgstr "Tipo Mimo" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__mimetype +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__mimetype +msgid "Mimetype" +msgstr "Tipo Mimo" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__name +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__name +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.fs_image_thumbnail_mixin_search_view +msgid "Name" +msgstr "Nombre" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__original_image +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__original_image +msgid "Original Image" +msgstr "Imagen Original" + +#. module: fs_image_thumbnail +#. odoo-python +#: code:addons/fs_image_thumbnail/models/fs_image_thumbnail_mixin.py:0 +#, python-format +msgid "The base name must be set when multiple images are given" +msgstr "El nombre base debe establecerse cuando se dan varias imágenes" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__base_name +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__base_name +msgid "The base name of the thumbnail image (without extension)" +msgstr "El nombre base de la imagen en miniatura (sin extensión)" + +#. module: fs_image_thumbnail +#. odoo-python +#: code:addons/fs_image_thumbnail/models/fs_image_thumbnail_mixin.py:0 +#, python-format +msgid "The image %(name)s must be attached to an attachment" +msgstr "La imagen %(name)s debe adjuntarse a un archivo adjunto" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,help:fs_image_thumbnail.field_fs_image_thumbnail_mixin__base_name +#: model:ir.model.fields,help:fs_image_thumbnail.field_fs_thumbnail__base_name +msgid "" +"The thumbnail image will be named as base_name + _ + size_x + _ + size_y + . + extension.\n" +"If not set, the base name will be the name of the original image.This base name is used to find all existing thumbnail of an image generated for the same base name." +msgstr "" +"La imagen en miniatura se denominará como nombre_base + _ + tamaño_x + _ + " +"tamaño_y + . + extensión.\n" +"Si no se establece, el nombre base será el nombre de la imagen original. " +"Este nombre base se utiliza para encontrar todas las miniaturas existentes " +"de una imagen generada para el mismo nombre base." + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_ir_attachment__thumbnail_ids +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.ir_attachment_form_view +msgid "Thumbnails" +msgstr "Miniaturas" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__size_x +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__size_x +msgid "X size" +msgstr "Tamaño X" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__size_y +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__size_y +msgid "Y size" +msgstr "Talla Y" diff --git a/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/i18n/fs_image_thumbnail.pot b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/i18n/fs_image_thumbnail.pot new file mode 100644 index 0000000..6c712d9 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/i18n/fs_image_thumbnail.pot @@ -0,0 +1,170 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_image_thumbnail +# +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_image_thumbnail +#: model:ir.model,name:fs_image_thumbnail.model_ir_attachment +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__attachment_id +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__attachment_id +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.fs_image_thumbnail_mixin_search_view +msgid "Attachment" +msgstr "" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,help:fs_image_thumbnail.field_fs_image_thumbnail_mixin__attachment_id +#: model:ir.model.fields,help:fs_image_thumbnail.field_fs_thumbnail__attachment_id +msgid "Attachment containing the original image" +msgstr "" + +#. module: fs_image_thumbnail +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.fs_image_thumbnail_mixin_search_view +msgid "Base Name" +msgstr "" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__create_uid +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.fs_thumbnail_search_view +msgid "Created by" +msgstr "" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__create_date +msgid "Created on" +msgstr "" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__display_name +msgid "Display Name" +msgstr "" + +#. module: fs_image_thumbnail +#: model:ir.model,name:fs_image_thumbnail.model_fs_image_thumbnail_mixin +msgid "Fs Image Thumbnail Mixin" +msgstr "" + +#. module: fs_image_thumbnail +#: model:ir.ui.menu,name:fs_image_thumbnail.fs_thumbnail_menu +msgid "Fs Image Thumbnails" +msgstr "" + +#. module: fs_image_thumbnail +#: model:ir.actions.act_window,name:fs_image_thumbnail.fs_thumbnail_act_window +msgid "Fs Thumbnail" +msgstr "" + +#. module: fs_image_thumbnail +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.fs_image_thumbnail_mixin_search_view +msgid "Group By" +msgstr "" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__id +msgid "ID" +msgstr "" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__image +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__image +msgid "Image" +msgstr "" + +#. module: fs_image_thumbnail +#: model:ir.model,name:fs_image_thumbnail.model_fs_thumbnail +msgid "Image Thumbnail" +msgstr "" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail____last_update +msgid "Last Modified on" +msgstr "" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__write_date +msgid "Last Updated on" +msgstr "" + +#. module: fs_image_thumbnail +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.fs_image_thumbnail_mixin_search_view +msgid "MimeType" +msgstr "" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__mimetype +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__mimetype +msgid "Mimetype" +msgstr "" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__name +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__name +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.fs_image_thumbnail_mixin_search_view +msgid "Name" +msgstr "" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__original_image +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__original_image +msgid "Original Image" +msgstr "" + +#. module: fs_image_thumbnail +#. odoo-python +#: code:addons/fs_image_thumbnail/models/fs_image_thumbnail_mixin.py:0 +#, python-format +msgid "The base name must be set when multiple images are given" +msgstr "" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__base_name +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__base_name +msgid "The base name of the thumbnail image (without extension)" +msgstr "" + +#. module: fs_image_thumbnail +#. odoo-python +#: code:addons/fs_image_thumbnail/models/fs_image_thumbnail_mixin.py:0 +#, python-format +msgid "The image %(name)s must be attached to an attachment" +msgstr "" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,help:fs_image_thumbnail.field_fs_image_thumbnail_mixin__base_name +#: model:ir.model.fields,help:fs_image_thumbnail.field_fs_thumbnail__base_name +msgid "" +"The thumbnail image will be named as base_name + _ + size_x + _ + size_y + . + extension.\n" +"If not set, the base name will be the name of the original image.This base name is used to find all existing thumbnail of an image generated for the same base name." +msgstr "" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_ir_attachment__thumbnail_ids +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.ir_attachment_form_view +msgid "Thumbnails" +msgstr "" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__size_x +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__size_x +msgid "X size" +msgstr "" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__size_y +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__size_y +msgid "Y size" +msgstr "" diff --git a/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/i18n/it.po b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/i18n/it.po new file mode 100644 index 0000000..b03afa1 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/i18n/it.po @@ -0,0 +1,178 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_image_thumbnail +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-12-12 11:33+0000\n" +"Last-Translator: mymage \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_image_thumbnail +#: model:ir.model,name:fs_image_thumbnail.model_ir_attachment +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__attachment_id +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__attachment_id +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.fs_image_thumbnail_mixin_search_view +msgid "Attachment" +msgstr "Allegato" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,help:fs_image_thumbnail.field_fs_image_thumbnail_mixin__attachment_id +#: model:ir.model.fields,help:fs_image_thumbnail.field_fs_thumbnail__attachment_id +msgid "Attachment containing the original image" +msgstr "Allegato contenente l'immagine originale" + +#. module: fs_image_thumbnail +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.fs_image_thumbnail_mixin_search_view +msgid "Base Name" +msgstr "Nome base" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__create_uid +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.fs_thumbnail_search_view +msgid "Created by" +msgstr "Creato da" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__create_date +msgid "Created on" +msgstr "Creato il" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__display_name +msgid "Display Name" +msgstr "Nome visualizzato" + +#. module: fs_image_thumbnail +#: model:ir.model,name:fs_image_thumbnail.model_fs_image_thumbnail_mixin +msgid "Fs Image Thumbnail Mixin" +msgstr "Mixin anteprima immagine FS" + +#. module: fs_image_thumbnail +#: model:ir.ui.menu,name:fs_image_thumbnail.fs_thumbnail_menu +msgid "Fs Image Thumbnails" +msgstr "Anteprima immagine FS" + +#. module: fs_image_thumbnail +#: model:ir.actions.act_window,name:fs_image_thumbnail.fs_thumbnail_act_window +msgid "Fs Thumbnail" +msgstr "Anteprima FS" + +#. module: fs_image_thumbnail +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.fs_image_thumbnail_mixin_search_view +msgid "Group By" +msgstr "Raggruppa per" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__id +msgid "ID" +msgstr "ID" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__image +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__image +msgid "Image" +msgstr "Immagine" + +#. module: fs_image_thumbnail +#: model:ir.model,name:fs_image_thumbnail.model_fs_thumbnail +msgid "Image Thumbnail" +msgstr "Anteprima immagine" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail____last_update +msgid "Last Modified on" +msgstr "Ultima modifica il" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__write_uid +msgid "Last Updated by" +msgstr "Ultimo aggiornamento di" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__write_date +msgid "Last Updated on" +msgstr "Ultimo aggiornamento il" + +#. module: fs_image_thumbnail +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.fs_image_thumbnail_mixin_search_view +msgid "MimeType" +msgstr "Tipo MIME" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__mimetype +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__mimetype +msgid "Mimetype" +msgstr "Tipo MIME" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__name +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__name +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.fs_image_thumbnail_mixin_search_view +msgid "Name" +msgstr "Nome" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__original_image +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__original_image +msgid "Original Image" +msgstr "Immagine originale" + +#. module: fs_image_thumbnail +#. odoo-python +#: code:addons/fs_image_thumbnail/models/fs_image_thumbnail_mixin.py:0 +#, python-format +msgid "The base name must be set when multiple images are given" +msgstr "Il nome base deve essere impostato qando vengono fornite più immagini" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__base_name +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__base_name +msgid "The base name of the thumbnail image (without extension)" +msgstr "Il nome base dell'immagIne anteprima (senza estensione)" + +#. module: fs_image_thumbnail +#. odoo-python +#: code:addons/fs_image_thumbnail/models/fs_image_thumbnail_mixin.py:0 +#, python-format +msgid "The image %(name)s must be attached to an attachment" +msgstr "L'immagine %(name)s deve essere collegata ad una allegato" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,help:fs_image_thumbnail.field_fs_image_thumbnail_mixin__base_name +#: model:ir.model.fields,help:fs_image_thumbnail.field_fs_thumbnail__base_name +msgid "" +"The thumbnail image will be named as base_name + _ + size_x + _ + size_y + . + extension.\n" +"If not set, the base name will be the name of the original image.This base name is used to find all existing thumbnail of an image generated for the same base name." +msgstr "" +"L'immagine anteprima verrà denominata come " +"nome_bae+_+dimensione_x+dimensione_y+.+estensione.\n" +"Se non impostato, il nome base sarà il nome dell'immagine originale. Questo " +"nome base è utilizzato per trovare tutte le anteprime di una immagine " +"generate per lo stesso nome base." + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_ir_attachment__thumbnail_ids +#: model_terms:ir.ui.view,arch_db:fs_image_thumbnail.ir_attachment_form_view +msgid "Thumbnails" +msgstr "Anteprime" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__size_x +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__size_x +msgid "X size" +msgstr "Dimensione X" + +#. module: fs_image_thumbnail +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_image_thumbnail_mixin__size_y +#: model:ir.model.fields,field_description:fs_image_thumbnail.field_fs_thumbnail__size_y +msgid "Y size" +msgstr "Dimensione Y" diff --git a/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/models/__init__.py b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/models/__init__.py new file mode 100644 index 0000000..0ef2d28 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/models/__init__.py @@ -0,0 +1,3 @@ +from . import fs_image_thumbnail_mixin +from . import fs_thumbnail +from . import ir_attachment diff --git a/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/models/fs_image_thumbnail_mixin.py b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/models/fs_image_thumbnail_mixin.py new file mode 100644 index 0000000..ab13a9c --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/models/fs_image_thumbnail_mixin.py @@ -0,0 +1,242 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from collections import OrderedDict + +from slugify import slugify + +from odoo import _, api, fields, models +from odoo.exceptions import UserError + +from odoo.addons.fs_image.fields import FSImage, FSImageValue + + +class FsImageThumbnailMixin(models.AbstractModel): + """Mixin defining what is a thumbnail image and providing a + method to generate a thumbnail image from an image. + + """ + + _name = "fs.image.thumbnail.mixin" + _description = "Fs Image Thumbnail Mixin" + + image = FSImage("Image", required=True) + original_image = FSImage("Original Image", compute="_compute_original_image") + size_x = fields.Integer("X size", required=True) + size_y = fields.Integer("Y size", required=True) + base_name = fields.Char( + "The base name of the thumbnail image (without extension)", + required=True, + help="The thumbnail image will be named as base_name " + "+ _ + size_x + _ + size_y + . + extension.\n" + "If not set, the base name will be the name of the original image." + "This base name is used to find all existing thumbnail of an image generated " + "for the same base name.", + ) + + attachment_id = fields.Many2one( + comodel_name="ir.attachment", + string="Attachment", + help="Attachment containing the original image", + required=True, + ondelete="cascade", + ) + name = fields.Char( + compute="_compute_name", + store=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_mimetype(self): + for record in self: + record.mimetype = record.image.mimetype if record.image else None + + @api.depends("attachment_id") + def _compute_original_image(self): + original_image_field = self._fields["original_image"] + for record in self: + value = None + if record.attachment_id: + value = original_image_field._convert_attachment_to_cache( + record.attachment_id + ) + record.original_image = value + + @api.model + def _resize(self, image: FSImage, size_x: int, size_y: int, fmt: str = "") -> bytes: + """Resize the given image to the given size. + + :param image: the image to resize + :param size_x: the new width of the image + :param size_y: the new height of the image + :param fmt: the output format of the image. Can be PNG, JPEG, GIF, or ICO. + Default to the format of the original image. BMP is converted to + PNG, other formats than those mentioned above are converted to JPEG. + :return: the resized image + """ + # image_process only accept PNG, JPEG, GIF, or ICO as output format + # in uppercase. Remove the dot if present and convert to uppercase. + fmt = fmt.upper().replace(".", "") + return image.image_process(size=(size_x, size_y), output_format=fmt) + + @api.model + def _get_resize_format(self, image: FSImage) -> str: + """Get the format to use to resize an image. + + :return: the format to use to resize an image + """ + fmt = ( + self.env["ir.config_parameter"] + .sudo() + .get_param("fs_image_thumbnail.resize_format") + ) + return fmt or image.extension + + @api.model + def _prepare_tumbnail( + self, image: FSImage, size_x: int, size_y: int, base_name: str + ) -> dict: + """Prepare the values to create a thumbnail image from the given image. + + :param image: the image to resize + :param size_x: the new width of the image + :param size_y: the new height of the image + :param base_name: the base name of the thumbnail image (without extension) + :return: the values to create a thumbnail image + """ + fmt = self._get_resize_format(image) + extension = fmt + # Add a dot before the extension if needed and convert to lowercase. + extension = extension.lower() + if extension and not extension.startswith("."): + extension = "." + extension + new_image = FSImageValue( + value=self._resize(image, size_x, size_y, fmt), + name="%s_%s_%s%s" % (base_name, size_x, size_y, extension), + alt_text=image.alt_text, + ) + return { + "image": new_image, + "size_x": size_x, + "size_y": size_y, + "base_name": base_name, + "attachment_id": image.attachment.id, + } + + @api.model + def _slugify_base_name(self, base_name: str) -> str: + """Slugify the given base name. + + :param base_name: the base name to slugify + :return: the slugified base name + """ + return slugify(base_name) if base_name else base_name + + @api.model + def _get_existing_thumbnail_domain( + self, *images: tuple[FSImageValue], base_name: str = "" + ) -> list: + """Get the domain to find existing thumbnail images from the given image. + + :param images: a list of images we want to find existing thumbnails + :param base_name: the base name of the thumbnail image (without extension) + The base name must be set when multiple images are given. + :return: the domain to find existing thumbnail images + """ + attachment_ids = [] + for image in images: + if image.attachment: + attachment_ids.append(image.attachment.id) + else: + raise UserError( + _( + "The image %(name)s must be attached to an attachment", + name=image.name, + ) + ) + base_name = self._get_slugified_base_name(*images, base_name=base_name) + return [ + ("attachment_id", "in", attachment_ids), + ("base_name", "=", base_name), + ] + + @api.model + def get_thumbnails( + self, *images: tuple[FSImageValue], base_name: str = "" + ) -> list["FsImageThumbnailMixin"]: + """Get existing thumbnail images from the given image. + + :param images: a list of images we want to find existing thumbnails + :param base_name: the base name of the thumbnail image (without extension) + The base name must be set when multiple images are given. + :return: a recordset of thumbnail images + """ + domain = self._get_existing_thumbnail_domain(*images, base_name=base_name) + return self.search(domain) + + @api.model + def get_or_create_thumbnails( + self, + *images: tuple[FSImageValue], + sizes: list[tuple[int, int]], + base_name: str = "" + ) -> OrderedDict[FSImageValue, list["FsImageThumbnailMixin"]]: + """Get or create a thumbnail images from the given image. + + :param images: the list of images we want to get or create thumbnails + :param sizes: the list of sizes to use to resize the image + (list of tuple (size_x, size_y)) + :param base_name: the base name of the thumbnail image (without extension) + The base name must be set when multiple images are given. + :return: an ordered dictionary where the key is the original image and + the value is a recordset of thumbnail images. The order of the dict + is the order of the images passed to the method. + """ + base_name = self._get_slugified_base_name(*images, base_name=base_name) + thumbnails = self.get_thumbnails(*images, base_name=base_name) + thumbnails_by_attachment_id = thumbnails.partition("attachment_id") + ret = OrderedDict[FSImageValue, list["FsImageThumbnailMixin"]]() + for image in images: + thumbnails_by_size = { + (thumbnail.size_x, thumbnail.size_y): thumbnail + for thumbnail in thumbnails_by_attachment_id.get(image.attachment, []) + } + ids_to_return = [] + for size_x, size_y in sizes: + thumbnail = thumbnails_by_size.get((size_x, size_y)) + if not thumbnail: + values = self._prepare_tumbnail(image, size_x, size_y, base_name) + # no creation possible outside of this method -> sudo() is + # required since no access rights defined on create + thumbnail = self.sudo().create(values) + ids_to_return.append(thumbnail.id) + # return the thumbnails browsed in the same security context as the method + # caller + ret[image] = self.browse(ids_to_return) + return ret + + @api.model + def _get_slugified_base_name( + self, *images: tuple[FSImageValue], base_name: str + ) -> str: + """Get the base name of the thumbnail image (without extension). + + :param images: the list of images we want to get the base name + :return: the base name of the thumbnail image + """ + if not base_name: + if len(images) > 1: + raise UserError( + _("The base name must be set when multiple images are given") + ) + base_name = images[0].name + return self._slugify_base_name(base_name) diff --git a/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/models/fs_thumbnail.py b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/models/fs_thumbnail.py new file mode 100644 index 0000000..bf2df3d --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/models/fs_thumbnail.py @@ -0,0 +1,11 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models + + +class FsThumbnail(models.Model): + + _name = "fs.thumbnail" + _inherit = "fs.image.thumbnail.mixin" + _description = "Image Thumbnail" diff --git a/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/models/ir_attachment.py b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/models/ir_attachment.py new file mode 100644 index 0000000..23116ec --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/models/ir_attachment.py @@ -0,0 +1,16 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class IrAttachment(models.Model): + + _inherit = "ir.attachment" + + thumbnail_ids = fields.One2many( + comodel_name="fs.thumbnail", + inverse_name="attachment_id", + string="Thumbnails", + readonly=True, + ) diff --git a/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/readme/CONTEXT.rst b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/readme/CONTEXT.rst new file mode 100644 index 0000000..4c7e055 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/readme/CONTEXT.rst @@ -0,0 +1,17 @@ +In some specific cases you may need to generate and store thumbnails of images in Odoo. +This is the case for example when you want to provide image in specific sizes for a website +or a mobile application. + +This module provides a generic way to generate thumbnails of images and store them in a +specific filesystem storage. Indeed, you could need to store the thumbnails in a different +storage than the original image (eg: store the thumbnails in a CDN) to make sure the +thumbnails are served quickly when requested by an external application and to +avoid to expose the original image storage. + +This module uses the `fs_image `_ +module to store the thumbnails in a filesystem storage. + +The `shopinvader_product_image `_ addon uses this module to generate and +store the thumbnails of the images of the products and categories to be accessible +by the website. diff --git a/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/readme/CONTRIBUTORS.rst b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000..1480ca2 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Laurent Mignon (https://acsone.eu) diff --git a/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/readme/CREDITS.rst b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/readme/CREDITS.rst new file mode 100644 index 0000000..82c081d --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/readme/CREDITS.rst @@ -0,0 +1,3 @@ +The development of this module has been financially supported by: + +* `Alcyon Belux `_ diff --git a/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/readme/DESCRIPTION.rst b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/readme/DESCRIPTION.rst new file mode 100644 index 0000000..6ce408a --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/readme/DESCRIPTION.rst @@ -0,0 +1,4 @@ +This module extends the **fs_image** addon to support the creation and the storage of +thumbnails for images. This module is a **technical module** and is not +meant to be installed by end-users. It only provides a mixin to be used +by other modules and a model to store the thumbnails. diff --git a/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/readme/HISTORY.rst b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/readme/HISTORY.rst new file mode 100644 index 0000000..f021207 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/readme/HISTORY.rst @@ -0,0 +1,11 @@ +16.0.1.0.1 (2023-10-04) +~~~~~~~~~~~~~~~~~~~~~~~ + +**Bugfixes** + +- The call to the method *get_or_create_thumbnails* on the *fs.image.thumbnail.mixin* + class returns now an ordered dictionary where the key is the original image and + the value is a recordset of thumbnail images. The order of the dict is the order + of the images passed to the method. This ensures that when you process the result + of the method you can be sure that the order of the images is the same as the + order of the images passed to the method. (`#282 `_) diff --git a/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/readme/USAGE.rst b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/readme/USAGE.rst new file mode 100644 index 0000000..4a53916 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/readme/USAGE.rst @@ -0,0 +1,57 @@ +This addon provides a convenient way to get and create if not exists image +thumbnails. All the logic is implemented by the abstract model +`fs.image.thumbnail.mixin`. The main method is `get_or_create_thumbnails` which +accepts a *FSImageValue* instance, a list of thumbnail sizes and a base name. + +When the method is called, it will check if the thumbnail exists for the given +sizes and base name. If not, it will create it. + +The `fs.thumbnail` model provided by this addon is a concrete implementation of +the abstract model `fs.image.thumbnail.mixin`. The motivation to implement all the +logic in an abstract model is to allow developers to create their own thumbnail +models. This could be useful if you want to store the thumbnails in a different +storage since you can specify the storage to use by model on the `fs.storage` +form view. + +Creating / retrieving thumbnails is as simple as: + +.. code-block:: python + + from odoo.addons.fs_image.fields import FSImageValue + + # create an attachment with a image file + attachment = self.env['ir.attachment'].create({ + 'name': 'test', + 'datas': base64.b64encode(open('test.png', 'rb').read()), + 'datas_fname': 'test.png', + }) + + # create a FSImageValue instance for the attachment + image_value = FSImageValue(attachment) + + # get or create the thumbnails + thumbnails = self.env['fs.thumbnail'].get_or_create_thumbnails( + image_value, [(800,600), (400, 200)], 'my base name') + + + +If you've a model with a *FSImage* field, the call to `get_or_create_thumbnails` +is even simpler: + +.. code-block:: python + + from odoo import models + from odoo.addons.fs_image.fields import FSImage + + class MyModel(models.Model): + _name = 'my.model' + + image = FSImage('Image') + + my_record = cls.env['my.model'].create({ + 'image': open('test.png', 'rb'), + }) + + # get or create the thumbnails + thumbnails = record.image.get_or_create_thumbnails(my_record.image, + [(800,600), (400, 200)], 'my base name') diff --git a/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/readme/newsfragments/.gitignore b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/readme/newsfragments/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/security/fs_thumbnail.xml b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/security/fs_thumbnail.xml new file mode 100644 index 0000000..7690d99 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/security/fs_thumbnail.xml @@ -0,0 +1,16 @@ + + + + + + fs.thumbnail access read + + + + + + + + + diff --git a/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/static/description/icon.png b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/static/description/icon.png new file mode 100644 index 0000000..3a0328b Binary files /dev/null and b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/static/description/icon.png differ diff --git a/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/static/description/index.html b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/static/description/index.html new file mode 100644 index 0000000..f02ae60 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/static/description/index.html @@ -0,0 +1,528 @@ + + + + + +Fs Image Thumbnail + + + +
+

Fs Image Thumbnail

+ + +

Alpha License: AGPL-3 OCA/storage Translate me on Weblate Try me on Runboat

+

This module extends the fs_image addon to support the creation and the storage of +thumbnails for images. This module is a technical module and is not +meant to be installed by end-users. It only provides a mixin to be used +by other modules and a model to store the thumbnails.

+
+

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

+
+

Table of contents

+ +
+

Use Cases / Context

+

In some specific cases you may need to generate and store thumbnails of images in Odoo. +This is the case for example when you want to provide image in specific sizes for a website +or a mobile application.

+

This module provides a generic way to generate thumbnails of images and store them in a +specific filesystem storage. Indeed, you could need to store the thumbnails in a different +storage than the original image (eg: store the thumbnails in a CDN) to make sure the +thumbnails are served quickly when requested by an external application and to +avoid to expose the original image storage.

+

This module uses the fs_image +module to store the thumbnails in a filesystem storage.

+

The shopinvader_product_image addon uses this module to generate and +store the thumbnails of the images of the products and categories to be accessible +by the website.

+
+
+

Usage

+

This addon provides a convenient way to get and create if not exists image +thumbnails. All the logic is implemented by the abstract model +fs.image.thumbnail.mixin. The main method is get_or_create_thumbnails which +accepts a FSImageValue instance, a list of thumbnail sizes and a base name.

+

When the method is called, it will check if the thumbnail exists for the given +sizes and base name. If not, it will create it.

+

The fs.thumbnail model provided by this addon is a concrete implementation of +the abstract model fs.image.thumbnail.mixin. The motivation to implement all the +logic in an abstract model is to allow developers to create their own thumbnail +models. This could be useful if you want to store the thumbnails in a different +storage since you can specify the storage to use by model on the fs.storage +form view.

+

Creating / retrieving thumbnails is as simple as:

+
+from odoo.addons.fs_image.fields import FSImageValue
+
+# create an attachment with a image file
+attachment = self.env['ir.attachment'].create({
+    'name': 'test',
+    'datas': base64.b64encode(open('test.png', 'rb').read()),
+    'datas_fname': 'test.png',
+})
+
+# create a FSImageValue instance for the attachment
+image_value = FSImageValue(attachment)
+
+# get or create the thumbnails
+thumbnails = self.env['fs.thumbnail'].get_or_create_thumbnails(
+    image_value, [(800,600), (400, 200)], 'my base name')
+
+

If you’ve a model with a FSImage field, the call to get_or_create_thumbnails +is even simpler:

+
+from odoo import models
+from odoo.addons.fs_image.fields import FSImage
+
+class MyModel(models.Model):
+    _name = 'my.model'
+
+    image = FSImage('Image')
+
+my_record = cls.env['my.model'].create({
+    'image': open('test.png', 'rb'),
+})
+
+# get or create the thumbnails
+thumbnails = record.image.get_or_create_thumbnails(my_record.image,
+    [(800,600), (400, 200)], 'my base name')
+
+
+
+

Changelog

+
+

16.0.1.0.1 (2023-10-04)

+

Bugfixes

+
    +
  • The call to the method get_or_create_thumbnails on the fs.image.thumbnail.mixin +class returns now an ordered dictionary where the key is the original image and +the value is a recordset of thumbnail images. The order of the dict is the order +of the images passed to the method. This ensures that when you process the result +of the method you can be sure that the order of the images is the same as the +order of the images passed to the method. (#282)
  • +
+
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub 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.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • ACSONE SA/NV
  • +
+
+ +
+

Other credits

+

The development of this module has been financially supported by:

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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.

+

Current maintainer:

+

lmignon

+

This module is part of the OCA/storage project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/tests/__init__.py b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/tests/__init__.py new file mode 100644 index 0000000..919947a --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/tests/__init__.py @@ -0,0 +1 @@ +from . import test_fs_image_thumbnail diff --git a/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/tests/test_fs_image_thumbnail.py b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/tests/test_fs_image_thumbnail.py new file mode 100644 index 0000000..3caa4b5 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/tests/test_fs_image_thumbnail.py @@ -0,0 +1,81 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +import base64 +import io + +from PIL import Image + +from odoo.tests.common import TransactionCase + +from odoo.addons.fs_image.fields import FSImageValue + + +class TestFsImageThumbnail(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.white_image = cls._create_image(32, 32, color="#FFFFFF") + + cls.image_attachment = cls.env["ir.attachment"].create( + { + "name": "Test Image", + "datas": base64.b64encode(cls.white_image), + "mimetype": "image/png", + } + ) + + cls.fs_image_value = FSImageValue(attachment=cls.image_attachment) + cls.fs_thumbnail_model = cls.env["fs.thumbnail"] + + def setUp(self): + super().setUp() + self.temp_dir = self.env["fs.storage"].create( + { + "name": "Temp FS Storage", + "protocol": "memory", + "code": "mem_dir", + "directory_path": "/tmp/", + "model_xmlids": "fs_image_thumbnail.model_fs_thumbnail", + } + ) + + @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 assert_image_size(self, value: bytes, width, height): + self.assertEqual(Image.open(io.BytesIO(value)).size, (width, height)) + + def test_create_multi(self): + self.assertFalse(self.image_attachment.thumbnail_ids) + thumbnails = self.fs_thumbnail_model.get_or_create_thumbnails( + self.fs_image_value, sizes=[(16, 16), (8, 8)], base_name="My super test" + )[self.fs_image_value] + self.assertEqual(len(thumbnails), 2) + self.assertEqual(thumbnails[0].name, "my-super-test_16_16.png") + self.assert_image_size(thumbnails[0].image.getvalue(), 16, 16) + self.assertEqual(thumbnails[1].name, "my-super-test_8_8.png") + self.assert_image_size(thumbnails[1].image.getvalue(), 8, 8) + + self.assertEqual(self.image_attachment.thumbnail_ids, thumbnails) + + # if we call the method again for the same size, we should get the same thumbnail + new_thumbnails = self.fs_thumbnail_model.get_or_create_thumbnails( + self.fs_image_value, sizes=[(16, 16), (8, 8)], base_name="My super test" + )[self.fs_image_value] + self.assertEqual(new_thumbnails, thumbnails) + + def test_create_with_specific_format(self): + self.env["ir.config_parameter"].set_param( + "fs_image_thumbnail.resize_format", "JPEG" + ) + thumbnail = self.fs_thumbnail_model.get_or_create_thumbnails( + self.fs_image_value, sizes=[(8, 8)], base_name="My super test" + )[self.fs_image_value] + self.assertEqual(thumbnail[0].name, "my-super-test_8_8.jpeg") + self.assertEqual(thumbnail[0].mimetype, "image/jpeg") + self.assert_image_size(thumbnail[0].image.getvalue(), 8, 8) diff --git a/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/views/fs_image_thumbnail_mixin.xml b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/views/fs_image_thumbnail_mixin.xml new file mode 100644 index 0000000..8dfac21 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/views/fs_image_thumbnail_mixin.xml @@ -0,0 +1,84 @@ + + + + + + fs.image.thumbnail.mixin.form (in fs_image_thumbnail) + fs.image.thumbnail.mixin + +
+
+
+ + +
+
+
+ + + fs.image.thumbnail.mixin.search (in fs_image_thumbnail) + fs.image.thumbnail.mixin + + + + + + + + + + + + + + + fs.image.thumbnail.mixin.tree (in fs_image_thumbnail) + fs.image.thumbnail.mixin + + + + + + + + + + + +
diff --git a/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/views/fs_thumbnail.xml b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/views/fs_thumbnail.xml new file mode 100644 index 0000000..9de1fd6 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/views/fs_thumbnail.xml @@ -0,0 +1,62 @@ + + + + + + fs.thumbnail.form + fs.thumbnail + + primary + + + + + + + + + + fs.thumbnail.search + fs.thumbnail + + primary + + + + + + + + + fs.thumbnail.tree + fs.thumbnail + + primary + + + + + + + + + + Fs Thumbnail + fs.thumbnail + tree,form + [] + {} + + + + Fs Image Thumbnails + + + + + diff --git a/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/views/ir_attachment.xml b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/views/ir_attachment.xml new file mode 100644 index 0000000..2855a1c --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image_thumbnail/fs_image_thumbnail/views/ir_attachment.xml @@ -0,0 +1,19 @@ + + + + + + ir.attachment.form (in fs_image_thumbnail) + ir.attachment + + + + + + + + + + + diff --git a/odoo-bringout-oca-storage-fs_image_thumbnail/pyproject.toml b/odoo-bringout-oca-storage-fs_image_thumbnail/pyproject.toml new file mode 100644 index 0000000..c251f20 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_image_thumbnail/pyproject.toml @@ -0,0 +1,44 @@ +[project] +name = "odoo-bringout-oca-storage-fs_image_thumbnail" +version = "16.0.0" +description = "Fs Image Thumbnail - + Generate and store thumbnail for images" +authors = [ + { name = "Ernad Husremovic", email = "hernad@bring.out.ba" } +] +dependencies = [ + "odoo-bringout-oca-storage-fs_image>=16.0.0", + "odoo-bringout-oca-storage-base_partition>=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_image_thumbnail"] + +[tool.rye] +managed = true +dev-dependencies = [ + "pytest>=8.4.1", +] diff --git a/odoo-bringout-oca-storage-fs_product_brand_multi_image/README.md b/odoo-bringout-oca-storage-fs_product_brand_multi_image/README.md new file mode 100644 index 0000000..1ad8c70 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_brand_multi_image/README.md @@ -0,0 +1,49 @@ +# Fs Product Brand Multi Image + +Odoo addon: fs_product_brand_multi_image + +## Installation + +```bash +pip install odoo-bringout-oca-storage-fs_product_brand_multi_image +``` + +## Dependencies + +This addon depends on: +- fs_base_multi_image +- product_brand +- sales_team +- image_tag + +## Manifest Information + +- **Name**: Fs Product Brand Multi Image +- **Version**: 16.0.1.0.0 +- **Category**: N/A +- **License**: AGPL-3 +- **Installable**: False + +## Source + +Based on [OCA/storage](https://github.com/OCA/storage) branch 16.0, addon `fs_product_brand_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 diff --git a/odoo-bringout-oca-storage-fs_product_brand_multi_image/doc/ARCHITECTURE.md b/odoo-bringout-oca-storage-fs_product_brand_multi_image/doc/ARCHITECTURE.md new file mode 100644 index 0000000..4c6d7fa --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_brand_multi_image/doc/ARCHITECTURE.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_product_brand_multi_image Module - fs_product_brand_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. diff --git a/odoo-bringout-oca-storage-fs_product_brand_multi_image/doc/CONFIGURATION.md b/odoo-bringout-oca-storage-fs_product_brand_multi_image/doc/CONFIGURATION.md new file mode 100644 index 0000000..7484975 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_brand_multi_image/doc/CONFIGURATION.md @@ -0,0 +1,3 @@ +# Configuration + +Refer to Odoo settings for fs_product_brand_multi_image. Configure related models, access rights, and options as needed. diff --git a/odoo-bringout-oca-storage-fs_product_brand_multi_image/doc/CONTROLLERS.md b/odoo-bringout-oca-storage-fs_product_brand_multi_image/doc/CONTROLLERS.md new file mode 100644 index 0000000..f628e77 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_brand_multi_image/doc/CONTROLLERS.md @@ -0,0 +1,3 @@ +# Controllers + +This module does not define custom HTTP controllers. diff --git a/odoo-bringout-oca-storage-fs_product_brand_multi_image/doc/DEPENDENCIES.md b/odoo-bringout-oca-storage-fs_product_brand_multi_image/doc/DEPENDENCIES.md new file mode 100644 index 0000000..ad340be --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_brand_multi_image/doc/DEPENDENCIES.md @@ -0,0 +1,8 @@ +# Dependencies + +This addon depends on: + +- [fs_base_multi_image](../../odoo-bringout-oca-storage-fs_base_multi_image) +- [product_brand](../../odoo-bringout-oca-brand-product_brand) +- [sales_team](../../odoo-bringout-oca-ocb-sales_team) +- [image_tag](../../odoo-bringout-oca-storage-image_tag) diff --git a/odoo-bringout-oca-storage-fs_product_brand_multi_image/doc/FAQ.md b/odoo-bringout-oca-storage-fs_product_brand_multi_image/doc/FAQ.md new file mode 100644 index 0000000..ce18c31 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_brand_multi_image/doc/FAQ.md @@ -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_product_brand_multi_image or install in UI. diff --git a/odoo-bringout-oca-storage-fs_product_brand_multi_image/doc/INSTALL.md b/odoo-bringout-oca-storage-fs_product_brand_multi_image/doc/INSTALL.md new file mode 100644 index 0000000..0cb7c09 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_brand_multi_image/doc/INSTALL.md @@ -0,0 +1,7 @@ +# Install + +```bash +pip install odoo-bringout-oca-storage-fs_product_brand_multi_image" +# or +uv pip install odoo-bringout-oca-storage-fs_product_brand_multi_image" +``` diff --git a/odoo-bringout-oca-storage-fs_product_brand_multi_image/doc/MODELS.md b/odoo-bringout-oca-storage-fs_product_brand_multi_image/doc/MODELS.md new file mode 100644 index 0000000..1d3ffee --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_brand_multi_image/doc/MODELS.md @@ -0,0 +1,15 @@ +# Models + +Detected core models and extensions in fs_product_brand_multi_image. + +```mermaid +classDiagram + class fs_product_brand_image + class fs_image_relation_mixin + class image_tag + class product_brand +``` + +Notes +- Classes show model technical names; fields omitted for brevity. +- Items listed under _inherit are extensions of existing models. diff --git a/odoo-bringout-oca-storage-fs_product_brand_multi_image/doc/OVERVIEW.md b/odoo-bringout-oca-storage-fs_product_brand_multi_image/doc/OVERVIEW.md new file mode 100644 index 0000000..4edc933 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_brand_multi_image/doc/OVERVIEW.md @@ -0,0 +1,6 @@ +# Overview + +Packaged Odoo addon: fs_product_brand_multi_image. Provides features documented in upstream Odoo 16 under this addon. + +- Source: OCA/OCB 16.0, addon fs_product_brand_multi_image +- License: LGPL-3 diff --git a/odoo-bringout-oca-storage-fs_product_brand_multi_image/doc/REPORTS.md b/odoo-bringout-oca-storage-fs_product_brand_multi_image/doc/REPORTS.md new file mode 100644 index 0000000..e0ea35f --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_brand_multi_image/doc/REPORTS.md @@ -0,0 +1,3 @@ +# Reports + +This module does not define custom reports. diff --git a/odoo-bringout-oca-storage-fs_product_brand_multi_image/doc/SECURITY.md b/odoo-bringout-oca-storage-fs_product_brand_multi_image/doc/SECURITY.md new file mode 100644 index 0000000..487fb26 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_brand_multi_image/doc/SECURITY.md @@ -0,0 +1,73 @@ +# Security + +Access control and security definitions in fs_product_brand_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_product_brand_image.xml](../fs_product_brand_multi_image/security/fs_product_brand_image.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_product_brand_image.xml](../fs_product_brand_multi_image/security/fs_product_brand_image.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 diff --git a/odoo-bringout-oca-storage-fs_product_brand_multi_image/doc/TROUBLESHOOTING.md b/odoo-bringout-oca-storage-fs_product_brand_multi_image/doc/TROUBLESHOOTING.md new file mode 100644 index 0000000..56853cb --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_brand_multi_image/doc/TROUBLESHOOTING.md @@ -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. diff --git a/odoo-bringout-oca-storage-fs_product_brand_multi_image/doc/USAGE.md b/odoo-bringout-oca-storage-fs_product_brand_multi_image/doc/USAGE.md new file mode 100644 index 0000000..d899b3d --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_brand_multi_image/doc/USAGE.md @@ -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_product_brand_multi_image +``` diff --git a/odoo-bringout-oca-storage-fs_product_brand_multi_image/doc/WIZARDS.md b/odoo-bringout-oca-storage-fs_product_brand_multi_image/doc/WIZARDS.md new file mode 100644 index 0000000..48e790d --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_brand_multi_image/doc/WIZARDS.md @@ -0,0 +1,3 @@ +# Wizards + +This module does not include UI wizards. diff --git a/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/README.rst b/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/README.rst new file mode 100644 index 0000000..0cff053 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/README.rst @@ -0,0 +1,103 @@ +============================ +Fs Product Brand Multi Image +============================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:97df71c6f6dd320804acb6d60ddc35f8d78a7ade5a3e9f7acddf16a613893a34 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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_product_brand_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_product_brand_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| + +Attach images stored into an external filesystem to product brands + +This addon is a drop-in replacement for the **storage_image_product_brand** addon. + +.. 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 `_ + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +Go to Sales > Configuration > Products > Product Brands. +A new field Images is available to upload or use existing images. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub 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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* ACSONE SA/NV + +Contributors +~~~~~~~~~~~~ + +* Sébastien Beau +* Quentin Groulard +* `Camptocamp `_ + + * Iván Todorovich + +* Laurent Mignon + +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 `__: + +|maintainer-lmignon| + +This module is part of the `OCA/storage `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/__init__.py b/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/__manifest__.py b/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/__manifest__.py new file mode 100644 index 0000000..4672dff --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/__manifest__.py @@ -0,0 +1,21 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Fs Product Brand Multi Image", + "summary": """ + Link images to product brands""", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "author": "ACSONE SA/NV,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/storage", + "depends": ["fs_base_multi_image", "product_brand", "sales_team", "image_tag"], + "data": [ + "security/fs_product_brand_image.xml", + "views/fs_product_brand_image.xml", + "views/product_brand.xml", + ], + "demo": [], + "maintainers": ["lmignon"], + "development_status": "Alpha", +} diff --git a/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/i18n/bs.po b/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/i18n/bs.po new file mode 100644 index 0000000..19da2e2 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/i18n/bs.po @@ -0,0 +1,133 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_product_brand_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_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_image_tag__apply_on +msgid "Apply On" +msgstr "Primjeni na" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__brand_id +#: model:ir.model.fields.selection,name:fs_product_brand_multi_image.selection__image_tag__apply_on__brand +msgid "Brand" +msgstr "Brand" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__create_uid +msgid "Created by" +msgstr "Kreirao" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__create_date +msgid "Created on" +msgstr "Kreirano" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__display_name +msgid "Display Name" +msgstr "Prikazani naziv" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__id +msgid "ID" +msgstr "ID" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__image_medium +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_product_brand__image_medium +msgid "Image (128)" +msgstr "Slika (128)" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_product_brand__image +msgid "Image (original)" +msgstr "Slika (originalna)" + +#. module: fs_product_brand_multi_image +#: model:ir.model,name:fs_product_brand_multi_image.model_image_tag +msgid "Image Tag" +msgstr "Oznaka slike" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_product_brand__image_ids +#: model_terms:ir.ui.view,arch_db:fs_product_brand_multi_image.product_brand_form_view +msgid "Images" +msgstr "Slike" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image____last_update +msgid "Last Modified on" +msgstr "Zadnje mijenjano" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__write_uid +msgid "Last Updated by" +msgstr "Zadnji ažurirao" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__write_date +msgid "Last Updated on" +msgstr "Zadnje ažurirano" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__link_existing +msgid "Link Existing" +msgstr "Povezivanje postojećeg" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__image_id +msgid "Linked image" +msgstr "Povezana slika" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__mimetype +msgid "Mimetype" +msgstr "Mimetype" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__name +msgid "Name" +msgstr "Naziv:" + +#. module: fs_product_brand_multi_image +#: model:ir.model,name:fs_product_brand_multi_image.model_product_brand +msgid "Product Brand" +msgstr "Brand proizvoda" + +#. module: fs_product_brand_multi_image +#: model:ir.model,name:fs_product_brand_multi_image.model_fs_product_brand_image +msgid "Product Brand Image" +msgstr "Slika brenda proizvoda" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__sequence +msgid "Sequence" +msgstr "Sekvenca" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__specific_image +msgid "Specific Image" +msgstr "Specifična slika" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__specific_image_medium +msgid "Specific Image (128)" +msgstr "Specifična slika (128)" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__tag_id +msgid "tag" +msgstr "oznaka" diff --git a/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/i18n/es.po b/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/i18n/es.po new file mode 100644 index 0000000..ae42697 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/i18n/es.po @@ -0,0 +1,145 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_product_brand_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 \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_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_image_tag__apply_on +msgid "Apply On" +msgstr "Aplicar En" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__brand_id +#: model:ir.model.fields.selection,name:fs_product_brand_multi_image.selection__image_tag__apply_on__brand +msgid "Brand" +msgstr "Marca" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__create_date +msgid "Created on" +msgstr "Creado el" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__display_name +msgid "Display Name" +msgstr "Mostrar Nombre" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__id +msgid "ID" +msgstr "ID (identificación)" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__image_medium +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_product_brand__image_medium +msgid "Image (128)" +msgstr "Imagen (128)" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_product_brand__image +msgid "Image (original)" +msgstr "Imagen (original)" + +#. module: fs_product_brand_multi_image +#: model:ir.model,name:fs_product_brand_multi_image.model_image_tag +msgid "Image Tag" +msgstr "Etiqueta de la Imagen" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_product_brand__image_ids +#: model_terms:ir.ui.view,arch_db:fs_product_brand_multi_image.product_brand_form_view +msgid "Images" +msgstr "Imágenes" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image____last_update +msgid "Last Modified on" +msgstr "Última Modificación el" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__write_uid +msgid "Last Updated by" +msgstr "Actualizado por Última vez por" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__write_date +msgid "Last Updated on" +msgstr "Última Actualización el" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__link_existing +msgid "Link Existing" +msgstr "Enlace Existente" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__image_id +msgid "Linked image" +msgstr "Imagen vinculada" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__mimetype +msgid "Mimetype" +msgstr "Tipo Mimo" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__name +msgid "Name" +msgstr "Nombre" + +#. module: fs_product_brand_multi_image +#: model:ir.model,name:fs_product_brand_multi_image.model_product_brand +msgid "Product Brand" +msgstr "Marca de Producto" + +#. module: fs_product_brand_multi_image +#: model:ir.model,name:fs_product_brand_multi_image.model_fs_product_brand_image +msgid "Product Brand Image" +msgstr "Imagen de Marca del Producto" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__sequence +msgid "Sequence" +msgstr "Secuencia" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__specific_image +msgid "Specific Image" +msgstr "Imagen Específica" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__specific_image_medium +msgid "Specific Image (128)" +msgstr "Imagen Específica (128)" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__tag_id +msgid "tag" +msgstr "etiqueta" + +#~ msgid "Image" +#~ msgstr "Imagen" + +#~ msgid "Image 128" +#~ msgstr "Imagen 128" + +#~ msgid "Specific Image 128" +#~ msgstr "Imagen Específica 128" diff --git a/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/i18n/fs_product_brand_multi_image.pot b/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/i18n/fs_product_brand_multi_image.pot new file mode 100644 index 0000000..a5857d5 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/i18n/fs_product_brand_multi_image.pot @@ -0,0 +1,133 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_product_brand_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_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_image_tag__apply_on +msgid "Apply On" +msgstr "" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__brand_id +#: model:ir.model.fields.selection,name:fs_product_brand_multi_image.selection__image_tag__apply_on__brand +msgid "Brand" +msgstr "" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__create_uid +msgid "Created by" +msgstr "" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__create_date +msgid "Created on" +msgstr "" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__display_name +msgid "Display Name" +msgstr "" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__id +msgid "ID" +msgstr "" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__image_medium +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_product_brand__image_medium +msgid "Image (128)" +msgstr "" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_product_brand__image +msgid "Image (original)" +msgstr "" + +#. module: fs_product_brand_multi_image +#: model:ir.model,name:fs_product_brand_multi_image.model_image_tag +msgid "Image Tag" +msgstr "" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_product_brand__image_ids +#: model_terms:ir.ui.view,arch_db:fs_product_brand_multi_image.product_brand_form_view +msgid "Images" +msgstr "" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image____last_update +msgid "Last Modified on" +msgstr "" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__write_date +msgid "Last Updated on" +msgstr "" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__link_existing +msgid "Link Existing" +msgstr "" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__image_id +msgid "Linked image" +msgstr "" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__mimetype +msgid "Mimetype" +msgstr "" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__name +msgid "Name" +msgstr "" + +#. module: fs_product_brand_multi_image +#: model:ir.model,name:fs_product_brand_multi_image.model_product_brand +msgid "Product Brand" +msgstr "" + +#. module: fs_product_brand_multi_image +#: model:ir.model,name:fs_product_brand_multi_image.model_fs_product_brand_image +msgid "Product Brand Image" +msgstr "" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__sequence +msgid "Sequence" +msgstr "" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__specific_image +msgid "Specific Image" +msgstr "" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__specific_image_medium +msgid "Specific Image (128)" +msgstr "" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__tag_id +msgid "tag" +msgstr "" diff --git a/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/i18n/it.po b/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/i18n/it.po new file mode 100644 index 0000000..5d6e27e --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/i18n/it.po @@ -0,0 +1,145 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_product_brand_multi_image +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-01-18 11:33+0000\n" +"Last-Translator: mymage \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_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_image_tag__apply_on +msgid "Apply On" +msgstr "Applica a" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__brand_id +#: model:ir.model.fields.selection,name:fs_product_brand_multi_image.selection__image_tag__apply_on__brand +msgid "Brand" +msgstr "Marca" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__create_uid +msgid "Created by" +msgstr "Creato da" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__create_date +msgid "Created on" +msgstr "Creato il" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__display_name +msgid "Display Name" +msgstr "Nome visualizzato" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__id +msgid "ID" +msgstr "ID" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__image_medium +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_product_brand__image_medium +msgid "Image (128)" +msgstr "Immagine 128" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_product_brand__image +msgid "Image (original)" +msgstr "Immagine (originale)" + +#. module: fs_product_brand_multi_image +#: model:ir.model,name:fs_product_brand_multi_image.model_image_tag +msgid "Image Tag" +msgstr "Etichetta immagine" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_product_brand__image_ids +#: model_terms:ir.ui.view,arch_db:fs_product_brand_multi_image.product_brand_form_view +msgid "Images" +msgstr "Immagini" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image____last_update +msgid "Last Modified on" +msgstr "Ultima modifica il" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__write_uid +msgid "Last Updated by" +msgstr "Ultimo aggiornamento di" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__write_date +msgid "Last Updated on" +msgstr "Ultimo aggiornamento il" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__link_existing +msgid "Link Existing" +msgstr "Collegamento esistente" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__image_id +msgid "Linked image" +msgstr "Immagine collegata" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__mimetype +msgid "Mimetype" +msgstr "Tipo MIME" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__name +msgid "Name" +msgstr "Nome" + +#. module: fs_product_brand_multi_image +#: model:ir.model,name:fs_product_brand_multi_image.model_product_brand +msgid "Product Brand" +msgstr "Marca del prodotto" + +#. module: fs_product_brand_multi_image +#: model:ir.model,name:fs_product_brand_multi_image.model_fs_product_brand_image +msgid "Product Brand Image" +msgstr "Immagine marca prodotto" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__sequence +msgid "Sequence" +msgstr "Sequenza" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__specific_image +msgid "Specific Image" +msgstr "Immagine specifica" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__specific_image_medium +msgid "Specific Image (128)" +msgstr "Immagine specifica (128)" + +#. module: fs_product_brand_multi_image +#: model:ir.model.fields,field_description:fs_product_brand_multi_image.field_fs_product_brand_image__tag_id +msgid "tag" +msgstr "etichetta" + +#~ msgid "Image" +#~ msgstr "Immagine" + +#~ msgid "Image 128" +#~ msgstr "Immagine 128" + +#~ msgid "Specific Image 128" +#~ msgstr "Immagine specifica 128" diff --git a/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/models/__init__.py b/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/models/__init__.py new file mode 100644 index 0000000..78b1d43 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/models/__init__.py @@ -0,0 +1,3 @@ +from . import fs_product_brand_image +from . import product_brand +from . import image_tag diff --git a/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/models/fs_product_brand_image.py b/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/models/fs_product_brand_image.py new file mode 100644 index 0000000..094cd08 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/models/fs_product_brand_image.py @@ -0,0 +1,21 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class FsProductBrandImage(models.Model): + _name = "fs.product.brand.image" + _inherit = "fs.image.relation.mixin" + _description = "Product Brand Image" + + brand_id = fields.Many2one( + "product.brand", + required=True, + ondelete="cascade", + ) + tag_id = fields.Many2one( + "image.tag", + string="tag", + domain=[("apply_on", "=", "brand")], + ) diff --git a/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/models/image_tag.py b/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/models/image_tag.py new file mode 100644 index 0000000..7a2cd37 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/models/image_tag.py @@ -0,0 +1,23 @@ +# Copyright 2021 Akretion (https://www.akretion.com). +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + + +from odoo import api, fields, models + + +class ImageTag(models.Model): + _inherit = "image.tag" + + @api.model + def _get_default_apply_on(self): + active_model = self.env.context.get("active_model") + if active_model == "product.brand.image.relation": + return "brand" + else: + return super()._get_default_apply_on() + + apply_on = fields.Selection( + selection_add=[("brand", "Brand")], + ondelete={"brand": "cascade"}, + ) diff --git a/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/models/product_brand.py b/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/models/product_brand.py new file mode 100644 index 0000000..6a9686c --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/models/product_brand.py @@ -0,0 +1,20 @@ +# Copyright 2020 ACSONE SA/NV +# Copyright 2021 Camptocamp (http://www.camptocamp.com). +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + +from odoo.addons.fs_image.fields import FSImage + + +class ProductBrand(models.Model): + _inherit = "product.brand" + + image_ids = fields.One2many( + string="Images", + comodel_name="fs.product.brand.image", + inverse_name="brand_id", + ) + image = FSImage(related="image_ids.image", readonly=True, store=False) + image_medium = FSImage(related="image_ids.image_medium", readonly=True, store=False) diff --git a/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/readme/CONTRIBUTORS.rst b/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000..12f7d30 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/readme/CONTRIBUTORS.rst @@ -0,0 +1,7 @@ +* Sébastien Beau +* Quentin Groulard +* `Camptocamp `_ + + * Iván Todorovich + +* Laurent Mignon diff --git a/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/readme/DESCRIPTION.rst b/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/readme/DESCRIPTION.rst new file mode 100644 index 0000000..76e6915 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/readme/DESCRIPTION.rst @@ -0,0 +1,3 @@ +Attach images stored into an external filesystem to product brands + +This addon is a drop-in replacement for the **storage_image_product_brand** addon. diff --git a/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/readme/USAGE.rst b/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/readme/USAGE.rst new file mode 100644 index 0000000..2d71d16 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/readme/USAGE.rst @@ -0,0 +1,2 @@ +Go to Sales > Configuration > Products > Product Brands. +A new field Images is available to upload or use existing images. diff --git a/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/security/fs_product_brand_image.xml b/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/security/fs_product_brand_image.xml new file mode 100644 index 0000000..72358a9 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/security/fs_product_brand_image.xml @@ -0,0 +1,23 @@ + + + + + fs.product.brand.image access read + + + + + + + + + fs.product.brand.image access edit + + + + + + + + diff --git a/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/static/description/icon.png b/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/static/description/icon.png new file mode 100644 index 0000000..3a0328b Binary files /dev/null and b/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/static/description/icon.png differ diff --git a/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/static/description/index.html b/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/static/description/index.html new file mode 100644 index 0000000..7d17a08 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/static/description/index.html @@ -0,0 +1,442 @@ + + + + + + +Fs Product Brand Multi Image + + + +
+

Fs Product Brand Multi Image

+ + +

Alpha License: AGPL-3 OCA/storage Translate me on Weblate Try me on Runboat

+

Attach images stored into an external filesystem to product brands

+

This addon is a drop-in replacement for the storage_image_product_brand addon.

+
+

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

+
+

Table of contents

+ +
+

Usage

+

Go to Sales > Configuration > Products > Product Brands. +A new field Images is available to upload or use existing images.

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub 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.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • ACSONE SA/NV
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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.

+

Current maintainer:

+

lmignon

+

This module is part of the OCA/storage project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/views/fs_product_brand_image.xml b/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/views/fs_product_brand_image.xml new file mode 100644 index 0000000..6c6c003 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/views/fs_product_brand_image.xml @@ -0,0 +1,22 @@ + + + + + product.brand.fs.image.form + fs.product.brand.image + + primary + + + + + + + diff --git a/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/views/product_brand.xml b/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/views/product_brand.xml new file mode 100644 index 0000000..5c17c7c --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_brand_multi_image/fs_product_brand_multi_image/views/product_brand.xml @@ -0,0 +1,50 @@ + + + + + product.brand + + + + 1 + + + + + + + + + + + + + + + + + + + product.brand + + + + kanban_image('product.brand', 'image_medium', record.id.raw_value) + + + + diff --git a/odoo-bringout-oca-storage-fs_product_brand_multi_image/pyproject.toml b/odoo-bringout-oca-storage-fs_product_brand_multi_image/pyproject.toml new file mode 100644 index 0000000..31fefec --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_brand_multi_image/pyproject.toml @@ -0,0 +1,46 @@ +[project] +name = "odoo-bringout-oca-storage-fs_product_brand_multi_image" +version = "16.0.0" +description = "Fs Product Brand Multi Image - + Link images to product brands" +authors = [ + { name = "Ernad Husremovic", email = "hernad@bring.out.ba" } +] +dependencies = [ + "odoo-bringout-oca-storage-fs_base_multi_image>=16.0.0", + "odoo-bringout-oca-storage-product_brand>=16.0.0", + "odoo-bringout-oca-ocb-sales_team>=16.0.0", + "odoo-bringout-oca-storage-image_tag>=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_product_brand_multi_image"] + +[tool.rye] +managed = true +dev-dependencies = [ + "pytest>=8.4.1", +] diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/README.md b/odoo-bringout-oca-storage-fs_product_multi_image/README.md new file mode 100644 index 0000000..de290e7 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/README.md @@ -0,0 +1,49 @@ +# Fs Product Multi Image + +Odoo addon: fs_product_multi_image + +## Installation + +```bash +pip install odoo-bringout-oca-storage-fs_product_multi_image +``` + +## Dependencies + +This addon depends on: +- fs_base_multi_image +- product +- sales_team +- image_tag + +## Manifest Information + +- **Name**: Fs Product Multi Image +- **Version**: 16.0.1.1.5 +- **Category**: N/A +- **License**: AGPL-3 +- **Installable**: False + +## Source + +Based on [OCA/storage](https://github.com/OCA/storage) branch 16.0, addon `fs_product_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 diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/doc/ARCHITECTURE.md b/odoo-bringout-oca-storage-fs_product_multi_image/doc/ARCHITECTURE.md new file mode 100644 index 0000000..243ae4d --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/doc/ARCHITECTURE.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_product_multi_image Module - fs_product_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. diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/doc/CONFIGURATION.md b/odoo-bringout-oca-storage-fs_product_multi_image/doc/CONFIGURATION.md new file mode 100644 index 0000000..fee0813 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/doc/CONFIGURATION.md @@ -0,0 +1,3 @@ +# Configuration + +Refer to Odoo settings for fs_product_multi_image. Configure related models, access rights, and options as needed. diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/doc/CONTROLLERS.md b/odoo-bringout-oca-storage-fs_product_multi_image/doc/CONTROLLERS.md new file mode 100644 index 0000000..f628e77 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/doc/CONTROLLERS.md @@ -0,0 +1,3 @@ +# Controllers + +This module does not define custom HTTP controllers. diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/doc/DEPENDENCIES.md b/odoo-bringout-oca-storage-fs_product_multi_image/doc/DEPENDENCIES.md new file mode 100644 index 0000000..3695ef2 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/doc/DEPENDENCIES.md @@ -0,0 +1,8 @@ +# Dependencies + +This addon depends on: + +- [fs_base_multi_image](../../odoo-bringout-oca-storage-fs_base_multi_image) +- [product](../../odoo-bringout-oca-ocb-product) +- [sales_team](../../odoo-bringout-oca-ocb-sales_team) +- [image_tag](../../odoo-bringout-oca-storage-image_tag) diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/doc/FAQ.md b/odoo-bringout-oca-storage-fs_product_multi_image/doc/FAQ.md new file mode 100644 index 0000000..34d574d --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/doc/FAQ.md @@ -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_product_multi_image or install in UI. diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/doc/INSTALL.md b/odoo-bringout-oca-storage-fs_product_multi_image/doc/INSTALL.md new file mode 100644 index 0000000..f6a5839 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/doc/INSTALL.md @@ -0,0 +1,7 @@ +# Install + +```bash +pip install odoo-bringout-oca-storage-fs_product_multi_image" +# or +uv pip install odoo-bringout-oca-storage-fs_product_multi_image" +``` diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/doc/MODELS.md b/odoo-bringout-oca-storage-fs_product_multi_image/doc/MODELS.md new file mode 100644 index 0000000..540bb48 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/doc/MODELS.md @@ -0,0 +1,19 @@ +# Models + +Detected core models and extensions in fs_product_multi_image. + +```mermaid +classDiagram + class fs_product_category_image + class fs_product_image + class fs_image_relation_mixin + class image_tag + class product_category + class product_product + class product_template + class product_template_attribute_line +``` + +Notes +- Classes show model technical names; fields omitted for brevity. +- Items listed under _inherit are extensions of existing models. diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/doc/OVERVIEW.md b/odoo-bringout-oca-storage-fs_product_multi_image/doc/OVERVIEW.md new file mode 100644 index 0000000..750c7b0 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/doc/OVERVIEW.md @@ -0,0 +1,6 @@ +# Overview + +Packaged Odoo addon: fs_product_multi_image. Provides features documented in upstream Odoo 16 under this addon. + +- Source: OCA/OCB 16.0, addon fs_product_multi_image +- License: LGPL-3 diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/doc/REPORTS.md b/odoo-bringout-oca-storage-fs_product_multi_image/doc/REPORTS.md new file mode 100644 index 0000000..e0ea35f --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/doc/REPORTS.md @@ -0,0 +1,3 @@ +# Reports + +This module does not define custom reports. diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/doc/SECURITY.md b/odoo-bringout-oca-storage-fs_product_multi_image/doc/SECURITY.md new file mode 100644 index 0000000..7459757 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/doc/SECURITY.md @@ -0,0 +1,76 @@ +# Security + +Access control and security definitions in fs_product_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_product_category_image.xml](../fs_product_multi_image/security/fs_product_category_image.xml)** +- **[fs_product_image.xml](../fs_product_multi_image/security/fs_product_image.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_product_category_image.xml](../fs_product_multi_image/security/fs_product_category_image.xml)** + - Security groups, categories, and XML-based rules +- **[fs_product_image.xml](../fs_product_multi_image/security/fs_product_image.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 diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/doc/TROUBLESHOOTING.md b/odoo-bringout-oca-storage-fs_product_multi_image/doc/TROUBLESHOOTING.md new file mode 100644 index 0000000..56853cb --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/doc/TROUBLESHOOTING.md @@ -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. diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/doc/USAGE.md b/odoo-bringout-oca-storage-fs_product_multi_image/doc/USAGE.md new file mode 100644 index 0000000..841553d --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/doc/USAGE.md @@ -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_product_multi_image +``` diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/doc/WIZARDS.md b/odoo-bringout-oca-storage-fs_product_multi_image/doc/WIZARDS.md new file mode 100644 index 0000000..48e790d --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/doc/WIZARDS.md @@ -0,0 +1,3 @@ +# Wizards + +This module does not include UI wizards. diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/README.rst b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/README.rst new file mode 100644 index 0000000..0e243cc --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/README.rst @@ -0,0 +1,126 @@ +====================== +Fs Product Multi Image +====================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:887ff161ea4c49e5a7b35f6e290c1e40512ad482dda3e2adb784bfea8db7c1cd + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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_product_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_product_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| + +Attach images to products and categories and store them on an external +filesystem instead of the database. + +This addon is a drop-in replacement for the **storage_image_product** addon. + +.. 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 `_ + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +On the category and product form, a new tab allows you to add images to the +related object. The images can be specific to the model or you can use an +existing one. + +On the link forms, you can add an image tag in addition to the image. In +the specific case of the product template, you can also specify for which +variant attribute values the image is valid. + +On the product variant form, the image tag will be automatically filled whith +the image tag of the product template for the same variant attribute values. + +In every case, a main image is computed and used as the default image for the +object. It depends on the sequence of the images (first one is the main one). + +Changelog +========= + +16.0.1.0.2 (2023-10-04) +~~~~~~~~~~~~~~~~~~~~~~~ + +**Bugfixes** + +- Ensures the variant_image_ids are sorted by sequence and name. Before this + change, the order was random and could change between runs. (`#282 `_) + +Bug Tracker +=========== + +Bugs are tracked on `GitHub 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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* ACSONE SA/NV + +Contributors +~~~~~~~~~~~~ + +* Laurent Mignon +* Raphaël Reverdy +* Denis Roussel +* Quentin Groulard +* `Camptocamp `_ + + * Iván Todorovich + +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 `__: + +|maintainer-lmignon| + +This module is part of the `OCA/storage `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/__init__.py b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/__manifest__.py b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/__manifest__.py new file mode 100644 index 0000000..2741284 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/__manifest__.py @@ -0,0 +1,26 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Fs Product Multi Image", + "summary": """ + Manage multi images from extenal file system on product""", + "version": "16.0.1.1.5", + "license": "AGPL-3", + "author": "ACSONE SA/NV,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/storage", + "depends": ["fs_base_multi_image", "product", "sales_team", "image_tag"], + "data": [ + "security/fs_product_category_image.xml", + "security/fs_product_image.xml", + "views/fs_product_category_image.xml", + "views/fs_product_image.xml", + "views/image_tag.xml", + "views/product_category.xml", + "views/product_product.xml", + "views/product_template.xml", + ], + "demo": [], + "maintainers": ["lmignon"], + "development_status": "Alpha", +} diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/i18n/bs.po b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/i18n/bs.po new file mode 100644 index 0000000..168104a --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/i18n/bs.po @@ -0,0 +1,226 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_product_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_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_image_tag__apply_on +msgid "Apply On" +msgstr "Primjeni na" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__attribute_value_ids +msgid "Attributes" +msgstr "Atributi" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__available_attribute_value_ids +msgid "Available Attributes" +msgstr "Dostupni atributi" + +#. module: fs_product_multi_image +#: model:ir.model.fields.selection,name:fs_product_multi_image.selection__image_tag__apply_on__category +msgid "Category" +msgstr "Kategorija" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__create_uid +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__create_uid +msgid "Created by" +msgstr "Kreirao" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__create_date +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__create_date +msgid "Created on" +msgstr "Kreirano" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__display_name +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__display_name +msgid "Display Name" +msgstr "Prikazani naziv" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_product__image_medium +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_template__image_medium +msgid "FS Image Medium" +msgstr "FS srednja slika" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_product__image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_template__image +msgid "FS Main Image" +msgstr "FS glavna slika" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__id +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__id +msgid "ID" +msgstr "ID" + +#. module: fs_product_multi_image +#: model_terms:ir.ui.view,arch_db:fs_product_multi_image.product_normal_form_view +msgid "If you need to edit the images, do it from the product template." +msgstr "Ako trebate uređivati slike, radite to iz predloška proizvoda." + +#. module: fs_product_multi_image +#: model_terms:ir.ui.view,arch_db:fs_product_multi_image.fs_product_category_image_kanban_view +#: model_terms:ir.ui.view,arch_db:fs_product_multi_image.fs_product_image_kanban_view +#: model_terms:ir.ui.view,arch_db:fs_product_multi_image.product_template_only_form_view +msgid "Image" +msgstr "Slika" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__image_medium +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__image_medium +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_category__image_medium +msgid "Image (128)" +msgstr "Slika (128)" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_category__image +msgid "Image (original)" +msgstr "Slika (originalna)" + +#. module: fs_product_multi_image +#: model:ir.model,name:fs_product_multi_image.model_image_tag +#: model:ir.ui.menu,name:fs_product_multi_image.menu_image_tag +msgid "Image Tag" +msgstr "Oznaka slike" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_category__image_ids +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_product__image_ids +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_template__image_ids +#: model_terms:ir.ui.view,arch_db:fs_product_multi_image.product_category_form_view +#: model_terms:ir.ui.view,arch_db:fs_product_multi_image.product_normal_form_view +#: model_terms:ir.ui.view,arch_db:fs_product_multi_image.product_template_only_form_view +msgid "Images" +msgstr "Slike" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image____last_update +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image____last_update +msgid "Last Modified on" +msgstr "Zadnje mijenjano" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__write_uid +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__write_uid +msgid "Last Updated by" +msgstr "Zadnji ažurirao" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__write_date +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__write_date +msgid "Last Updated on" +msgstr "Zadnje ažurirano" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__link_existing +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__link_existing +msgid "Link Existing" +msgstr "Povezivanje postojećeg" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__image_id +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__image_id +msgid "Linked image" +msgstr "Povezana slika" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_product__main_image_id +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_template__main_image_id +msgid "Main Image" +msgstr "Glavna slika" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__mimetype +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__mimetype +msgid "Mimetype" +msgstr "Mimetype" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__name +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__name +msgid "Name" +msgstr "Naziv:" + +#. module: fs_product_multi_image +#: model:ir.model,name:fs_product_multi_image.model_product_template +#: model:ir.model.fields.selection,name:fs_product_multi_image.selection__image_tag__apply_on__product +msgid "Product" +msgstr "Artikal" + +#. module: fs_product_multi_image +#: model:ir.model,name:fs_product_multi_image.model_product_category +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__product_categ_id +msgid "Product Category" +msgstr "Kategorija proizvoda" + +#. module: fs_product_multi_image +#: model:ir.model,name:fs_product_multi_image.model_fs_product_category_image +msgid "Product Category Image" +msgstr "Slika kategorije proizvoda" + +#. module: fs_product_multi_image +#: model:ir.model,name:fs_product_multi_image.model_fs_product_image +msgid "Product Image" +msgstr "Slika proizvoda" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__product_tmpl_id +msgid "Product Template" +msgstr "Predložak artikla" + +#. module: fs_product_multi_image +#: model:ir.model,name:fs_product_multi_image.model_product_template_attribute_line +msgid "Product Template Attribute Line" +msgstr "Predloška artikla stavka atributa" + +#. module: fs_product_multi_image +#: model:ir.model,name:fs_product_multi_image.model_product_product +msgid "Product Variant" +msgstr "Varijanta proizvoda" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__sequence +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__sequence +msgid "Sequence" +msgstr "Sekvenca" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__specific_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__specific_image +msgid "Specific Image" +msgstr "Specifična slika" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__specific_image_medium +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__specific_image_medium +msgid "Specific Image (128)" +msgstr "Specifična slika (128)" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__tag_id +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__tag_id +msgid "Tag" +msgstr "Oznaka" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_product__variant_image_ids +msgid "Variant Images" +msgstr "Slike varijanti" diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/i18n/es.po b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/i18n/es.po new file mode 100644 index 0000000..3351d15 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/i18n/es.po @@ -0,0 +1,236 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_product_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 \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_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_image_tag__apply_on +msgid "Apply On" +msgstr "Aplicar en" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__attribute_value_ids +msgid "Attributes" +msgstr "Atributos" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__available_attribute_value_ids +msgid "Available Attributes" +msgstr "Atributos Disponibles" + +#. module: fs_product_multi_image +#: model:ir.model.fields.selection,name:fs_product_multi_image.selection__image_tag__apply_on__category +msgid "Category" +msgstr "Categoría" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__create_uid +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__create_date +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__create_date +msgid "Created on" +msgstr "Creado el" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__display_name +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__display_name +msgid "Display Name" +msgstr "Mostrar Nombre" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_product__image_medium +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_template__image_medium +msgid "FS Image Medium" +msgstr "" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_product__image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_template__image +msgid "FS Main Image" +msgstr "" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__id +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__id +msgid "ID" +msgstr "ID (identificación)" + +#. module: fs_product_multi_image +#: model_terms:ir.ui.view,arch_db:fs_product_multi_image.product_normal_form_view +msgid "If you need to edit the images, do it from the product template." +msgstr "" +"Si necesita editar las imágenes, hágalo desde la plantilla del producto." + +#. module: fs_product_multi_image +#: model_terms:ir.ui.view,arch_db:fs_product_multi_image.fs_product_category_image_kanban_view +#: model_terms:ir.ui.view,arch_db:fs_product_multi_image.fs_product_image_kanban_view +#: model_terms:ir.ui.view,arch_db:fs_product_multi_image.product_template_only_form_view +msgid "Image" +msgstr "Imagen" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__image_medium +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__image_medium +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_category__image_medium +msgid "Image (128)" +msgstr "Imagen (128)" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_category__image +msgid "Image (original)" +msgstr "Imagen (original)" + +#. module: fs_product_multi_image +#: model:ir.model,name:fs_product_multi_image.model_image_tag +#: model:ir.ui.menu,name:fs_product_multi_image.menu_image_tag +msgid "Image Tag" +msgstr "Etiqueta de la Imagen" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_category__image_ids +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_product__image_ids +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_template__image_ids +#: model_terms:ir.ui.view,arch_db:fs_product_multi_image.product_category_form_view +#: model_terms:ir.ui.view,arch_db:fs_product_multi_image.product_normal_form_view +#: model_terms:ir.ui.view,arch_db:fs_product_multi_image.product_template_only_form_view +msgid "Images" +msgstr "Imágenes" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image____last_update +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image____last_update +msgid "Last Modified on" +msgstr "Última Modificación el" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__write_uid +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__write_uid +msgid "Last Updated by" +msgstr "Actualizado por Última vez por" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__write_date +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__write_date +msgid "Last Updated on" +msgstr "Última Actualización el" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__link_existing +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__link_existing +msgid "Link Existing" +msgstr "Enlace Existente" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__image_id +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__image_id +msgid "Linked image" +msgstr "Imagen vinculada" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_product__main_image_id +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_template__main_image_id +msgid "Main Image" +msgstr "Imagen Principal" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__mimetype +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__mimetype +msgid "Mimetype" +msgstr "Tipo Mimo" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__name +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__name +msgid "Name" +msgstr "Nombre" + +#. module: fs_product_multi_image +#: model:ir.model,name:fs_product_multi_image.model_product_template +#: model:ir.model.fields.selection,name:fs_product_multi_image.selection__image_tag__apply_on__product +msgid "Product" +msgstr "Producto" + +#. module: fs_product_multi_image +#: model:ir.model,name:fs_product_multi_image.model_product_category +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__product_categ_id +msgid "Product Category" +msgstr "Categoría de Producto" + +#. module: fs_product_multi_image +#: model:ir.model,name:fs_product_multi_image.model_fs_product_category_image +msgid "Product Category Image" +msgstr "Imagen de Categoría de Producto" + +#. module: fs_product_multi_image +#: model:ir.model,name:fs_product_multi_image.model_fs_product_image +msgid "Product Image" +msgstr "Imagen del Producto" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__product_tmpl_id +msgid "Product Template" +msgstr "Plantilla del Producto" + +#. module: fs_product_multi_image +#: model:ir.model,name:fs_product_multi_image.model_product_template_attribute_line +msgid "Product Template Attribute Line" +msgstr "Plantilla de Línea de Atributo de Producto" + +#. module: fs_product_multi_image +#: model:ir.model,name:fs_product_multi_image.model_product_product +msgid "Product Variant" +msgstr "Variante del Producto" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__sequence +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__sequence +msgid "Sequence" +msgstr "Secuencia" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__specific_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__specific_image +msgid "Specific Image" +msgstr "Imagen Específica" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__specific_image_medium +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__specific_image_medium +msgid "Specific Image (128)" +msgstr "Imagen Específica (128)" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__tag_id +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__tag_id +msgid "Tag" +msgstr "Etiqueta" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_product__variant_image_ids +msgid "Variant Images" +msgstr "Imágenes de Variante" + +#~ msgid "Image 128" +#~ msgstr "Imagen 128" + +#~ msgid "Specific Image 128" +#~ msgstr "Imagen Específica 128" diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/i18n/fr.po b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/i18n/fr.po new file mode 100644 index 0000000..47c6c3a --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/i18n/fr.po @@ -0,0 +1,229 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_product_multi_image +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-02-19 10:34+0000\n" +"Last-Translator: \"Benjamin Willig (ACSONE)\" \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 4.17\n" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_image_tag__apply_on +msgid "Apply On" +msgstr "Appliqué à" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__attribute_value_ids +msgid "Attributes" +msgstr "Attributs" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__available_attribute_value_ids +msgid "Available Attributes" +msgstr "Attributs disponibles" + +#. module: fs_product_multi_image +#: model:ir.model.fields.selection,name:fs_product_multi_image.selection__image_tag__apply_on__category +msgid "Category" +msgstr "Catégorie" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__create_uid +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__create_uid +msgid "Created by" +msgstr "Créé par" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__create_date +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__create_date +msgid "Created on" +msgstr "Créé le" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__display_name +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__display_name +msgid "Display Name" +msgstr "Nom Affiché" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_product__image_medium +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_template__image_medium +msgid "FS Image Medium" +msgstr "" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_product__image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_template__image +msgid "FS Main Image" +msgstr "" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__id +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__id +msgid "ID" +msgstr "" + +#. module: fs_product_multi_image +#: model_terms:ir.ui.view,arch_db:fs_product_multi_image.product_normal_form_view +msgid "If you need to edit the images, do it from the product template." +msgstr "Pour éditer l'image, rendez vous sur la fiche template de produit." + +#. module: fs_product_multi_image +#: model_terms:ir.ui.view,arch_db:fs_product_multi_image.fs_product_category_image_kanban_view +#: model_terms:ir.ui.view,arch_db:fs_product_multi_image.fs_product_image_kanban_view +#: model_terms:ir.ui.view,arch_db:fs_product_multi_image.product_template_only_form_view +msgid "Image" +msgstr "Image" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__image_medium +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__image_medium +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_category__image_medium +msgid "Image (128)" +msgstr "Image (128)" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_category__image +msgid "Image (original)" +msgstr "Image (originale)" + +#. module: fs_product_multi_image +#: model:ir.model,name:fs_product_multi_image.model_image_tag +#: model:ir.ui.menu,name:fs_product_multi_image.menu_image_tag +msgid "Image Tag" +msgstr "Tag d'image" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_category__image_ids +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_product__image_ids +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_template__image_ids +#: model_terms:ir.ui.view,arch_db:fs_product_multi_image.product_category_form_view +#: model_terms:ir.ui.view,arch_db:fs_product_multi_image.product_normal_form_view +#: model_terms:ir.ui.view,arch_db:fs_product_multi_image.product_template_only_form_view +msgid "Images" +msgstr "Images" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image____last_update +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image____last_update +msgid "Last Modified on" +msgstr "" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__write_uid +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__write_date +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__write_date +msgid "Last Updated on" +msgstr "" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__link_existing +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__link_existing +msgid "Link Existing" +msgstr "Lier une image existante" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__image_id +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__image_id +msgid "Linked image" +msgstr "Image liée" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_product__main_image_id +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_template__main_image_id +msgid "Main Image" +msgstr "Image principale" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__mimetype +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__mimetype +msgid "Mimetype" +msgstr "" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__name +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__name +msgid "Name" +msgstr "Nom" + +#. module: fs_product_multi_image +#: model:ir.model,name:fs_product_multi_image.model_product_template +#: model:ir.model.fields.selection,name:fs_product_multi_image.selection__image_tag__apply_on__product +msgid "Product" +msgstr "Produit" + +#. module: fs_product_multi_image +#: model:ir.model,name:fs_product_multi_image.model_product_category +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__product_categ_id +msgid "Product Category" +msgstr "Catégorie de produit" + +#. module: fs_product_multi_image +#: model:ir.model,name:fs_product_multi_image.model_fs_product_category_image +msgid "Product Category Image" +msgstr "Catégorie d'image de produit" + +#. module: fs_product_multi_image +#: model:ir.model,name:fs_product_multi_image.model_fs_product_image +msgid "Product Image" +msgstr "Image produit" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__product_tmpl_id +msgid "Product Template" +msgstr "Template d'article" + +#. module: fs_product_multi_image +#: model:ir.model,name:fs_product_multi_image.model_product_template_attribute_line +msgid "Product Template Attribute Line" +msgstr "" + +#. module: fs_product_multi_image +#: model:ir.model,name:fs_product_multi_image.model_product_product +msgid "Product Variant" +msgstr "" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__sequence +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__sequence +msgid "Sequence" +msgstr "Séquence" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__specific_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__specific_image +msgid "Specific Image" +msgstr "Image spécifique" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__specific_image_medium +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__specific_image_medium +msgid "Specific Image (128)" +msgstr "Image spécifique (128)" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__tag_id +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__tag_id +msgid "Tag" +msgstr "Tag" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_product__variant_image_ids +msgid "Variant Images" +msgstr "" diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/i18n/fs_product_multi_image.pot b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/i18n/fs_product_multi_image.pot new file mode 100644 index 0000000..75be908 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/i18n/fs_product_multi_image.pot @@ -0,0 +1,226 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_product_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_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_image_tag__apply_on +msgid "Apply On" +msgstr "" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__attribute_value_ids +msgid "Attributes" +msgstr "" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__available_attribute_value_ids +msgid "Available Attributes" +msgstr "" + +#. module: fs_product_multi_image +#: model:ir.model.fields.selection,name:fs_product_multi_image.selection__image_tag__apply_on__category +msgid "Category" +msgstr "" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__create_uid +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__create_uid +msgid "Created by" +msgstr "" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__create_date +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__create_date +msgid "Created on" +msgstr "" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__display_name +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__display_name +msgid "Display Name" +msgstr "" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_product__image_medium +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_template__image_medium +msgid "FS Image Medium" +msgstr "" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_product__image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_template__image +msgid "FS Main Image" +msgstr "" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__id +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__id +msgid "ID" +msgstr "" + +#. module: fs_product_multi_image +#: model_terms:ir.ui.view,arch_db:fs_product_multi_image.product_normal_form_view +msgid "If you need to edit the images, do it from the product template." +msgstr "" + +#. module: fs_product_multi_image +#: model_terms:ir.ui.view,arch_db:fs_product_multi_image.fs_product_category_image_kanban_view +#: model_terms:ir.ui.view,arch_db:fs_product_multi_image.fs_product_image_kanban_view +#: model_terms:ir.ui.view,arch_db:fs_product_multi_image.product_template_only_form_view +msgid "Image" +msgstr "" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__image_medium +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__image_medium +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_category__image_medium +msgid "Image (128)" +msgstr "" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_category__image +msgid "Image (original)" +msgstr "" + +#. module: fs_product_multi_image +#: model:ir.model,name:fs_product_multi_image.model_image_tag +#: model:ir.ui.menu,name:fs_product_multi_image.menu_image_tag +msgid "Image Tag" +msgstr "" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_category__image_ids +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_product__image_ids +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_template__image_ids +#: model_terms:ir.ui.view,arch_db:fs_product_multi_image.product_category_form_view +#: model_terms:ir.ui.view,arch_db:fs_product_multi_image.product_normal_form_view +#: model_terms:ir.ui.view,arch_db:fs_product_multi_image.product_template_only_form_view +msgid "Images" +msgstr "" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image____last_update +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image____last_update +msgid "Last Modified on" +msgstr "" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__write_uid +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__write_date +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__write_date +msgid "Last Updated on" +msgstr "" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__link_existing +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__link_existing +msgid "Link Existing" +msgstr "" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__image_id +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__image_id +msgid "Linked image" +msgstr "" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_product__main_image_id +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_template__main_image_id +msgid "Main Image" +msgstr "" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__mimetype +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__mimetype +msgid "Mimetype" +msgstr "" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__name +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__name +msgid "Name" +msgstr "" + +#. module: fs_product_multi_image +#: model:ir.model,name:fs_product_multi_image.model_product_template +#: model:ir.model.fields.selection,name:fs_product_multi_image.selection__image_tag__apply_on__product +msgid "Product" +msgstr "" + +#. module: fs_product_multi_image +#: model:ir.model,name:fs_product_multi_image.model_product_category +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__product_categ_id +msgid "Product Category" +msgstr "" + +#. module: fs_product_multi_image +#: model:ir.model,name:fs_product_multi_image.model_fs_product_category_image +msgid "Product Category Image" +msgstr "" + +#. module: fs_product_multi_image +#: model:ir.model,name:fs_product_multi_image.model_fs_product_image +msgid "Product Image" +msgstr "" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__product_tmpl_id +msgid "Product Template" +msgstr "" + +#. module: fs_product_multi_image +#: model:ir.model,name:fs_product_multi_image.model_product_template_attribute_line +msgid "Product Template Attribute Line" +msgstr "" + +#. module: fs_product_multi_image +#: model:ir.model,name:fs_product_multi_image.model_product_product +msgid "Product Variant" +msgstr "" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__sequence +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__sequence +msgid "Sequence" +msgstr "" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__specific_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__specific_image +msgid "Specific Image" +msgstr "" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__specific_image_medium +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__specific_image_medium +msgid "Specific Image (128)" +msgstr "" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__tag_id +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__tag_id +msgid "Tag" +msgstr "" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_product__variant_image_ids +msgid "Variant Images" +msgstr "" diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/i18n/it.po b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/i18n/it.po new file mode 100644 index 0000000..46bb05f --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/i18n/it.po @@ -0,0 +1,235 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_product_multi_image +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2025-04-02 09:06+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.10.2\n" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_image_tag__apply_on +msgid "Apply On" +msgstr "Applica a" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__attribute_value_ids +msgid "Attributes" +msgstr "Attributi" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__available_attribute_value_ids +msgid "Available Attributes" +msgstr "Attributi disponibili" + +#. module: fs_product_multi_image +#: model:ir.model.fields.selection,name:fs_product_multi_image.selection__image_tag__apply_on__category +msgid "Category" +msgstr "Categoria" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__create_uid +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__create_uid +msgid "Created by" +msgstr "Creato da" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__create_date +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__create_date +msgid "Created on" +msgstr "Creato il" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__display_name +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__display_name +msgid "Display Name" +msgstr "Nome visualizzato" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_product__image_medium +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_template__image_medium +msgid "FS Image Medium" +msgstr "Immagine FS media" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_product__image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_template__image +msgid "FS Main Image" +msgstr "Immagine principale FS" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__id +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__id +msgid "ID" +msgstr "ID" + +#. module: fs_product_multi_image +#: model_terms:ir.ui.view,arch_db:fs_product_multi_image.product_normal_form_view +msgid "If you need to edit the images, do it from the product template." +msgstr "Se è necessario modificare le immagini, farlo dal modello prodotto." + +#. module: fs_product_multi_image +#: model_terms:ir.ui.view,arch_db:fs_product_multi_image.fs_product_category_image_kanban_view +#: model_terms:ir.ui.view,arch_db:fs_product_multi_image.fs_product_image_kanban_view +#: model_terms:ir.ui.view,arch_db:fs_product_multi_image.product_template_only_form_view +msgid "Image" +msgstr "Immagine" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__image_medium +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__image_medium +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_category__image_medium +msgid "Image (128)" +msgstr "Immagine 128" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_category__image +msgid "Image (original)" +msgstr "Immagine (originale)" + +#. module: fs_product_multi_image +#: model:ir.model,name:fs_product_multi_image.model_image_tag +#: model:ir.ui.menu,name:fs_product_multi_image.menu_image_tag +msgid "Image Tag" +msgstr "Etichetta immagine" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_category__image_ids +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_product__image_ids +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_template__image_ids +#: model_terms:ir.ui.view,arch_db:fs_product_multi_image.product_category_form_view +#: model_terms:ir.ui.view,arch_db:fs_product_multi_image.product_normal_form_view +#: model_terms:ir.ui.view,arch_db:fs_product_multi_image.product_template_only_form_view +msgid "Images" +msgstr "Immagini" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image____last_update +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image____last_update +msgid "Last Modified on" +msgstr "Ultima modifica il" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__write_uid +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__write_uid +msgid "Last Updated by" +msgstr "Ultimo aggiornamento di" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__write_date +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__write_date +msgid "Last Updated on" +msgstr "Ultimo aggiornamento il" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__link_existing +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__link_existing +msgid "Link Existing" +msgstr "Collegamento esistente" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__image_id +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__image_id +msgid "Linked image" +msgstr "Immagine collegata" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_product__main_image_id +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_template__main_image_id +msgid "Main Image" +msgstr "Immagine principale" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__mimetype +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__mimetype +msgid "Mimetype" +msgstr "Tipo MIME" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__name +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__name +msgid "Name" +msgstr "Nome" + +#. module: fs_product_multi_image +#: model:ir.model,name:fs_product_multi_image.model_product_template +#: model:ir.model.fields.selection,name:fs_product_multi_image.selection__image_tag__apply_on__product +msgid "Product" +msgstr "Prodotto" + +#. module: fs_product_multi_image +#: model:ir.model,name:fs_product_multi_image.model_product_category +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__product_categ_id +msgid "Product Category" +msgstr "Categoria prodotto" + +#. module: fs_product_multi_image +#: model:ir.model,name:fs_product_multi_image.model_fs_product_category_image +msgid "Product Category Image" +msgstr "Immagine categoria prodotto" + +#. module: fs_product_multi_image +#: model:ir.model,name:fs_product_multi_image.model_fs_product_image +msgid "Product Image" +msgstr "Immagine prodotto" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__product_tmpl_id +msgid "Product Template" +msgstr "Modello prodotto" + +#. module: fs_product_multi_image +#: model:ir.model,name:fs_product_multi_image.model_product_template_attribute_line +msgid "Product Template Attribute Line" +msgstr "Riga attributo modello prodotto" + +#. module: fs_product_multi_image +#: model:ir.model,name:fs_product_multi_image.model_product_product +msgid "Product Variant" +msgstr "Variante prodotto" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__sequence +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__sequence +msgid "Sequence" +msgstr "Sequenza" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__specific_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__specific_image +msgid "Specific Image" +msgstr "Immagine specifica" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__specific_image_medium +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__specific_image_medium +msgid "Specific Image (128)" +msgstr "Immagine specifica (128)" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_category_image__tag_id +#: model:ir.model.fields,field_description:fs_product_multi_image.field_fs_product_image__tag_id +msgid "Tag" +msgstr "Etichetta" + +#. module: fs_product_multi_image +#: model:ir.model.fields,field_description:fs_product_multi_image.field_product_product__variant_image_ids +msgid "Variant Images" +msgstr "Immagini varianti" + +#~ msgid "Image 128" +#~ msgstr "Immagine 128" + +#~ msgid "Specific Image 128" +#~ msgstr "Immagine specifica 128" diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/models/__init__.py b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/models/__init__.py new file mode 100644 index 0000000..17c75c0 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/models/__init__.py @@ -0,0 +1,7 @@ +from . import fs_product_category_image +from . import fs_product_image +from . import image_tag +from . import product_category +from . import product_template +from . import product_product +from . import product_template_attribute_line diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/models/fs_product_category_image.py b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/models/fs_product_category_image.py new file mode 100644 index 0000000..d0075cf --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/models/fs_product_category_image.py @@ -0,0 +1,24 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class FsProductCategoryImage(models.Model): + _name = "fs.product.category.image" + _inherit = "fs.image.relation.mixin" + _description = "Product Category Image" + + product_categ_id = fields.Many2one( + comodel_name="product.category", + string="Product Category", + ondelete="cascade", + index=True, + ) + + tag_id = fields.Many2one( + "image.tag", + string="Tag", + domain=[("apply_on", "=", "category")], + index=True, + ) diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/models/fs_product_image.py b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/models/fs_product_image.py new file mode 100644 index 0000000..e5584be --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/models/fs_product_image.py @@ -0,0 +1,50 @@ +# Copyright 2023 ACSONE SA/NV +# Copyright 2018 Akretion (http://www.akretion.com). +# @author Raphaël Reverdy +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class FsProductImage(models.Model): + _name = "fs.product.image" + _inherit = "fs.image.relation.mixin" + _description = "Product Image" + + product_tmpl_id = fields.Many2one( + comodel_name="product.template", + string="Product Template", + ondelete="cascade", + index=True, + ) + attribute_value_ids = fields.Many2many( + "product.attribute.value", + string="Attributes", + domain="[('id', 'in', available_attribute_value_ids)]", + ) + # This field will list all attribute value used by the template + # in order to filter the attribute value available for the current image + available_attribute_value_ids = fields.Many2many( + "product.attribute.value", + string="Available Attributes", + compute="_compute_available_attribute", + ) + tag_id = fields.Many2one( + "image.tag", + string="Tag", + domain=[("apply_on", "=", "product")], + index=True, + ) + + @api.depends("product_tmpl_id.attribute_line_ids.value_ids") + def _compute_available_attribute(self): + for rec in self: + rec.available_attribute_value_ids = rec.product_tmpl_id.mapped( + "attribute_line_ids.value_ids" + ) + + def _match_variant(self, variant): + variant_attribute_values = variant.mapped( + "product_template_attribute_value_ids.product_attribute_value_id" + ) + return not bool(self.attribute_value_ids - variant_attribute_values) diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/models/image_tag.py b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/models/image_tag.py new file mode 100644 index 0000000..e608eb2 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/models/image_tag.py @@ -0,0 +1,27 @@ +# Copyright 2023 ACSONE SA/NV +# Copyright 2018 Akretion (http://www.akretion.com). +# @author Raphaël Reverdy +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + + +from odoo import api, fields, models + + +class ImageTag(models.Model): + _inherit = "image.tag" + + @api.model + def _get_default_apply_on(self): + active_model = self.env.context.get("active_model") + return ( + "product" + if active_model == "product.image.relation" + else "category" + if active_model == "category.image.relation" + else super()._get_default_apply_on() + ) + + apply_on = fields.Selection( + selection_add=[("product", "Product"), ("category", "Category")], + ondelete={"product": "cascade", "category": "cascade"}, + ) diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/models/product_category.py b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/models/product_category.py new file mode 100644 index 0000000..e40c91a --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/models/product_category.py @@ -0,0 +1,19 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + +from odoo.addons.fs_image.fields import FSImage + + +class ProductCategory(models.Model): + + _inherit = "product.category" + + image_ids = fields.One2many( + string="Images", + comodel_name="fs.product.category.image", + inverse_name="product_categ_id", + ) + image = FSImage(related="image_ids.image", readonly=True, store=False) + image_medium = FSImage(related="image_ids.image_medium", readonly=True, store=False) diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/models/product_product.py b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/models/product_product.py new file mode 100644 index 0000000..fd87e9a --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/models/product_product.py @@ -0,0 +1,74 @@ +# Copyright 2017 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# Copyright 2021 Camptocamp SA (http://www.camptocamp.com) +# @author Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import api, fields, models + +from odoo.addons.fs_image.fields import FSImage + + +class ProductProduct(models.Model): + _inherit = "product.product" + + variant_image_ids = fields.Many2many( + "fs.product.image", + compute="_compute_variant_image_ids", + store=True, + string="Variant Images", + ) + main_image_id = fields.Many2one( + string="Main Image", + comodel_name="fs.product.image", + compute="_compute_main_image_id", + # Store it to improve perfs + store=True, + ) + image = FSImage( + string="FS Main Image", + related="main_image_id.image", + readonly=True, + store=False, + ) + image_medium = FSImage( + "FS Image Medium", + related="main_image_id.image_medium", + readonly=True, + store=False, + ) + + @api.depends( + "product_tmpl_id.image_ids", + "product_tmpl_id.image_ids.sequence", + "product_tmpl_id.image_ids.attribute_value_ids", + "product_template_attribute_value_ids", + ) + def _compute_variant_image_ids(self): + for variant in self: + variant_image_ids = variant.image_ids.filtered( + lambda i: i._match_variant(variant) + ) + variant_image_ids = variant_image_ids.sorted( + key=lambda i: (i.sequence, i.name or "") + ) + variant.variant_image_ids = variant_image_ids + + @api.depends("variant_image_ids", "variant_image_ids.sequence") + def _compute_main_image_id(self): + for record in self: + record.main_image_id = record._get_main_image() + + def _select_main_image(self, images): + return fields.first(images.sorted(key=lambda i: (i.sequence, i.id))).id + + def _get_main_image(self): + match_image = self.variant_image_ids.filtered( + lambda i: i.attribute_value_ids + == self.mapped( + "product_template_attribute_value_ids.product_attribute_value_id" + ) + ) + if match_image: + return self._select_main_image(match_image) + return self._select_main_image(self.variant_image_ids) diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/models/product_template.py b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/models/product_template.py new file mode 100644 index 0000000..3ed6c44 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/models/product_template.py @@ -0,0 +1,42 @@ +# 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.fields import FSImage + + +class ProductTemplate(models.Model): + + _inherit = "product.template" + + image_ids = fields.One2many( + string="Images", comodel_name="fs.product.image", inverse_name="product_tmpl_id" + ) + main_image_id = fields.Many2one( + string="Main Image", + comodel_name="fs.product.image", + compute="_compute_main_image_id", + # Store it to improve perfs + store=True, + ) + image = FSImage( + string="FS Main Image", + related="main_image_id.image", + readonly=True, + store=False, + ) + image_medium = FSImage( + string="FS Image Medium", + related="main_image_id.image_medium", + readonly=True, + store=False, + ) + + @api.depends("image_ids", "image_ids.sequence") + def _compute_main_image_id(self): + for record in self: + image_ids = record.image_ids.sorted( + key=lambda i: f"{i.sequence},{str(i.id)}" + ) + record.main_image_id = image_ids and image_ids[0] or None diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/models/product_template_attribute_line.py b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/models/product_template_attribute_line.py new file mode 100644 index 0000000..b6b6915 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/models/product_template_attribute_line.py @@ -0,0 +1,31 @@ +# Copyright 2023 ACSONE SA/NV +# Copyright 2017 Akretion (http://www.akretion.com). +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models + + +class ProductTemplateAttributeLine(models.Model): + + _inherit = "product.template.attribute.line" + + def write(self, values): + res = super().write(values) + if "value_ids" in values: + product_image_attribute_value_ids = self.product_tmpl_id.image_ids.mapped( + "attribute_value_ids" + ).filtered(lambda x: x.attribute_id == self.attribute_id) + available_attribute_values_ids = self.value_ids + to_remove = product_image_attribute_value_ids.filtered( + lambda x: x not in available_attribute_values_ids + ) + if to_remove: + for image in self.product_tmpl_id.image_ids: + image.attribute_value_ids -= to_remove + return res + + def unlink(self): + for line in self: + for image in line.product_tmpl_id.image_ids: + image.attribute_value_ids -= line.value_ids + return super().unlink() diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/readme/CONTRIBUTORS.rst b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000..33d0863 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/readme/CONTRIBUTORS.rst @@ -0,0 +1,7 @@ +* Laurent Mignon +* Raphaël Reverdy +* Denis Roussel +* Quentin Groulard +* `Camptocamp `_ + + * Iván Todorovich diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/readme/DESCRIPTION.rst b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/readme/DESCRIPTION.rst new file mode 100644 index 0000000..b1ae446 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/readme/DESCRIPTION.rst @@ -0,0 +1,4 @@ +Attach images to products and categories and store them on an external +filesystem instead of the database. + +This addon is a drop-in replacement for the **storage_image_product** addon. diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/readme/HISTORY.rst b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/readme/HISTORY.rst new file mode 100644 index 0000000..3e28f36 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/readme/HISTORY.rst @@ -0,0 +1,7 @@ +16.0.1.0.2 (2023-10-04) +~~~~~~~~~~~~~~~~~~~~~~~ + +**Bugfixes** + +- Ensures the variant_image_ids are sorted by sequence and name. Before this + change, the order was random and could change between runs. (`#282 `_) diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/readme/USAGE.rst b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/readme/USAGE.rst new file mode 100644 index 0000000..43c2b47 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/readme/USAGE.rst @@ -0,0 +1,13 @@ +On the category and product form, a new tab allows you to add images to the +related object. The images can be specific to the model or you can use an +existing one. + +On the link forms, you can add an image tag in addition to the image. In +the specific case of the product template, you can also specify for which +variant attribute values the image is valid. + +On the product variant form, the image tag will be automatically filled whith +the image tag of the product template for the same variant attribute values. + +In every case, a main image is computed and used as the default image for the +object. It depends on the sequence of the images (first one is the main one). diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/readme/newsfragments/.gitignore b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/readme/newsfragments/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/security/fs_product_category_image.xml b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/security/fs_product_category_image.xml new file mode 100644 index 0000000..20c6ec6 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/security/fs_product_category_image.xml @@ -0,0 +1,34 @@ + + + + + + fs.product.category.image access read + + + + + + + + + fs.product.category.image access system admin + + + + + + + + + fs.product.category.image access sales manager + + + + + + + + + diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/security/fs_product_image.xml b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/security/fs_product_image.xml new file mode 100644 index 0000000..4ecbaba --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/security/fs_product_image.xml @@ -0,0 +1,34 @@ + + + + + + fs.product.image access read + + + + + + + + + fs.product.image access erp manager + + + + + + + + + fs.product.image access sales manager + + + + + + + + + diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/static/description/icon.png b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/static/description/icon.png new file mode 100644 index 0000000..3a0328b Binary files /dev/null and b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/static/description/icon.png differ diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/static/description/index.html b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/static/description/index.html new file mode 100644 index 0000000..092f4a0 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/static/description/index.html @@ -0,0 +1,469 @@ + + + + + +Fs Product Multi Image + + + +
+

Fs Product Multi Image

+ + +

Alpha License: AGPL-3 OCA/storage Translate me on Weblate Try me on Runboat

+

Attach images to products and categories and store them on an external +filesystem instead of the database.

+

This addon is a drop-in replacement for the storage_image_product addon.

+
+

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

+
+

Table of contents

+ +
+

Usage

+

On the category and product form, a new tab allows you to add images to the +related object. The images can be specific to the model or you can use an +existing one.

+

On the link forms, you can add an image tag in addition to the image. In +the specific case of the product template, you can also specify for which +variant attribute values the image is valid.

+

On the product variant form, the image tag will be automatically filled whith +the image tag of the product template for the same variant attribute values.

+

In every case, a main image is computed and used as the default image for the +object. It depends on the sequence of the images (first one is the main one).

+
+
+

Changelog

+
+

16.0.1.0.2 (2023-10-04)

+

Bugfixes

+
    +
  • Ensures the variant_image_ids are sorted by sequence and name. Before this +change, the order was random and could change between runs. (#282)
  • +
+
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub 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.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • ACSONE SA/NV
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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.

+

Current maintainer:

+

lmignon

+

This module is part of the OCA/storage project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/tests/__init__.py b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/tests/__init__.py new file mode 100644 index 0000000..c686b22 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/tests/__init__.py @@ -0,0 +1 @@ +from . import test_fs_product_multi_image diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/tests/test_fs_product_multi_image.py b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/tests/test_fs_product_multi_image.py new file mode 100644 index 0000000..4d58f43 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/tests/test_fs_product_multi_image.py @@ -0,0 +1,282 @@ +# Copyright 2017 Akretion (http://www.akretion.com). +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import base64 +import io + +from PIL import Image + +from odoo.tests.common import TransactionCase + + +class TestFsProductMultiImage(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.white_image = cls._create_image(16, 16, color="#FFFFFF") + cls.black_image = cls._create_image(16, 16, color="#000000") + cls.logo_image = cls._create_image(16, 16, color="#FFA500") + cls.template = cls.env.ref("product.product_product_4_product_template") + cls.product_a = cls.env.ref("product.product_product_4") + cls.product_b = cls.env.ref("product.product_product_4b") + cls.product_c = cls.env.ref("product.product_product_4c") + cls.image_white = cls.env["fs.image"].create( + { + "image": { + "filename": "white.png", + "content": base64.b64encode(cls.white_image), + } + } + ) + cls.image_logo = cls.env["fs.image"].create( + { + "image": { + "filename": "logo.png", + "content": base64.b64encode(cls.logo_image), + } + } + ) + cls.image_black = cls.env["fs.image"].create( + { + "image": { + "filename": "black.png", + "content": base64.b64encode(cls.black_image), + } + } + ) + cls.is_sale_addon_installed = cls.env["ir.module.module"].search( + [("name", "=", "sale"), ("state", "=", "installed")] + ) + + def setUp(self): + super().setUp() + self.temp_dir = self.env["fs.storage"].create( + { + "name": "Temp FS Storage", + "protocol": "memory", + "code": "mem_dir", + "directory_path": "/tmp/", + "model_xmlids": "fs_product_multi_image.model_fs_product_category_image," + "fs_product_multi_image.model_fs_product_image", + } + ) + + @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 test_available_attribute_value(self): + # The template have already 5 attribute values + # see demo data of ipad + image = self.env["fs.product.image"].new({"product_tmpl_id": self.template.id}) + expected = 4 + if self.is_sale_addon_installed: + expected += 1 + self.assertEqual(len(image.available_attribute_value_ids), expected) + + def test_add_image_for_all_variant(self): + self.assertEqual(len(self.product_a.variant_image_ids), 0) + image = self.env["fs.product.image"].create( + { + "product_tmpl_id": self.template.id, + "specific_image": { + "filename": "white.png", + "content": base64.b64encode(self.white_image), + }, + } + ) + self.assertEqual(self.product_a.image.getvalue(), self.white_image) + self.assertEqual(self.product_a.variant_image_ids, image) + self.assertEqual(self.product_a.main_image_id, image) + self.assertEqual(self.product_b.image.getvalue(), self.white_image) + self.assertEqual(self.product_b.variant_image_ids, image) + self.assertEqual(self.product_b.main_image_id, image) + self.assertEqual(self.product_c.image.getvalue(), self.white_image) + self.assertEqual(self.product_c.variant_image_ids, image) + self.assertEqual(self.product_c.main_image_id, image) + + def test_add_image_for_white_variant(self): + image = self.env["fs.product.image"].create( + { + "product_tmpl_id": self.template.id, + "image_id": self.image_white.id, + "attribute_value_ids": [ + (6, 0, [self.env.ref("product.product_attribute_value_3").id]) + ], + } + ) + # White product should have the image + self.assertEqual(self.product_a.variant_image_ids, image) + self.assertEqual(self.product_a.main_image_id, image) + self.assertEqual(self.product_c.variant_image_ids, image) + self.assertEqual(self.product_c.main_image_id, image) + # Black product should not have the image + self.assertEqual(len(self.product_b.variant_image_ids), 0) + self.assertFalse(self.product_b.main_image_id) + + def _create_multiple_images(self): + logo = self.env["fs.product.image"].create( + { + "product_tmpl_id": self.template.id, + "image_id": self.image_logo.id, + "sequence": 10, + "link_existing": True, + } + ) + image_wh = self.env["fs.product.image"].create( + { + "product_tmpl_id": self.template.id, + "image_id": self.image_white.id, + "attribute_value_ids": [ + (6, 0, [self.env.ref("product.product_attribute_value_3").id]) + ], + "sequence": 2, + "link_existing": True, + } + ) + image_bk = self.env["fs.product.image"].create( + { + "product_tmpl_id": self.template.id, + "image_id": self.image_black.id, + "attribute_value_ids": [ + (6, 0, [self.env.ref("product.product_attribute_value_4").id]) + ], + "sequence": 1, + "link_existing": True, + } + ) + return logo, image_wh, image_bk + + def test_add_image_for_white_and_black_variant(self): + logo, image_wh, image_bk = self._create_multiple_images() + # White product should have the white image and the logo + self.assertEqual(self.product_a.variant_image_ids, image_wh + logo) + self.assertEqual(self.product_c.variant_image_ids, image_wh + logo) + # Black product should have the black image and the logo + self.assertEqual(self.product_b.variant_image_ids, image_bk + logo) + + def test_image_variant_sequence(self): + logo, image_wh, image_bk = self._create_multiple_images() + # White product should have the white image and the logo + self.assertEqual(self.product_a.variant_image_ids, image_wh + logo) + # white product should have images sorted by sequence + self.assertListEqual( + self.product_a.variant_image_ids.mapped("sequence"), + [image_wh.sequence, logo.sequence], + ) + # change sequence + image_wh.sequence = 20 + logo.sequence = 10 + self.assertListEqual( + self.product_a.variant_image_ids.mapped("sequence"), + [logo.sequence, image_wh.sequence], + ) + + def _test_main_images(self, expected): + for image, products in expected: + for prod in products: + self.assertEqual(prod.image.getvalue(), image) + + def test_main_image_and_urls(self): + logo, image_wh, image_bk = self._create_multiple_images() + # Template should have the one w/ lower sequence + expected = ((self.black_image, self.template),) + self._test_main_images(expected) + # Should have different main images + expected = ( + (self.white_image, self.product_a + self.product_c), + (self.black_image, self.product_b), + ) + self._test_main_images(expected) + # Change image order, change main image + logo.sequence = 0 + image_wh.sequence = 10 + expected = ((self.logo_image, self.template),) + self._test_main_images(expected) + expected = ( + (self.logo_image, self.product_a + self.product_c), + (self.logo_image, self.product_b), + ) + self._test_main_images(expected) + + def test_main_image_attribute(self): + """ + Attach the image to the template and check the first image of the + variant is the one with same attributes + """ + self.env["fs.product.image"].create( + { + "product_tmpl_id": self.template.id, + "image_id": self.image_logo.id, + "sequence": 1, + "link_existing": True, + } + ) + self.env["fs.product.image"].create( + { + "product_tmpl_id": self.template.id, + "image_id": self.image_white.id, + "attribute_value_ids": [ + ( + 6, + 0, + [ + self.env.ref("product.product_attribute_value_4").id, + self.env.ref("product.product_attribute_value_1").id, + ], + ) + ], + "sequence": 10, + "link_existing": True, + } + ) + # The variant should not take the only with the lowest sequence but + # the one with same attributes + expected = ((self.white_image, self.product_b),) + self._test_main_images(expected) + expected = ((self.logo_image, self.product_c + self.product_a),) + self._test_main_images(expected) + + def test_drop_template_attribute_value_propagation_to_image(self): + black_image = self.env["fs.product.image"].create( + { + "product_tmpl_id": self.template.id, + "image_id": self.image_black.id, + "attribute_value_ids": [ + ( + 6, + 0, + [ + self.env.ref("product.product_attribute_value_4").id, + self.env.ref("product.product_attribute_value_1").id, + ], + ) + ], + "sequence": 10, + "link_existing": True, + } + ) + # Remove Color black from variant tab: + self.template.attribute_line_ids.sudo().filtered( + lambda x: x.display_name == "Color" + ).value_ids -= self.env.ref("product.product_attribute_value_4") + # Attribute black is removed from image: + self.assertTrue( + self.env.ref("product.product_attribute_value_4") + not in black_image.attribute_value_ids + ) + + # Remove Leg attribute line from variant tab: + self.template.attribute_line_ids.sudo().filtered( + lambda x: x.display_name == "Legs" + ).unlink() + # Product image attribute values from Legs are removed: + self.assertTrue( + self.env.ref("product.product_attribute_value_1") + not in black_image.attribute_value_ids + ) diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/views/fs_product_category_image.xml b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/views/fs_product_category_image.xml new file mode 100644 index 0000000..f1ec8c6 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/views/fs_product_category_image.xml @@ -0,0 +1,51 @@ + + + + + + product.category.fs.image.form + fs.product.category.image + + primary + + + + + + + + + fs.product.category.image.kanban + fs.product.category.image + + primary + + + + + +
+ Image +
+
+ +
+ +
+
+
+
+ +
diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/views/fs_product_image.xml b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/views/fs_product_image.xml new file mode 100644 index 0000000..7acd5c4 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/views/fs_product_image.xml @@ -0,0 +1,62 @@ + + + + + + fs.product.image.form + fs.product.image + + primary + + + + + + + + + + + fs.product.image.kanban + fs.product.image + + primary + + + + + +
+ Image +
+
+ +
+ +
+
+
+
+ +
diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/views/image_tag.xml b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/views/image_tag.xml new file mode 100644 index 0000000..925ab53 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/views/image_tag.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/views/product_category.xml b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/views/product_category.xml new file mode 100644 index 0000000..d5e3cf3 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/views/product_category.xml @@ -0,0 +1,38 @@ + + + + + + product.category.form + product.category + + + +
+ +
+ + + + + + + + + + +
+
+ + + +
diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/views/product_product.xml b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/views/product_product.xml new file mode 100644 index 0000000..b7534f2 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/views/product_product.xml @@ -0,0 +1,57 @@ + + + + + product.product + + + + + kanban_image('product.product', 'image_medium', record.id.raw_value) + + + + + product.product + + + + 1 + + + + + + +

+ If you need to edit the images, do it from the product template. +

+ +
+
+
+
+ + product.product + + + + 1 + + + + + + +
diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/views/product_template.xml b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/views/product_template.xml new file mode 100644 index 0000000..eebe4cd --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/fs_product_multi_image/views/product_template.xml @@ -0,0 +1,53 @@ + + + + + product.template + + + + + 1 + + + + + + + + + + + + + + + + + + + + + + product.template + + + + + kanban_image('product.template', 'image_medium', record.id.raw_value) + + + + diff --git a/odoo-bringout-oca-storage-fs_product_multi_image/pyproject.toml b/odoo-bringout-oca-storage-fs_product_multi_image/pyproject.toml new file mode 100644 index 0000000..59213c8 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_image/pyproject.toml @@ -0,0 +1,46 @@ +[project] +name = "odoo-bringout-oca-storage-fs_product_multi_image" +version = "16.0.0" +description = "Fs Product Multi Image - + Manage multi images from extenal file system on product" +authors = [ + { name = "Ernad Husremovic", email = "hernad@bring.out.ba" } +] +dependencies = [ + "odoo-bringout-oca-storage-fs_base_multi_image>=16.0.0", + "odoo-bringout-oca-ocb-product>=16.0.0", + "odoo-bringout-oca-ocb-sales_team>=16.0.0", + "odoo-bringout-oca-storage-image_tag>=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_product_multi_image"] + +[tool.rye] +managed = true +dev-dependencies = [ + "pytest>=8.4.1", +] diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/README.md b/odoo-bringout-oca-storage-fs_product_multi_media/README.md new file mode 100644 index 0000000..48f0f45 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_media/README.md @@ -0,0 +1,48 @@ +# Fs Product Multi Media + +Odoo addon: fs_product_multi_media + +## Installation + +```bash +pip install odoo-bringout-oca-storage-fs_product_multi_media +``` + +## Dependencies + +This addon depends on: +- fs_base_multi_media +- product +- sales_team + +## Manifest Information + +- **Name**: Fs Product Multi Media +- **Version**: 16.0.1.0.2 +- **Category**: N/A +- **License**: AGPL-3 +- **Installable**: False + +## Source + +Based on [OCA/storage](https://github.com/OCA/storage) branch 16.0, addon `fs_product_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 diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/doc/ARCHITECTURE.md b/odoo-bringout-oca-storage-fs_product_multi_media/doc/ARCHITECTURE.md new file mode 100644 index 0000000..591a00b --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_media/doc/ARCHITECTURE.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_product_multi_media Module - fs_product_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. diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/doc/CONFIGURATION.md b/odoo-bringout-oca-storage-fs_product_multi_media/doc/CONFIGURATION.md new file mode 100644 index 0000000..47bce85 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_media/doc/CONFIGURATION.md @@ -0,0 +1,3 @@ +# Configuration + +Refer to Odoo settings for fs_product_multi_media. Configure related models, access rights, and options as needed. diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/doc/CONTROLLERS.md b/odoo-bringout-oca-storage-fs_product_multi_media/doc/CONTROLLERS.md new file mode 100644 index 0000000..f628e77 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_media/doc/CONTROLLERS.md @@ -0,0 +1,3 @@ +# Controllers + +This module does not define custom HTTP controllers. diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/doc/DEPENDENCIES.md b/odoo-bringout-oca-storage-fs_product_multi_media/doc/DEPENDENCIES.md new file mode 100644 index 0000000..8605759 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_media/doc/DEPENDENCIES.md @@ -0,0 +1,7 @@ +# Dependencies + +This addon depends on: + +- [fs_base_multi_media](../../odoo-bringout-oca-storage-fs_base_multi_media) +- [product](../../odoo-bringout-oca-ocb-product) +- [sales_team](../../odoo-bringout-oca-ocb-sales_team) diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/doc/FAQ.md b/odoo-bringout-oca-storage-fs_product_multi_media/doc/FAQ.md new file mode 100644 index 0000000..74466b3 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_media/doc/FAQ.md @@ -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_product_multi_media or install in UI. diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/doc/INSTALL.md b/odoo-bringout-oca-storage-fs_product_multi_media/doc/INSTALL.md new file mode 100644 index 0000000..6856220 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_media/doc/INSTALL.md @@ -0,0 +1,7 @@ +# Install + +```bash +pip install odoo-bringout-oca-storage-fs_product_multi_media" +# or +uv pip install odoo-bringout-oca-storage-fs_product_multi_media" +``` diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/doc/MODELS.md b/odoo-bringout-oca-storage-fs_product_multi_media/doc/MODELS.md new file mode 100644 index 0000000..c366344 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_media/doc/MODELS.md @@ -0,0 +1,18 @@ +# Models + +Detected core models and extensions in fs_product_multi_media. + +```mermaid +classDiagram + class fs_product_category_media + class fs_product_media + class fs_media_relation_mixin + class product_category + class product_product + class product_template + class product_template_attribute_line +``` + +Notes +- Classes show model technical names; fields omitted for brevity. +- Items listed under _inherit are extensions of existing models. diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/doc/OVERVIEW.md b/odoo-bringout-oca-storage-fs_product_multi_media/doc/OVERVIEW.md new file mode 100644 index 0000000..636fc60 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_media/doc/OVERVIEW.md @@ -0,0 +1,6 @@ +# Overview + +Packaged Odoo addon: fs_product_multi_media. Provides features documented in upstream Odoo 16 under this addon. + +- Source: OCA/OCB 16.0, addon fs_product_multi_media +- License: LGPL-3 diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/doc/REPORTS.md b/odoo-bringout-oca-storage-fs_product_multi_media/doc/REPORTS.md new file mode 100644 index 0000000..e0ea35f --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_media/doc/REPORTS.md @@ -0,0 +1,3 @@ +# Reports + +This module does not define custom reports. diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/doc/SECURITY.md b/odoo-bringout-oca-storage-fs_product_multi_media/doc/SECURITY.md new file mode 100644 index 0000000..303bbe7 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_media/doc/SECURITY.md @@ -0,0 +1,76 @@ +# Security + +Access control and security definitions in fs_product_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_product_category_media.xml](../fs_product_multi_media/security/fs_product_category_media.xml)** +- **[fs_product_media.xml](../fs_product_multi_media/security/fs_product_media.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_product_category_media.xml](../fs_product_multi_media/security/fs_product_category_media.xml)** + - Security groups, categories, and XML-based rules +- **[fs_product_media.xml](../fs_product_multi_media/security/fs_product_media.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 diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/doc/TROUBLESHOOTING.md b/odoo-bringout-oca-storage-fs_product_multi_media/doc/TROUBLESHOOTING.md new file mode 100644 index 0000000..56853cb --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_media/doc/TROUBLESHOOTING.md @@ -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. diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/doc/USAGE.md b/odoo-bringout-oca-storage-fs_product_multi_media/doc/USAGE.md new file mode 100644 index 0000000..7198504 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_media/doc/USAGE.md @@ -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_product_multi_media +``` diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/doc/WIZARDS.md b/odoo-bringout-oca-storage-fs_product_multi_media/doc/WIZARDS.md new file mode 100644 index 0000000..48e790d --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_media/doc/WIZARDS.md @@ -0,0 +1,3 @@ +# Wizards + +This module does not include UI wizards. diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/README.rst b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/README.rst new file mode 100644 index 0000000..529605f --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/README.rst @@ -0,0 +1,115 @@ +====================== +Fs Product Multi Media +====================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:3911858ee6494b88ab7dce64232131c25f9ef04a96d9e4604f06b2c56f24ec76 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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_product_multi_media + :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_product_multi_media + :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| + +Attach medias to products and categories and store them on an external +filesystem instead of the database. + +This addon is a drop-in replacement for the **storage_media_product** addon. + +.. 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 `_ + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +On the category and product form, a new tab allows you to add medias to the +related object. The medias can be specific to the model or you can use an +existing one. + +When attaching a media to a product you can also specify for which variant +attribute values the media is valid. + +Changelog +========= + +16.0.1.0.2 (2023-10-04) +~~~~~~~~~~~~~~~~~~~~~~~ + +**Bugfixes** + +- Ensures the variant_media_ids are sorted by sequence and name. Before this + change, the order was random and could change between runs. (`#282 `_) + +Bug Tracker +=========== + +Bugs are tracked on `GitHub 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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* ACSONE SA/NV +* Akretion + +Contributors +~~~~~~~~~~~~ + +* Raphaël Reverdy +* Laurent Mignon (https://www.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 `__: + +|maintainer-lmignon| + +This module is part of the `OCA/storage `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/__init__.py b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/__manifest__.py b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/__manifest__.py new file mode 100644 index 0000000..67192d9 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/__manifest__.py @@ -0,0 +1,25 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Fs Product Multi Media", + "summary": """ + Link media to products and categories""", + "version": "16.0.1.0.2", + "license": "AGPL-3", + "author": "ACSONE SA/NV,Akretion,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/storage", + "depends": ["fs_base_multi_media", "product", "sales_team"], + "data": [ + "security/fs_product_category_media.xml", + "security/fs_product_media.xml", + "views/fs_product_category_media.xml", + "views/fs_product_media.xml", + "views/product_category.xml", + "views/product_product.xml", + "views/product_template.xml", + ], + "demo": [], + "maintainers": ["lmignon"], + "development_status": "Alpha", +} diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/i18n/bs.po b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/i18n/bs.po new file mode 100644 index 0000000..69bc7bc --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/i18n/bs.po @@ -0,0 +1,171 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_product_multi_media +# +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_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__attribute_value_ids +msgid "Attributes" +msgstr "Atributi" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__available_attribute_value_ids +msgid "Available Attributes" +msgstr "Dostupni atributi" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__create_uid +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__create_uid +msgid "Created by" +msgstr "Kreirao" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__create_date +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__create_date +msgid "Created on" +msgstr "Kreirano" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__display_name +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__display_name +msgid "Display Name" +msgstr "Prikazani naziv" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__id +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__id +msgid "ID" +msgstr "ID" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media____last_update +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media____last_update +msgid "Last Modified on" +msgstr "Zadnje mijenjano" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__write_uid +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__write_uid +msgid "Last Updated by" +msgstr "Zadnji ažurirao" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__write_date +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__write_date +msgid "Last Updated on" +msgstr "Zadnje ažurirano" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__link_existing +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__link_existing +msgid "Link existing media" +msgstr "Poveži postojeći medij" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__media_id +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__media_id +msgid "Linked media" +msgstr "Povezani medij" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__file +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__file +msgid "Media" +msgstr "Datoteka" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__media_type_id +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__media_type_id +msgid "Media Type" +msgstr "Tip medija" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_product_category__media_ids +#: model:ir.model.fields,field_description:fs_product_multi_media.field_product_product__media_ids +#: model:ir.model.fields,field_description:fs_product_multi_media.field_product_template__media_ids +#: model_terms:ir.ui.view,arch_db:fs_product_multi_media.product_category_form_view +#: model_terms:ir.ui.view,arch_db:fs_product_multi_media.product_normal_form_view +#: model_terms:ir.ui.view,arch_db:fs_product_multi_media.product_template_only_form_view +msgid "Medias" +msgstr "Mediji" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__mimetype +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__mimetype +msgid "Mimetype" +msgstr "Mimetype" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__name +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__name +msgid "Name" +msgstr "Naziv:" + +#. module: fs_product_multi_media +#: model:ir.model,name:fs_product_multi_media.model_product_template +msgid "Product" +msgstr "Artikal" + +#. module: fs_product_multi_media +#: model:ir.model,name:fs_product_multi_media.model_product_category +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__product_categ_id +msgid "Product Category" +msgstr "Kategorija proizvoda" + +#. module: fs_product_multi_media +#: model:ir.model,name:fs_product_multi_media.model_fs_product_category_media +msgid "Product Category Media" +msgstr "Medij kategorije proizvoda" + +#. module: fs_product_multi_media +#: model:ir.model,name:fs_product_multi_media.model_fs_product_media +msgid "Product Media" +msgstr "Medij proizvoda" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__product_tmpl_id +msgid "Product Template" +msgstr "Predložak artikla" + +#. module: fs_product_multi_media +#: model:ir.model,name:fs_product_multi_media.model_product_template_attribute_line +msgid "Product Template Attribute Line" +msgstr "Predloška artikla stavka atributa" + +#. module: fs_product_multi_media +#: model:ir.model,name:fs_product_multi_media.model_product_product +msgid "Product Variant" +msgstr "Varijanta proizvoda" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__sequence +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__sequence +msgid "Sequence" +msgstr "Sekvenca" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__specific_file +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__specific_file +msgid "Specific Media" +msgstr "Specifični medij" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__specific_media_type_id +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__specific_media_type_id +msgid "Specific Media Type" +msgstr "Specifični tip medija" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_product_product__variant_media_ids +msgid "Variant Medias" +msgstr "Mediji varijanti" diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/i18n/es.po b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/i18n/es.po new file mode 100644 index 0000000..d082f02 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/i18n/es.po @@ -0,0 +1,174 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_product_multi_media +# +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 \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_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__attribute_value_ids +msgid "Attributes" +msgstr "Atributos" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__available_attribute_value_ids +msgid "Available Attributes" +msgstr "Atributos Disponibles" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__create_uid +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__create_date +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__create_date +msgid "Created on" +msgstr "Creado el" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__display_name +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__display_name +msgid "Display Name" +msgstr "Mostrar Nombre" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__id +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__id +msgid "ID" +msgstr "ID (identificación)" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media____last_update +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media____last_update +msgid "Last Modified on" +msgstr "Última Modificación el" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__write_uid +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__write_uid +msgid "Last Updated by" +msgstr "Actualizado por Última vez por" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__write_date +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__write_date +msgid "Last Updated on" +msgstr "Última Actualización el" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__link_existing +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__link_existing +msgid "Link existing media" +msgstr "Vincular los medios existentes" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__media_id +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__media_id +msgid "Linked media" +msgstr "Medios vinculados" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__file +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__file +msgid "Media" +msgstr "Medios de comunicación" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__media_type_id +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__media_type_id +msgid "Media Type" +msgstr "Tipo de Medio" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_product_category__media_ids +#: model:ir.model.fields,field_description:fs_product_multi_media.field_product_product__media_ids +#: model:ir.model.fields,field_description:fs_product_multi_media.field_product_template__media_ids +#: model_terms:ir.ui.view,arch_db:fs_product_multi_media.product_category_form_view +#: model_terms:ir.ui.view,arch_db:fs_product_multi_media.product_normal_form_view +#: model_terms:ir.ui.view,arch_db:fs_product_multi_media.product_template_only_form_view +msgid "Medias" +msgstr "Medios" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__mimetype +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__mimetype +msgid "Mimetype" +msgstr "Tipo Mimo" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__name +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__name +msgid "Name" +msgstr "Nombre" + +#. module: fs_product_multi_media +#: model:ir.model,name:fs_product_multi_media.model_product_template +msgid "Product" +msgstr "Producto" + +#. module: fs_product_multi_media +#: model:ir.model,name:fs_product_multi_media.model_product_category +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__product_categ_id +msgid "Product Category" +msgstr "Categoría de Producto" + +#. module: fs_product_multi_media +#: model:ir.model,name:fs_product_multi_media.model_fs_product_category_media +msgid "Product Category Media" +msgstr "Categoría de productos Medios de Comunicación" + +#. module: fs_product_multi_media +#: model:ir.model,name:fs_product_multi_media.model_fs_product_media +msgid "Product Media" +msgstr "Medios del Producto" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__product_tmpl_id +msgid "Product Template" +msgstr "Plantilla del Producto" + +#. module: fs_product_multi_media +#: model:ir.model,name:fs_product_multi_media.model_product_template_attribute_line +msgid "Product Template Attribute Line" +msgstr "Plantilla de Línea de Atributo de Producto" + +#. module: fs_product_multi_media +#: model:ir.model,name:fs_product_multi_media.model_product_product +msgid "Product Variant" +msgstr "Variante de Producto" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__sequence +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__sequence +msgid "Sequence" +msgstr "Secuencia" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__specific_file +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__specific_file +msgid "Specific Media" +msgstr "Medios Específicos" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__specific_media_type_id +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__specific_media_type_id +msgid "Specific Media Type" +msgstr "Tipo de Medio Específico" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_product_product__variant_media_ids +msgid "Variant Medias" +msgstr "Medios Variantes" diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/i18n/fs_product_multi_media.pot b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/i18n/fs_product_multi_media.pot new file mode 100644 index 0000000..e8d9908 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/i18n/fs_product_multi_media.pot @@ -0,0 +1,171 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_product_multi_media +# +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_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__attribute_value_ids +msgid "Attributes" +msgstr "" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__available_attribute_value_ids +msgid "Available Attributes" +msgstr "" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__create_uid +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__create_uid +msgid "Created by" +msgstr "" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__create_date +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__create_date +msgid "Created on" +msgstr "" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__display_name +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__display_name +msgid "Display Name" +msgstr "" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__id +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__id +msgid "ID" +msgstr "" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media____last_update +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media____last_update +msgid "Last Modified on" +msgstr "" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__write_uid +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__write_date +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__write_date +msgid "Last Updated on" +msgstr "" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__link_existing +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__link_existing +msgid "Link existing media" +msgstr "" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__media_id +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__media_id +msgid "Linked media" +msgstr "" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__file +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__file +msgid "Media" +msgstr "" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__media_type_id +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__media_type_id +msgid "Media Type" +msgstr "" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_product_category__media_ids +#: model:ir.model.fields,field_description:fs_product_multi_media.field_product_product__media_ids +#: model:ir.model.fields,field_description:fs_product_multi_media.field_product_template__media_ids +#: model_terms:ir.ui.view,arch_db:fs_product_multi_media.product_category_form_view +#: model_terms:ir.ui.view,arch_db:fs_product_multi_media.product_normal_form_view +#: model_terms:ir.ui.view,arch_db:fs_product_multi_media.product_template_only_form_view +msgid "Medias" +msgstr "" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__mimetype +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__mimetype +msgid "Mimetype" +msgstr "" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__name +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__name +msgid "Name" +msgstr "" + +#. module: fs_product_multi_media +#: model:ir.model,name:fs_product_multi_media.model_product_template +msgid "Product" +msgstr "" + +#. module: fs_product_multi_media +#: model:ir.model,name:fs_product_multi_media.model_product_category +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__product_categ_id +msgid "Product Category" +msgstr "" + +#. module: fs_product_multi_media +#: model:ir.model,name:fs_product_multi_media.model_fs_product_category_media +msgid "Product Category Media" +msgstr "" + +#. module: fs_product_multi_media +#: model:ir.model,name:fs_product_multi_media.model_fs_product_media +msgid "Product Media" +msgstr "" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__product_tmpl_id +msgid "Product Template" +msgstr "" + +#. module: fs_product_multi_media +#: model:ir.model,name:fs_product_multi_media.model_product_template_attribute_line +msgid "Product Template Attribute Line" +msgstr "" + +#. module: fs_product_multi_media +#: model:ir.model,name:fs_product_multi_media.model_product_product +msgid "Product Variant" +msgstr "" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__sequence +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__sequence +msgid "Sequence" +msgstr "" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__specific_file +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__specific_file +msgid "Specific Media" +msgstr "" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__specific_media_type_id +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__specific_media_type_id +msgid "Specific Media Type" +msgstr "" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_product_product__variant_media_ids +msgid "Variant Medias" +msgstr "" diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/i18n/it.po b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/i18n/it.po new file mode 100644 index 0000000..05bf0cc --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/i18n/it.po @@ -0,0 +1,174 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_product_multi_media +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-11-29 20:33+0000\n" +"Last-Translator: mymage \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_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__attribute_value_ids +msgid "Attributes" +msgstr "Attributi" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__available_attribute_value_ids +msgid "Available Attributes" +msgstr "Attributi disponibili" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__create_uid +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__create_uid +msgid "Created by" +msgstr "Creato da" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__create_date +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__create_date +msgid "Created on" +msgstr "Creato il" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__display_name +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__display_name +msgid "Display Name" +msgstr "Nome visualizzato" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__id +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__id +msgid "ID" +msgstr "ID" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media____last_update +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media____last_update +msgid "Last Modified on" +msgstr "Ultima modifica il" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__write_uid +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__write_uid +msgid "Last Updated by" +msgstr "Ultimo aggiornamento di" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__write_date +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__write_date +msgid "Last Updated on" +msgstr "Ultimo aggiornamento il" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__link_existing +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__link_existing +msgid "Link existing media" +msgstr "Collega media esistente" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__media_id +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__media_id +msgid "Linked media" +msgstr "Media collegato" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__file +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__file +msgid "Media" +msgstr "Media" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__media_type_id +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__media_type_id +msgid "Media Type" +msgstr "Tipo media" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_product_category__media_ids +#: model:ir.model.fields,field_description:fs_product_multi_media.field_product_product__media_ids +#: model:ir.model.fields,field_description:fs_product_multi_media.field_product_template__media_ids +#: model_terms:ir.ui.view,arch_db:fs_product_multi_media.product_category_form_view +#: model_terms:ir.ui.view,arch_db:fs_product_multi_media.product_normal_form_view +#: model_terms:ir.ui.view,arch_db:fs_product_multi_media.product_template_only_form_view +msgid "Medias" +msgstr "Media" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__mimetype +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__mimetype +msgid "Mimetype" +msgstr "Tipo MIME" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__name +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__name +msgid "Name" +msgstr "Nome" + +#. module: fs_product_multi_media +#: model:ir.model,name:fs_product_multi_media.model_product_template +msgid "Product" +msgstr "Prodotto" + +#. module: fs_product_multi_media +#: model:ir.model,name:fs_product_multi_media.model_product_category +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__product_categ_id +msgid "Product Category" +msgstr "Categoria prodotto" + +#. module: fs_product_multi_media +#: model:ir.model,name:fs_product_multi_media.model_fs_product_category_media +msgid "Product Category Media" +msgstr "Media categoria prodotto" + +#. module: fs_product_multi_media +#: model:ir.model,name:fs_product_multi_media.model_fs_product_media +msgid "Product Media" +msgstr "Media prodotto" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__product_tmpl_id +msgid "Product Template" +msgstr "Modello prodotto" + +#. module: fs_product_multi_media +#: model:ir.model,name:fs_product_multi_media.model_product_template_attribute_line +msgid "Product Template Attribute Line" +msgstr "Riga attributo modello prodotto" + +#. module: fs_product_multi_media +#: model:ir.model,name:fs_product_multi_media.model_product_product +msgid "Product Variant" +msgstr "Variante prodotto" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__sequence +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__sequence +msgid "Sequence" +msgstr "Sequenza" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__specific_file +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__specific_file +msgid "Specific Media" +msgstr "Media specifico" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_category_media__specific_media_type_id +#: model:ir.model.fields,field_description:fs_product_multi_media.field_fs_product_media__specific_media_type_id +msgid "Specific Media Type" +msgstr "Tipo media specifico" + +#. module: fs_product_multi_media +#: model:ir.model.fields,field_description:fs_product_multi_media.field_product_product__variant_media_ids +msgid "Variant Medias" +msgstr "Media variante" diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/models/__init__.py b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/models/__init__.py new file mode 100644 index 0000000..18e5bfc --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/models/__init__.py @@ -0,0 +1,6 @@ +from . import fs_product_category_media +from . import fs_product_media +from . import product_category +from . import product_template +from . import product_product +from . import product_template_attribute_line diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/models/fs_product_category_media.py b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/models/fs_product_category_media.py new file mode 100644 index 0000000..527699b --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/models/fs_product_category_media.py @@ -0,0 +1,17 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class FsProductCategoryMedia(models.Model): + _name = "fs.product.category.media" + _inherit = "fs.media.relation.mixin" + _description = "Product Category Media" + + product_categ_id = fields.Many2one( + comodel_name="product.category", + string="Product Category", + ondelete="cascade", + index=True, + ) diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/models/fs_product_media.py b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/models/fs_product_media.py new file mode 100644 index 0000000..2796e97 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/models/fs_product_media.py @@ -0,0 +1,44 @@ +# Copyright 2023 ACSONE SA/NV +# Copyright 2018 Akretion (http://www.akretion.com). +# @author Raphaël Reverdy +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class FsProductMedia(models.Model): + _name = "fs.product.media" + _inherit = "fs.media.relation.mixin" + _description = "Product Media" + + product_tmpl_id = fields.Many2one( + comodel_name="product.template", + string="Product Template", + ondelete="cascade", + index=True, + ) + attribute_value_ids = fields.Many2many( + "product.attribute.value", + string="Attributes", + domain="[('id', 'in', available_attribute_value_ids)]", + ) + # This field will list all attribute value used by the template + # in order to filter the attribute value available for the current media + available_attribute_value_ids = fields.Many2many( + "product.attribute.value", + string="Available Attributes", + compute="_compute_available_attribute", + ) + + @api.depends("product_tmpl_id.attribute_line_ids.value_ids") + def _compute_available_attribute(self): + for rec in self: + rec.available_attribute_value_ids = rec.product_tmpl_id.mapped( + "attribute_line_ids.value_ids" + ) + + def _match_variant(self, variant): + variant_attribute_values = variant.mapped( + "product_template_attribute_value_ids.product_attribute_value_id" + ) + return not bool(self.attribute_value_ids - variant_attribute_values) diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/models/product_category.py b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/models/product_category.py new file mode 100644 index 0000000..869e3a7 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/models/product_category.py @@ -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 ProductCategory(models.Model): + + _inherit = "product.category" + + media_ids = fields.One2many( + string="Medias", + comodel_name="fs.product.category.media", + inverse_name="product_categ_id", + ) diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/models/product_product.py b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/models/product_product.py new file mode 100644 index 0000000..b7801b4 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/models/product_product.py @@ -0,0 +1,34 @@ +# Copyright 2017 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# Copyright 2021 Camptocamp SA (http://www.camptocamp.com) +# @author Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import api, fields, models + + +class ProductProduct(models.Model): + _inherit = "product.product" + + variant_media_ids = fields.Many2many( + "fs.product.media", + compute="_compute_variant_media_ids", + store=True, + string="Variant Medias", + ) + + @api.depends( + "product_tmpl_id.media_ids", + "product_tmpl_id.media_ids.sequence", + "product_tmpl_id.media_ids.attribute_value_ids", + "product_template_attribute_value_ids", + ) + def _compute_variant_media_ids(self): + for variant in self: + variant_media_ids = variant.media_ids.filtered( + lambda i: i._match_variant(variant) + ) + variant_media_ids = variant_media_ids.sorted( + key=lambda i: (i.sequence, i.name) + ) + variant.variant_media_ids = variant_media_ids diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/models/product_template.py b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/models/product_template.py new file mode 100644 index 0000000..2fe5ddc --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/models/product_template.py @@ -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 ProductTemplate(models.Model): + + _inherit = "product.template" + + media_ids = fields.One2many( + string="Medias", + comodel_name="fs.product.media", + inverse_name="product_tmpl_id", + ) diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/models/product_template_attribute_line.py b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/models/product_template_attribute_line.py new file mode 100644 index 0000000..fd85234 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/models/product_template_attribute_line.py @@ -0,0 +1,31 @@ +# Copyright 2023 ACSONE SA/NV +# Copyright 2017 Akretion (http://www.akretion.com). +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models + + +class ProductTemplateAttributeLine(models.Model): + + _inherit = "product.template.attribute.line" + + def write(self, values): + res = super().write(values) + if "value_ids" in values: + product_media_attribute_value_ids = self.product_tmpl_id.media_ids.mapped( + "attribute_value_ids" + ).filtered(lambda x: x.attribute_id == self.attribute_id) + available_attribute_values_ids = self.value_ids + to_remove = product_media_attribute_value_ids.filtered( + lambda x: x not in available_attribute_values_ids + ) + if to_remove: + for media in self.product_tmpl_id.media_ids: + media.attribute_value_ids -= to_remove + return res + + def unlink(self): + for line in self: + for media in line.product_tmpl_id.media_ids: + media.attribute_value_ids -= line.value_ids + return super().unlink() diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/readme/CONTRIBUTORS.rst b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000..be1d6a6 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Raphaël Reverdy +* Laurent Mignon (https://www.acsone.eu/) diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/readme/DESCRIPTION.rst b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/readme/DESCRIPTION.rst new file mode 100644 index 0000000..b4a8c78 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/readme/DESCRIPTION.rst @@ -0,0 +1,4 @@ +Attach medias to products and categories and store them on an external +filesystem instead of the database. + +This addon is a drop-in replacement for the **storage_media_product** addon. diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/readme/HISTORY.rst b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/readme/HISTORY.rst new file mode 100644 index 0000000..60921bc --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/readme/HISTORY.rst @@ -0,0 +1,7 @@ +16.0.1.0.2 (2023-10-04) +~~~~~~~~~~~~~~~~~~~~~~~ + +**Bugfixes** + +- Ensures the variant_media_ids are sorted by sequence and name. Before this + change, the order was random and could change between runs. (`#282 `_) diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/readme/USAGE.rst b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/readme/USAGE.rst new file mode 100644 index 0000000..3887f12 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/readme/USAGE.rst @@ -0,0 +1,6 @@ +On the category and product form, a new tab allows you to add medias to the +related object. The medias can be specific to the model or you can use an +existing one. + +When attaching a media to a product you can also specify for which variant +attribute values the media is valid. diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/readme/newsfragments/.gitignore b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/readme/newsfragments/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/security/fs_product_category_media.xml b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/security/fs_product_category_media.xml new file mode 100644 index 0000000..2650bb3 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/security/fs_product_category_media.xml @@ -0,0 +1,34 @@ + + + + + + fs.product.category.media access read + + + + + + + + + fs.product.category.media access system admin + + + + + + + + + fs.product.category.media access sales manager + + + + + + + + + diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/security/fs_product_media.xml b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/security/fs_product_media.xml new file mode 100644 index 0000000..c0a9374 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/security/fs_product_media.xml @@ -0,0 +1,34 @@ + + + + + + fs.product.media access read + + + + + + + + + fs.product.media access erp manager + + + + + + + + + fs.product.media access sales manager + + + + + + + + + diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/static/description/icon.png b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/static/description/icon.png new file mode 100644 index 0000000..3a0328b Binary files /dev/null and b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/static/description/icon.png differ diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/static/description/index.html b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/static/description/index.html new file mode 100644 index 0000000..0e54d52 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/static/description/index.html @@ -0,0 +1,457 @@ + + + + + + +Fs Product Multi Media + + + +
+

Fs Product Multi Media

+ + +

Alpha License: AGPL-3 OCA/storage Translate me on Weblate Try me on Runboat

+

Attach medias to products and categories and store them on an external +filesystem instead of the database.

+

This addon is a drop-in replacement for the storage_media_product addon.

+
+

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

+
+

Table of contents

+ +
+

Usage

+

On the category and product form, a new tab allows you to add medias to the +related object. The medias can be specific to the model or you can use an +existing one.

+

When attaching a media to a product you can also specify for which variant +attribute values the media is valid.

+
+
+

Changelog

+
+

16.0.1.0.2 (2023-10-04)

+

Bugfixes

+
    +
  • Ensures the variant_media_ids are sorted by sequence and name. Before this +change, the order was random and could change between runs. (#282)
  • +
+
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub 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.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • ACSONE SA/NV
  • +
  • Akretion
  • +
+
+ +
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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.

+

Current maintainer:

+

lmignon

+

This module is part of the OCA/storage project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/tests/__init__.py b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/tests/__init__.py new file mode 100644 index 0000000..db4b297 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/tests/__init__.py @@ -0,0 +1 @@ +from . import test_fs_product_multi_media diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/tests/test_fs_product_multi_media.py b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/tests/test_fs_product_multi_media.py new file mode 100644 index 0000000..fe6f229 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/tests/test_fs_product_multi_media.py @@ -0,0 +1,219 @@ +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import base64 + +from odoo.tests.common import TransactionCase + + +class TestFsProductMultiMedia(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.media_content_a = b"media content a" + cls.media_content_b = b"media content b" + cls.media_content_c = b"media content c" + cls.template = cls.env.ref("product.product_product_4_product_template") + cls.product_a = cls.env.ref("product.product_product_4") + cls.product_b = cls.env.ref("product.product_product_4b") + cls.product_c = cls.env.ref("product.product_product_4c") + cls.media_type_a = cls.env["fs.media.type"].create( + {"name": "Media Type A", "code": "media_type_a"} + ) + cls.media_type_b = cls.env["fs.media.type"].create( + {"name": "Media Type B", "code": "media_type_b"} + ) + cls.media_type_c = cls.env["fs.media.type"].create( + {"name": "Media Type C", "code": "media_type_c"} + ) + cls.media_a = cls.env["fs.media"].create( + { + "file": { + "filename": "a.txt", + "content": base64.b64encode(cls.media_content_a), + }, + "media_type_id": cls.media_type_a.id, + } + ) + cls.media_c = cls.env["fs.media"].create( + { + "file": { + "filename": "c.txt", + "content": base64.b64encode(cls.media_content_c), + }, + "media_type_id": cls.media_type_c.id, + } + ) + cls.media_b = cls.env["fs.media"].create( + { + "file": { + "filename": "b.txt", + "content": base64.b64encode(cls.media_content_b), + }, + "media_type_id": cls.media_type_b.id, + } + ) + cls.is_sale_addon_installed = cls.env["ir.module.module"].search( + [("name", "=", "sale"), ("state", "=", "installed")] + ) + + def setUp(self): + super().setUp() + self.temp_dir = self.env["fs.storage"].create( + { + "name": "Temp FS Storage", + "protocol": "memory", + "code": "mem_dir", + "directory_path": "/tmp/", + "model_xmlids": "fs_product_multi_media.model_fs_product_category_media," + "fs_product_multi_media.model_fs_product_media", + } + ) + + def test_available_attribute_value(self): + # The template have already 5 attribute values + # see demo data of ipad + media = self.env["fs.product.media"].new({"product_tmpl_id": self.template.id}) + expected = 4 + if self.is_sale_addon_installed: + expected += 1 + self.assertEqual(len(media.available_attribute_value_ids), expected) + + def test_add_image_for_all_variant(self): + self.assertEqual(len(self.product_a.variant_media_ids), 0) + media = self.env["fs.product.media"].create( + { + "product_tmpl_id": self.template.id, + "specific_file": { + "filename": "a.txt", + "content": base64.b64encode(self.media_content_a), + }, + } + ) + self.assertEqual(self.product_a.variant_media_ids, media) + self.assertEqual( + self.product_a.variant_media_ids.file.getvalue(), self.media_content_a + ) + self.assertEqual(self.product_b.variant_media_ids, media) + self.assertEqual( + self.product_b.variant_media_ids.file.getvalue(), self.media_content_a + ) + self.assertEqual(self.product_c.variant_media_ids, media) + self.assertEqual( + self.product_c.variant_media_ids.file.getvalue(), self.media_content_a + ) + + def test_add_media_for_white_variant(self): + media = self.env["fs.product.media"].create( + { + "product_tmpl_id": self.template.id, + "media_id": self.media_a.id, + "attribute_value_ids": [ + (6, 0, [self.env.ref("product.product_attribute_value_3").id]) + ], + } + ) + # White product should have the media + self.assertEqual(self.product_a.variant_media_ids, media) + self.assertEqual(self.product_c.variant_media_ids, media) + # Black product should not have the media + self.assertEqual(len(self.product_b.variant_media_ids), 0) + + def _create_multiple_medias(self): + media_c = self.env["fs.product.media"].create( + { + "product_tmpl_id": self.template.id, + "media_id": self.media_c.id, + "sequence": 10, + "link_existing": True, + } + ) + media_a = self.env["fs.product.media"].create( + { + "product_tmpl_id": self.template.id, + "media_id": self.media_a.id, + "attribute_value_ids": [ + (6, 0, [self.env.ref("product.product_attribute_value_3").id]) + ], + "sequence": 2, + "link_existing": True, + } + ) + media_b = self.env["fs.product.media"].create( + { + "product_tmpl_id": self.template.id, + "media_id": self.media_b.id, + "attribute_value_ids": [ + (6, 0, [self.env.ref("product.product_attribute_value_4").id]) + ], + "sequence": 1, + "link_existing": True, + } + ) + return media_c, media_a, media_b + + def test_add_media_for_white_and_black_variant(self): + media_c, media_a, media_b = self._create_multiple_medias() + # White product should have the media_a and the media_c + self.assertEqual(self.product_a.variant_media_ids, media_a + media_c) + self.assertEqual(self.product_c.variant_media_ids, media_a + media_c) + # Black product should have the media_b and the media_c + self.assertEqual(self.product_b.variant_media_ids, media_b + media_c) + + def test_media_variant_sequence(self): + media_c, media_a, media_b = self._create_multiple_medias() + # White product should have the media_a and the media_c + self.assertEqual(self.product_a.variant_media_ids, media_a + media_c) + # white product should have medias sorted by sequence + self.assertListEqual( + self.product_a.variant_media_ids.mapped("sequence"), + [media_a.sequence, media_c.sequence], + ) + # change sequence + media_c.sequence = 10 + media_a.sequence = 20 + self.assertListEqual( + self.product_a.variant_media_ids.mapped("sequence"), + [media_c.sequence, media_a.sequence], + ) + + def test_drop_template_attribute_value_propagation_to_media(self): + media_content_b = self.env["fs.product.media"].create( + { + "product_tmpl_id": self.template.id, + "media_id": self.media_b.id, + "attribute_value_ids": [ + ( + 6, + 0, + [ + self.env.ref("product.product_attribute_value_4").id, + self.env.ref("product.product_attribute_value_1").id, + ], + ) + ], + "sequence": 10, + "link_existing": True, + } + ) + # Remove Color black from variant tab: + self.template.attribute_line_ids.sudo().filtered( + lambda x: x.display_name == "Color" + ).value_ids -= self.env.ref("product.product_attribute_value_4") + # Attribute black is removed from media: + self.assertTrue( + self.env.ref("product.product_attribute_value_4") + not in media_content_b.attribute_value_ids + ) + + # Remove Leg attribute line from variant tab: + attr = self.template.attribute_line_ids.sudo().filtered( + lambda x: x.display_name == "Legs" + ) + attr.unlink() + # Product media attribute values from Legs are removed: + self.assertTrue( + self.env.ref("product.product_attribute_value_1") + not in media_content_b.attribute_value_ids + ) diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/views/fs_product_category_media.xml b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/views/fs_product_category_media.xml new file mode 100644 index 0000000..257f346 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/views/fs_product_category_media.xml @@ -0,0 +1,22 @@ + + + + + + product.category.fs.media.form + fs.product.category.media + + primary + + + + + + + + + diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/views/fs_product_media.xml b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/views/fs_product_media.xml new file mode 100644 index 0000000..0fda034 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/views/fs_product_media.xml @@ -0,0 +1,31 @@ + + + + + + fs.product.media.form + fs.product.media + + primary + + + + + + + + + diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/views/product_category.xml b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/views/product_category.xml new file mode 100644 index 0000000..ffba582 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/views/product_category.xml @@ -0,0 +1,24 @@ + + + + + + product.category.form + product.category + + + + + + + + + + + + + diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/views/product_product.xml b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/views/product_product.xml new file mode 100644 index 0000000..0ba5795 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/views/product_product.xml @@ -0,0 +1,19 @@ + + + + + product.product + + + + + + + + + + diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/views/product_template.xml b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/views/product_template.xml new file mode 100644 index 0000000..8959276 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_media/fs_product_multi_media/views/product_template.xml @@ -0,0 +1,20 @@ + + + + + product.template + + + + + + + + + + + diff --git a/odoo-bringout-oca-storage-fs_product_multi_media/pyproject.toml b/odoo-bringout-oca-storage-fs_product_multi_media/pyproject.toml new file mode 100644 index 0000000..1653f2c --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_multi_media/pyproject.toml @@ -0,0 +1,45 @@ +[project] +name = "odoo-bringout-oca-storage-fs_product_multi_media" +version = "16.0.0" +description = "Fs Product Multi Media - + Link media to products and categories" +authors = [ + { name = "Ernad Husremovic", email = "hernad@bring.out.ba" } +] +dependencies = [ + "odoo-bringout-oca-storage-fs_base_multi_media>=16.0.0", + "odoo-bringout-oca-ocb-product>=16.0.0", + "odoo-bringout-oca-ocb-sales_team>=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_product_multi_media"] + +[tool.rye] +managed = true +dev-dependencies = [ + "pytest>=8.4.1", +] diff --git a/odoo-bringout-oca-storage-fs_product_public_category_multi_image/README.md b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/README.md new file mode 100644 index 0000000..e36d504 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/README.md @@ -0,0 +1,48 @@ +# Fs Product Public Category Multi Image + +Odoo addon: fs_product_public_category_multi_image + +## Installation + +```bash +pip install odoo-bringout-oca-storage-fs_product_public_category_multi_image +``` + +## Dependencies + +This addon depends on: +- fs_base_multi_image +- website_sale +- image_tag + +## Manifest Information + +- **Name**: Fs Product Public Category Multi Image +- **Version**: 16.0.1.0.0 +- **Category**: N/A +- **License**: AGPL-3 +- **Installable**: False + +## Source + +Based on [OCA/storage](https://github.com/OCA/storage) branch 16.0, addon `fs_product_public_category_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 diff --git a/odoo-bringout-oca-storage-fs_product_public_category_multi_image/doc/ARCHITECTURE.md b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/doc/ARCHITECTURE.md new file mode 100644 index 0000000..a024c4b --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/doc/ARCHITECTURE.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_product_public_category_multi_image Module - fs_product_public_category_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. diff --git a/odoo-bringout-oca-storage-fs_product_public_category_multi_image/doc/CONFIGURATION.md b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/doc/CONFIGURATION.md new file mode 100644 index 0000000..dbafea0 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/doc/CONFIGURATION.md @@ -0,0 +1,3 @@ +# Configuration + +Refer to Odoo settings for fs_product_public_category_multi_image. Configure related models, access rights, and options as needed. diff --git a/odoo-bringout-oca-storage-fs_product_public_category_multi_image/doc/CONTROLLERS.md b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/doc/CONTROLLERS.md new file mode 100644 index 0000000..f628e77 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/doc/CONTROLLERS.md @@ -0,0 +1,3 @@ +# Controllers + +This module does not define custom HTTP controllers. diff --git a/odoo-bringout-oca-storage-fs_product_public_category_multi_image/doc/DEPENDENCIES.md b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/doc/DEPENDENCIES.md new file mode 100644 index 0000000..009b33a --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/doc/DEPENDENCIES.md @@ -0,0 +1,7 @@ +# Dependencies + +This addon depends on: + +- [fs_base_multi_image](../../odoo-bringout-oca-storage-fs_base_multi_image) +- [website_sale](../../odoo-bringout-oca-ocb-website_sale) +- [image_tag](../../odoo-bringout-oca-storage-image_tag) diff --git a/odoo-bringout-oca-storage-fs_product_public_category_multi_image/doc/FAQ.md b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/doc/FAQ.md new file mode 100644 index 0000000..70ca69f --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/doc/FAQ.md @@ -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_product_public_category_multi_image or install in UI. diff --git a/odoo-bringout-oca-storage-fs_product_public_category_multi_image/doc/INSTALL.md b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/doc/INSTALL.md new file mode 100644 index 0000000..2dffe5e --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/doc/INSTALL.md @@ -0,0 +1,7 @@ +# Install + +```bash +pip install odoo-bringout-oca-storage-fs_product_public_category_multi_image" +# or +uv pip install odoo-bringout-oca-storage-fs_product_public_category_multi_image" +``` diff --git a/odoo-bringout-oca-storage-fs_product_public_category_multi_image/doc/MODELS.md b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/doc/MODELS.md new file mode 100644 index 0000000..f1b9750 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/doc/MODELS.md @@ -0,0 +1,15 @@ +# Models + +Detected core models and extensions in fs_product_public_category_multi_image. + +```mermaid +classDiagram + class fs_product_public_category_image + class fs_image_relation_mixin + class image_tag + class product_public_category +``` + +Notes +- Classes show model technical names; fields omitted for brevity. +- Items listed under _inherit are extensions of existing models. diff --git a/odoo-bringout-oca-storage-fs_product_public_category_multi_image/doc/OVERVIEW.md b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/doc/OVERVIEW.md new file mode 100644 index 0000000..fd59f00 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/doc/OVERVIEW.md @@ -0,0 +1,6 @@ +# Overview + +Packaged Odoo addon: fs_product_public_category_multi_image. Provides features documented in upstream Odoo 16 under this addon. + +- Source: OCA/OCB 16.0, addon fs_product_public_category_multi_image +- License: LGPL-3 diff --git a/odoo-bringout-oca-storage-fs_product_public_category_multi_image/doc/REPORTS.md b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/doc/REPORTS.md new file mode 100644 index 0000000..e0ea35f --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/doc/REPORTS.md @@ -0,0 +1,3 @@ +# Reports + +This module does not define custom reports. diff --git a/odoo-bringout-oca-storage-fs_product_public_category_multi_image/doc/SECURITY.md b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/doc/SECURITY.md new file mode 100644 index 0000000..fce8c0a --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/doc/SECURITY.md @@ -0,0 +1,73 @@ +# Security + +Access control and security definitions in fs_product_public_category_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_product_public_category_image.xml](../fs_product_public_category_multi_image/security/fs_product_public_category_image.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_product_public_category_image.xml](../fs_product_public_category_multi_image/security/fs_product_public_category_image.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 diff --git a/odoo-bringout-oca-storage-fs_product_public_category_multi_image/doc/TROUBLESHOOTING.md b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/doc/TROUBLESHOOTING.md new file mode 100644 index 0000000..56853cb --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/doc/TROUBLESHOOTING.md @@ -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. diff --git a/odoo-bringout-oca-storage-fs_product_public_category_multi_image/doc/USAGE.md b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/doc/USAGE.md new file mode 100644 index 0000000..50575f0 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/doc/USAGE.md @@ -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_product_public_category_multi_image +``` diff --git a/odoo-bringout-oca-storage-fs_product_public_category_multi_image/doc/WIZARDS.md b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/doc/WIZARDS.md new file mode 100644 index 0000000..48e790d --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/doc/WIZARDS.md @@ -0,0 +1,3 @@ +# Wizards + +This module does not include UI wizards. diff --git a/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/README.rst b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/README.rst new file mode 100644 index 0000000..0a31535 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/README.rst @@ -0,0 +1,87 @@ +====================================== +Fs Product Public Category Multi Image +====================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:d738d87f799859c8be87121897cd1f848a68a1852585a074c5c00b987b974991 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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_product_public_category_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_product_public_category_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| + +Attach images to public categories + +.. 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 `_ + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +A) Public Categories + + Go to Website > eCommerce > Products > eCommerce Categories. + A new section Images is available to upload or use an existing image. + +For uploading and managing the images see the module fs_product_multi_image. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub 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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Contributors +~~~~~~~~~~~~ + +* Juany Davila +* Bernat Puig + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/storage `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/__init__.py b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/__manifest__.py b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/__manifest__.py new file mode 100644 index 0000000..dc19b2f --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/__manifest__.py @@ -0,0 +1,20 @@ +# Copyright 2024 ForgeFlow (http://www.forgeflow.com). +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Fs Product Public Category Multi Image", + "summary": """ + Manage multi images from extenal file system on eCommerce public categories""", + "version": "16.0.1.0.0", + "license": "AGPL-3", + "author": "Odoo Community Association (OCA)", + "website": "https://github.com/OCA/storage", + "depends": ["fs_base_multi_image", "website_sale", "image_tag"], + "data": [ + "security/fs_product_public_category_image.xml", + "views/fs_product_public_category_image.xml", + "views/product_public_category.xml", + ], + "demo": [], + "development_status": "Alpha", +} diff --git a/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/i18n/bs.po b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/i18n/bs.po new file mode 100644 index 0000000..8612283 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/i18n/bs.po @@ -0,0 +1,133 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_product_public_category_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_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_image_tag__apply_on +msgid "Apply On" +msgstr "Primjeni na" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__create_uid +msgid "Created by" +msgstr "Kreirao" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__create_date +msgid "Created on" +msgstr "Kreirano" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__display_name +msgid "Display Name" +msgstr "Prikazani naziv" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__id +msgid "ID" +msgstr "ID" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__image_medium +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_product_public_category__image_medium +msgid "Image (128)" +msgstr "Slika (128)" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_product_public_category__image +msgid "Image (original)" +msgstr "Slika (originalna)" + +#. module: fs_product_public_category_multi_image +#: model:ir.model,name:fs_product_public_category_multi_image.model_image_tag +msgid "Image Tag" +msgstr "Oznaka slike" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_product_public_category__image_ids +#: model_terms:ir.ui.view,arch_db:fs_product_public_category_multi_image.product_public_category_form_view +msgid "Images" +msgstr "Slike" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image____last_update +msgid "Last Modified on" +msgstr "Zadnje mijenjano" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__write_uid +msgid "Last Updated by" +msgstr "Zadnji ažurirao" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__write_date +msgid "Last Updated on" +msgstr "Zadnje ažurirano" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__link_existing +msgid "Link Existing" +msgstr "Povezivanje postojećeg" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__image_id +msgid "Linked image" +msgstr "Povezana slika" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__mimetype +msgid "Mimetype" +msgstr "Mimetype" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__name +msgid "Name" +msgstr "Naziv:" + +#. module: fs_product_public_category_multi_image +#: model:ir.model,name:fs_product_public_category_multi_image.model_fs_product_public_category_image +msgid "Product Public Category Image" +msgstr "Slika javne kategorije proizvoda" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__public_category_id +#: model:ir.model.fields.selection,name:fs_product_public_category_multi_image.selection__image_tag__apply_on__public_category +msgid "Public Category" +msgstr "Javna kategorija" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__sequence +msgid "Sequence" +msgstr "Sekvenca" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__specific_image +msgid "Specific Image" +msgstr "Specifična slika" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__specific_image_medium +msgid "Specific Image (128)" +msgstr "Specifična slika (128)" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__tag_id +msgid "Tag" +msgstr "Oznaka" + +#. module: fs_product_public_category_multi_image +#: model:ir.model,name:fs_product_public_category_multi_image.model_product_public_category +msgid "Website Product Category" +msgstr "Websajt kategorija proizvoda" diff --git a/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/i18n/fs_product_public_category_multi_image.pot b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/i18n/fs_product_public_category_multi_image.pot new file mode 100644 index 0000000..55167f9 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/i18n/fs_product_public_category_multi_image.pot @@ -0,0 +1,133 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_product_public_category_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_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_image_tag__apply_on +msgid "Apply On" +msgstr "" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__create_uid +msgid "Created by" +msgstr "" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__create_date +msgid "Created on" +msgstr "" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__display_name +msgid "Display Name" +msgstr "" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__id +msgid "ID" +msgstr "" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__image_medium +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_product_public_category__image_medium +msgid "Image (128)" +msgstr "" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_product_public_category__image +msgid "Image (original)" +msgstr "" + +#. module: fs_product_public_category_multi_image +#: model:ir.model,name:fs_product_public_category_multi_image.model_image_tag +msgid "Image Tag" +msgstr "" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_product_public_category__image_ids +#: model_terms:ir.ui.view,arch_db:fs_product_public_category_multi_image.product_public_category_form_view +msgid "Images" +msgstr "" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image____last_update +msgid "Last Modified on" +msgstr "" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__write_date +msgid "Last Updated on" +msgstr "" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__link_existing +msgid "Link Existing" +msgstr "" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__image_id +msgid "Linked image" +msgstr "" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__mimetype +msgid "Mimetype" +msgstr "" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__name +msgid "Name" +msgstr "" + +#. module: fs_product_public_category_multi_image +#: model:ir.model,name:fs_product_public_category_multi_image.model_fs_product_public_category_image +msgid "Product Public Category Image" +msgstr "" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__public_category_id +#: model:ir.model.fields.selection,name:fs_product_public_category_multi_image.selection__image_tag__apply_on__public_category +msgid "Public Category" +msgstr "" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__sequence +msgid "Sequence" +msgstr "" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__specific_image +msgid "Specific Image" +msgstr "" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__specific_image_medium +msgid "Specific Image (128)" +msgstr "" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__tag_id +msgid "Tag" +msgstr "" + +#. module: fs_product_public_category_multi_image +#: model:ir.model,name:fs_product_public_category_multi_image.model_product_public_category +msgid "Website Product Category" +msgstr "" diff --git a/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/i18n/it.po b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/i18n/it.po new file mode 100644 index 0000000..21739d1 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/i18n/it.po @@ -0,0 +1,136 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_product_public_category_multi_image +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-11-11 14:06+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.6.2\n" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_image_tag__apply_on +msgid "Apply On" +msgstr "Applica a" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__create_uid +msgid "Created by" +msgstr "Creato da" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__create_date +msgid "Created on" +msgstr "Creato il" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__display_name +msgid "Display Name" +msgstr "Nome visualizzato" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__id +msgid "ID" +msgstr "ID" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__image_medium +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_product_public_category__image_medium +msgid "Image (128)" +msgstr "Immagine 128" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_product_public_category__image +msgid "Image (original)" +msgstr "Immagine (originale)" + +#. module: fs_product_public_category_multi_image +#: model:ir.model,name:fs_product_public_category_multi_image.model_image_tag +msgid "Image Tag" +msgstr "Etichetta immagine" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_product_public_category__image_ids +#: model_terms:ir.ui.view,arch_db:fs_product_public_category_multi_image.product_public_category_form_view +msgid "Images" +msgstr "Immagini" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image____last_update +msgid "Last Modified on" +msgstr "Ultima modifica il" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__write_uid +msgid "Last Updated by" +msgstr "Ultimo aggiornamento di" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__write_date +msgid "Last Updated on" +msgstr "Ultimo aggiornamento il" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__link_existing +msgid "Link Existing" +msgstr "Collegamento esistente" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__image_id +msgid "Linked image" +msgstr "Immagine collegata" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__mimetype +msgid "Mimetype" +msgstr "Tipo MIME" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__name +msgid "Name" +msgstr "Nome" + +#. module: fs_product_public_category_multi_image +#: model:ir.model,name:fs_product_public_category_multi_image.model_fs_product_public_category_image +msgid "Product Public Category Image" +msgstr "Immagine prodotto categoria pubblica" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__public_category_id +#: model:ir.model.fields.selection,name:fs_product_public_category_multi_image.selection__image_tag__apply_on__public_category +msgid "Public Category" +msgstr "Categoria pubblica" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__sequence +msgid "Sequence" +msgstr "Sequenza" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__specific_image +msgid "Specific Image" +msgstr "Immagine specifica" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__specific_image_medium +msgid "Specific Image (128)" +msgstr "Immagine specifica (128)" + +#. module: fs_product_public_category_multi_image +#: model:ir.model.fields,field_description:fs_product_public_category_multi_image.field_fs_product_public_category_image__tag_id +msgid "Tag" +msgstr "Etichetta" + +#. module: fs_product_public_category_multi_image +#: model:ir.model,name:fs_product_public_category_multi_image.model_product_public_category +msgid "Website Product Category" +msgstr "Categoria prodotto sito web" diff --git a/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/models/__init__.py b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/models/__init__.py new file mode 100644 index 0000000..a9ec9b9 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/models/__init__.py @@ -0,0 +1,3 @@ +from . import fs_product_public_category_image +from . import image_tag +from . import product_public_category diff --git a/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/models/fs_product_public_category_image.py b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/models/fs_product_public_category_image.py new file mode 100644 index 0000000..4a20840 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/models/fs_product_public_category_image.py @@ -0,0 +1,23 @@ +# Copyright 2024 ForgeFlow (http://www.forgeflow.com). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import fields, models + + +class FsProductPublicCategoryImage(models.Model): + _name = "fs.product.public.category.image" + _inherit = "fs.image.relation.mixin" + _description = "Product Public Category Image" + + public_category_id = fields.Many2one( + "product.public.category", + required=True, + ondelete="cascade", + index=True, + ) + tag_id = fields.Many2one( + "image.tag", + string="Tag", + domain=[("apply_on", "=", "public.category")], + index=True, + ) diff --git a/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/models/image_tag.py b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/models/image_tag.py new file mode 100644 index 0000000..3d757b8 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/models/image_tag.py @@ -0,0 +1,23 @@ +# Copyright 2024 ForgeFlow (http://www.forgeflow.com). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + + +from odoo import api, fields, models + + +class ImageTag(models.Model): + _inherit = "image.tag" + + @api.model + def _get_default_apply_on(self): + active_model = self.env.context.get("active_model") + return ( + "public.category" + if active_model == "public.category.image.relation" + else super()._get_default_apply_on() + ) + + apply_on = fields.Selection( + selection_add=[("public.category", "Public Category")], + ondelete={"product": "cascade", "category": "cascade"}, + ) diff --git a/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/models/product_public_category.py b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/models/product_public_category.py new file mode 100644 index 0000000..228cceb --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/models/product_public_category.py @@ -0,0 +1,18 @@ +# Copyright 2024 ForgeFlow (http://www.forgeflow.com). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import fields, models + +from odoo.addons.fs_image.fields import FSImage + + +class ProductPublicCategory(models.Model): + _inherit = "product.public.category" + + image_ids = fields.One2many( + string="Images", + comodel_name="fs.product.public.category.image", + inverse_name="public_category_id", + ) + image = FSImage(related="image_ids.image", readonly=True, store=False) + image_medium = FSImage(related="image_ids.image_medium", readonly=True, store=False) diff --git a/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/readme/CONTRIBUTORS.rst b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000..2c8e1c7 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Juany Davila +* Bernat Puig diff --git a/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/readme/DESCRIPTION.rst b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/readme/DESCRIPTION.rst new file mode 100644 index 0000000..e021256 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +Attach images to public categories diff --git a/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/readme/USAGE.rst b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/readme/USAGE.rst new file mode 100644 index 0000000..486c849 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/readme/USAGE.rst @@ -0,0 +1,6 @@ +A) Public Categories + + Go to Website > eCommerce > Products > eCommerce Categories. + A new section Images is available to upload or use an existing image. + +For uploading and managing the images see the module fs_product_multi_image. diff --git a/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/security/fs_product_public_category_image.xml b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/security/fs_product_public_category_image.xml new file mode 100644 index 0000000..12898d8 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/security/fs_product_public_category_image.xml @@ -0,0 +1,34 @@ + + + + + + fs.product.public.category.image access read + + + + + + + + + fs.product.public.category.image access system admin + + + + + + + + + fs.product.public.category.image access sales manager + + + + + + + + + diff --git a/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/static/description/icon.png b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/static/description/icon.png new file mode 100644 index 0000000..3a0328b Binary files /dev/null and b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/static/description/icon.png differ diff --git a/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/static/description/index.html b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/static/description/index.html new file mode 100644 index 0000000..4cbe215 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/static/description/index.html @@ -0,0 +1,434 @@ + + + + + +Fs Product Public Category Multi Image + + + +
+

Fs Product Public Category Multi Image

+ + +

Alpha License: AGPL-3 OCA/storage Translate me on Weblate Try me on Runboat

+

Attach images to public categories

+
+

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

+
+

Table of contents

+ +
+

Usage

+
    +
  1. Public Categories

    +

    Go to Website > eCommerce > Products > eCommerce Categories. +A new section Images is available to upload or use an existing image.

    +
  2. +
+

For uploading and managing the images see the module fs_product_multi_image.

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub 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.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+ +
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/storage project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/tests/__init__.py b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/tests/__init__.py new file mode 100644 index 0000000..c686b22 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/tests/__init__.py @@ -0,0 +1 @@ +from . import test_fs_product_multi_image diff --git a/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/tests/test_fs_product_multi_image.py b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/tests/test_fs_product_multi_image.py new file mode 100644 index 0000000..35e64f1 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/tests/test_fs_product_multi_image.py @@ -0,0 +1,92 @@ +# Copyright 2017 Akretion (http://www.akretion.com). +# Copyright 2023 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import base64 +import io + +from PIL import Image + +from odoo.tests.common import TransactionCase + + +class TestFsProductMultiImage(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.white_image = cls._create_image(16, 16, color="#FFFFFF") + cls.logo_image = cls._create_image(16, 16, color="#FFA500") + cls.product_public_category = cls.env["product.public.category"].create( + { + "name": "Public Category", + } + ) + cls.image_white = cls.env["fs.image"].create( + { + "image": { + "filename": "white.png", + "content": base64.b64encode(cls.white_image), + } + } + ) + cls.image_logo = cls.env["fs.image"].create( + { + "image": { + "filename": "logo.png", + "content": base64.b64encode(cls.logo_image), + } + } + ) + cls.image_tag = cls.env["image.tag"].create( + { + "name": "Icon", + "apply_on": "public.category", + } + ) + + def setUp(self): + super().setUp() + self.temp_dir = self.env["fs.storage"].create( + { + "name": "Temp FS Storage", + "protocol": "memory", + "code": "mem_dir", + "directory_path": "/tmp/", + "model_xmlids": "fs_product_multi_image.model_fs_product_category_image," + "fs_product_multi_image.model_fs_product_image", + } + ) + + @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 _create_multiple_images(self): + logo = self.env["fs.product.public.category.image"].create( + { + "public_category_id": self.product_public_category.id, + "tag_id": self.image_tag.id, + "image_id": self.image_logo.id, + "sequence": 10, + "link_existing": True, + } + ) + image_wh = self.env["fs.product.public.category.image"].create( + { + "public_category_id": self.product_public_category.id, + "tag_id": self.image_tag.id, + "image_id": self.image_white.id, + "sequence": 2, + "link_existing": True, + } + ) + return logo, image_wh + + def test_add_image_for_product_public_category(self): + logo, image_wh = self._create_multiple_images() + # White product should have the white image and the logo + self.assertEqual(self.product_public_category.image_ids, image_wh + logo) diff --git a/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/views/fs_product_public_category_image.xml b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/views/fs_product_public_category_image.xml new file mode 100644 index 0000000..6fd05b9 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/views/fs_product_public_category_image.xml @@ -0,0 +1,24 @@ + + + + + + product.public.category.fs.image.form + fs.product.public.category.image + + primary + + + + + + + + diff --git a/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/views/product_public_category.xml b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/views/product_public_category.xml new file mode 100644 index 0000000..73e3e39 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/fs_product_public_category_multi_image/views/product_public_category.xml @@ -0,0 +1,22 @@ + + + + product.public.category + + + +
+ + + + + +
+
+
+
+
diff --git a/odoo-bringout-oca-storage-fs_product_public_category_multi_image/pyproject.toml b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/pyproject.toml new file mode 100644 index 0000000..600d4e9 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_product_public_category_multi_image/pyproject.toml @@ -0,0 +1,45 @@ +[project] +name = "odoo-bringout-oca-storage-fs_product_public_category_multi_image" +version = "16.0.0" +description = "Fs Product Public Category Multi Image - + Manage multi images from extenal file system on eCommerce public categories" +authors = [ + { name = "Ernad Husremovic", email = "hernad@bring.out.ba" } +] +dependencies = [ + "odoo-bringout-oca-storage-fs_base_multi_image>=16.0.0", + "odoo-bringout-oca-ocb-website_sale>=16.0.0", + "odoo-bringout-oca-storage-image_tag>=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_product_public_category_multi_image"] + +[tool.rye] +managed = true +dev-dependencies = [ + "pytest>=8.4.1", +] diff --git a/odoo-bringout-oca-storage-fs_storage/README.md b/odoo-bringout-oca-storage-fs_storage/README.md new file mode 100644 index 0000000..b55902b --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage/README.md @@ -0,0 +1,48 @@ +# Filesystem Storage Backend + +Odoo addon: fs_storage + +## Installation + +```bash +pip install odoo-bringout-oca-storage-fs_storage +``` + +## Dependencies + +This addon depends on: +- base +- base_sparse_field +- server_environment + +## Manifest Information + +- **Name**: Filesystem Storage Backend +- **Version**: 16.0.1.3.5 +- **Category**: FS Storage +- **License**: LGPL-3 +- **Installable**: True + +## Source + +Based on [OCA/storage](https://github.com/OCA/storage) branch 16.0, addon `fs_storage`. + +## License + +This package maintains the original LGPL-3 license from the upstream Odoo project. + +## Documentation + +- Overview: doc/OVERVIEW.md +- Architecture: doc/ARCHITECTURE.md +- Models: doc/MODELS.md +- Controllers: doc/CONTROLLERS.md +- Wizards: doc/WIZARDS.md +- 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 diff --git a/odoo-bringout-oca-storage-fs_storage/doc/ARCHITECTURE.md b/odoo-bringout-oca-storage-fs_storage/doc/ARCHITECTURE.md new file mode 100644 index 0000000..4a45402 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage/doc/ARCHITECTURE.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_storage Module - fs_storage + 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. diff --git a/odoo-bringout-oca-storage-fs_storage/doc/CONFIGURATION.md b/odoo-bringout-oca-storage-fs_storage/doc/CONFIGURATION.md new file mode 100644 index 0000000..78cf6ec --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage/doc/CONFIGURATION.md @@ -0,0 +1,3 @@ +# Configuration + +Refer to Odoo settings for fs_storage. Configure related models, access rights, and options as needed. diff --git a/odoo-bringout-oca-storage-fs_storage/doc/CONTROLLERS.md b/odoo-bringout-oca-storage-fs_storage/doc/CONTROLLERS.md new file mode 100644 index 0000000..f628e77 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage/doc/CONTROLLERS.md @@ -0,0 +1,3 @@ +# Controllers + +This module does not define custom HTTP controllers. diff --git a/odoo-bringout-oca-storage-fs_storage/doc/DEPENDENCIES.md b/odoo-bringout-oca-storage-fs_storage/doc/DEPENDENCIES.md new file mode 100644 index 0000000..b48a32f --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage/doc/DEPENDENCIES.md @@ -0,0 +1,7 @@ +# Dependencies + +This addon depends on: + +- base +- [base_sparse_field](../../odoo-bringout-oca-ocb-base_sparse_field) +- server_environment diff --git a/odoo-bringout-oca-storage-fs_storage/doc/FAQ.md b/odoo-bringout-oca-storage-fs_storage/doc/FAQ.md new file mode 100644 index 0000000..278e177 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage/doc/FAQ.md @@ -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_storage or install in UI. diff --git a/odoo-bringout-oca-storage-fs_storage/doc/INSTALL.md b/odoo-bringout-oca-storage-fs_storage/doc/INSTALL.md new file mode 100644 index 0000000..5c91820 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage/doc/INSTALL.md @@ -0,0 +1,7 @@ +# Install + +```bash +pip install odoo-bringout-oca-storage-fs_storage" +# or +uv pip install odoo-bringout-oca-storage-fs_storage" +``` diff --git a/odoo-bringout-oca-storage-fs_storage/doc/MODELS.md b/odoo-bringout-oca-storage-fs_storage/doc/MODELS.md new file mode 100644 index 0000000..dc6dde1 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage/doc/MODELS.md @@ -0,0 +1,13 @@ +# Models + +Detected core models and extensions in fs_storage. + +```mermaid +classDiagram + class fs_storage + class server_env_mixin +``` + +Notes +- Classes show model technical names; fields omitted for brevity. +- Items listed under _inherit are extensions of existing models. diff --git a/odoo-bringout-oca-storage-fs_storage/doc/OVERVIEW.md b/odoo-bringout-oca-storage-fs_storage/doc/OVERVIEW.md new file mode 100644 index 0000000..d950aaa --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage/doc/OVERVIEW.md @@ -0,0 +1,6 @@ +# Overview + +Packaged Odoo addon: fs_storage. Provides features documented in upstream Odoo 16 under this addon. + +- Source: OCA/OCB 16.0, addon fs_storage +- License: LGPL-3 diff --git a/odoo-bringout-oca-storage-fs_storage/doc/REPORTS.md b/odoo-bringout-oca-storage-fs_storage/doc/REPORTS.md new file mode 100644 index 0000000..e0ea35f --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage/doc/REPORTS.md @@ -0,0 +1,3 @@ +# Reports + +This module does not define custom reports. diff --git a/odoo-bringout-oca-storage-fs_storage/doc/SECURITY.md b/odoo-bringout-oca-storage-fs_storage/doc/SECURITY.md new file mode 100644 index 0000000..aa08085 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage/doc/SECURITY.md @@ -0,0 +1,34 @@ +# Security + +Access control and security definitions in fs_storage. + +## Access Control Lists (ACLs) + +Model access permissions defined in: +- **[ir.model.access.csv](../fs_storage/security/ir.model.access.csv)** + - 2 model access rules + +## Record Rules + +Row-level security rules defined in: + +```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: +- **[ir.model.access.csv](../fs_storage/security/ir.model.access.csv)** + - Model access permissions (CRUD rights) + +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 diff --git a/odoo-bringout-oca-storage-fs_storage/doc/TROUBLESHOOTING.md b/odoo-bringout-oca-storage-fs_storage/doc/TROUBLESHOOTING.md new file mode 100644 index 0000000..56853cb --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage/doc/TROUBLESHOOTING.md @@ -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. diff --git a/odoo-bringout-oca-storage-fs_storage/doc/USAGE.md b/odoo-bringout-oca-storage-fs_storage/doc/USAGE.md new file mode 100644 index 0000000..0e104fe --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage/doc/USAGE.md @@ -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_storage +``` diff --git a/odoo-bringout-oca-storage-fs_storage/doc/WIZARDS.md b/odoo-bringout-oca-storage-fs_storage/doc/WIZARDS.md new file mode 100644 index 0000000..48e790d --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage/doc/WIZARDS.md @@ -0,0 +1,3 @@ +# Wizards + +This module does not include UI wizards. diff --git a/odoo-bringout-oca-storage-fs_storage/fs_storage/README.rst b/odoo-bringout-oca-storage-fs_storage/fs_storage/README.rst new file mode 100644 index 0000000..c8e3309 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage/fs_storage/README.rst @@ -0,0 +1,291 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +========================== +Filesystem Storage Backend +========================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:b9f95387306ce78e4543bc0b90f958fa188ba244dd6df41af486078d2d358fdf + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstorage-lightgray.png?logo=github + :target: https://github.com/OCA/storage/tree/16.0/fs_storage + :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_storage + :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 that allows you to define filesystem like +storage for your data. It's used by other addons to store their data in a +transparent way into different kind of storages. + +Through the fs.storage record, you get access to an object that implements +the `fsspec.spec.AbstractFileSystem `_ interface and therefore give +you an unified interface to access your data whatever the storage protocol you +decide to use. + +The list of supported protocols depends on the installed fsspec implementations. +By default, the addon will install the following protocols: + +* LocalFileSystem +* MemoryFileSystem +* ZipFileSystem +* TarFileSystem +* FTPFileSystem +* CachingFileSystem +* WholeFileSystem +* SimplCacheFileSystem +* ReferenceFileSystem +* GenericFileSystem +* DirFileSystem +* DatabricksFileSystem +* GitHubFileSystem +* JupiterFileSystem +* OdooFileSystem + +The OdooFileSystem is the one that allows you to store your data into a directory +mounted into your Odoo's storage directory. This is the default FS Storage +when creating a new fs.storage record. + +Others protocols are available through the installation of additional +python packages: + +* DropboxDriveFileSystem -> `pip install fsspec[dropbox]` +* HTTPFileSystem -> `pip install fsspec[http]` +* HTTPSFileSystem -> `pip install fsspec[http]` +* GCSFileSystem -> `pip install fsspec[gcs]` +* GSFileSystem -> `pip install fsspec[gs]` +* GoogleDriveFileSystem -> `pip install gdrivefs` +* SFTPFileSystem -> `pip install fsspec[sftp]` +* HaddoopFileSystem -> `pip install fsspec[hdfs]` +* S3FileSystem -> `pip install fsspec[s3]` +* WandbFS -> `pip install wandbfs` +* OCIFileSystem -> `pip install fsspec[oci]` +* AsyncLocalFileSystem -> `pip install 'morefs[asynclocalfs]` +* AzureDatalakeFileSystem -> `pip install fsspec[adl]` +* AzureBlobFileSystem -> `pip install fsspec[abfs]` +* DaskWorkerFileSystem -> `pip install fsspec[dask]` +* GitFileSystem -> `pip install fsspec[git]` +* SMBFileSystem -> `pip install fsspec[smb]` +* LibArchiveFileSystem -> `pip install fsspec[libarchive]` +* OSSFileSystem -> `pip install ossfs` +* WebdavFileSystem -> `pip install webdav4` +* DVCFileSystem -> `pip install dvc` +* XRootDFileSystem -> `pip install fsspec-xrootd` + +This list of supported protocols is not exhaustive or could change in the future +depending on the fsspec releases. You can find more information about the +supported protocols on the `fsspec documentation +`_. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +Configuration +~~~~~~~~~~~~~ + +When you create a new backend, you must specify the following: + +* The name of the backend. This is the name that will be used to + identify the backend into Odoo +* The code of the backend. This code will identify the backend into the store_fname + field of the ir.attachment model. This code must be unique. It will be used + as scheme. example of the store_fname field: ``odoofs://abs34Tg11``. +* The protocol used by the backend. The protocol refers to the supported + protocols of the fsspec python package. +* A directory path. This is a root directory from which the filesystem will + be mounted. This directory must exist. +* The protocol options. These are the options that will be passed to the + fsspec python package when creating the filesystem. These options depend + on the protocol used and are described in the fsspec documentation. +* Resolve env vars. This options resolves the protocol options values starting + with $ from environment variables +* Check Connection Method. If set, Odoo will always check the connection before using + a storage and it will remove the fs connection from the cache if the check fails. + + * ``Create Marker file`` : create a hidden file on remote and then check it exists with + Use it if you have write access to the remote and if it is not an issue to leave + the marker file in the root directory. + * ``List file`` : list all files from the root directory. You can use it if the directory + path does not contain a big list of files (for performance reasons) + +Some protocols defined in the fsspec package are wrappers around other +protocols. For example, the SimpleCacheFileSystem protocol is a wrapper +around any local filesystem protocol. In such cases, you must specify into the +protocol options the protocol to be wrapped and the options to be passed to +the wrapped protocol. + +For example, if you want to create a backend that uses the SimpleCacheFileSystem +protocol, after selecting the SimpleCacheFileSystem protocol, you must specify +the protocol options as follows: + +.. code-block:: python + + { + "directory_path": "/tmp/my_backend", + "target_protocol": "odoofs", + "target_options": {...}, + } + +In this example, the SimpleCacheFileSystem protocol will be used as a wrapper +around the odoofs protocol. + +Server Environment +~~~~~~~~~~~~~~~~~~ + +To ease the management of the filesystem storages configuration accross the different +environments, the configuration of the filesystem storages can be defined in +environment files or directly in the main configuration file. For example, the +configuration of a filesystem storage with the code `fsprod` can be provided in the +main configuration file as follows: + +.. code-block:: ini + + [fs_storage.fsprod] + protocol=s3 + options={"endpoint_url": "https://my_s3_server/", "key": "KEY", "secret": "SECRET"} + directory_path=my_bucket + +To work, a `storage.backend` record must exist with the code `fsprod` into the database. +In your configuration section, you can specify the value for the following fields: + +* `protocol` +* `options` +* `directory_path` + +Migration from storage_backend +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The fs_storage addon can be used to replace the storage_backend addon. (It has +been designed to be a drop-in replacement for the storage_backend addon). To +ease the migration, the `fs.storage` model defines the high-level methods +available in the storage_backend model. These methods are: + +* `add` +* `get` +* `list_files` +* `find_files` +* `move_files` +* `delete` + +These methods are wrappers around the methods of the `fsspec.AbstractFileSystem` +class (see https://filesystem-spec.readthedocs.io/en/latest/api.html#fsspec.spec.AbstractFileSystem). +These methods are marked as deprecated and will be removed in a future version (V18) +of the addon. You should use the methods of the `fsspec.AbstractFileSystem` class +instead since they are more flexible and powerful. You can access the instance +of the `fsspec.AbstractFileSystem` class using the `fs` property of a `fs.storage` +record. + +Known issues / Roadmap +====================== + +* Transactions: fsspec comes with a transactional mechanism that once started, + gathers all the files created during the transaction, and if the transaction + is committed, moves them to their final locations. It would be useful to + bridge this with the transactional mechanism of odoo. This would allow to + ensure that all the files created during a transaction are either all + moved to their final locations, or all deleted if the transaction is rolled + back. This mechanism is only valid for files created during the transaction + by a call to the `open` method of the file system. It is not valid for others + operations, such as `rm`, `mv_file`, ... . + +Changelog +========= + +16.0.1.2.0 (2024-02-06) +~~~~~~~~~~~~~~~~~~~~~~~ + +**Features** + +- Invalidate FS filesystem object cache when the connection fails, forcing a reconnection. (`#320 `_) + + +16.0.1.1.0 (2023-12-22) +~~~~~~~~~~~~~~~~~~~~~~~ + +**Features** + +- Add parameter on storage backend to resolve protocol options values starting with $ from environment variables (`#303 `_) + + +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 `_) + + +16.0.1.0.2 (2023-10-09) +~~~~~~~~~~~~~~~~~~~~~~~ + +**Bugfixes** + +- Avoid config error when using the webdav protocol. The auth option is expected + to be a tuple not a list. Since our config is loaded from a json file, we + cannot use tuples. The fix converts the list to a tuple when the config is + related to a webdav protocol and the auth option is into the confix. (`#285 `_) + +Bug Tracker +=========== + +Bugs are tracked on `GitHub 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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* ACSONE SA/NV + +Contributors +~~~~~~~~~~~~ + +* Laurent Mignon +* Sébastien BEAU + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/storage `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/odoo-bringout-oca-storage-fs_storage/fs_storage/__init__.py b/odoo-bringout-oca-storage-fs_storage/fs_storage/__init__.py new file mode 100644 index 0000000..6f3a6c7 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage/fs_storage/__init__.py @@ -0,0 +1,7 @@ +# register protocols first +from . import odoo_file_system +from . import rooted_dir_file_system + +# then add normal imports +from . import models +from . import wizards diff --git a/odoo-bringout-oca-storage-fs_storage/fs_storage/__manifest__.py b/odoo-bringout-oca-storage-fs_storage/fs_storage/__manifest__.py new file mode 100644 index 0000000..e3d3e9e --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage/fs_storage/__manifest__.py @@ -0,0 +1,23 @@ +# Copyright 2017 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +{ + "name": "Filesystem Storage Backend", + "summary": "Implement the concept of Storage with amazon S3, sftp...", + "version": "16.0.1.3.5", + "category": "FS Storage", + "website": "https://github.com/OCA/storage", + "author": " ACSONE SA/NV, Odoo Community Association (OCA)", + "license": "LGPL-3", + "development_status": "Beta", + "installable": True, + "depends": ["base", "base_sparse_field", "server_environment"], + "data": [ + "views/fs_storage_view.xml", + "security/ir.model.access.csv", + "wizards/fs_test_connection.xml", + ], + "demo": ["demo/fs_storage.xml"], + "external_dependencies": {"python": ["fsspec>=2024.5.0"]}, +} diff --git a/odoo-bringout-oca-storage-fs_storage/fs_storage/demo/fs_storage.xml b/odoo-bringout-oca-storage-fs_storage/fs_storage/demo/fs_storage.xml new file mode 100644 index 0000000..a1fd01f --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage/fs_storage/demo/fs_storage.xml @@ -0,0 +1,8 @@ + + + + Odoo Filesystem Backend + odoofs + odoofs + + diff --git a/odoo-bringout-oca-storage-fs_storage/fs_storage/i18n/bs.po b/odoo-bringout-oca-storage-fs_storage/fs_storage/i18n/bs.po new file mode 100644 index 0000000..e3b2a5e --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage/fs_storage/i18n/bs.po @@ -0,0 +1,325 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_storage +# +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_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +msgid "Available options" +msgstr "Dostupne opcije" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__options_properties +msgid "Available properties" +msgstr "Dostupna svojstva" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__check_connection_method +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection__check_connection_method +msgid "Check Connection Method" +msgstr "Metoda provjere konekcije" + +#. module: fs_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_test_connection_form_view +msgid "Close" +msgstr "Zatvori" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__code +msgid "Code" +msgstr "Šifra" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "Connection Test Failed!" +msgstr "Provjera povezivanja nije uspjela!" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "Connection Test Succeeded!" +msgstr "Provjera povezivanja uspješna!" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "Create Marker file" +msgstr "Kreiraj marker datoteku" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__create_uid +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection__create_uid +msgid "Created by" +msgstr "Kreirao" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__create_date +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection__create_date +msgid "Created on" +msgstr "Kreirano" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__options_protocol +msgid "Describes Protocol" +msgstr "Opisuje protokol" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__directory_path +msgid "Directory Path" +msgstr "Putanja direktorija" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__directory_path_env_default +msgid "Directory Path Env Default" +msgstr "Env zadana putanja direktorija" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__directory_path_env_is_editable +msgid "Directory Path Env Is Editable" +msgstr "Env putanja direktorija je uređiva" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__display_name +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection__display_name +msgid "Display Name" +msgstr "Prikazani naziv" + +#. module: fs_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +msgid "Enter you fsspec options here." +msgstr "Unesite vaše fsspec opcije ovdje." + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__eval_options_from_env_env_is_editable +msgid "Eval Options From Env Env Is Editable" +msgstr "Eval opcije iz Env Env je uređivo" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "Everything seems properly set up!" +msgstr "Sve izgleda ispravno podešeno!" + +#. module: fs_storage +#: model:ir.actions.act_window,name:fs_storage.act_open_fs_storage_view +#: model:ir.model,name:fs_storage.model_fs_storage +#: model:ir.ui.menu,name:fs_storage.menu_fs_storage +#: model:ir.ui.menu,name:fs_storage.menu_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_search_view +msgid "FS Storage" +msgstr "FS skladište" + +#. module: fs_storage +#: model:ir.actions.act_window,name:fs_storage.act_open_fs_test_connection_view +msgid "FS Test Connection" +msgstr "FS testiranje konekcije" + +#. module: fs_storage +#: model:ir.model,name:fs_storage.model_fs_test_connection +msgid "FS Test Connection Wizard" +msgstr "FS čarobnjak testiranja konekcije" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__id +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection__id +msgid "ID" +msgstr "ID" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__json_options +msgid "Json Options" +msgstr "Json opcije" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage____last_update +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection____last_update +msgid "Last Modified on" +msgstr "Zadnje mijenjano" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__write_uid +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection__write_uid +msgid "Last Updated by" +msgstr "Zadnji ažurirao" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__write_date +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection__write_date +msgid "Last Updated on" +msgstr "Zadnje ažurirano" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "List File" +msgstr "Lista datoteka" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__name +msgid "Name" +msgstr "Naziv:" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__options +msgid "Options" +msgstr "Opcije" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__options_env_default +msgid "Options Env Default" +msgstr "Env zadane opcije" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__options_env_is_editable +msgid "Options Env Is Editable" +msgstr "Env opcije je uređivo" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__protocol +msgid "Protocol" +msgstr "Protokol" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__protocol_descr +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +msgid "Protocol Descr" +msgstr "Opis protokola" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__protocol_env_default +msgid "Protocol Env Default" +msgstr "Zadana vrijednost protokola okruženja" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__protocol_env_is_editable +msgid "Protocol Env Is Editable" +msgstr "Protokol okruženja može se mijenjati" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__directory_path +#: model:ir.model.fields,help:fs_storage.field_fs_storage__directory_path_env_default +msgid "Relative path to the directory to store the file" +msgstr "Relativni put do direktorija za čuvanje datoteka" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__eval_options_from_env +msgid "Resolve env vars" +msgstr "Riješiti env vars" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__eval_options_from_env_env_default +msgid "Resolve env vars Env Default" +msgstr "Riješiti env vars Env zadano" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__eval_options_from_env +#: model:ir.model.fields,help:fs_storage.field_fs_storage__eval_options_from_env_env_default +msgid "" +"Resolve options values starting with $ from environment variables. e.g\n" +" {\n" +" \"endpoint_url\": \"$AWS_ENDPOINT_URL\",\n" +" }\n" +" " +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__server_env_defaults +msgid "Server Env Defaults" +msgstr "Zadane vrijednosti serverskog okruženja" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__check_connection_method +msgid "" +"Set a method if you want the connection to remote to be checked every time the storage is used, in order to remove the obsolete connection from the cache.\n" +"* Create Marker file : Create a file on remote and check it exists\n" +"* List File : List all files from root directory" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection__storage_id +msgid "Storage" +msgstr "Skladište" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_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_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_test_connection_form_view +msgid "Test Connection" +msgstr "Testiraj vezu" + +#. module: fs_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_test_connection_form_view +msgid "Test connection" +msgstr "Testiraj konekciju" + +#. module: fs_storage +#: model:ir.model.constraint,message:fs_storage.constraint_fs_storage_code_uniq +msgid "The code must be unique" +msgstr "Kod mora biti jedinstven" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "The options must be a valid JSON" +msgstr "Opcije moraju biti važeći JSON" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__json_options +msgid "The options used to initialize the filesystem.\n" +msgstr "Opcije korišćene za inicijalizaciju sistema datoteka.\\\n" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__options +#: model:ir.model.fields,help:fs_storage.field_fs_storage__options_env_default +msgid "" +"The options used to initialize the filesystem.\n" +"This is a JSON field that depends on the protocol used.\n" +"For example, for the sftp protocol, you can provide the following:\n" +"{\n" +" 'host': 'my.sftp.server',\n" +" 'ssh_kwrags': {\n" +" 'username': 'myuser',\n" +" 'password': 'mypassword',\n" +" 'port': 22,\n" +" }\n" +"}\n" +"For more information, please refer to the fsspec documentation:\n" +"https://filesystem-spec.readthedocs.io/en/latest/api.html#built-in-implementations" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__options_protocol +#: model:ir.model.fields,help:fs_storage.field_fs_storage__protocol +#: model:ir.model.fields,help:fs_storage.field_fs_storage__protocol_env_default +msgid "" +"The protocol used to access the content of filesystem.\n" +"This list is the one supported by the fsspec library (see https://filesystem-spec.readthedocs.io/en/latest). A filesystem protocolis added by default and refers to the odoo local filesystem.\n" +"Pay attention that according to the protocol, some options must beprovided through the options field." +msgstr "" diff --git a/odoo-bringout-oca-storage-fs_storage/fs_storage/i18n/de.po b/odoo-bringout-oca-storage-fs_storage/fs_storage/i18n/de.po new file mode 100644 index 0000000..22d1524 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage/fs_storage/i18n/de.po @@ -0,0 +1,326 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_storage +# +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: de\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_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +msgid "Available options" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__options_properties +msgid "Available properties" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__check_connection_method +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection__check_connection_method +msgid "Check Connection Method" +msgstr "" + +#. module: fs_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_test_connection_form_view +msgid "Close" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__code +msgid "Code" +msgstr "" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "Connection Test Failed!" +msgstr "" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "Connection Test Succeeded!" +msgstr "" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "Create Marker file" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__create_uid +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection__create_uid +msgid "Created by" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__create_date +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection__create_date +msgid "Created on" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__options_protocol +msgid "Describes Protocol" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__directory_path +msgid "Directory Path" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__directory_path_env_default +msgid "Directory Path Env Default" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__directory_path_env_is_editable +msgid "Directory Path Env Is Editable" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__display_name +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection__display_name +msgid "Display Name" +msgstr "" + +#. module: fs_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +msgid "Enter you fsspec options here." +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__eval_options_from_env_env_is_editable +msgid "Eval Options From Env Env Is Editable" +msgstr "" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "Everything seems properly set up!" +msgstr "" + +#. module: fs_storage +#: model:ir.actions.act_window,name:fs_storage.act_open_fs_storage_view +#: model:ir.model,name:fs_storage.model_fs_storage +#: model:ir.ui.menu,name:fs_storage.menu_fs_storage +#: model:ir.ui.menu,name:fs_storage.menu_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_search_view +msgid "FS Storage" +msgstr "" + +#. module: fs_storage +#: model:ir.actions.act_window,name:fs_storage.act_open_fs_test_connection_view +msgid "FS Test Connection" +msgstr "" + +#. module: fs_storage +#: model:ir.model,name:fs_storage.model_fs_test_connection +msgid "FS Test Connection Wizard" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__id +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection__id +msgid "ID" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__json_options +msgid "Json Options" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage____last_update +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection____last_update +msgid "Last Modified on" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__write_uid +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__write_date +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection__write_date +msgid "Last Updated on" +msgstr "" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "List File" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__name +msgid "Name" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__options +msgid "Options" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__options_env_default +msgid "Options Env Default" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__options_env_is_editable +msgid "Options Env Is Editable" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__protocol +msgid "Protocol" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__protocol_descr +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +msgid "Protocol Descr" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__protocol_env_default +msgid "Protocol Env Default" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__protocol_env_is_editable +msgid "Protocol Env Is Editable" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__directory_path +#: model:ir.model.fields,help:fs_storage.field_fs_storage__directory_path_env_default +msgid "Relative path to the directory to store the file" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__eval_options_from_env +msgid "Resolve env vars" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__eval_options_from_env_env_default +msgid "Resolve env vars Env Default" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__eval_options_from_env +#: model:ir.model.fields,help:fs_storage.field_fs_storage__eval_options_from_env_env_default +msgid "" +"Resolve options values starting with $ from environment variables. e.g\n" +" {\n" +" \"endpoint_url\": \"$AWS_ENDPOINT_URL\",\n" +" }\n" +" " +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__server_env_defaults +msgid "Server Env Defaults" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__check_connection_method +msgid "" +"Set a method if you want the connection to remote to be checked every time the storage is used, in order to remove the obsolete connection from the cache.\n" +"* Create Marker file : Create a file on remote and check it exists\n" +"* List File : List all files from root directory" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection__storage_id +msgid "Storage" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_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_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_test_connection_form_view +msgid "Test Connection" +msgstr "" + +#. module: fs_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_test_connection_form_view +msgid "Test connection" +msgstr "" + +#. module: fs_storage +#: model:ir.model.constraint,message:fs_storage.constraint_fs_storage_code_uniq +msgid "The code must be unique" +msgstr "" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "The options must be a valid JSON" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__json_options +msgid "The options used to initialize the filesystem.\n" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__options +#: model:ir.model.fields,help:fs_storage.field_fs_storage__options_env_default +msgid "" +"The options used to initialize the filesystem.\n" +"This is a JSON field that depends on the protocol used.\n" +"For example, for the sftp protocol, you can provide the following:\n" +"{\n" +" 'host': 'my.sftp.server',\n" +" 'ssh_kwrags': {\n" +" 'username': 'myuser',\n" +" 'password': 'mypassword',\n" +" 'port': 22,\n" +" }\n" +"}\n" +"For more information, please refer to the fsspec documentation:\n" +"https://filesystem-spec.readthedocs.io/en/latest/api.html#built-in-implementations" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__options_protocol +#: model:ir.model.fields,help:fs_storage.field_fs_storage__protocol +#: model:ir.model.fields,help:fs_storage.field_fs_storage__protocol_env_default +msgid "" +"The protocol used to access the content of filesystem.\n" +"This list is the one supported by the fsspec library (see https://filesystem-spec.readthedocs.io/en/latest). A filesystem protocolis added by default and refers to the odoo local filesystem.\n" +"Pay attention that according to the protocol, some options must beprovided through the options field." +msgstr "" diff --git a/odoo-bringout-oca-storage-fs_storage/fs_storage/i18n/es.po b/odoo-bringout-oca-storage-fs_storage/fs_storage/i18n/es.po new file mode 100644 index 0000000..1b0b7bb --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage/fs_storage/i18n/es.po @@ -0,0 +1,364 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_storage +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-12-28 02:07+0000\n" +"Last-Translator: Ivorra78 \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_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +msgid "Available options" +msgstr "Opciones disponibles" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__options_properties +msgid "Available properties" +msgstr "Propiedades disponibles" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__check_connection_method +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection__check_connection_method +msgid "Check Connection Method" +msgstr "" + +#. module: fs_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_test_connection_form_view +msgid "Close" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__code +msgid "Code" +msgstr "Código" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "Connection Test Failed!" +msgstr "¡Conexión de prueba fallida!" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "Connection Test Succeeded!" +msgstr "¡Conexión de prueba exitosa!" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "Create Marker file" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__create_uid +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__create_date +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection__create_date +msgid "Created on" +msgstr "Creado el" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__options_protocol +msgid "Describes Protocol" +msgstr "Descripción del Protocolo" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__directory_path +msgid "Directory Path" +msgstr "Ruta del Directorio" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__directory_path_env_default +msgid "Directory Path Env Default" +msgstr "Ruta del Directorio Env Predet" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__directory_path_env_is_editable +msgid "Directory Path Env Is Editable" +msgstr "La Ruta de Directorio Env es Editable" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__display_name +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection__display_name +msgid "Display Name" +msgstr "Mostrar Nombre" + +#. module: fs_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +msgid "Enter you fsspec options here." +msgstr "Introduzca aquí sus opciones fsspec." + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__eval_options_from_env_env_is_editable +msgid "Eval Options From Env Env Is Editable" +msgstr "" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "Everything seems properly set up!" +msgstr "¡Todo parece correctamente configurado!" + +#. module: fs_storage +#: model:ir.actions.act_window,name:fs_storage.act_open_fs_storage_view +#: model:ir.model,name:fs_storage.model_fs_storage +#: model:ir.ui.menu,name:fs_storage.menu_fs_storage +#: model:ir.ui.menu,name:fs_storage.menu_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_search_view +msgid "FS Storage" +msgstr "Almacenamiento FS" + +#. module: fs_storage +#: model:ir.actions.act_window,name:fs_storage.act_open_fs_test_connection_view +msgid "FS Test Connection" +msgstr "" + +#. module: fs_storage +#: model:ir.model,name:fs_storage.model_fs_test_connection +msgid "FS Test Connection Wizard" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__id +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection__id +msgid "ID" +msgstr "ID (identificación)" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__json_options +msgid "Json Options" +msgstr "Opciones Json" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage____last_update +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection____last_update +msgid "Last Modified on" +msgstr "Última Modificación el" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__write_uid +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection__write_uid +msgid "Last Updated by" +msgstr "Actualizado por Última vez por" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__write_date +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection__write_date +msgid "Last Updated on" +msgstr "Última Actualización el" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "List File" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__name +msgid "Name" +msgstr "Nombre" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__options +msgid "Options" +msgstr "Opciones" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__options_env_default +msgid "Options Env Default" +msgstr "Opciones Env Por Defecto" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__options_env_is_editable +msgid "Options Env Is Editable" +msgstr "Las Opciones Env son Editables" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__protocol +msgid "Protocol" +msgstr "Protocolo" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__protocol_descr +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +msgid "Protocol Descr" +msgstr "Descr. del Protocolo" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__protocol_env_default +msgid "Protocol Env Default" +msgstr "Protocolo Env Predeterminado" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__protocol_env_is_editable +msgid "Protocol Env Is Editable" +msgstr "El Protocolo Env es Editable" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__directory_path +#: model:ir.model.fields,help:fs_storage.field_fs_storage__directory_path_env_default +msgid "Relative path to the directory to store the file" +msgstr "Ruta relativa al directorio para almacenar el archivo" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__eval_options_from_env +msgid "Resolve env vars" +msgstr "Resolver las variables de entorno" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__eval_options_from_env_env_default +msgid "Resolve env vars Env Default" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__eval_options_from_env +#: model:ir.model.fields,help:fs_storage.field_fs_storage__eval_options_from_env_env_default +msgid "" +"Resolve options values starting with $ from environment variables. e.g\n" +" {\n" +" \"endpoint_url\": \"$AWS_ENDPOINT_URL\",\n" +" }\n" +" " +msgstr "" +"Resuelva los valores de opciones que comienzan con $ a partir de variables " +"de entorno. p.ej\n" +" {\n" +" \"endpoint_url\": \"$AWS_ENDPOINT_URL\",\n" +" }\n" +" " + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__server_env_defaults +msgid "Server Env Defaults" +msgstr "Valores por defecto del Entorno de Servidor" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__check_connection_method +msgid "" +"Set a method if you want the connection to remote to be checked every time " +"the storage is used, in order to remove the obsolete connection from the " +"cache.\n" +"* Create Marker file : Create a file on remote and check it exists\n" +"* List File : List all files from root directory" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection__storage_id +msgid "Storage" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_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_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_test_connection_form_view +msgid "Test Connection" +msgstr "" + +#. module: fs_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_test_connection_form_view +msgid "Test connection" +msgstr "Probar conexión" + +#. module: fs_storage +#: model:ir.model.constraint,message:fs_storage.constraint_fs_storage_code_uniq +msgid "The code must be unique" +msgstr "El código tiene que ser único" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "The options must be a valid JSON" +msgstr "Las opciones tienen que estar definidas en un JSON válido" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__json_options +msgid "The options used to initialize the filesystem.\n" +msgstr "Las opciones utilizadas para inicializar el sistema de archivos.\n" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__options +#: model:ir.model.fields,help:fs_storage.field_fs_storage__options_env_default +msgid "" +"The options used to initialize the filesystem.\n" +"This is a JSON field that depends on the protocol used.\n" +"For example, for the sftp protocol, you can provide the following:\n" +"{\n" +" 'host': 'my.sftp.server',\n" +" 'ssh_kwrags': {\n" +" 'username': 'myuser',\n" +" 'password': 'mypassword',\n" +" 'port': 22,\n" +" }\n" +"}\n" +"For more information, please refer to the fsspec documentation:\n" +"https://filesystem-spec.readthedocs.io/en/latest/api.html#built-in-" +"implementations" +msgstr "" +"Las opciones utilizadas para inicializar el sistema de ficheros.\n" +"Este es un campo JSON que depende del protocolo utilizado.\n" +"Por ejemplo, para el protocolo sftp, puede proporcionar lo siguiente:\n" +"{\n" +" 'host': 'mi.sftp.server',\n" +" 'ssh_kwrags': {\n" +" 'username': 'myuser',\n" +" 'password': 'mypassword',\n" +" ' port': 22,\n" +" }\n" +"}\n" +"Para más información, consulta la documentación de fsspec:\n" +"https://filesystem-spec.readthedocs.io/en/latest/api.html#built-in-" +"implementations" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__options_protocol +#: model:ir.model.fields,help:fs_storage.field_fs_storage__protocol +#: model:ir.model.fields,help:fs_storage.field_fs_storage__protocol_env_default +msgid "" +"The protocol used to access the content of filesystem.\n" +"This list is the one supported by the fsspec library (see https://filesystem-" +"spec.readthedocs.io/en/latest). A filesystem protocolis added by default and " +"refers to the odoo local filesystem.\n" +"Pay attention that according to the protocol, some options must beprovided " +"through the options field." +msgstr "" +"El protocolo utilizado para acceder al contenido del sistema de ficheros.\n" +"Esta lista es la soportada por la librería fsspec (ver https://filesystem-" +"spec.readthedocs.io/en/latest). Un protocolo de sistema de archivos es " +"agregado por defecto y se refiere al sistema de archivos local de odoo.\n" +"Preste atención que de acuerdo al protocolo, algunas opciones deben ser " +"provistas a través del campo opciones." diff --git a/odoo-bringout-oca-storage-fs_storage/fs_storage/i18n/fs_storage.pot b/odoo-bringout-oca-storage-fs_storage/fs_storage/i18n/fs_storage.pot new file mode 100644 index 0000000..706727c --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage/fs_storage/i18n/fs_storage.pot @@ -0,0 +1,325 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_storage +# +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_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +msgid "Available options" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__options_properties +msgid "Available properties" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__check_connection_method +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection__check_connection_method +msgid "Check Connection Method" +msgstr "" + +#. module: fs_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_test_connection_form_view +msgid "Close" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__code +msgid "Code" +msgstr "" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "Connection Test Failed!" +msgstr "" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "Connection Test Succeeded!" +msgstr "" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "Create Marker file" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__create_uid +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection__create_uid +msgid "Created by" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__create_date +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection__create_date +msgid "Created on" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__options_protocol +msgid "Describes Protocol" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__directory_path +msgid "Directory Path" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__directory_path_env_default +msgid "Directory Path Env Default" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__directory_path_env_is_editable +msgid "Directory Path Env Is Editable" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__display_name +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection__display_name +msgid "Display Name" +msgstr "" + +#. module: fs_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +msgid "Enter you fsspec options here." +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__eval_options_from_env_env_is_editable +msgid "Eval Options From Env Env Is Editable" +msgstr "" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "Everything seems properly set up!" +msgstr "" + +#. module: fs_storage +#: model:ir.actions.act_window,name:fs_storage.act_open_fs_storage_view +#: model:ir.model,name:fs_storage.model_fs_storage +#: model:ir.ui.menu,name:fs_storage.menu_fs_storage +#: model:ir.ui.menu,name:fs_storage.menu_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_search_view +msgid "FS Storage" +msgstr "" + +#. module: fs_storage +#: model:ir.actions.act_window,name:fs_storage.act_open_fs_test_connection_view +msgid "FS Test Connection" +msgstr "" + +#. module: fs_storage +#: model:ir.model,name:fs_storage.model_fs_test_connection +msgid "FS Test Connection Wizard" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__id +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection__id +msgid "ID" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__json_options +msgid "Json Options" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage____last_update +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection____last_update +msgid "Last Modified on" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__write_uid +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__write_date +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection__write_date +msgid "Last Updated on" +msgstr "" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "List File" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__name +msgid "Name" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__options +msgid "Options" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__options_env_default +msgid "Options Env Default" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__options_env_is_editable +msgid "Options Env Is Editable" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__protocol +msgid "Protocol" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__protocol_descr +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +msgid "Protocol Descr" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__protocol_env_default +msgid "Protocol Env Default" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__protocol_env_is_editable +msgid "Protocol Env Is Editable" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__directory_path +#: model:ir.model.fields,help:fs_storage.field_fs_storage__directory_path_env_default +msgid "Relative path to the directory to store the file" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__eval_options_from_env +msgid "Resolve env vars" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__eval_options_from_env_env_default +msgid "Resolve env vars Env Default" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__eval_options_from_env +#: model:ir.model.fields,help:fs_storage.field_fs_storage__eval_options_from_env_env_default +msgid "" +"Resolve options values starting with $ from environment variables. e.g\n" +" {\n" +" \"endpoint_url\": \"$AWS_ENDPOINT_URL\",\n" +" }\n" +" " +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__server_env_defaults +msgid "Server Env Defaults" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__check_connection_method +msgid "" +"Set a method if you want the connection to remote to be checked every time the storage is used, in order to remove the obsolete connection from the cache.\n" +"* Create Marker file : Create a file on remote and check it exists\n" +"* List File : List all files from root directory" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection__storage_id +msgid "Storage" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_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_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_test_connection_form_view +msgid "Test Connection" +msgstr "" + +#. module: fs_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_test_connection_form_view +msgid "Test connection" +msgstr "" + +#. module: fs_storage +#: model:ir.model.constraint,message:fs_storage.constraint_fs_storage_code_uniq +msgid "The code must be unique" +msgstr "" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "The options must be a valid JSON" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__json_options +msgid "The options used to initialize the filesystem.\n" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__options +#: model:ir.model.fields,help:fs_storage.field_fs_storage__options_env_default +msgid "" +"The options used to initialize the filesystem.\n" +"This is a JSON field that depends on the protocol used.\n" +"For example, for the sftp protocol, you can provide the following:\n" +"{\n" +" 'host': 'my.sftp.server',\n" +" 'ssh_kwrags': {\n" +" 'username': 'myuser',\n" +" 'password': 'mypassword',\n" +" 'port': 22,\n" +" }\n" +"}\n" +"For more information, please refer to the fsspec documentation:\n" +"https://filesystem-spec.readthedocs.io/en/latest/api.html#built-in-implementations" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__options_protocol +#: model:ir.model.fields,help:fs_storage.field_fs_storage__protocol +#: model:ir.model.fields,help:fs_storage.field_fs_storage__protocol_env_default +msgid "" +"The protocol used to access the content of filesystem.\n" +"This list is the one supported by the fsspec library (see https://filesystem-spec.readthedocs.io/en/latest). A filesystem protocolis added by default and refers to the odoo local filesystem.\n" +"Pay attention that according to the protocol, some options must beprovided through the options field." +msgstr "" diff --git a/odoo-bringout-oca-storage-fs_storage/fs_storage/i18n/it.po b/odoo-bringout-oca-storage-fs_storage/fs_storage/i18n/it.po new file mode 100644 index 0000000..4a1b46a --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage/fs_storage/i18n/it.po @@ -0,0 +1,368 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_storage +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2025-05-16 06:39+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.10.4\n" + +#. module: fs_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +msgid "Available options" +msgstr "Opzioni disponibili" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__options_properties +msgid "Available properties" +msgstr "Proprietà disponibili" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__check_connection_method +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection__check_connection_method +msgid "Check Connection Method" +msgstr "Controlla metodo di connessione" + +#. module: fs_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_test_connection_form_view +msgid "Close" +msgstr "Chiudi" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__code +msgid "Code" +msgstr "Codice" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "Connection Test Failed!" +msgstr "Test connessione fallito!" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "Connection Test Succeeded!" +msgstr "Test connessione avvenuto con successo!" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "Create Marker file" +msgstr "Crea file marcatore" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__create_uid +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection__create_uid +msgid "Created by" +msgstr "Creato da" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__create_date +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection__create_date +msgid "Created on" +msgstr "Creato il" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__options_protocol +msgid "Describes Protocol" +msgstr "Descrive protocollo" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__directory_path +msgid "Directory Path" +msgstr "Percorso cartella" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__directory_path_env_default +msgid "Directory Path Env Default" +msgstr "Percorso cartella ambiente predefinito" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__directory_path_env_is_editable +msgid "Directory Path Env Is Editable" +msgstr "Percorso cartella ambiente è modificabile" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__display_name +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection__display_name +msgid "Display Name" +msgstr "Nome visualizzato" + +#. module: fs_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +msgid "Enter you fsspec options here." +msgstr "Inserire qui le opzioni FSspec." + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__eval_options_from_env_env_is_editable +msgid "Eval Options From Env Env Is Editable" +msgstr "La valutazione opzioni da ambiente è modificabile" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "Everything seems properly set up!" +msgstr "Tutto sembra impostato correttamente!" + +#. module: fs_storage +#: model:ir.actions.act_window,name:fs_storage.act_open_fs_storage_view +#: model:ir.model,name:fs_storage.model_fs_storage +#: model:ir.ui.menu,name:fs_storage.menu_fs_storage +#: model:ir.ui.menu,name:fs_storage.menu_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_search_view +msgid "FS Storage" +msgstr "Deposito FS" + +#. module: fs_storage +#: model:ir.actions.act_window,name:fs_storage.act_open_fs_test_connection_view +msgid "FS Test Connection" +msgstr "Test connessione FS" + +#. module: fs_storage +#: model:ir.model,name:fs_storage.model_fs_test_connection +msgid "FS Test Connection Wizard" +msgstr "Procedura guidata test connessione FS" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__id +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection__id +msgid "ID" +msgstr "ID" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__json_options +msgid "Json Options" +msgstr "Opzioni JSON" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage____last_update +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection____last_update +msgid "Last Modified on" +msgstr "Ultima modifica il" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__write_uid +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection__write_uid +msgid "Last Updated by" +msgstr "Ultimo aggiornamento di" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__write_date +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection__write_date +msgid "Last Updated on" +msgstr "Ultimo aggiornamento il" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "List File" +msgstr "Elenco file" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__name +msgid "Name" +msgstr "Nome" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__options +msgid "Options" +msgstr "Opzioni" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__options_env_default +msgid "Options Env Default" +msgstr "Opzioni ambiente predefinite" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__options_env_is_editable +msgid "Options Env Is Editable" +msgstr "Opzioni ambiente sono modificabili" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__protocol +msgid "Protocol" +msgstr "Protcollo" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__protocol_descr +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +msgid "Protocol Descr" +msgstr "Descrizione protocollo" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__protocol_env_default +msgid "Protocol Env Default" +msgstr "Protocollo ambiene predefinito" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__protocol_env_is_editable +msgid "Protocol Env Is Editable" +msgstr "Protocollo ambiente è modificabile" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__directory_path +#: model:ir.model.fields,help:fs_storage.field_fs_storage__directory_path_env_default +msgid "Relative path to the directory to store the file" +msgstr "Percorso relativo alla cartella per archiviare il file" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__eval_options_from_env +msgid "Resolve env vars" +msgstr "Risole variabili ambiente" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__eval_options_from_env_env_default +msgid "Resolve env vars Env Default" +msgstr "Risolvi le variabili ambiente predefinito" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__eval_options_from_env +#: model:ir.model.fields,help:fs_storage.field_fs_storage__eval_options_from_env_env_default +msgid "" +"Resolve options values starting with $ from environment variables. e.g\n" +" {\n" +" \"endpoint_url\": \"$AWS_ENDPOINT_URL\",\n" +" }\n" +" " +msgstr "" +"Risolve valori opzioni iniziando con $ dalle variabili ambiente, es.\n" +" {\n" +" \"endpoint_url\": \"$AWS_ENDPOINT_URL\",\n" +" }\n" +" " + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__server_env_defaults +msgid "Server Env Defaults" +msgstr "Predefiniti ambiente server" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__check_connection_method +msgid "" +"Set a method if you want the connection to remote to be checked every time " +"the storage is used, in order to remove the obsolete connection from the " +"cache.\n" +"* Create Marker file : Create a file on remote and check it exists\n" +"* List File : List all files from root directory" +msgstr "" +"Impostare un metodo se si vuole verificare la connessione remota ogni volta " +"che il deposito è utilizzato, per eliminare la connessione obsoleta dalla " +"cache.\n" +"* Crea file marcatore : crea un file in remoto e controlla se esiste\n" +"* Elenco file : elenca tutti i file dalla cartella radice" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_test_connection__storage_id +msgid "Storage" +msgstr "Deposito" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_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_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_test_connection_form_view +msgid "Test Connection" +msgstr "Prova connessione" + +#. module: fs_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_form_view +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_test_connection_form_view +msgid "Test connection" +msgstr "Prova connessione" + +#. module: fs_storage +#: model:ir.model.constraint,message:fs_storage.constraint_fs_storage_code_uniq +msgid "The code must be unique" +msgstr "Il codice deve essere univoco" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "The options must be a valid JSON" +msgstr "L'opzione deve essere un JSON valido" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__json_options +msgid "The options used to initialize the filesystem.\n" +msgstr "Le opzioni per inizializzare il filesystem.\n" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__options +#: model:ir.model.fields,help:fs_storage.field_fs_storage__options_env_default +msgid "" +"The options used to initialize the filesystem.\n" +"This is a JSON field that depends on the protocol used.\n" +"For example, for the sftp protocol, you can provide the following:\n" +"{\n" +" 'host': 'my.sftp.server',\n" +" 'ssh_kwrags': {\n" +" 'username': 'myuser',\n" +" 'password': 'mypassword',\n" +" 'port': 22,\n" +" }\n" +"}\n" +"For more information, please refer to the fsspec documentation:\n" +"https://filesystem-spec.readthedocs.io/en/latest/api.html#built-in-" +"implementations" +msgstr "" +"Le opzioni utilizzate per inizializzare il filesystem.\n" +"Questo è uncampo JSON che dipende dal protocollo utilizzato.\n" +"Per esempio, per il protocollo SFTP, si può fornire il seguente:\n" +"{\n" +" 'host': 'my.sftp.server',\n" +" 'ssh_kwrags': {\n" +" 'username': 'myuser',\n" +" 'password': 'mypassword',\n" +" 'port': 22,\n" +" }\n" +"}\n" +"Per ulteriori informazioni, fare riferimento alla documentazione FSspec:\n" +"https://filesystem-spec.readthedocs.io/en/latest/api.html#built-in-" +"implementations" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__options_protocol +#: model:ir.model.fields,help:fs_storage.field_fs_storage__protocol +#: model:ir.model.fields,help:fs_storage.field_fs_storage__protocol_env_default +msgid "" +"The protocol used to access the content of filesystem.\n" +"This list is the one supported by the fsspec library (see https://filesystem-" +"spec.readthedocs.io/en/latest). A filesystem protocolis added by default and " +"refers to the odoo local filesystem.\n" +"Pay attention that according to the protocol, some options must beprovided " +"through the options field." +msgstr "" +"Il protocollo è utilizzato per accedere al contenuto del filesystem.\n" +"Questo elenco è quello supportato dalla libreria FSspec (vedere https://" +"filesystem-spec.readthedocs.io/en/latest). Un protocollo filesystem è " +"aggiunto in modo predefinito e fa riferimento al filesystem locale Odoo.\n" +"Fare attenzione che in accordo con il protocollo, alcune opzioni devono " +"essere fonrite attraverso il campo opzioni." diff --git a/odoo-bringout-oca-storage-fs_storage/fs_storage/i18n/storage_backend.pot b/odoo-bringout-oca-storage-fs_storage/fs_storage/i18n/storage_backend.pot new file mode 100644 index 0000000..4550381 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage/fs_storage/i18n/storage_backend.pot @@ -0,0 +1,144 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_storage +# +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_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__backend_type_env_default +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__directory_path_env_default +msgid " Env Default" +msgstr "" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/components/filesystem_adapter.py:0 +#, python-format +msgid "Access to %s is forbidden" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__backend_type +msgid "Backend Type" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__backend_type_env_is_editable +msgid "Backend Type Env Is Editable" +msgstr "" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "Connection Test Failed!" +msgstr "" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "Connection Test Succeeded!" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__create_uid +msgid "Created by" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__create_date +msgid "Created on" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__directory_path +msgid "Directory Path" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__directory_path_env_is_editable +msgid "Directory Path Env Is Editable" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__display_name +msgid "Display Name" +msgstr "" + +#. module: fs_storage +#. odoo-python +#: code:addons/fs_storage/models/fs_storage.py:0 +#, python-format +msgid "Everything seems properly set up!" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields.selection,name:fs_storage.selection__fs_storage__backend_type__filesystem +msgid "Filesystem" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__has_validation +msgid "Has Validation" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__id +msgid "ID" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage____last_update +msgid "Last Modified on" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__write_date +msgid "Last Updated on" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__name +msgid "Name" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,help:fs_storage.field_fs_storage__directory_path +#: model:ir.model.fields,help:fs_storage.field_fs_storage__directory_path_env_default +msgid "Relative path to the directory to store the file" +msgstr "" + +#. module: fs_storage +#: model:ir.model.fields,field_description:fs_storage.field_fs_storage__server_env_defaults +msgid "Server Env Defaults" +msgstr "" + +#. module: fs_storage +#: model:ir.actions.act_window,name:fs_storage.act_open_fs_storage_view +#: model:ir.model,name:fs_storage.model_fs_storage +#: model:ir.ui.menu,name:fs_storage.menu_storage +#: model:ir.ui.menu,name:fs_storage.menu_fs_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_view_form +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_view_search +msgid "FS Storage" +msgstr "" + +#. module: fs_storage +#: model_terms:ir.ui.view,arch_db:fs_storage.fs_storage_view_form +msgid "Test connection" +msgstr "" diff --git a/odoo-bringout-oca-storage-fs_storage/fs_storage/models/__init__.py b/odoo-bringout-oca-storage-fs_storage/fs_storage/models/__init__.py new file mode 100644 index 0000000..349bb04 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage/fs_storage/models/__init__.py @@ -0,0 +1 @@ +from . import fs_storage diff --git a/odoo-bringout-oca-storage-fs_storage/fs_storage/models/fs_storage.py b/odoo-bringout-oca-storage-fs_storage/fs_storage/models/fs_storage.py new file mode 100644 index 0000000..57f88d2 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage/fs_storage/models/fs_storage.py @@ -0,0 +1,522 @@ +# Copyright 2023 ACSONE SA/NV (https://www.acsone.eu). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +from __future__ import annotations + +import base64 +import functools +import inspect +import json +import logging +import os.path +import re +import warnings +from typing import AnyStr + +import fsspec + +from odoo import _, api, fields, models, tools +from odoo.exceptions import ValidationError + +from odoo.addons.base_sparse_field.models.fields import Serialized + +_logger = logging.getLogger(__name__) + + +# TODO: useful for the whole OCA? +def deprecated(reason): + """Mark functions or classes as deprecated. + + Emit warning at execution. + + The @deprecated is used with a 'reason'. + + .. code-block:: python + + @deprecated("please, use another function") + def old_function(x, y): + pass + """ + + def decorator(func1): + + if inspect.isclass(func1): + fmt1 = "Call to deprecated class {name} ({reason})." + else: + fmt1 = "Call to deprecated function {name} ({reason})." + + @functools.wraps(func1) + def new_func1(*args, **kwargs): + warnings.simplefilter("always", DeprecationWarning) + warnings.warn( + fmt1.format(name=func1.__name__, reason=reason), + category=DeprecationWarning, + stacklevel=2, + ) + warnings.simplefilter("default", DeprecationWarning) + return func1(*args, **kwargs) + + return new_func1 + + return decorator + + +class FSStorage(models.Model): + _name = "fs.storage" + _inherit = "server.env.mixin" + _description = "FS Storage" + + __slots__ = ("__fs", "__odoo_storage_path") + + def __init__(self, env, ids=(), prefetch_ids=()): + super().__init__(env, ids=ids, prefetch_ids=prefetch_ids) + self.__fs = None + self.__odoo_storage_path = None + + name = fields.Char(required=True) + code = fields.Char( + required=True, + help="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.", + ) + protocol = fields.Selection( + selection="_get_protocols", + required=True, + help="The protocol used to access the content of filesystem.\n" + "This list is the one supported by the fsspec library (see " + "https://filesystem-spec.readthedocs.io/en/latest). A filesystem protocol" + "is added by default and refers to the odoo local filesystem.\n" + "Pay attention that according to the protocol, some options must be" + "provided through the options field.", + ) + protocol_descr = fields.Text( + compute="_compute_protocol_descr", + ) + options = fields.Text( + help="The options used to initialize the filesystem.\n" + "This is a JSON field that depends on the protocol used.\n" + "For example, for the sftp protocol, you can provide the following:\n" + "{\n" + " 'host': 'my.sftp.server',\n" + " 'ssh_kwrags': {\n" + " 'username': 'myuser',\n" + " 'password': 'mypassword',\n" + " 'port': 22,\n" + " }\n" + "}\n" + "For more information, please refer to the fsspec documentation:\n" + "https://filesystem-spec.readthedocs.io/en/latest/api.html#built-in-implementations" + ) + + json_options = Serialized( + help="The options used to initialize the filesystem.\n", + compute="_compute_json_options", + inverse="_inverse_json_options", + ) + + eval_options_from_env = fields.Boolean( + string="Resolve env vars", + help="""Resolve options values starting with $ from environment variables. e.g + { + "endpoint_url": "$AWS_ENDPOINT_URL", + } + """, + ) + + directory_path = fields.Char( + help="Relative path to the directory to store the file" + ) + + # the next fields are used to display documentation to help the user + # to configure the backend + options_protocol = fields.Selection( + string="Describes Protocol", + selection="_get_options_protocol", + compute="_compute_protocol_descr", + help="The protocol used to access the content of filesystem.\n" + "This list is the one supported by the fsspec library (see " + "https://filesystem-spec.readthedocs.io/en/latest). A filesystem protocol" + "is added by default and refers to the odoo local filesystem.\n" + "Pay attention that according to the protocol, some options must be" + "provided through the options field.", + ) + options_properties = fields.Text( + string="Available properties", + compute="_compute_options_properties", + store=False, + ) + check_connection_method = fields.Selection( + selection="_get_check_connection_method_selection", + default="marker_file", + help="Set a method if you want the connection to remote to be checked every " + "time the storage is used, in order to remove the obsolete connection from" + " the cache.\n" + "* Create Marker file : Create a file on remote and check it exists\n" + "* List File : List all files from root directory", + ) + + _sql_constraints = [ + ( + "code_uniq", + "unique(code)", + "The code must be unique", + ), + ] + + _server_env_section_name_field = "code" + + @api.model + def _get_check_connection_method_selection(self): + return [ + ("marker_file", _("Create Marker file")), + ("ls", _("List File")), + ] + + @property + def _server_env_fields(self): + return { + "protocol": {}, + "options": {}, + "directory_path": {}, + "eval_options_from_env": {}, + } + + def write(self, vals): + self.__fs = None + self.clear_caches() + return super().write(vals) + + @api.model + @tools.ormcache() + def get_id_by_code_map(self): + """Return a dictionary with the code as key and the id as value.""" + return {rec.code: rec.id for rec in self.sudo().search([])} + + @api.model + def get_id_by_code(self, code): + """Return the id of the filesystem associated to the given code.""" + return self.get_id_by_code_map().get(code) + + @api.model + def get_by_code(self, code) -> FSStorage: + """Return the filesystem associated to the given code.""" + res = self.browse() + res_id = self.get_id_by_code(code) + if res_id: + res = self.browse(res_id) + return res + + @api.model + @tools.ormcache() + def get_storage_codes(self): + """Return the list of codes of the existing filesystems.""" + return [s.code for s in self.search([])] + + @api.model + @tools.ormcache("code") + def get_fs_by_code(self, code): + """Return the filesystem associated to the given code. + + :param code: the code of the filesystem + """ + fs = None + fs_storage = self.get_by_code(code) + if fs_storage: + fs = fs_storage.fs + return fs + + def copy(self, default=None): + default = default or {} + if "code" not in default: + default["code"] = "{}_copy".format(self.code) + return super().copy(default) + + @api.model + def _get_protocols(self) -> list[tuple[str, str]]: + protocol = [("odoofs", "Odoo's FileSystem")] + for p in fsspec.available_protocols(): + try: + cls = fsspec.get_filesystem_class(p) + protocol.append((p, f"{p} ({cls.__name__})")) + except Exception as e: + _logger.debug("Cannot load the protocol %s. Reason: %s", p, e) + return protocol + + @api.constrains("options") + def _check_options(self) -> None: + for rec in self: + try: + json.loads(rec.options or "{}") + except Exception as e: + raise ValidationError(_("The options must be a valid JSON")) from e + + @api.depends("options") + def _compute_json_options(self) -> None: + for rec in self: + rec.json_options = json.loads(rec.options or "{}") + + def _inverse_json_options(self) -> None: + for rec in self: + rec.options = json.dumps(rec.json_options) + + @api.depends("protocol") + def _compute_protocol_descr(self) -> None: + for rec in self: + rec.protocol_descr = fsspec.get_filesystem_class(rec.protocol).__doc__ + rec.options_protocol = rec.protocol + + @api.model + def _get_options_protocol(self) -> list[tuple[str, str]]: + protocol = [("odoofs", "Odoo's Filesystem")] + for p in fsspec.available_protocols(): + try: + fsspec.get_filesystem_class(p) + protocol.append((p, p)) + except Exception as e: + _logger.debug("Cannot load the protocol %s. Reason: %s", p, e) + return protocol + + @api.depends("options_protocol") + def _compute_options_properties(self) -> None: + for rec in self: + cls = fsspec.get_filesystem_class(rec.options_protocol) + signature = inspect.signature(cls.__init__) + doc = inspect.getdoc(cls.__init__) + rec.options_properties = f"__init__{signature}\n{doc}" + + def _get_marker_file_name(self): + return ".odoo_fs_storage_%s.marker" % self.id + + def _marker_file_check_connection(self, fs): + marker_file_name = self._get_marker_file_name() + try: + fs.info(marker_file_name) + except FileNotFoundError: + fs.touch(marker_file_name) + + def _ls_check_connection(self, fs): + fs.ls("", detail=False) + + def _check_connection(self, fs, check_connection_method): + if check_connection_method == "marker_file": + self._marker_file_check_connection(fs) + elif check_connection_method == "ls": + self._ls_check_connection(fs) + return True + + @property + def fs(self) -> fsspec.AbstractFileSystem: + """Get the fsspec filesystem for this backend.""" + self.ensure_one() + if not self.__fs: + self.__fs = self.sudo()._get_filesystem() + if not tools.config["test_enable"]: + # Check whether we need to invalidate FS cache or not. + # Use a marker file to limit the scope of the LS command for performance. + try: + self._check_connection(self.__fs, self.check_connection_method) + except Exception as e: + self.__fs.clear_instance_cache() + self.__fs = None + raise e + return self.__fs + + def _get_filesystem_storage_path(self) -> str: + """Get the path to the storage directory. + + This path is relative to the odoo filestore.and is used as root path + when the protocol is filesystem. + """ + self.ensure_one() + path = os.path.join(self.env["ir.attachment"]._filestore(), "storage") + if not os.path.exists(path): + os.makedirs(path) + return path + + @property + def _odoo_storage_path(self) -> str: + """Get the path to the storage directory. + + This path is relative to the odoo filestore.and is used as root path + when the protocol is filesystem. + """ + if not self.__odoo_storage_path: + self.__odoo_storage_path = self._get_filesystem_storage_path() + return self.__odoo_storage_path + + def _recursive_add_odoo_storage_path(self, options: dict) -> dict: + """Add the odoo storage path to the options. + + This is a recursive function that will add the odoo_storage_path + option to the nested target_options if the target_protocol is + odoofs + """ + if "target_protocol" in options: + target_options = options.get("target_options", {}) + if options["target_protocol"] == "odoofs": + target_options["odoo_storage_path"] = self._odoo_storage_path + options["target_options"] = target_options + self._recursive_add_odoo_storage_path(target_options) + return options + + def _eval_options_from_env(self, options): + values = {} + for key, value in options.items(): + if isinstance(value, dict): + values[key] = self._eval_options_from_env(value) + elif isinstance(value, str) and value.startswith("$"): + env_variable_name = value[1:] + env_variable_value = os.getenv(env_variable_name) + if env_variable_value is not None: + values[key] = env_variable_value + else: + values[key] = value + _logger.warning( + "Environment variable %s is not set for fs_storage %s.", + env_variable_name, + self.display_name, + ) + else: + values[key] = value + return values + + def _get_fs_options(self): + options = self.json_options + if not self.eval_options_from_env: + return options + return self._eval_options_from_env(self.json_options) + + def _get_filesystem(self) -> fsspec.AbstractFileSystem: + """Get the fsspec filesystem for this backend. + + See https://filesystem-spec.readthedocs.io/en/latest/api.html + #fsspec.spec.AbstractFileSystem + + :return: fsspec.AbstractFileSystem + """ + self.ensure_one() + options = self._get_fs_options() + if self.protocol == "odoofs": + options["odoo_storage_path"] = self._odoo_storage_path + # Webdav protocol handler does need the auth to be a tuple not a list ! + if ( + self.protocol == "webdav" + and "auth" in options + and isinstance(options["auth"], list) + ): + options["auth"] = tuple(options["auth"]) + options = self._recursive_add_odoo_storage_path(options) + fs = fsspec.filesystem(self.protocol, **options) + directory_path = self.directory_path + if directory_path: + fs = fsspec.filesystem("rooted_dir", path=directory_path, fs=fs) + return fs + + # Deprecated methods used to ease the migration from the storage_backend addons + # to the fs_storage addons. These methods will be removed in the future (Odoo 18) + @deprecated("Please use _get_filesystem() instead and the fsspec API directly.") + def add(self, relative_path, data, binary=True, **kwargs) -> None: + if not binary: + data = base64.b64decode(data) + path = relative_path.split(self.fs.sep)[:-1] + if not self.fs.exists(self.fs.sep.join(path)): + self.fs.makedirs(self.fs.sep.join(path)) + with self.fs.open(relative_path, "wb", **kwargs) as f: + f.write(data) + + @deprecated("Please use _get_filesystem() instead and the fsspec API directly.") + def get(self, relative_path, binary=True, **kwargs) -> AnyStr: + data = self.fs.read_bytes(relative_path, **kwargs) + if not binary and data: + data = base64.b64encode(data) + return data + + @deprecated("Please use _get_filesystem() instead and the fsspec API directly.") + def list_files(self, relative_path="", pattern=False) -> list[str]: + relative_path = relative_path or self.fs.root_marker + if not self.fs.exists(relative_path): + return [] + if pattern: + relative_path = self.fs.sep.join([relative_path, pattern]) + return self.fs.glob(relative_path) + return self.fs.ls(relative_path, detail=False) + + @deprecated("Please use _get_filesystem() instead and the fsspec API directly.") + def find_files(self, pattern, relative_path="", **kw) -> list[str]: + """Find files matching given pattern. + + :param pattern: regex expression + :param relative_path: optional relative path containing files + :return: list of file paths as full paths from the root + """ + result = [] + relative_path = relative_path or self.fs.root_marker + if not self.fs.exists(relative_path): + return [] + regex = re.compile(pattern) + for file_path in self.fs.ls(relative_path, detail=False): + # fs.ls returns a relative path + if regex.match(os.path.basename(file_path)): + result.append(file_path) + return result + + @deprecated("Please use _get_filesystem() instead and the fsspec API directly.") + def move_files(self, files, destination_path, **kw) -> None: + """Move files to given destination. + + :param files: list of file paths to be moved + :param destination_path: directory path where to move files + :return: None + """ + for file_path in files: + self.fs.move( + file_path, + self.fs.sep.join([destination_path, os.path.basename(file_path)]), + **kw, + ) + + @deprecated("Please use _get_filesystem() instead and the fsspec API directly.") + def delete(self, relative_path) -> None: + self.fs.rm_file(relative_path) + + def action_test_config(self): + self.ensure_one() + if self.check_connection_method: + return self._test_config(self.check_connection_method) + else: + action = self.env["ir.actions.actions"]._for_xml_id( + "fs_storage.act_open_fs_test_connection_view" + ) + action["context"] = {"active_model": "fs.storage", "active_id": self.id} + return action + + def _test_config(self, connection_method): + try: + self._check_connection(self.fs, connection_method) + title = _("Connection Test Succeeded!") + message = _("Everything seems properly set up!") + msg_type = "success" + except Exception as err: + title = _("Connection Test Failed!") + message = str(err) + msg_type = "danger" + return { + "type": "ir.actions.client", + "tag": "display_notification", + "params": { + "title": title, + "message": message, + "type": msg_type, + "sticky": False, + }, + } + + def _get_root_filesystem(self, fs=None): + if not fs: + self.ensure_one() + fs = self.fs + while hasattr(fs, "fs"): + fs = fs.fs + return fs diff --git a/odoo-bringout-oca-storage-fs_storage/fs_storage/odoo_file_system.py b/odoo-bringout-oca-storage-fs_storage/fs_storage/odoo_file_system.py new file mode 100644 index 0000000..1827a31 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage/fs_storage/odoo_file_system.py @@ -0,0 +1,50 @@ +# Copyright 2023 ACSONE SA/NV (https://www.acsone.eu). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + + +from fsspec.registry import register_implementation + +from .rooted_dir_file_system import RootedDirFileSystem + + +class OdooFileSystem(RootedDirFileSystem): + """A directory-based filesystem for Odoo. + + This filesystem is mounted from a specific subdirectory of the Odoo + filestore directory. + + It extends the RootedDirFileSystem to avoid going outside the + specific subdirectory nor the Odoo filestore directory. + + Parameters: + odoo_storage_path: The path of the subdirectory of the Odoo filestore + directory to mount. This parameter is required and is always provided + by the Odoo FS Storage even if it is explicitly defined in the + storage options. + fs: AbstractFileSystem + An instantiated filesystem to wrap. + target_protocol, target_options: + if fs is none, construct it from these + """ + + def __init__( + self, + *, + odoo_storage_path, + fs=None, + target_protocol=None, + target_options=None, + **storage_options + ): + if not odoo_storage_path: + raise ValueError("odoo_storage_path is required") + super().__init__( + path=odoo_storage_path, + fs=fs, + target_protocol=target_protocol, + target_options=target_options, + **storage_options + ) + + +register_implementation("odoofs", OdooFileSystem) diff --git a/odoo-bringout-oca-storage-fs_storage/fs_storage/readme/CONTRIBUTORS.rst b/odoo-bringout-oca-storage-fs_storage/fs_storage/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000..60c32f1 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage/fs_storage/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Laurent Mignon +* Sébastien BEAU diff --git a/odoo-bringout-oca-storage-fs_storage/fs_storage/readme/DESCRIPTION.rst b/odoo-bringout-oca-storage-fs_storage/fs_storage/readme/DESCRIPTION.rst new file mode 100644 index 0000000..1cfc5c1 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage/fs_storage/readme/DESCRIPTION.rst @@ -0,0 +1,63 @@ +This addon is a technical addon that allows you to define filesystem like +storage for your data. It's used by other addons to store their data in a +transparent way into different kind of storages. + +Through the fs.storage record, you get access to an object that implements +the `fsspec.spec.AbstractFileSystem `_ interface and therefore give +you an unified interface to access your data whatever the storage protocol you +decide to use. + +The list of supported protocols depends on the installed fsspec implementations. +By default, the addon will install the following protocols: + +* LocalFileSystem +* MemoryFileSystem +* ZipFileSystem +* TarFileSystem +* FTPFileSystem +* CachingFileSystem +* WholeFileSystem +* SimplCacheFileSystem +* ReferenceFileSystem +* GenericFileSystem +* DirFileSystem +* DatabricksFileSystem +* GitHubFileSystem +* JupiterFileSystem +* OdooFileSystem + +The OdooFileSystem is the one that allows you to store your data into a directory +mounted into your Odoo's storage directory. This is the default FS Storage +when creating a new fs.storage record. + +Others protocols are available through the installation of additional +python packages: + +* DropboxDriveFileSystem -> `pip install fsspec[dropbox]` +* HTTPFileSystem -> `pip install fsspec[http]` +* HTTPSFileSystem -> `pip install fsspec[http]` +* GCSFileSystem -> `pip install fsspec[gcs]` +* GSFileSystem -> `pip install fsspec[gs]` +* GoogleDriveFileSystem -> `pip install gdrivefs` +* SFTPFileSystem -> `pip install fsspec[sftp]` +* HaddoopFileSystem -> `pip install fsspec[hdfs]` +* S3FileSystem -> `pip install fsspec[s3]` +* WandbFS -> `pip install wandbfs` +* OCIFileSystem -> `pip install fsspec[oci]` +* AsyncLocalFileSystem -> `pip install 'morefs[asynclocalfs]` +* AzureDatalakeFileSystem -> `pip install fsspec[adl]` +* AzureBlobFileSystem -> `pip install fsspec[abfs]` +* DaskWorkerFileSystem -> `pip install fsspec[dask]` +* GitFileSystem -> `pip install fsspec[git]` +* SMBFileSystem -> `pip install fsspec[smb]` +* LibArchiveFileSystem -> `pip install fsspec[libarchive]` +* OSSFileSystem -> `pip install ossfs` +* WebdavFileSystem -> `pip install webdav4` +* DVCFileSystem -> `pip install dvc` +* XRootDFileSystem -> `pip install fsspec-xrootd` + +This list of supported protocols is not exhaustive or could change in the future +depending on the fsspec releases. You can find more information about the +supported protocols on the `fsspec documentation +`_. diff --git a/odoo-bringout-oca-storage-fs_storage/fs_storage/readme/HISTORY.rst b/odoo-bringout-oca-storage-fs_storage/fs_storage/readme/HISTORY.rst new file mode 100644 index 0000000..538aca9 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage/fs_storage/readme/HISTORY.rst @@ -0,0 +1,33 @@ +16.0.1.2.0 (2024-02-06) +~~~~~~~~~~~~~~~~~~~~~~~ + +**Features** + +- Invalidate FS filesystem object cache when the connection fails, forcing a reconnection. (`#320 `_) + + +16.0.1.1.0 (2023-12-22) +~~~~~~~~~~~~~~~~~~~~~~~ + +**Features** + +- Add parameter on storage backend to resolve protocol options values starting with $ from environment variables (`#303 `_) + + +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 `_) + + +16.0.1.0.2 (2023-10-09) +~~~~~~~~~~~~~~~~~~~~~~~ + +**Bugfixes** + +- Avoid config error when using the webdav protocol. The auth option is expected + to be a tuple not a list. Since our config is loaded from a json file, we + cannot use tuples. The fix converts the list to a tuple when the config is + related to a webdav protocol and the auth option is into the confix. (`#285 `_) diff --git a/odoo-bringout-oca-storage-fs_storage/fs_storage/readme/ROADMAP.rst b/odoo-bringout-oca-storage-fs_storage/fs_storage/readme/ROADMAP.rst new file mode 100644 index 0000000..0c69799 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage/fs_storage/readme/ROADMAP.rst @@ -0,0 +1,9 @@ +* Transactions: fsspec comes with a transactional mechanism that once started, + gathers all the files created during the transaction, and if the transaction + is committed, moves them to their final locations. It would be useful to + bridge this with the transactional mechanism of odoo. This would allow to + ensure that all the files created during a transaction are either all + moved to their final locations, or all deleted if the transaction is rolled + back. This mechanism is only valid for files created during the transaction + by a call to the `open` method of the file system. It is not valid for others + operations, such as `rm`, `mv_file`, ... . diff --git a/odoo-bringout-oca-storage-fs_storage/fs_storage/readme/USAGE.rst b/odoo-bringout-oca-storage-fs_storage/fs_storage/readme/USAGE.rst new file mode 100644 index 0000000..165148f --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage/fs_storage/readme/USAGE.rst @@ -0,0 +1,94 @@ +Configuration +~~~~~~~~~~~~~ + +When you create a new backend, you must specify the following: + +* The name of the backend. This is the name that will be used to + identify the backend into Odoo +* The code of the backend. This code will identify the backend into the store_fname + field of the ir.attachment model. This code must be unique. It will be used + as scheme. example of the store_fname field: ``odoofs://abs34Tg11``. +* The protocol used by the backend. The protocol refers to the supported + protocols of the fsspec python package. +* A directory path. This is a root directory from which the filesystem will + be mounted. This directory must exist. +* The protocol options. These are the options that will be passed to the + fsspec python package when creating the filesystem. These options depend + on the protocol used and are described in the fsspec documentation. +* Resolve env vars. This options resolves the protocol options values starting + with $ from environment variables +* Check Connection Method. If set, Odoo will always check the connection before using + a storage and it will remove the fs connection from the cache if the check fails. + + * ``Create Marker file`` : create a hidden file on remote and then check it exists with + Use it if you have write access to the remote and if it is not an issue to leave + the marker file in the root directory. + * ``List file`` : list all files from the root directory. You can use it if the directory + path does not contain a big list of files (for performance reasons) + +Some protocols defined in the fsspec package are wrappers around other +protocols. For example, the SimpleCacheFileSystem protocol is a wrapper +around any local filesystem protocol. In such cases, you must specify into the +protocol options the protocol to be wrapped and the options to be passed to +the wrapped protocol. + +For example, if you want to create a backend that uses the SimpleCacheFileSystem +protocol, after selecting the SimpleCacheFileSystem protocol, you must specify +the protocol options as follows: + +.. code-block:: python + + { + "directory_path": "/tmp/my_backend", + "target_protocol": "odoofs", + "target_options": {...}, + } + +In this example, the SimpleCacheFileSystem protocol will be used as a wrapper +around the odoofs protocol. + +Server Environment +~~~~~~~~~~~~~~~~~~ + +To ease the management of the filesystem storages configuration accross the different +environments, the configuration of the filesystem storages can be defined in +environment files or directly in the main configuration file. For example, the +configuration of a filesystem storage with the code `fsprod` can be provided in the +main configuration file as follows: + +.. code-block:: ini + + [fs_storage.fsprod] + protocol=s3 + options={"endpoint_url": "https://my_s3_server/", "key": "KEY", "secret": "SECRET"} + directory_path=my_bucket + +To work, a `storage.backend` record must exist with the code `fsprod` into the database. +In your configuration section, you can specify the value for the following fields: + +* `protocol` +* `options` +* `directory_path` + +Migration from storage_backend +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The fs_storage addon can be used to replace the storage_backend addon. (It has +been designed to be a drop-in replacement for the storage_backend addon). To +ease the migration, the `fs.storage` model defines the high-level methods +available in the storage_backend model. These methods are: + +* `add` +* `get` +* `list_files` +* `find_files` +* `move_files` +* `delete` + +These methods are wrappers around the methods of the `fsspec.AbstractFileSystem` +class (see https://filesystem-spec.readthedocs.io/en/latest/api.html#fsspec.spec.AbstractFileSystem). +These methods are marked as deprecated and will be removed in a future version (V18) +of the addon. You should use the methods of the `fsspec.AbstractFileSystem` class +instead since they are more flexible and powerful. You can access the instance +of the `fsspec.AbstractFileSystem` class using the `fs` property of a `fs.storage` +record. diff --git a/odoo-bringout-oca-storage-fs_storage/fs_storage/readme/newsfragments/.gitignore b/odoo-bringout-oca-storage-fs_storage/fs_storage/readme/newsfragments/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/odoo-bringout-oca-storage-fs_storage/fs_storage/rooted_dir_file_system.py b/odoo-bringout-oca-storage-fs_storage/fs_storage/rooted_dir_file_system.py new file mode 100644 index 0000000..3b2edae --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage/fs_storage/rooted_dir_file_system.py @@ -0,0 +1,37 @@ +# Copyright 2023 ACSONE SA/NV (https://www.acsone.eu). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import os + +from fsspec.implementations.dirfs import DirFileSystem +from fsspec.implementations.local import make_path_posix +from fsspec.registry import register_implementation + + +class RootedDirFileSystem(DirFileSystem): + """A directory-based filesystem that uses path as a root. + + The main purpose of this filesystem is to ensure that paths are always + a sub path of the initial path. IOW, it is not possible to go outside + the initial path. That's the only difference with the DirFileSystem provided + by fsspec. + + This one should be provided by fsspec itself. We should propose a PR. + """ + + def _join(self, path): + path = super()._join(path) + # Ensure that the path is a subpath of the root path by resolving + # any relative paths. + # Since the path separator is not always the same on all systems, + # we need to normalize the path separator. + path_posix = os.path.normpath(make_path_posix(path)) + root_posix = os.path.normpath(make_path_posix(self.path)) + if not path_posix.startswith(root_posix): + raise PermissionError( + "Path %s is not a subpath of the root path %s" % (path, self.path) + ) + return path + + +register_implementation("rooted_dir", RootedDirFileSystem) diff --git a/odoo-bringout-oca-storage-fs_storage/fs_storage/security/ir.model.access.csv b/odoo-bringout-oca-storage-fs_storage/fs_storage/security/ir.model.access.csv new file mode 100644 index 0000000..c1a81aa --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage/fs_storage/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_fs_storage_edit,fs_storage edit,model_fs_storage,base.group_system,1,1,1,1 +access_fs_test_connection,fs.test.connection.access,model_fs_test_connection,base.group_system,1,1,1,1 diff --git a/odoo-bringout-oca-storage-fs_storage/fs_storage/static/description/icon.png b/odoo-bringout-oca-storage-fs_storage/fs_storage/static/description/icon.png new file mode 100644 index 0000000..3a0328b Binary files /dev/null and b/odoo-bringout-oca-storage-fs_storage/fs_storage/static/description/icon.png differ diff --git a/odoo-bringout-oca-storage-fs_storage/fs_storage/static/description/index.html b/odoo-bringout-oca-storage-fs_storage/fs_storage/static/description/index.html new file mode 100644 index 0000000..74b005d --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage/fs_storage/static/description/index.html @@ -0,0 +1,641 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Filesystem Storage Backend

+ +

Beta License: LGPL-3 OCA/storage Translate me on Weblate Try me on Runboat

+

This addon is a technical addon that allows you to define filesystem like +storage for your data. It’s used by other addons to store their data in a +transparent way into different kind of storages.

+

Through the fs.storage record, you get access to an object that implements +the fsspec.spec.AbstractFileSystem interface and therefore give +you an unified interface to access your data whatever the storage protocol you +decide to use.

+

The list of supported protocols depends on the installed fsspec implementations. +By default, the addon will install the following protocols:

+
    +
  • LocalFileSystem
  • +
  • MemoryFileSystem
  • +
  • ZipFileSystem
  • +
  • TarFileSystem
  • +
  • FTPFileSystem
  • +
  • CachingFileSystem
  • +
  • WholeFileSystem
  • +
  • SimplCacheFileSystem
  • +
  • ReferenceFileSystem
  • +
  • GenericFileSystem
  • +
  • DirFileSystem
  • +
  • DatabricksFileSystem
  • +
  • GitHubFileSystem
  • +
  • JupiterFileSystem
  • +
  • OdooFileSystem
  • +
+

The OdooFileSystem is the one that allows you to store your data into a directory +mounted into your Odoo’s storage directory. This is the default FS Storage +when creating a new fs.storage record.

+

Others protocols are available through the installation of additional +python packages:

+
    +
  • DropboxDriveFileSystem -> pip install fsspec[dropbox]
  • +
  • HTTPFileSystem -> pip install fsspec[http]
  • +
  • HTTPSFileSystem -> pip install fsspec[http]
  • +
  • GCSFileSystem -> pip install fsspec[gcs]
  • +
  • GSFileSystem -> pip install fsspec[gs]
  • +
  • GoogleDriveFileSystem -> pip install gdrivefs
  • +
  • SFTPFileSystem -> pip install fsspec[sftp]
  • +
  • HaddoopFileSystem -> pip install fsspec[hdfs]
  • +
  • S3FileSystem -> pip install fsspec[s3]
  • +
  • WandbFS -> pip install wandbfs
  • +
  • OCIFileSystem -> pip install fsspec[oci]
  • +
  • AsyncLocalFileSystem -> pip install ‘morefs[asynclocalfs]
  • +
  • AzureDatalakeFileSystem -> pip install fsspec[adl]
  • +
  • AzureBlobFileSystem -> pip install fsspec[abfs]
  • +
  • DaskWorkerFileSystem -> pip install fsspec[dask]
  • +
  • GitFileSystem -> pip install fsspec[git]
  • +
  • SMBFileSystem -> pip install fsspec[smb]
  • +
  • LibArchiveFileSystem -> pip install fsspec[libarchive]
  • +
  • OSSFileSystem -> pip install ossfs
  • +
  • WebdavFileSystem -> pip install webdav4
  • +
  • DVCFileSystem -> pip install dvc
  • +
  • XRootDFileSystem -> pip install fsspec-xrootd
  • +
+

This list of supported protocols is not exhaustive or could change in the future +depending on the fsspec releases. You can find more information about the +supported protocols on the fsspec documentation.

+

Table of contents

+ +
+

Usage

+
+

Configuration

+

When you create a new backend, you must specify the following:

+
    +
  • The name of the backend. This is the name that will be used to +identify the backend into Odoo
  • +
  • The code of the backend. This code will identify the backend into the store_fname +field of the ir.attachment model. This code must be unique. It will be used +as scheme. example of the store_fname field: odoofs://abs34Tg11.
  • +
  • The protocol used by the backend. The protocol refers to the supported +protocols of the fsspec python package.
  • +
  • A directory path. This is a root directory from which the filesystem will +be mounted. This directory must exist.
  • +
  • The protocol options. These are the options that will be passed to the +fsspec python package when creating the filesystem. These options depend +on the protocol used and are described in the fsspec documentation.
  • +
  • Resolve env vars. This options resolves the protocol options values starting +with $ from environment variables
  • +
  • Check Connection Method. If set, Odoo will always check the connection before using +a storage and it will remove the fs connection from the cache if the check fails.
      +
    • Create Marker file : create a hidden file on remote and then check it exists with +Use it if you have write access to the remote and if it is not an issue to leave +the marker file in the root directory.
    • +
    • List file : list all files from the root directory. You can use it if the directory +path does not contain a big list of files (for performance reasons)
    • +
    +
  • +
+

Some protocols defined in the fsspec package are wrappers around other +protocols. For example, the SimpleCacheFileSystem protocol is a wrapper +around any local filesystem protocol. In such cases, you must specify into the +protocol options the protocol to be wrapped and the options to be passed to +the wrapped protocol.

+

For example, if you want to create a backend that uses the SimpleCacheFileSystem +protocol, after selecting the SimpleCacheFileSystem protocol, you must specify +the protocol options as follows:

+
+{
+    "directory_path": "/tmp/my_backend",
+    "target_protocol": "odoofs",
+    "target_options": {...},
+}
+
+

In this example, the SimpleCacheFileSystem protocol will be used as a wrapper +around the odoofs protocol.

+
+
+

Server Environment

+

To ease the management of the filesystem storages configuration accross the different +environments, the configuration of the filesystem storages can be defined in +environment files or directly in the main configuration file. For example, the +configuration of a filesystem storage with the code fsprod can be provided in the +main configuration file as follows:

+
+[fs_storage.fsprod]
+protocol=s3
+options={"endpoint_url": "https://my_s3_server/", "key": "KEY", "secret": "SECRET"}
+directory_path=my_bucket
+
+

To work, a storage.backend record must exist with the code fsprod into the database. +In your configuration section, you can specify the value for the following fields:

+
    +
  • protocol
  • +
  • options
  • +
  • directory_path
  • +
+
+
+

Migration from storage_backend

+

The fs_storage addon can be used to replace the storage_backend addon. (It has +been designed to be a drop-in replacement for the storage_backend addon). To +ease the migration, the fs.storage model defines the high-level methods +available in the storage_backend model. These methods are:

+
    +
  • add
  • +
  • get
  • +
  • list_files
  • +
  • find_files
  • +
  • move_files
  • +
  • delete
  • +
+

These methods are wrappers around the methods of the fsspec.AbstractFileSystem +class (see https://filesystem-spec.readthedocs.io/en/latest/api.html#fsspec.spec.AbstractFileSystem). +These methods are marked as deprecated and will be removed in a future version (V18) +of the addon. You should use the methods of the fsspec.AbstractFileSystem class +instead since they are more flexible and powerful. You can access the instance +of the fsspec.AbstractFileSystem class using the fs property of a fs.storage +record.

+
+
+
+

Known issues / Roadmap

+
    +
  • Transactions: fsspec comes with a transactional mechanism that once started, +gathers all the files created during the transaction, and if the transaction +is committed, moves them to their final locations. It would be useful to +bridge this with the transactional mechanism of odoo. This would allow to +ensure that all the files created during a transaction are either all +moved to their final locations, or all deleted if the transaction is rolled +back. This mechanism is only valid for files created during the transaction +by a call to the open method of the file system. It is not valid for others +operations, such as rm, mv_file, … .
  • +
+
+
+

Changelog

+
+

16.0.1.2.0 (2024-02-06)

+

Features

+
    +
  • Invalidate FS filesystem object cache when the connection fails, forcing a reconnection. (#320)
  • +
+
+
+

16.0.1.1.0 (2023-12-22)

+

Features

+
    +
  • Add parameter on storage backend to resolve protocol options values starting with $ from environment variables (#303)
  • +
+
+
+

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)
  • +
+
+
+

16.0.1.0.2 (2023-10-09)

+

Bugfixes

+
    +
  • Avoid config error when using the webdav protocol. The auth option is expected +to be a tuple not a list. Since our config is loaded from a json file, we +cannot use tuples. The fix converts the list to a tuple when the config is +related to a webdav protocol and the auth option is into the confix. (#285)
  • +
+
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub 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.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • ACSONE SA/NV
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/storage project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+
+ + diff --git a/odoo-bringout-oca-storage-fs_storage/fs_storage/tests/__init__.py b/odoo-bringout-oca-storage-fs_storage/fs_storage/tests/__init__.py new file mode 100644 index 0000000..ffb9e7e --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage/fs_storage/tests/__init__.py @@ -0,0 +1,2 @@ +from . import common +from . import test_fs_storage diff --git a/odoo-bringout-oca-storage-fs_storage/fs_storage/tests/common.py b/odoo-bringout-oca-storage-fs_storage/fs_storage/tests/common.py new file mode 100644 index 0000000..a6b2ca0 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage/fs_storage/tests/common.py @@ -0,0 +1,46 @@ +# Copyright 2023 ACSONE SA/NV (http://acsone.eu). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). +import base64 +import os +import shutil +import tempfile +from unittest import mock + +from odoo.tests.common import TransactionCase + +from ..models.fs_storage import FSStorage + + +class TestFSStorageCase(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.backend: FSStorage = cls.env.ref("fs_storage.default_fs_storage") + cls.backend.json_options = {"target_options": {"auto_mkdir": "True"}} + cls.filedata = base64.b64encode(b"This is a simple file") + cls.filename = "test_file.txt" + cls.case_with_subdirectory = "subdirectory/here" + cls.demo_user = cls.env.ref("base.user_demo") + cls.temp_dir = tempfile.mkdtemp() + + def setUp(self): + super().setUp() + mocked_backend = mock.patch.object( + self.backend.__class__, "_get_filesystem_storage_path" + ) + mocked_get_filesystem_storage_path = mocked_backend.start() + mocked_get_filesystem_storage_path.return_value = self.temp_dir + self.backend.write({"directory_path": self.temp_dir}) + + # pylint: disable=unused-variable + @self.addCleanup + def stop_mock(): + mocked_backend.stop() + # recursively delete the tempdir + if os.path.exists(self.temp_dir): + shutil.rmtree(self.temp_dir) + + def _create_file(self, backend: FSStorage, filename: str, filedata: str): + with backend.fs.open(filename, "wb") as f: + f.write(filedata) diff --git a/odoo-bringout-oca-storage-fs_storage/fs_storage/tests/test_fs_storage.py b/odoo-bringout-oca-storage-fs_storage/fs_storage/tests/test_fs_storage.py new file mode 100644 index 0000000..0623080 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage/fs_storage/tests/test_fs_storage.py @@ -0,0 +1,152 @@ +# Copyright 2023 ACSONE SA/NV (http://acsone.eu). +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). +import warnings +from unittest import mock + +from odoo.tests import Form +from odoo.tools import mute_logger + +from .common import TestFSStorageCase + + +class TestFSStorage(TestFSStorageCase): + @mute_logger("py.warnings") + def _test_deprecated_setting_and_getting_data(self): + # Check that the directory is empty + warnings.filterwarnings("ignore") + files = self.backend.list_files() + self.assertNotIn(self.filename, files) + + # Add a new file + self.backend.add( + self.filename, self.filedata, mimetype="text/plain", binary=False + ) + + # Check that the file exist + files = self.backend.list_files() + self.assertIn(self.filename, files) + + # Retrieve the file added + data = self.backend.get(self.filename, binary=False) + self.assertEqual(data, self.filedata) + + # Delete the file + self.backend.delete(self.filename) + files = self.backend.list_files() + self.assertNotIn(self.filename, files) + + @mute_logger("py.warnings") + def _test_deprecated_find_files(self): + warnings.filterwarnings("ignore") + self.backend.add( + self.filename, self.filedata, mimetype="text/plain", binary=False + ) + try: + res = self.backend.find_files(r".*\.txt") + self.assertListEqual([self.filename], res) + res = self.backend.find_files(r".*\.text") + self.assertListEqual([], res) + finally: + self.backend.delete(self.filename) + + def test_deprecated_setting_and_getting_data_from_root(self): + self._test_deprecated_setting_and_getting_data() + + def test_deprecated_setting_and_getting_data_from_dir(self): + self.backend.directory_path = self.case_with_subdirectory + self._test_deprecated_setting_and_getting_data() + + def test_deprecated_find_files_from_root(self): + self._test_deprecated_find_files() + + def test_deprecated_find_files_from_dir(self): + self.backend.directory_path = self.case_with_subdirectory + self._test_deprecated_find_files() + + def test_ensure_one_fs_by_record(self): + # in this test we ensure that we've one fs by record + backend_ids = [] + for i in range(4): + backend_ids.append( + self.backend.create( + {"name": f"name{i}", "directory_path": f"{i}", "code": f"code{i}"} + ).id + ) + records = self.backend.browse(backend_ids) + fs = None + for rec in records: + self.assertNotEqual(fs, rec.fs) + + def test_relative_access(self): + self.backend.directory_path = self.case_with_subdirectory + self._create_file(self.backend, self.filename, self.filedata) + other_subdirectory = "other_subdirectory" + backend2 = self.backend.copy({"directory_path": other_subdirectory}) + self._create_file(backend2, self.filename, self.filedata) + with self.assertRaises(PermissionError), self.env.cr.savepoint(): + # check that we can't access outside the subdirectory + backend2.fs.ls("../") + with self.assertRaises(PermissionError), self.env.cr.savepoint(): + # check that we can't access the file into another subdirectory + backend2.fs.ls(f"../{self.case_with_subdirectory}") + self.backend.fs.rm_file(self.filename) + backend2.fs.rm_file(self.filename) + + def test_recursive_add_odoo_storage_path_to_options(self): + options = { + "directory_path": "/tmp/my_backend", + "target_protocol": "odoofs", + } + self.backend._recursive_add_odoo_storage_path(options) + self.assertEqual( + self.backend._odoo_storage_path, + options.get("target_options").get("odoo_storage_path"), + ) + options = { + "directory_path": "/tmp/my_backend", + "target_protocol": "dir", + "target_options": { + "path": "/my_backend", + "target_protocol": "odoofs", + }, + } + self.backend._recursive_add_odoo_storage_path(options) + self.assertEqual( + self.backend._odoo_storage_path, + options.get("target_options") + .get("target_options") + .get("odoo_storage_path"), + ) + + def test_interface_values(self): + protocol = "file" # should be available by default in the list of protocols + with Form(self.env["fs.storage"]) as new_storage: + new_storage.name = "Test storage" + new_storage.code = "code" + new_storage.protocol = protocol + self.assertEqual(new_storage.protocol, protocol) + # the options should follow the protocol + self.assertEqual(new_storage.options_protocol, protocol) + description = new_storage.protocol_descr + self.assertTrue("Interface to files on local storage" in description) + # this is still true after saving + self.assertEqual(new_storage.options_protocol, protocol) + + def test_options_env(self): + self.backend.json_options = {"key": {"sub_key": "$KEY_VAR"}} + eval_json_options = {"key": {"sub_key": "TEST"}} + options = self.backend._get_fs_options() + self.assertDictEqual(options, self.backend.json_options) + self.backend.eval_options_from_env = True + with mock.patch.dict("os.environ", {"KEY_VAR": "TEST"}): + options = self.backend._get_fs_options() + self.assertDictEqual(options, eval_json_options) + with self.assertLogs(level="WARNING") as log: + options = self.backend._get_fs_options() + self.assertIn( + ( + f"Environment variable KEY_VAR is not set for " + f"fs_storage {self.backend.display_name}." + ), + log.output[0], + ) diff --git a/odoo-bringout-oca-storage-fs_storage/fs_storage/views/fs_storage_view.xml b/odoo-bringout-oca-storage-fs_storage/fs_storage/views/fs_storage_view.xml new file mode 100644 index 0000000..712bb2a --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage/fs_storage/views/fs_storage_view.xml @@ -0,0 +1,119 @@ + + + + fs.storage.tree (in fs_storage) + fs.storage + + + + + + + + + + fs.storage.form (in fs_storage) + fs.storage + +
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + fs.storage.search (in fs_storage) + fs.storage + + + + + + + + + FS Storage + ir.actions.act_window + fs.storage + tree,form + + [] + {} + + + + + form + + + + + + tree + + + + +
diff --git a/odoo-bringout-oca-storage-fs_storage/fs_storage/wizards/__init__.py b/odoo-bringout-oca-storage-fs_storage/fs_storage/wizards/__init__.py new file mode 100644 index 0000000..197ee33 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage/fs_storage/wizards/__init__.py @@ -0,0 +1 @@ +from . import fs_test_connection diff --git a/odoo-bringout-oca-storage-fs_storage/fs_storage/wizards/fs_test_connection.py b/odoo-bringout-oca-storage-fs_storage/fs_storage/wizards/fs_test_connection.py new file mode 100644 index 0000000..ebaf615 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage/fs_storage/wizards/fs_test_connection.py @@ -0,0 +1,26 @@ +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import api, fields, models + + +class FSTestConnection(models.TransientModel): + _name = "fs.test.connection" + _description = "FS Test Connection Wizard" + + def _get_check_connection_method_selection(self): + return self.env["fs.storage"]._get_check_connection_method_selection() + + storage_id = fields.Many2one("fs.storage") + check_connection_method = fields.Selection( + selection="_get_check_connection_method_selection", + required=True, + ) + + @api.model + def default_get(self, field_list): + res = super().default_get(field_list) + res["storage_id"] = self.env.context.get("active_id", False) + return res + + def action_test_config(self): + return self.storage_id._test_config(self.check_connection_method) diff --git a/odoo-bringout-oca-storage-fs_storage/fs_storage/wizards/fs_test_connection.xml b/odoo-bringout-oca-storage-fs_storage/fs_storage/wizards/fs_test_connection.xml new file mode 100644 index 0000000..2abed80 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage/fs_storage/wizards/fs_test_connection.xml @@ -0,0 +1,31 @@ + + + + fs.test.connection.form + fs.test.connection + +
+ + + + +
+
+
+
+
+ + FS Test Connection + ir.actions.act_window + fs.test.connection + form + new + +
diff --git a/odoo-bringout-oca-storage-fs_storage/pyproject.toml b/odoo-bringout-oca-storage-fs_storage/pyproject.toml new file mode 100644 index 0000000..ae9a2b7 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage/pyproject.toml @@ -0,0 +1,44 @@ +[project] +name = "odoo-bringout-oca-storage-fs_storage" +version = "16.0.0" +description = "Filesystem Storage Backend - Implement the concept of Storage with amazon S3, sftp..." +authors = [ + { name = "Ernad Husremovic", email = "hernad@bring.out.ba" } +] +dependencies = [ + "odoo-bringout-oca-ocb-base>=16.0.0", + "odoo-bringout-oca-storage-base_sparse_field>=16.0.0", + "odoo-bringout-oca-storage-server_environment>=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_storage"] + +[tool.rye] +managed = true +dev-dependencies = [ + "pytest>=8.4.1", +] diff --git a/odoo-bringout-oca-storage-fs_storage_backup/README.md b/odoo-bringout-oca-storage-fs_storage_backup/README.md new file mode 100644 index 0000000..7da2113 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage_backup/README.md @@ -0,0 +1,47 @@ +# Filesystem Storage Backup + +Odoo addon: fs_storage_backup + +## Installation + +```bash +pip install odoo-bringout-oca-storage-fs_storage_backup +``` + +## Dependencies + +This addon depends on: +- fs_storage +- mail + +## Manifest Information + +- **Name**: Filesystem Storage Backup +- **Version**: 16.0.1.0.1 +- **Category**: Technical +- **License**: AGPL-3 +- **Installable**: False + +## Source + +Based on [OCA/storage](https://github.com/OCA/storage) branch 16.0, addon `fs_storage_backup`. + +## 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 diff --git a/odoo-bringout-oca-storage-fs_storage_backup/doc/ARCHITECTURE.md b/odoo-bringout-oca-storage-fs_storage_backup/doc/ARCHITECTURE.md new file mode 100644 index 0000000..9326cc3 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage_backup/doc/ARCHITECTURE.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_storage_backup Module - fs_storage_backup + 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. diff --git a/odoo-bringout-oca-storage-fs_storage_backup/doc/CONFIGURATION.md b/odoo-bringout-oca-storage-fs_storage_backup/doc/CONFIGURATION.md new file mode 100644 index 0000000..8d01623 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage_backup/doc/CONFIGURATION.md @@ -0,0 +1,3 @@ +# Configuration + +Refer to Odoo settings for fs_storage_backup. Configure related models, access rights, and options as needed. diff --git a/odoo-bringout-oca-storage-fs_storage_backup/doc/CONTROLLERS.md b/odoo-bringout-oca-storage-fs_storage_backup/doc/CONTROLLERS.md new file mode 100644 index 0000000..f628e77 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage_backup/doc/CONTROLLERS.md @@ -0,0 +1,3 @@ +# Controllers + +This module does not define custom HTTP controllers. diff --git a/odoo-bringout-oca-storage-fs_storage_backup/doc/DEPENDENCIES.md b/odoo-bringout-oca-storage-fs_storage_backup/doc/DEPENDENCIES.md new file mode 100644 index 0000000..4dea08f --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage_backup/doc/DEPENDENCIES.md @@ -0,0 +1,6 @@ +# Dependencies + +This addon depends on: + +- [fs_storage](../../odoo-bringout-oca-storage-fs_storage) +- [mail](../../odoo-bringout-oca-ocb-mail) diff --git a/odoo-bringout-oca-storage-fs_storage_backup/doc/FAQ.md b/odoo-bringout-oca-storage-fs_storage_backup/doc/FAQ.md new file mode 100644 index 0000000..3d0ee8e --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage_backup/doc/FAQ.md @@ -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_storage_backup or install in UI. diff --git a/odoo-bringout-oca-storage-fs_storage_backup/doc/INSTALL.md b/odoo-bringout-oca-storage-fs_storage_backup/doc/INSTALL.md new file mode 100644 index 0000000..88dafab --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage_backup/doc/INSTALL.md @@ -0,0 +1,7 @@ +# Install + +```bash +pip install odoo-bringout-oca-storage-fs_storage_backup" +# or +uv pip install odoo-bringout-oca-storage-fs_storage_backup" +``` diff --git a/odoo-bringout-oca-storage-fs_storage_backup/doc/MODELS.md b/odoo-bringout-oca-storage-fs_storage_backup/doc/MODELS.md new file mode 100644 index 0000000..672fc64 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage_backup/doc/MODELS.md @@ -0,0 +1,12 @@ +# Models + +Detected core models and extensions in fs_storage_backup. + +```mermaid +classDiagram + class fs_storage +``` + +Notes +- Classes show model technical names; fields omitted for brevity. +- Items listed under _inherit are extensions of existing models. diff --git a/odoo-bringout-oca-storage-fs_storage_backup/doc/OVERVIEW.md b/odoo-bringout-oca-storage-fs_storage_backup/doc/OVERVIEW.md new file mode 100644 index 0000000..2d0d7fc --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage_backup/doc/OVERVIEW.md @@ -0,0 +1,6 @@ +# Overview + +Packaged Odoo addon: fs_storage_backup. Provides features documented in upstream Odoo 16 under this addon. + +- Source: OCA/OCB 16.0, addon fs_storage_backup +- License: LGPL-3 diff --git a/odoo-bringout-oca-storage-fs_storage_backup/doc/REPORTS.md b/odoo-bringout-oca-storage-fs_storage_backup/doc/REPORTS.md new file mode 100644 index 0000000..e0ea35f --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage_backup/doc/REPORTS.md @@ -0,0 +1,3 @@ +# Reports + +This module does not define custom reports. diff --git a/odoo-bringout-oca-storage-fs_storage_backup/doc/SECURITY.md b/odoo-bringout-oca-storage-fs_storage_backup/doc/SECURITY.md new file mode 100644 index 0000000..e07da9d --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage_backup/doc/SECURITY.md @@ -0,0 +1,8 @@ +# Security + +This module does not define custom security rules or access controls beyond Odoo defaults. + +Default Odoo security applies: +- Base user access through standard groups +- Model access inherited from dependencies +- No custom row-level security rules diff --git a/odoo-bringout-oca-storage-fs_storage_backup/doc/TROUBLESHOOTING.md b/odoo-bringout-oca-storage-fs_storage_backup/doc/TROUBLESHOOTING.md new file mode 100644 index 0000000..56853cb --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage_backup/doc/TROUBLESHOOTING.md @@ -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. diff --git a/odoo-bringout-oca-storage-fs_storage_backup/doc/USAGE.md b/odoo-bringout-oca-storage-fs_storage_backup/doc/USAGE.md new file mode 100644 index 0000000..63fda6c --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage_backup/doc/USAGE.md @@ -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_storage_backup +``` diff --git a/odoo-bringout-oca-storage-fs_storage_backup/doc/WIZARDS.md b/odoo-bringout-oca-storage-fs_storage_backup/doc/WIZARDS.md new file mode 100644 index 0000000..48e790d --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage_backup/doc/WIZARDS.md @@ -0,0 +1,3 @@ +# Wizards + +This module does not include UI wizards. diff --git a/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/README.rst b/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/README.rst new file mode 100644 index 0000000..0e1f8b9 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/README.rst @@ -0,0 +1,103 @@ +========================= +Filesystem Storage Backup +========================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:3292ef4f97f5dcf8a5364cba204599da2169dd30020b9b488ddb72885f85f85b + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstorage-lightgray.png?logo=github + :target: https://github.com/OCA/storage/tree/16.0/fs_storage_backup + :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_storage_backup + :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| + +With this module you can configure one or more database backup +locations. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +1. Go to Settings > Technical > FS Storage > FS Storage +2. Select a filesystem you want to use for backups. **NOTE: Make sure + you don't use the filestore as backup location otherwise it's + possible you'll back up the backup** +3. Enable ``Use For Backups`` +4. Follow it (using the chatter) if you want to get notified when a + backup fails +5. To know if the backup is working correctly you can run the scheduled + action (``Backup database and delete old backups``) manually to test + it. + +Usage +===== + +The backup is done automatically by a scheduled action +(``Backup database and delete old backups``). + +Known issues / Roadmap +====================== + +- **Configurable backup frequency**: e.g. backup every 7 days in s3 and + every 4 hours on a FTP server. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub 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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Onestein + +Contributors +------------ + +- Dennis Sluijk d.sluijk@onestein.nl (https://onestein.nl) + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/storage `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/__init__.py b/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/__manifest__.py b/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/__manifest__.py new file mode 100644 index 0000000..4f1acec --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/__manifest__.py @@ -0,0 +1,14 @@ +{ + "name": "Filesystem Storage Backup", + "category": "Technical", + "version": "16.0.1.0.1", + "license": "AGPL-3", + "author": "Onestein, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/storage", + "depends": ["fs_storage", "mail"], + "data": [ + "data/ir_cron_data.xml", + "data/mail_message_subtype_data.xml", + "views/fs_storage_view.xml", + ], +} diff --git a/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/data/ir_cron_data.xml b/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/data/ir_cron_data.xml new file mode 100644 index 0000000..c343f1f --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/data/ir_cron_data.xml @@ -0,0 +1,18 @@ + + + + Backup database and delete old backups + 1 + days + -1 + True + + + code + model.cron_backup_db() + + + diff --git a/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/data/mail_message_subtype_data.xml b/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/data/mail_message_subtype_data.xml new file mode 100644 index 0000000..e56ff2c --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/data/mail_message_subtype_data.xml @@ -0,0 +1,15 @@ + + + + Backup Failed + Backup failed + fs.storage + + + + Backup Cleanup Failed + Failed to clean up old backups + fs.storage + + + diff --git a/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/i18n/bs.po b/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/i18n/bs.po new file mode 100644 index 0000000..f93b47d --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/i18n/bs.po @@ -0,0 +1,177 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_storage_backup +# +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_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_needaction +msgid "Action Needed" +msgstr "Potrebna akcija" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_attachment_count +msgid "Attachment Count" +msgstr "Broj priloga" + +#. module: fs_storage_backup +#: model:mail.message.subtype,name:fs_storage_backup.message_subtype_cleanup_failed +msgid "Backup Cleanup Failed" +msgstr "Čišćenje sigurnosnih kopija neuspješno" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__backup_dir +msgid "Backup Directory" +msgstr "Direktorij za sigurnosne kopije" + +#. module: fs_storage_backup +#: model:mail.message.subtype,name:fs_storage_backup.message_subtype_backup_failed +msgid "Backup Failed" +msgstr "Backup nije uspio" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__backup_filename_format +msgid "Backup Filename" +msgstr "Naziv datoteke sigurnosne kopije" + +#. module: fs_storage_backup +#: model:ir.actions.server,name:fs_storage_backup.cron_backup_db_ir_actions_server +#: model:ir.cron,cron_name:fs_storage_backup.cron_backup_db +msgid "Backup database and delete old backups" +msgstr "Kreiraj sigurnosnu kopiju baze podataka i obriši stare kopije" + +#. module: fs_storage_backup +#: model:mail.message.subtype,description:fs_storage_backup.message_subtype_backup_failed +msgid "Backup failed" +msgstr "Kreiranje sigurnosne kopije neuspješno" + +#. module: fs_storage_backup +#: model_terms:ir.ui.view,arch_db:fs_storage_backup.fs_storage_form_view +msgid "Backups" +msgstr "Sigurnosne kopije" + +#. module: fs_storage_backup +#. odoo-python +#: code:addons/fs_storage_backup/models/fs_storage.py:0 +#, python-format +msgid "Database backup failed" +msgstr "Kreiranje sigurnosne kopije baze podataka neuspješno" + +#. module: fs_storage_backup +#: model:ir.model,name:fs_storage_backup.model_fs_storage +msgid "FS Storage" +msgstr "FS skladište" + +#. module: fs_storage_backup +#. odoo-python +#: code:addons/fs_storage_backup/models/fs_storage.py:0 +#: model:mail.message.subtype,description:fs_storage_backup.message_subtype_cleanup_failed +#, python-format +msgid "Failed to clean up old backups" +msgstr "Neuspješno čišćenje starih sigurnosnih kopija" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_follower_ids +msgid "Followers" +msgstr "Pratioci" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_partner_ids +msgid "Followers (Partners)" +msgstr "Pratioci (Partneri)" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__has_message +msgid "Has Message" +msgstr "Ima poruku" + +#. module: fs_storage_backup +#: model:ir.model.fields,help:fs_storage_backup.field_fs_storage__message_needaction +msgid "If checked, new messages require your attention." +msgstr "Ako je zakačeno, nove poruke će zahtjevati vašu pažnju" + +#. module: fs_storage_backup +#: model:ir.model.fields,help:fs_storage_backup.field_fs_storage__message_has_error +msgid "If checked, some messages have a delivery error." +msgstr "Ako je označeno neke poruke mogu imati grešku u dostavi." + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__backup_include_filestore +msgid "Include Filestore In Backup" +msgstr "Uključi skladište datoteka u sigurnosnu kopiju" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_is_follower +msgid "Is Follower" +msgstr "Pratilac" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__backup_keep_time +msgid "Keep backups of (in days)" +msgstr "Čuvaj sigurnosne kopije (u danima)" + +#. module: fs_storage_backup +#. odoo-python +#: code:addons/fs_storage_backup/models/fs_storage.py:0 +#, python-format +msgid "Keep backups of (in days) must be greater or than 0." +msgstr "Čuvaj sigurnosne kopije (u danima) mora biti veće od 0." + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_main_attachment_id +msgid "Main Attachment" +msgstr "Glavna zakačka" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_has_error +msgid "Message Delivery error" +msgstr "Greška pri isporuci poruke" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_ids +msgid "Messages" +msgstr "Poruke" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_needaction_counter +msgid "Number of Actions" +msgstr "Broj akcija" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_has_error_counter +msgid "Number of errors" +msgstr "Broj grešaka" + +#. module: fs_storage_backup +#: model:ir.model.fields,help:fs_storage_backup.field_fs_storage__message_needaction_counter +msgid "Number of messages requiring action" +msgstr "Broj poruka koje zahtijevaju aktivnost" + +#. module: fs_storage_backup +#: model:ir.model.fields,help:fs_storage_backup.field_fs_storage__message_has_error_counter +msgid "Number of messages with delivery error" +msgstr "Broj poruka sa greškama pri isporuci" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__use_for_backup +msgid "Use For Backups" +msgstr "Koristi za sigurnosne kopije" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__website_message_ids +msgid "Website Messages" +msgstr "Poruke sa website-a" + +#. module: fs_storage_backup +#: model:ir.model.fields,help:fs_storage_backup.field_fs_storage__website_message_ids +msgid "Website communication history" +msgstr "Povijest komunikacije Web stranice" diff --git a/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/i18n/fs_storage_backup.pot b/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/i18n/fs_storage_backup.pot new file mode 100644 index 0000000..1579934 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/i18n/fs_storage_backup.pot @@ -0,0 +1,177 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_storage_backup +# +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_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_needaction +msgid "Action Needed" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_attachment_count +msgid "Attachment Count" +msgstr "" + +#. module: fs_storage_backup +#: model:mail.message.subtype,name:fs_storage_backup.message_subtype_cleanup_failed +msgid "Backup Cleanup Failed" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__backup_dir +msgid "Backup Directory" +msgstr "" + +#. module: fs_storage_backup +#: model:mail.message.subtype,name:fs_storage_backup.message_subtype_backup_failed +msgid "Backup Failed" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__backup_filename_format +msgid "Backup Filename" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.actions.server,name:fs_storage_backup.cron_backup_db_ir_actions_server +#: model:ir.cron,cron_name:fs_storage_backup.cron_backup_db +msgid "Backup database and delete old backups" +msgstr "" + +#. module: fs_storage_backup +#: model:mail.message.subtype,description:fs_storage_backup.message_subtype_backup_failed +msgid "Backup failed" +msgstr "" + +#. module: fs_storage_backup +#: model_terms:ir.ui.view,arch_db:fs_storage_backup.fs_storage_form_view +msgid "Backups" +msgstr "" + +#. module: fs_storage_backup +#. odoo-python +#: code:addons/fs_storage_backup/models/fs_storage.py:0 +#, python-format +msgid "Database backup failed" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model,name:fs_storage_backup.model_fs_storage +msgid "FS Storage" +msgstr "" + +#. module: fs_storage_backup +#. odoo-python +#: code:addons/fs_storage_backup/models/fs_storage.py:0 +#: model:mail.message.subtype,description:fs_storage_backup.message_subtype_cleanup_failed +#, python-format +msgid "Failed to clean up old backups" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_follower_ids +msgid "Followers" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_partner_ids +msgid "Followers (Partners)" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__has_message +msgid "Has Message" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,help:fs_storage_backup.field_fs_storage__message_needaction +msgid "If checked, new messages require your attention." +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,help:fs_storage_backup.field_fs_storage__message_has_error +msgid "If checked, some messages have a delivery error." +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__backup_include_filestore +msgid "Include Filestore In Backup" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_is_follower +msgid "Is Follower" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__backup_keep_time +msgid "Keep backups of (in days)" +msgstr "" + +#. module: fs_storage_backup +#. odoo-python +#: code:addons/fs_storage_backup/models/fs_storage.py:0 +#, python-format +msgid "Keep backups of (in days) must be greater or than 0." +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_main_attachment_id +msgid "Main Attachment" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_has_error +msgid "Message Delivery error" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_ids +msgid "Messages" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_needaction_counter +msgid "Number of Actions" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_has_error_counter +msgid "Number of errors" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,help:fs_storage_backup.field_fs_storage__message_needaction_counter +msgid "Number of messages requiring action" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,help:fs_storage_backup.field_fs_storage__message_has_error_counter +msgid "Number of messages with delivery error" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__use_for_backup +msgid "Use For Backups" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__website_message_ids +msgid "Website Messages" +msgstr "" + +#. module: fs_storage_backup +#: model:ir.model.fields,help:fs_storage_backup.field_fs_storage__website_message_ids +msgid "Website communication history" +msgstr "" diff --git a/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/i18n/it.po b/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/i18n/it.po new file mode 100644 index 0000000..cca4374 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/i18n/it.po @@ -0,0 +1,180 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fs_storage_backup +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2025-05-29 16:26+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.10.4\n" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_needaction +msgid "Action Needed" +msgstr "Azione richiesta" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_attachment_count +msgid "Attachment Count" +msgstr "Conteggio allegati" + +#. module: fs_storage_backup +#: model:mail.message.subtype,name:fs_storage_backup.message_subtype_cleanup_failed +msgid "Backup Cleanup Failed" +msgstr "Pulitura backup fallita" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__backup_dir +msgid "Backup Directory" +msgstr "Cartella backup" + +#. module: fs_storage_backup +#: model:mail.message.subtype,name:fs_storage_backup.message_subtype_backup_failed +msgid "Backup Failed" +msgstr "Backup fallito" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__backup_filename_format +msgid "Backup Filename" +msgstr "Nome file backup" + +#. module: fs_storage_backup +#: model:ir.actions.server,name:fs_storage_backup.cron_backup_db_ir_actions_server +#: model:ir.cron,cron_name:fs_storage_backup.cron_backup_db +msgid "Backup database and delete old backups" +msgstr "Esegui backup del database e cancella i vecchi backup" + +#. module: fs_storage_backup +#: model:mail.message.subtype,description:fs_storage_backup.message_subtype_backup_failed +msgid "Backup failed" +msgstr "Backup fallito" + +#. module: fs_storage_backup +#: model_terms:ir.ui.view,arch_db:fs_storage_backup.fs_storage_form_view +msgid "Backups" +msgstr "Backup" + +#. module: fs_storage_backup +#. odoo-python +#: code:addons/fs_storage_backup/models/fs_storage.py:0 +#, python-format +msgid "Database backup failed" +msgstr "Backup del database non riuscito" + +#. module: fs_storage_backup +#: model:ir.model,name:fs_storage_backup.model_fs_storage +msgid "FS Storage" +msgstr "Deposito FS" + +#. module: fs_storage_backup +#. odoo-python +#: code:addons/fs_storage_backup/models/fs_storage.py:0 +#: model:mail.message.subtype,description:fs_storage_backup.message_subtype_cleanup_failed +#, python-format +msgid "Failed to clean up old backups" +msgstr "Pulizia vecchi backup fallita" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_follower_ids +msgid "Followers" +msgstr "Seguito da" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_partner_ids +msgid "Followers (Partners)" +msgstr "Seguito da (partner)" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__has_message +msgid "Has Message" +msgstr "Ha un messaggio" + +#. module: fs_storage_backup +#: model:ir.model.fields,help:fs_storage_backup.field_fs_storage__message_needaction +msgid "If checked, new messages require your attention." +msgstr "Se selezionata, nuovi messaggi richiedono attenzione." + +#. module: fs_storage_backup +#: model:ir.model.fields,help:fs_storage_backup.field_fs_storage__message_has_error +msgid "If checked, some messages have a delivery error." +msgstr "Se selezionata, alcuni messaggi hanno un errore di consegna." + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__backup_include_filestore +msgid "Include Filestore In Backup" +msgstr "Includere Filestore nel backup" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_is_follower +msgid "Is Follower" +msgstr "Segue" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__backup_keep_time +msgid "Keep backups of (in days)" +msgstr "Mantenere backup per (in giorni)" + +#. module: fs_storage_backup +#. odoo-python +#: code:addons/fs_storage_backup/models/fs_storage.py:0 +#, python-format +msgid "Keep backups of (in days) must be greater or than 0." +msgstr "Mantenere il backup per (in giorni) deve essere 0 o maggiore." + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_main_attachment_id +msgid "Main Attachment" +msgstr "Allegato principale" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_has_error +msgid "Message Delivery error" +msgstr "Errore di consegna messaggio" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_ids +msgid "Messages" +msgstr "Messaggi" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_needaction_counter +msgid "Number of Actions" +msgstr "Numero di azioni" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__message_has_error_counter +msgid "Number of errors" +msgstr "Numero di errori" + +#. module: fs_storage_backup +#: model:ir.model.fields,help:fs_storage_backup.field_fs_storage__message_needaction_counter +msgid "Number of messages requiring action" +msgstr "Numero di messaggi che richiedono un'azione" + +#. module: fs_storage_backup +#: model:ir.model.fields,help:fs_storage_backup.field_fs_storage__message_has_error_counter +msgid "Number of messages with delivery error" +msgstr "Numero di messaggi con errore di consegna" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__use_for_backup +msgid "Use For Backups" +msgstr "Usa per backup" + +#. module: fs_storage_backup +#: model:ir.model.fields,field_description:fs_storage_backup.field_fs_storage__website_message_ids +msgid "Website Messages" +msgstr "Messaggi sito web" + +#. module: fs_storage_backup +#: model:ir.model.fields,help:fs_storage_backup.field_fs_storage__website_message_ids +msgid "Website communication history" +msgstr "Cronologia comunicazioni sito web" diff --git a/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/models/__init__.py b/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/models/__init__.py new file mode 100644 index 0000000..349bb04 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/models/__init__.py @@ -0,0 +1 @@ +from . import fs_storage diff --git a/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/models/fs_storage.py b/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/models/fs_storage.py new file mode 100644 index 0000000..8a4dccc --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/models/fs_storage.py @@ -0,0 +1,116 @@ +import logging +import traceback +from datetime import timedelta, timezone +from os import path + +from odoo import _, api, fields, models, tools +from odoo.exceptions import ValidationError +from odoo.service import db + +_logger = logging.getLogger(__name__) + + +class FSStorage(models.Model): + _name = "fs.storage" + _inherit = ["fs.storage", "mail.thread"] # Use queue_job_cron instead? + + use_for_backup = fields.Boolean(string="Use For Backups") + backup_include_filestore = fields.Boolean( + string="Include Filestore In Backup", + ) + backup_filename_format = fields.Char( + string="Backup Filename", default="backup-%(db)s-%(dt)s.%(ext)s" + ) + backup_keep_time = fields.Integer(string="Keep backups of (in days)", default=7) + backup_dir = fields.Char(string="Backup Directory", default="backups") + + @property + def _server_env_fields(self): + env_fields = super()._server_env_fields + env_fields.update( + { + "use_for_backup": {}, + "backup_include_filestore": {}, + "backup_filename_format": {"no_default_field": False}, + "backup_keep_time": {"no_default_field": False}, + "backup_dir": {"no_default_field": False}, + } + ) + return env_fields + + @api.constrains("backup_keep_time") + def _constrain_backup_keep_time(self): + if self.backup_keep_time < 1: + raise ValidationError( + _("Keep backups of (in days) must be greater or than 0.") + ) + + def _get_backup_format(self): + self.ensure_one() + return self.backup_include_filestore and "zip" or "dump" + + def _get_backup_path(self): + self.ensure_one() + file_ext = self._get_backup_format() + current_datetime = fields.Datetime.now().strftime("%Y%m%d%H%M%S") + return path.join( + self.backup_dir, + self.backup_filename_format + % {"db": self.env.cr.dbname, "dt": current_datetime, "ext": file_ext}, + ) + + def backup_db(self): + self.ensure_one() + try: + backup_path = self._get_backup_path() + self.fs.makedirs(self.backup_dir, exist_ok=True) + if self.fs.exists(backup_path): + raise Exception("File already exists (%s)." % backup_path) + backup_file = self.fs.open(backup_path, "w") + list_db = tools.config["list_db"] + if not list_db: + tools.config["list_db"] = True + db.dump_db( + self.env.cr.dbname, + backup_file.buffer, + backup_format=self._get_backup_format(), + ) + tools.config["list_db"] = list_db + except Exception as e: + _logger.exception("Database backup failed: %s", e) + self.message_post( + subject=_("Database backup failed"), + body="
%s
" % tools.html_escape(traceback.format_exc()), + subtype_id=self.env.ref( + "fs_storage_backup.message_subtype_backup_failed" + ).id, + ) + + def cleanup_old_backups(self): + self.ensure_one() + expiry_date = fields.Datetime.now() - timedelta(days=self.backup_keep_time) + try: + files = self.fs.ls(self.backup_dir, detail=False) + for file_path in files: + file_dt = self.fs.modified(file_path) + file_dt = file_dt.astimezone(timezone.utc) + file_dt = file_dt.replace(tzinfo=None) + if file_dt < expiry_date: + self.fs.rm(file_path) + except Exception as e: + _logger.exception("Failed to clean up old backups: %s", e) + self.message_post( + subject=_("Failed to clean up old backups"), + body="
%s
" % tools.html_escape(traceback.format_exc()), + subtype_id=self.env.ref( + "fs_storage_backup.message_subtype_cleanup_failed" + ).id, + ) + + @api.model + def cron_backup_db(self): + # use_for_backup is not searchable + storages = self.search([]) + for storage in storages.filtered(lambda s: s.use_for_backup): + storage.backup_db() + storage.cleanup_old_backups() diff --git a/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/readme/CONFIGURE.md b/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/readme/CONFIGURE.md new file mode 100644 index 0000000..8d674fe --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/readme/CONFIGURE.md @@ -0,0 +1,7 @@ +1. Go to Settings > Technical > FS Storage > FS Storage +2. Select a filesystem you want to use for backups. + **NOTE: Make sure you don't use the filestore as backup location otherwise it's possible you'll back up the backup** +3. Enable `Use For Backups` +4. Follow it (using the chatter) if you want to get notified when a backup fails +5. To know if the backup is working correctly you can run the scheduled action + (`Backup database and delete old backups`) manually to test it. diff --git a/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/readme/CONTRIBUTORS.md b/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/readme/CONTRIBUTORS.md new file mode 100644 index 0000000..8a94e60 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +- Dennis Sluijk (https://onestein.nl) diff --git a/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/readme/DESCRIPTION.md b/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/readme/DESCRIPTION.md new file mode 100644 index 0000000..25f1c1b --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/readme/DESCRIPTION.md @@ -0,0 +1 @@ +With this module you can configure one or more database backup locations. \ No newline at end of file diff --git a/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/readme/ROADMAP.md b/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/readme/ROADMAP.md new file mode 100644 index 0000000..9727d47 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/readme/ROADMAP.md @@ -0,0 +1 @@ +- **Configurable backup frequency**: e.g. backup every 7 days in s3 and every 4 hours on a FTP server. \ No newline at end of file diff --git a/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/readme/USAGE.md b/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/readme/USAGE.md new file mode 100644 index 0000000..be546dc --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/readme/USAGE.md @@ -0,0 +1 @@ +The backup is done automatically by a scheduled action (`Backup database and delete old backups`). \ No newline at end of file diff --git a/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/static/description/icon.png b/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/static/description/icon.png new file mode 100644 index 0000000..1dcc49c Binary files /dev/null and b/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/static/description/icon.png differ diff --git a/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/static/description/index.html b/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/static/description/index.html new file mode 100644 index 0000000..e526d67 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/static/description/index.html @@ -0,0 +1,454 @@ + + + + + +Filesystem Storage Backup + + + +
+

Filesystem Storage Backup

+ + +

Beta License: AGPL-3 OCA/storage Translate me on Weblate Try me on Runboat

+

With this module you can configure one or more database backup +locations.

+

Table of contents

+ +
+

Configuration

+
    +
  1. Go to Settings > Technical > FS Storage > FS Storage
  2. +
  3. Select a filesystem you want to use for backups. NOTE: Make sure +you don’t use the filestore as backup location otherwise it’s +possible you’ll back up the backup
  4. +
  5. Enable Use For Backups
  6. +
  7. Follow it (using the chatter) if you want to get notified when a +backup fails
  8. +
  9. To know if the backup is working correctly you can run the scheduled +action (Backup database and delete old backups) manually to test +it.
  10. +
+
+
+

Usage

+

The backup is done automatically by a scheduled action +(Backup database and delete old backups).

+
+
+

Known issues / Roadmap

+
    +
  • Configurable backup frequency: e.g. backup every 7 days in s3 and +every 4 hours on a FTP server.
  • +
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub 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.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Onestein
  • +
+
+ +
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/storage project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/tests/__init__.py b/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/tests/__init__.py new file mode 100644 index 0000000..8b6238c --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/tests/__init__.py @@ -0,0 +1 @@ +from . import test_backup diff --git a/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/tests/test_backup.py b/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/tests/test_backup.py new file mode 100644 index 0000000..b6662da --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/tests/test_backup.py @@ -0,0 +1,58 @@ +from odoo.tests.common import TransactionCase + + +class TestBackup(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.fs_storage = cls.env.ref("fs_storage.default_fs_storage") + cls.fs_storage.use_for_backup = True + cls.fs_storage.backup_dir = "backups" + cls.fs_storage.backup_filename_format = "backup-%(db)s-%(dt)s.%(ext)s" + cls.fs_storage.backup_keep_time = 7 + + def test_with_filestore(self): + self.fs_storage.backup_include_filestore = True + old_counts = {} + for fs_storage in self.env["fs.storage"].search( + [("use_for_backup", "=", True)] + ): + fs_storage.fs.makedirs(fs_storage.backup_dir, exist_ok=True) + old_counts[fs_storage.id] = len( + fs_storage.fs.ls(fs_storage.backup_dir, detail=False) + ) + self.env["fs.storage"].cron_backup_db() # Backup all locations + for fs_storage in self.env["fs.storage"].search( + [("use_for_backup", "=", True)] + ): + new_count = len(fs_storage.fs.ls(fs_storage.backup_dir, detail=False)) + self.assertEqual(old_counts[fs_storage.id] + 1, new_count) + + def test_without_filestore(self): + self.fs_storage.fs.makedirs(self.fs_storage.backup_dir, exist_ok=True) + files = self.fs_storage.fs.ls(self.fs_storage.backup_dir, detail=False) + old_count = len(list(filter(lambda f: f.endswith(".dump"), files))) + self.fs_storage.backup_include_filestore = False + self.fs_storage.backup_db() + files = self.fs_storage.fs.ls(self.fs_storage.backup_dir, detail=False) + new_count = len(list(filter(lambda f: f.endswith(".dump"), files))) + self.assertEqual(old_count + 1, new_count) + + def test_cleanup_no_dir(self): + self.fs_storage.backup_dir = "backups123" + with self.assertLogs(level="ERROR"): + self.fs_storage.cleanup_old_backups() + + def test_no_connection(self): + fs_storage = self.env["fs.storage"].create( + { + "name": "FTP", + "code": "ftp", + "protocol": "ftp", + "directory_path": ".", + "options": '{"host": "host", "port": 21}', # Non existent host + "use_for_backup": True, + } + ) + with self.assertLogs(level="ERROR"): + fs_storage.backup_db() diff --git a/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/views/fs_storage_view.xml b/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/views/fs_storage_view.xml new file mode 100644 index 0000000..398ab2f --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage_backup/fs_storage_backup/views/fs_storage_view.xml @@ -0,0 +1,35 @@ + + + + + fs.storage + + + + + + + + + + +
+ + +
+
+
+
+
diff --git a/odoo-bringout-oca-storage-fs_storage_backup/pyproject.toml b/odoo-bringout-oca-storage-fs_storage_backup/pyproject.toml new file mode 100644 index 0000000..5278cc9 --- /dev/null +++ b/odoo-bringout-oca-storage-fs_storage_backup/pyproject.toml @@ -0,0 +1,43 @@ +[project] +name = "odoo-bringout-oca-storage-fs_storage_backup" +version = "16.0.0" +description = "Filesystem Storage Backup - Odoo addon" +authors = [ + { name = "Ernad Husremovic", email = "hernad@bring.out.ba" } +] +dependencies = [ + "odoo-bringout-oca-storage-fs_storage>=16.0.0", + "odoo-bringout-oca-ocb-mail>=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_storage_backup"] + +[tool.rye] +managed = true +dev-dependencies = [ + "pytest>=8.4.1", +] diff --git a/odoo-bringout-oca-storage-image_tag/README.md b/odoo-bringout-oca-storage-image_tag/README.md new file mode 100644 index 0000000..ca95b9a --- /dev/null +++ b/odoo-bringout-oca-storage-image_tag/README.md @@ -0,0 +1,46 @@ +# Image Tag + +Odoo addon: image_tag + +## Installation + +```bash +pip install odoo-bringout-oca-storage-image_tag +``` + +## Dependencies + +This addon depends on: +- server_environment + +## Manifest Information + +- **Name**: Image Tag +- **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 `image_tag`. + +## 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 diff --git a/odoo-bringout-oca-storage-image_tag/doc/ARCHITECTURE.md b/odoo-bringout-oca-storage-image_tag/doc/ARCHITECTURE.md new file mode 100644 index 0000000..947faee --- /dev/null +++ b/odoo-bringout-oca-storage-image_tag/doc/ARCHITECTURE.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 Image_tag Module - image_tag + 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. diff --git a/odoo-bringout-oca-storage-image_tag/doc/CONFIGURATION.md b/odoo-bringout-oca-storage-image_tag/doc/CONFIGURATION.md new file mode 100644 index 0000000..b83d297 --- /dev/null +++ b/odoo-bringout-oca-storage-image_tag/doc/CONFIGURATION.md @@ -0,0 +1,3 @@ +# Configuration + +Refer to Odoo settings for image_tag. Configure related models, access rights, and options as needed. diff --git a/odoo-bringout-oca-storage-image_tag/doc/CONTROLLERS.md b/odoo-bringout-oca-storage-image_tag/doc/CONTROLLERS.md new file mode 100644 index 0000000..f628e77 --- /dev/null +++ b/odoo-bringout-oca-storage-image_tag/doc/CONTROLLERS.md @@ -0,0 +1,3 @@ +# Controllers + +This module does not define custom HTTP controllers. diff --git a/odoo-bringout-oca-storage-image_tag/doc/DEPENDENCIES.md b/odoo-bringout-oca-storage-image_tag/doc/DEPENDENCIES.md new file mode 100644 index 0000000..552c898 --- /dev/null +++ b/odoo-bringout-oca-storage-image_tag/doc/DEPENDENCIES.md @@ -0,0 +1,5 @@ +# Dependencies + +This addon depends on: + +- server_environment diff --git a/odoo-bringout-oca-storage-image_tag/doc/FAQ.md b/odoo-bringout-oca-storage-image_tag/doc/FAQ.md new file mode 100644 index 0000000..012c91a --- /dev/null +++ b/odoo-bringout-oca-storage-image_tag/doc/FAQ.md @@ -0,0 +1,4 @@ +# FAQ + +- Q: Which Odoo version? A: 16.0 (OCA/OCB packaged). +- Q: How to enable? A: Start server with --addon image_tag or install in UI. diff --git a/odoo-bringout-oca-storage-image_tag/doc/INSTALL.md b/odoo-bringout-oca-storage-image_tag/doc/INSTALL.md new file mode 100644 index 0000000..b46e885 --- /dev/null +++ b/odoo-bringout-oca-storage-image_tag/doc/INSTALL.md @@ -0,0 +1,7 @@ +# Install + +```bash +pip install odoo-bringout-oca-storage-image_tag" +# or +uv pip install odoo-bringout-oca-storage-image_tag" +``` diff --git a/odoo-bringout-oca-storage-image_tag/doc/MODELS.md b/odoo-bringout-oca-storage-image_tag/doc/MODELS.md new file mode 100644 index 0000000..11c4248 --- /dev/null +++ b/odoo-bringout-oca-storage-image_tag/doc/MODELS.md @@ -0,0 +1,12 @@ +# Models + +Detected core models and extensions in image_tag. + +```mermaid +classDiagram + class image_tag +``` + +Notes +- Classes show model technical names; fields omitted for brevity. +- Items listed under _inherit are extensions of existing models. diff --git a/odoo-bringout-oca-storage-image_tag/doc/OVERVIEW.md b/odoo-bringout-oca-storage-image_tag/doc/OVERVIEW.md new file mode 100644 index 0000000..a67d0b2 --- /dev/null +++ b/odoo-bringout-oca-storage-image_tag/doc/OVERVIEW.md @@ -0,0 +1,6 @@ +# Overview + +Packaged Odoo addon: image_tag. Provides features documented in upstream Odoo 16 under this addon. + +- Source: OCA/OCB 16.0, addon image_tag +- License: LGPL-3 diff --git a/odoo-bringout-oca-storage-image_tag/doc/REPORTS.md b/odoo-bringout-oca-storage-image_tag/doc/REPORTS.md new file mode 100644 index 0000000..e0ea35f --- /dev/null +++ b/odoo-bringout-oca-storage-image_tag/doc/REPORTS.md @@ -0,0 +1,3 @@ +# Reports + +This module does not define custom reports. diff --git a/odoo-bringout-oca-storage-image_tag/doc/SECURITY.md b/odoo-bringout-oca-storage-image_tag/doc/SECURITY.md new file mode 100644 index 0000000..7706de4 --- /dev/null +++ b/odoo-bringout-oca-storage-image_tag/doc/SECURITY.md @@ -0,0 +1,77 @@ +# Security + +Access control and security definitions in image_tag. + +## 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: +- **[image_tag.xml](../image_tag/security/image_tag.xml)** +- **[res_groups.xml](../image_tag/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: +- **[image_tag.xml](../image_tag/security/image_tag.xml)** + - Security groups, categories, and XML-based rules +- **[res_groups.xml](../image_tag/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 diff --git a/odoo-bringout-oca-storage-image_tag/doc/TROUBLESHOOTING.md b/odoo-bringout-oca-storage-image_tag/doc/TROUBLESHOOTING.md new file mode 100644 index 0000000..56853cb --- /dev/null +++ b/odoo-bringout-oca-storage-image_tag/doc/TROUBLESHOOTING.md @@ -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. diff --git a/odoo-bringout-oca-storage-image_tag/doc/USAGE.md b/odoo-bringout-oca-storage-image_tag/doc/USAGE.md new file mode 100644 index 0000000..8c35fb7 --- /dev/null +++ b/odoo-bringout-oca-storage-image_tag/doc/USAGE.md @@ -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 image_tag +``` diff --git a/odoo-bringout-oca-storage-image_tag/doc/WIZARDS.md b/odoo-bringout-oca-storage-image_tag/doc/WIZARDS.md new file mode 100644 index 0000000..48e790d --- /dev/null +++ b/odoo-bringout-oca-storage-image_tag/doc/WIZARDS.md @@ -0,0 +1,3 @@ +# Wizards + +This module does not include UI wizards. diff --git a/odoo-bringout-oca-storage-image_tag/image_tag/README.rst b/odoo-bringout-oca-storage-image_tag/image_tag/README.rst new file mode 100644 index 0000000..81ab696 --- /dev/null +++ b/odoo-bringout-oca-storage-image_tag/image_tag/README.rst @@ -0,0 +1,85 @@ +========= +Image Tag +========= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:05203837238360e0c011f8ed825bbf70e20f56a5157f18585a37353e9a148a86 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstorage-lightgray.png?logo=github + :target: https://github.com/OCA/storage/tree/16.0/image_tag + :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-image_tag + :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 provide only one basic model used to define image's tags. These +tags are used by other addons to enrich the image's information of an image +linked to an other model. The `fs_product_multi_image` addon use this model. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +To manage the list of available tags, you must have the ``Image Tag Manager`` +role. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub 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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* ACSONE SA/NV +* Akretion + +Contributors +~~~~~~~~~~~~ + +* Laurent Mignon + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/storage `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/odoo-bringout-oca-storage-image_tag/image_tag/__init__.py b/odoo-bringout-oca-storage-image_tag/image_tag/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/odoo-bringout-oca-storage-image_tag/image_tag/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/odoo-bringout-oca-storage-image_tag/image_tag/__manifest__.py b/odoo-bringout-oca-storage-image_tag/image_tag/__manifest__.py new file mode 100644 index 0000000..0bbe310 --- /dev/null +++ b/odoo-bringout-oca-storage-image_tag/image_tag/__manifest__.py @@ -0,0 +1,19 @@ +# Copyright 2023 ACSONE SA/NV +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +{ + "name": "Image Tag", + "summary": """ + Image tag model""", + "version": "16.0.1.0.1", + "license": "AGPL-3", + "author": "ACSONE SA/NV,Akretion,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/storage", + "depends": ["server_environment"], + "data": [ + "security/res_groups.xml", + "security/image_tag.xml", + "views/image_tag.xml", + ], + "demo": [], +} diff --git a/odoo-bringout-oca-storage-image_tag/image_tag/i18n/bs.po b/odoo-bringout-oca-storage-image_tag/image_tag/i18n/bs.po new file mode 100644 index 0000000..5fdfaae --- /dev/null +++ b/odoo-bringout-oca-storage-image_tag/image_tag/i18n/bs.po @@ -0,0 +1,86 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * image_tag +# +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: image_tag +#: model:ir.model.fields,field_description:image_tag.field_image_tag__apply_on +msgid "Apply On" +msgstr "Primjeni na" + +#. module: image_tag +#: model:ir.model.fields,field_description:image_tag.field_image_tag__create_uid +msgid "Created by" +msgstr "Kreirao" + +#. module: image_tag +#: model:ir.model.fields,field_description:image_tag.field_image_tag__create_date +msgid "Created on" +msgstr "Kreirano" + +#. module: image_tag +#: model:ir.model.fields,field_description:image_tag.field_image_tag__display_name +msgid "Display Name" +msgstr "Prikazani naziv" + +#. module: image_tag +#: model:ir.model.fields,field_description:image_tag.field_image_tag__id +msgid "ID" +msgstr "ID" + +#. module: image_tag +#: model:ir.actions.act_window,name:image_tag.act_open_image_tag_view +#: model:ir.model,name:image_tag.model_image_tag +#: model_terms:ir.ui.view,arch_db:image_tag.image_tag_view_search +msgid "Image Tag" +msgstr "Oznaka slike" + +#. module: image_tag +#: model:res.groups,name:image_tag.group_image_tag_manager +msgid "Image Tag Manager" +msgstr "Menadžer oznaka slika" + +#. module: image_tag +#: model:ir.model.fields,field_description:image_tag.field_image_tag____last_update +msgid "Last Modified on" +msgstr "Zadnje mijenjano" + +#. module: image_tag +#: model:ir.model.fields,field_description:image_tag.field_image_tag__write_uid +msgid "Last Updated by" +msgstr "Zadnji ažurirao" + +#. module: image_tag +#: model:ir.model.fields,field_description:image_tag.field_image_tag__write_date +msgid "Last Updated on" +msgstr "Zadnje ažurirano" + +#. module: image_tag +#: model:ir.model.fields,field_description:image_tag.field_image_tag__name +msgid "Name" +msgstr "Naziv:" + +#. module: image_tag +#: model:ir.model.fields,field_description:image_tag.field_image_tag__server_env_defaults +msgid "Server Env Defaults" +msgstr "Zadane vrijednosti server okruženja" + +#. module: image_tag +#: model:ir.model.fields,field_description:image_tag.field_image_tag__tech_name +msgid "Tech Name" +msgstr "Tehnički naziv" + +#. module: image_tag +#: model:ir.model.fields,help:image_tag.field_image_tag__tech_name +msgid "Unique name for technical purposes. Eg: server env keys." +msgstr "Jedinstveni naziv za tehničke svrhe. Npr: ključevi server okruženja." diff --git a/odoo-bringout-oca-storage-image_tag/image_tag/i18n/es.po b/odoo-bringout-oca-storage-image_tag/image_tag/i18n/es.po new file mode 100644 index 0000000..36459bc --- /dev/null +++ b/odoo-bringout-oca-storage-image_tag/image_tag/i18n/es.po @@ -0,0 +1,90 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * image_tag +# +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 \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: image_tag +#: model:ir.model.fields,field_description:image_tag.field_image_tag__apply_on +msgid "Apply On" +msgstr "Aplicar En" + +#. module: image_tag +#: model:ir.model.fields,field_description:image_tag.field_image_tag__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: image_tag +#: model:ir.model.fields,field_description:image_tag.field_image_tag__create_date +msgid "Created on" +msgstr "Creado el" + +#. module: image_tag +#: model:ir.model.fields,field_description:image_tag.field_image_tag__display_name +msgid "Display Name" +msgstr "Mostrar Nombre" + +#. module: image_tag +#: model:ir.model.fields,field_description:image_tag.field_image_tag__id +msgid "ID" +msgstr "ID (identificación)" + +#. module: image_tag +#: model:ir.actions.act_window,name:image_tag.act_open_image_tag_view +#: model:ir.model,name:image_tag.model_image_tag +#: model_terms:ir.ui.view,arch_db:image_tag.image_tag_view_search +msgid "Image Tag" +msgstr "Etiqueta de Imagen" + +#. module: image_tag +#: model:res.groups,name:image_tag.group_image_tag_manager +msgid "Image Tag Manager" +msgstr "Gestor de Etiquetas de Imagen" + +#. module: image_tag +#: model:ir.model.fields,field_description:image_tag.field_image_tag____last_update +msgid "Last Modified on" +msgstr "Última Modifiación el" + +#. module: image_tag +#: model:ir.model.fields,field_description:image_tag.field_image_tag__write_uid +msgid "Last Updated by" +msgstr "Actualizado por Última vez por" + +#. module: image_tag +#: model:ir.model.fields,field_description:image_tag.field_image_tag__write_date +msgid "Last Updated on" +msgstr "Última Actualización el" + +#. module: image_tag +#: model:ir.model.fields,field_description:image_tag.field_image_tag__name +msgid "Name" +msgstr "Nombre" + +#. module: image_tag +#: model:ir.model.fields,field_description:image_tag.field_image_tag__server_env_defaults +msgid "Server Env Defaults" +msgstr "" + +#. module: image_tag +#: model:ir.model.fields,field_description:image_tag.field_image_tag__tech_name +msgid "Tech Name" +msgstr "Nombre Técnico" + +#. module: image_tag +#: model:ir.model.fields,help:image_tag.field_image_tag__tech_name +msgid "Unique name for technical purposes. Eg: server env keys." +msgstr "" +"Nombre único con fines técnicos. Por ejemplo: claves de ent del servidor." diff --git a/odoo-bringout-oca-storage-image_tag/image_tag/i18n/image_tag.pot b/odoo-bringout-oca-storage-image_tag/image_tag/i18n/image_tag.pot new file mode 100644 index 0000000..9229bba --- /dev/null +++ b/odoo-bringout-oca-storage-image_tag/image_tag/i18n/image_tag.pot @@ -0,0 +1,86 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * image_tag +# +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: image_tag +#: model:ir.model.fields,field_description:image_tag.field_image_tag__apply_on +msgid "Apply On" +msgstr "" + +#. module: image_tag +#: model:ir.model.fields,field_description:image_tag.field_image_tag__create_uid +msgid "Created by" +msgstr "" + +#. module: image_tag +#: model:ir.model.fields,field_description:image_tag.field_image_tag__create_date +msgid "Created on" +msgstr "" + +#. module: image_tag +#: model:ir.model.fields,field_description:image_tag.field_image_tag__display_name +msgid "Display Name" +msgstr "" + +#. module: image_tag +#: model:ir.model.fields,field_description:image_tag.field_image_tag__id +msgid "ID" +msgstr "" + +#. module: image_tag +#: model:ir.actions.act_window,name:image_tag.act_open_image_tag_view +#: model:ir.model,name:image_tag.model_image_tag +#: model_terms:ir.ui.view,arch_db:image_tag.image_tag_view_search +msgid "Image Tag" +msgstr "" + +#. module: image_tag +#: model:res.groups,name:image_tag.group_image_tag_manager +msgid "Image Tag Manager" +msgstr "" + +#. module: image_tag +#: model:ir.model.fields,field_description:image_tag.field_image_tag____last_update +msgid "Last Modified on" +msgstr "" + +#. module: image_tag +#: model:ir.model.fields,field_description:image_tag.field_image_tag__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: image_tag +#: model:ir.model.fields,field_description:image_tag.field_image_tag__write_date +msgid "Last Updated on" +msgstr "" + +#. module: image_tag +#: model:ir.model.fields,field_description:image_tag.field_image_tag__name +msgid "Name" +msgstr "" + +#. module: image_tag +#: model:ir.model.fields,field_description:image_tag.field_image_tag__server_env_defaults +msgid "Server Env Defaults" +msgstr "" + +#. module: image_tag +#: model:ir.model.fields,field_description:image_tag.field_image_tag__tech_name +msgid "Tech Name" +msgstr "" + +#. module: image_tag +#: model:ir.model.fields,help:image_tag.field_image_tag__tech_name +msgid "Unique name for technical purposes. Eg: server env keys." +msgstr "" diff --git a/odoo-bringout-oca-storage-image_tag/image_tag/i18n/it.po b/odoo-bringout-oca-storage-image_tag/image_tag/i18n/it.po new file mode 100644 index 0000000..3d4d2f1 --- /dev/null +++ b/odoo-bringout-oca-storage-image_tag/image_tag/i18n/it.po @@ -0,0 +1,89 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * image_tag +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2025-07-23 11:25+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.10.4\n" + +#. module: image_tag +#: model:ir.model.fields,field_description:image_tag.field_image_tag__apply_on +msgid "Apply On" +msgstr "Applica a" + +#. module: image_tag +#: model:ir.model.fields,field_description:image_tag.field_image_tag__create_uid +msgid "Created by" +msgstr "Creato da" + +#. module: image_tag +#: model:ir.model.fields,field_description:image_tag.field_image_tag__create_date +msgid "Created on" +msgstr "Creato il" + +#. module: image_tag +#: model:ir.model.fields,field_description:image_tag.field_image_tag__display_name +msgid "Display Name" +msgstr "Nome visualizzato" + +#. module: image_tag +#: model:ir.model.fields,field_description:image_tag.field_image_tag__id +msgid "ID" +msgstr "ID" + +#. module: image_tag +#: model:ir.actions.act_window,name:image_tag.act_open_image_tag_view +#: model:ir.model,name:image_tag.model_image_tag +#: model_terms:ir.ui.view,arch_db:image_tag.image_tag_view_search +msgid "Image Tag" +msgstr "Etichetta immagine" + +#. module: image_tag +#: model:res.groups,name:image_tag.group_image_tag_manager +msgid "Image Tag Manager" +msgstr "Gestore etichetta immagine" + +#. module: image_tag +#: model:ir.model.fields,field_description:image_tag.field_image_tag____last_update +msgid "Last Modified on" +msgstr "Ultima modifica il" + +#. module: image_tag +#: model:ir.model.fields,field_description:image_tag.field_image_tag__write_uid +msgid "Last Updated by" +msgstr "Ultimo aggiornamento di" + +#. module: image_tag +#: model:ir.model.fields,field_description:image_tag.field_image_tag__write_date +msgid "Last Updated on" +msgstr "Ultimo aggiornamento il" + +#. module: image_tag +#: model:ir.model.fields,field_description:image_tag.field_image_tag__name +msgid "Name" +msgstr "Nome" + +#. module: image_tag +#: model:ir.model.fields,field_description:image_tag.field_image_tag__server_env_defaults +msgid "Server Env Defaults" +msgstr "Predefiniti ambiente server" + +#. module: image_tag +#: model:ir.model.fields,field_description:image_tag.field_image_tag__tech_name +msgid "Tech Name" +msgstr "Nome tecnico" + +#. module: image_tag +#: model:ir.model.fields,help:image_tag.field_image_tag__tech_name +msgid "Unique name for technical purposes. Eg: server env keys." +msgstr "Nome univoco per motivi tecnici. Es: chiavi server ambiente." diff --git a/odoo-bringout-oca-storage-image_tag/image_tag/models/__init__.py b/odoo-bringout-oca-storage-image_tag/image_tag/models/__init__.py new file mode 100644 index 0000000..888490a --- /dev/null +++ b/odoo-bringout-oca-storage-image_tag/image_tag/models/__init__.py @@ -0,0 +1 @@ +from . import image_tag diff --git a/odoo-bringout-oca-storage-image_tag/image_tag/models/image_tag.py b/odoo-bringout-oca-storage-image_tag/image_tag/models/image_tag.py new file mode 100644 index 0000000..86f2558 --- /dev/null +++ b/odoo-bringout-oca-storage-image_tag/image_tag/models/image_tag.py @@ -0,0 +1,22 @@ +# Copyright 2023 ACSONE SA/NV +# Copyright 2018 Akretion (http://www.akretion.com). +# @author Raphaël Reverdy +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import api, fields, models + + +class ImageTag(models.Model): + _name = "image.tag" + _inherit = ["server.env.techname.mixin"] + _description = "Image Tag" + + @api.model + def _get_default_apply_on(self): + return False + + name = fields.Char(required=True) + apply_on = fields.Selection( + selection=[], + default=lambda self: self._get_default_apply_on(), + ) diff --git a/odoo-bringout-oca-storage-image_tag/image_tag/readme/CONTRIBUTORS.rst b/odoo-bringout-oca-storage-image_tag/image_tag/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000..172b2d2 --- /dev/null +++ b/odoo-bringout-oca-storage-image_tag/image_tag/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Laurent Mignon diff --git a/odoo-bringout-oca-storage-image_tag/image_tag/readme/DESCRIPTION.rst b/odoo-bringout-oca-storage-image_tag/image_tag/readme/DESCRIPTION.rst new file mode 100644 index 0000000..40adead --- /dev/null +++ b/odoo-bringout-oca-storage-image_tag/image_tag/readme/DESCRIPTION.rst @@ -0,0 +1,3 @@ +This addon provide only one basic model used to define image's tags. These +tags are used by other addons to enrich the image's information of an image +linked to an other model. The `fs_product_multi_image` addon use this model. diff --git a/odoo-bringout-oca-storage-image_tag/image_tag/readme/USAGE.rst b/odoo-bringout-oca-storage-image_tag/image_tag/readme/USAGE.rst new file mode 100644 index 0000000..314932f --- /dev/null +++ b/odoo-bringout-oca-storage-image_tag/image_tag/readme/USAGE.rst @@ -0,0 +1,2 @@ +To manage the list of available tags, you must have the ``Image Tag Manager`` +role. diff --git a/odoo-bringout-oca-storage-image_tag/image_tag/security/image_tag.xml b/odoo-bringout-oca-storage-image_tag/image_tag/security/image_tag.xml new file mode 100644 index 0000000..61a18fa --- /dev/null +++ b/odoo-bringout-oca-storage-image_tag/image_tag/security/image_tag.xml @@ -0,0 +1,25 @@ + + + + + + image.tag access read + + + + + + + + + + image.tag access read + + + + + + + + diff --git a/odoo-bringout-oca-storage-image_tag/image_tag/security/res_groups.xml b/odoo-bringout-oca-storage-image_tag/image_tag/security/res_groups.xml new file mode 100644 index 0000000..7ba612b --- /dev/null +++ b/odoo-bringout-oca-storage-image_tag/image_tag/security/res_groups.xml @@ -0,0 +1,10 @@ + + + + Image Tag Manager + + + diff --git a/odoo-bringout-oca-storage-image_tag/image_tag/static/description/icon.png b/odoo-bringout-oca-storage-image_tag/image_tag/static/description/icon.png new file mode 100644 index 0000000..3a0328b Binary files /dev/null and b/odoo-bringout-oca-storage-image_tag/image_tag/static/description/icon.png differ diff --git a/odoo-bringout-oca-storage-image_tag/image_tag/static/description/index.html b/odoo-bringout-oca-storage-image_tag/image_tag/static/description/index.html new file mode 100644 index 0000000..a37630a --- /dev/null +++ b/odoo-bringout-oca-storage-image_tag/image_tag/static/description/index.html @@ -0,0 +1,430 @@ + + + + + + +Image Tag + + + +
+

Image Tag

+ + +

Beta License: AGPL-3 OCA/storage Translate me on Weblate Try me on Runboat

+

This addon provide only one basic model used to define image’s tags. These +tags are used by other addons to enrich the image’s information of an image +linked to an other model. The fs_product_multi_image addon use this model.

+

Table of contents

+ +
+

Usage

+

To manage the list of available tags, you must have the Image Tag Manager +role.

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub 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.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • ACSONE SA/NV
  • +
  • Akretion
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/storage project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/odoo-bringout-oca-storage-image_tag/image_tag/views/image_tag.xml b/odoo-bringout-oca-storage-image_tag/image_tag/views/image_tag.xml new file mode 100644 index 0000000..6ecda60 --- /dev/null +++ b/odoo-bringout-oca-storage-image_tag/image_tag/views/image_tag.xml @@ -0,0 +1,37 @@ + + + + image.tag + + + + + + + + + + image.tag + + + + + + + + + Image Tag + ir.actions.act_window + image.tag + tree + + [] + {} + + + + + tree + + + diff --git a/odoo-bringout-oca-storage-image_tag/pyproject.toml b/odoo-bringout-oca-storage-image_tag/pyproject.toml new file mode 100644 index 0000000..37e525a --- /dev/null +++ b/odoo-bringout-oca-storage-image_tag/pyproject.toml @@ -0,0 +1,43 @@ +[project] +name = "odoo-bringout-oca-storage-image_tag" +version = "16.0.0" +description = "Image Tag - + Image tag model" +authors = [ + { name = "Ernad Husremovic", email = "hernad@bring.out.ba" } +] +dependencies = [ + "odoo-bringout-oca-storage-server_environment>=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 = ["image_tag"] + +[tool.rye] +managed = true +dev-dependencies = [ + "pytest>=8.4.1", +] diff --git a/odoo-bringout-oca-storage-storage_backend/README.md b/odoo-bringout-oca-storage-storage_backend/README.md new file mode 100644 index 0000000..d36ecf1 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend/README.md @@ -0,0 +1,48 @@ +# Storage Bakend + +Odoo addon: storage_backend + +## Installation + +```bash +pip install odoo-bringout-oca-storage-storage_backend +``` + +## Dependencies + +This addon depends on: +- base +- component +- server_environment + +## Manifest Information + +- **Name**: Storage Bakend +- **Version**: 16.0.1.1.0 +- **Category**: Storage +- **License**: LGPL-3 +- **Installable**: True + +## Source + +Based on [OCA/storage](https://github.com/OCA/storage) branch 16.0, addon `storage_backend`. + +## License + +This package maintains the original LGPL-3 license from the upstream Odoo project. + +## Documentation + +- Overview: doc/OVERVIEW.md +- Architecture: doc/ARCHITECTURE.md +- Models: doc/MODELS.md +- Controllers: doc/CONTROLLERS.md +- Wizards: doc/WIZARDS.md +- 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 diff --git a/odoo-bringout-oca-storage-storage_backend/doc/ARCHITECTURE.md b/odoo-bringout-oca-storage-storage_backend/doc/ARCHITECTURE.md new file mode 100644 index 0000000..ed92d8d --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend/doc/ARCHITECTURE.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 Storage_backend Module - storage_backend + 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. diff --git a/odoo-bringout-oca-storage-storage_backend/doc/CONFIGURATION.md b/odoo-bringout-oca-storage-storage_backend/doc/CONFIGURATION.md new file mode 100644 index 0000000..f99239c --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend/doc/CONFIGURATION.md @@ -0,0 +1,3 @@ +# Configuration + +Refer to Odoo settings for storage_backend. Configure related models, access rights, and options as needed. diff --git a/odoo-bringout-oca-storage-storage_backend/doc/CONTROLLERS.md b/odoo-bringout-oca-storage-storage_backend/doc/CONTROLLERS.md new file mode 100644 index 0000000..f628e77 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend/doc/CONTROLLERS.md @@ -0,0 +1,3 @@ +# Controllers + +This module does not define custom HTTP controllers. diff --git a/odoo-bringout-oca-storage-storage_backend/doc/DEPENDENCIES.md b/odoo-bringout-oca-storage-storage_backend/doc/DEPENDENCIES.md new file mode 100644 index 0000000..a2c44f0 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend/doc/DEPENDENCIES.md @@ -0,0 +1,7 @@ +# Dependencies + +This addon depends on: + +- base +- [component](../../odoo-bringout-oca-connector-component) +- server_environment diff --git a/odoo-bringout-oca-storage-storage_backend/doc/FAQ.md b/odoo-bringout-oca-storage-storage_backend/doc/FAQ.md new file mode 100644 index 0000000..669f337 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend/doc/FAQ.md @@ -0,0 +1,4 @@ +# FAQ + +- Q: Which Odoo version? A: 16.0 (OCA/OCB packaged). +- Q: How to enable? A: Start server with --addon storage_backend or install in UI. diff --git a/odoo-bringout-oca-storage-storage_backend/doc/INSTALL.md b/odoo-bringout-oca-storage-storage_backend/doc/INSTALL.md new file mode 100644 index 0000000..39041ae --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend/doc/INSTALL.md @@ -0,0 +1,7 @@ +# Install + +```bash +pip install odoo-bringout-oca-storage-storage_backend" +# or +uv pip install odoo-bringout-oca-storage-storage_backend" +``` diff --git a/odoo-bringout-oca-storage-storage_backend/doc/MODELS.md b/odoo-bringout-oca-storage-storage_backend/doc/MODELS.md new file mode 100644 index 0000000..b3178ce --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend/doc/MODELS.md @@ -0,0 +1,12 @@ +# Models + +Detected core models and extensions in storage_backend. + +```mermaid +classDiagram + class storage_backend +``` + +Notes +- Classes show model technical names; fields omitted for brevity. +- Items listed under _inherit are extensions of existing models. diff --git a/odoo-bringout-oca-storage-storage_backend/doc/OVERVIEW.md b/odoo-bringout-oca-storage-storage_backend/doc/OVERVIEW.md new file mode 100644 index 0000000..be0d0fd --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend/doc/OVERVIEW.md @@ -0,0 +1,6 @@ +# Overview + +Packaged Odoo addon: storage_backend. Provides features documented in upstream Odoo 16 under this addon. + +- Source: OCA/OCB 16.0, addon storage_backend +- License: LGPL-3 diff --git a/odoo-bringout-oca-storage-storage_backend/doc/REPORTS.md b/odoo-bringout-oca-storage-storage_backend/doc/REPORTS.md new file mode 100644 index 0000000..e0ea35f --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend/doc/REPORTS.md @@ -0,0 +1,3 @@ +# Reports + +This module does not define custom reports. diff --git a/odoo-bringout-oca-storage-storage_backend/doc/SECURITY.md b/odoo-bringout-oca-storage-storage_backend/doc/SECURITY.md new file mode 100644 index 0000000..211f439 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend/doc/SECURITY.md @@ -0,0 +1,34 @@ +# Security + +Access control and security definitions in storage_backend. + +## Access Control Lists (ACLs) + +Model access permissions defined in: +- **[ir.model.access.csv](../storage_backend/security/ir.model.access.csv)** + - 1 model access rules + +## Record Rules + +Row-level security rules defined in: + +```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: +- **[ir.model.access.csv](../storage_backend/security/ir.model.access.csv)** + - Model access permissions (CRUD rights) + +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 diff --git a/odoo-bringout-oca-storage-storage_backend/doc/TROUBLESHOOTING.md b/odoo-bringout-oca-storage-storage_backend/doc/TROUBLESHOOTING.md new file mode 100644 index 0000000..56853cb --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend/doc/TROUBLESHOOTING.md @@ -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. diff --git a/odoo-bringout-oca-storage-storage_backend/doc/USAGE.md b/odoo-bringout-oca-storage-storage_backend/doc/USAGE.md new file mode 100644 index 0000000..7ef3847 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend/doc/USAGE.md @@ -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 storage_backend +``` diff --git a/odoo-bringout-oca-storage-storage_backend/doc/WIZARDS.md b/odoo-bringout-oca-storage-storage_backend/doc/WIZARDS.md new file mode 100644 index 0000000..48e790d --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend/doc/WIZARDS.md @@ -0,0 +1,3 @@ +# Wizards + +This module does not include UI wizards. diff --git a/odoo-bringout-oca-storage-storage_backend/pyproject.toml b/odoo-bringout-oca-storage-storage_backend/pyproject.toml new file mode 100644 index 0000000..1f92339 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend/pyproject.toml @@ -0,0 +1,44 @@ +[project] +name = "odoo-bringout-oca-storage-storage_backend" +version = "16.0.0" +description = "Storage Bakend - Implement the concept of Storage with amazon S3, sftp..." +authors = [ + { name = "Ernad Husremovic", email = "hernad@bring.out.ba" } +] +dependencies = [ + "odoo-bringout-oca-ocb-base>=16.0.0", + "odoo-bringout-oca-storage-component>=16.0.0", + "odoo-bringout-oca-storage-server_environment>=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 = ["storage_backend"] + +[tool.rye] +managed = true +dev-dependencies = [ + "pytest>=8.4.1", +] diff --git a/odoo-bringout-oca-storage-storage_backend/storage_backend/README.rst b/odoo-bringout-oca-storage-storage_backend/storage_backend/README.rst new file mode 100644 index 0000000..593be98 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend/storage_backend/README.rst @@ -0,0 +1,82 @@ +============== +Storage Bakend +============== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:e06faaef8e877cb770756ffc80fe1426d83d0130f6f1c239e9f16995156676ed + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png + :target: https://odoo-community.org/page/development-status + :alt: Production/Stable +.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstorage-lightgray.png?logo=github + :target: https://github.com/OCA/storage/tree/16.0/storage_backend + :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-storage_backend + :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| + + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub 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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Akretion + +Contributors +~~~~~~~~~~~~ + +* Sébastien BEAU +* Raphaël Reverdy +* Florian da Costa +* Cédric Pigeon +* Renato Lima +* Benoît Guillot +* Laurent Mignon +* Denis Roussel + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/storage `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/odoo-bringout-oca-storage-storage_backend/storage_backend/__init__.py b/odoo-bringout-oca-storage-storage_backend/storage_backend/__init__.py new file mode 100644 index 0000000..0f00a67 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend/storage_backend/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import components diff --git a/odoo-bringout-oca-storage-storage_backend/storage_backend/__manifest__.py b/odoo-bringout-oca-storage-storage_backend/storage_backend/__manifest__.py new file mode 100644 index 0000000..271fd09 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend/storage_backend/__manifest__.py @@ -0,0 +1,21 @@ +# Copyright 2017 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +{ + "name": "Storage Bakend", + "summary": "Implement the concept of Storage with amazon S3, sftp...", + "version": "16.0.1.1.0", + "category": "Storage", + "website": "https://github.com/OCA/storage", + "author": " Akretion, Odoo Community Association (OCA)", + "license": "LGPL-3", + "development_status": "Production/Stable", + "installable": True, + "depends": ["base", "component", "server_environment"], + "data": [ + "views/backend_storage_view.xml", + "data/data.xml", + "security/ir.model.access.csv", + ], +} diff --git a/odoo-bringout-oca-storage-storage_backend/storage_backend/components/__init__.py b/odoo-bringout-oca-storage-storage_backend/storage_backend/components/__init__.py new file mode 100644 index 0000000..264029f --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend/storage_backend/components/__init__.py @@ -0,0 +1,2 @@ +from . import base_adapter +from . import filesystem_adapter diff --git a/odoo-bringout-oca-storage-storage_backend/storage_backend/components/base_adapter.py b/odoo-bringout-oca-storage-storage_backend/storage_backend/components/base_adapter.py new file mode 100644 index 0000000..2ae2992 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend/storage_backend/components/base_adapter.py @@ -0,0 +1,69 @@ +# Copyright 2017 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +# Copyright 2020 ACSONE SA/NV () +# @author Simone Orsi + +import os +import re + +from odoo.addons.component.core import AbstractComponent + + +class BaseStorageAdapter(AbstractComponent): + _name = "base.storage.adapter" + _collection = "storage.backend" + + def _fullpath(self, relative_path): + dp = self.collection.directory_path + if not dp or relative_path.startswith(dp): + return relative_path + return os.path.join(dp, relative_path) + + def add(self, relative_path, data, **kwargs): + raise NotImplementedError + + def get(self, relative_path, **kwargs): + raise NotImplementedError + + def list(self, relative_path=""): + raise NotImplementedError + + def find_files(self, pattern, relative_path="", **kwargs): + """Find files matching given pattern. + + :param pattern: regex expression + :param relative_path: optional relative path containing files + :return: list of file paths as full paths from the root + """ + regex = re.compile(pattern) + filelist = self.list(relative_path) + files_matching = [ + regex.match(file_).group() for file_ in filelist if regex.match(file_) + ] + filepaths = [] + if files_matching: + filepaths = [ + os.path.join(self._fullpath(relative_path) or "", filename) + for filename in files_matching + ] + return filepaths + + def move_files(self, files, destination_path, **kwargs): + """Move files to given destination. + + :param files: list of file paths to be moved + :param destination_path: directory path where to move files + :return: None + """ + raise NotImplementedError + + def delete(self, relative_path): + raise NotImplementedError + + # You can define `validate_config` on your own adapter + # to make validation button available on UI. + # This method should simply pass smoothly when validation is ok, + # otherwise it should raise an exception. + # def validate_config(self): + # raise NotImplementedError diff --git a/odoo-bringout-oca-storage-storage_backend/storage_backend/components/filesystem_adapter.py b/odoo-bringout-oca-storage-storage_backend/storage_backend/components/filesystem_adapter.py new file mode 100644 index 0000000..101c3cc --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend/storage_backend/components/filesystem_adapter.py @@ -0,0 +1,76 @@ +# Copyright 2017 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import logging +import os +import shutil + +from odoo import _ +from odoo.exceptions import AccessError + +from odoo.addons.component.core import Component + +_logger = logging.getLogger(__name__) + + +def is_safe_path(basedir, path): + return os.path.realpath(path).startswith(basedir) + + +class FileSystemStorageBackend(Component): + _name = "filesystem.adapter" + _inherit = "base.storage.adapter" + _usage = "filesystem" + + def _basedir(self): + return os.path.join(self.env["ir.attachment"]._filestore(), "storage") + + def _fullpath(self, relative_path): + """This will build the full path for the file, we force to + store the data inside the filestore in the directory 'storage". + Becarefull if you implement your own custom path, end user + should never be able to write or read unwanted filesystem file""" + full_path = super(FileSystemStorageBackend, self)._fullpath(relative_path) + base_dir = self._basedir() + full_path = os.path.join(base_dir, full_path) + if not is_safe_path(base_dir, full_path): + raise AccessError(_("Access to %s is forbidden") % full_path) + return full_path + + def add(self, relative_path, data, **kwargs): + full_path = self._fullpath(relative_path) + dirname = os.path.dirname(full_path) + if not os.path.isdir(dirname): + os.makedirs(dirname) + with open(full_path, "wb") as my_file: + my_file.write(data) + + def get(self, relative_path, **kwargs): + full_path = self._fullpath(relative_path) + with open(full_path, "rb") as my_file: + data = my_file.read() + return data + + def list(self, relative_path=""): + full_path = self._fullpath(relative_path) + if os.path.isdir(full_path): + return os.listdir(full_path) + return [] + + def delete(self, relative_path): + full_path = self._fullpath(relative_path) + try: + os.remove(full_path) + except FileNotFoundError: + _logger.warning("File not found in %s", full_path) + + def move_files(self, files, destination_path): + result = [] + for file_path in files: + if not os.path.exists(destination_path): + os.makedirs(destination_path) + filename = os.path.basename(file_path) + destination_file = os.path.join(destination_path, filename) + result.append(shutil.move(file_path, destination_file)) + return result diff --git a/odoo-bringout-oca-storage-storage_backend/storage_backend/data/data.xml b/odoo-bringout-oca-storage-storage_backend/storage_backend/data/data.xml new file mode 100644 index 0000000..1b025cf --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend/storage_backend/data/data.xml @@ -0,0 +1,7 @@ + + + + Filesystem Backend + filesystem + + diff --git a/odoo-bringout-oca-storage-storage_backend/storage_backend/i18n/bs.po b/odoo-bringout-oca-storage-storage_backend/storage_backend/i18n/bs.po new file mode 100644 index 0000000..39a5376 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend/storage_backend/i18n/bs.po @@ -0,0 +1,148 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * storage_backend +# +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: storage_backend +#. odoo-python +#: code:addons/storage_backend/components/filesystem_adapter.py:0 +#, python-format +msgid "Access to %s is forbidden" +msgstr "Pristup %s je zabranjen" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__backend_type +msgid "Backend Type" +msgstr "Tip pozadine" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__backend_type_env_default +msgid "Backend Type Env Default" +msgstr "Zadano okruženje tipa pozadine" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__backend_type_env_is_editable +msgid "Backend Type Env Is Editable" +msgstr "Tip pozadine okruženja se može uređivati" + +#. module: storage_backend +#. odoo-python +#: code:addons/storage_backend/models/storage_backend.py:0 +#, python-format +msgid "Connection Test Failed!" +msgstr "Provjera povezivanja nije uspjela!" + +#. module: storage_backend +#. odoo-python +#: code:addons/storage_backend/models/storage_backend.py:0 +#, python-format +msgid "Connection Test Succeeded!" +msgstr "Provjera povezivanja uspješna!" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__create_uid +msgid "Created by" +msgstr "Kreirao" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__create_date +msgid "Created on" +msgstr "Kreirano" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__directory_path +msgid "Directory Path" +msgstr "Putanja direktorija" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__directory_path_env_default +msgid "Directory Path Env Default" +msgstr "Zadano okruženje putanje direktorija" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__directory_path_env_is_editable +msgid "Directory Path Env Is Editable" +msgstr "Putanja direktorija okruženja se može uređivati" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__display_name +msgid "Display Name" +msgstr "Prikazani naziv" + +#. module: storage_backend +#. odoo-python +#: code:addons/storage_backend/models/storage_backend.py:0 +#, python-format +msgid "Everything seems properly set up!" +msgstr "Sve izgleda ispravno postavljeno!" + +#. module: storage_backend +#: model:ir.model.fields.selection,name:storage_backend.selection__storage_backend__backend_type__filesystem +msgid "Filesystem" +msgstr "Datotečni sustav" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__has_validation +msgid "Has Validation" +msgstr "Ima validaciju" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__id +msgid "ID" +msgstr "ID" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend____last_update +msgid "Last Modified on" +msgstr "Zadnje mijenjano" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__write_uid +msgid "Last Updated by" +msgstr "Zadnji ažurirao" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__write_date +msgid "Last Updated on" +msgstr "Zadnje ažurirano" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__name +msgid "Name" +msgstr "Naziv:" + +#. module: storage_backend +#: model:ir.model.fields,help:storage_backend.field_storage_backend__directory_path +#: model:ir.model.fields,help:storage_backend.field_storage_backend__directory_path_env_default +msgid "Relative path to the directory to store the file" +msgstr "Relativna putanja do direktorija za skladištenje datoteke" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__server_env_defaults +msgid "Server Env Defaults" +msgstr "Zadane vrijednosti server okruženja" + +#. module: storage_backend +#: model:ir.actions.act_window,name:storage_backend.act_open_storage_backend_view +#: model:ir.model,name:storage_backend.model_storage_backend +#: model:ir.ui.menu,name:storage_backend.menu_storage +#: model:ir.ui.menu,name:storage_backend.menu_storage_backend +#: model_terms:ir.ui.view,arch_db:storage_backend.storage_backend_view_form +#: model_terms:ir.ui.view,arch_db:storage_backend.storage_backend_view_search +msgid "Storage Backend" +msgstr "Pozadina skladištenja" + +#. module: storage_backend +#: model_terms:ir.ui.view,arch_db:storage_backend.storage_backend_view_form +msgid "Test connection" +msgstr "Testiraj vezu" diff --git a/odoo-bringout-oca-storage-storage_backend/storage_backend/i18n/es.po b/odoo-bringout-oca-storage-storage_backend/storage_backend/i18n/es.po new file mode 100644 index 0000000..9d19a97 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend/storage_backend/i18n/es.po @@ -0,0 +1,151 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * storage_backend +# +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 \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: storage_backend +#. odoo-python +#: code:addons/storage_backend/components/filesystem_adapter.py:0 +#, python-format +msgid "Access to %s is forbidden" +msgstr "El acceso a %s está prohibido" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__backend_type +msgid "Backend Type" +msgstr "Tipo de servidor" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__backend_type_env_default +msgid "Backend Type Env Default" +msgstr "Tipo de servidor Ent Por defecto" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__backend_type_env_is_editable +msgid "Backend Type Env Is Editable" +msgstr "El Tipo de Entorno del Servidor es Editable" + +#. module: storage_backend +#. odoo-python +#: code:addons/storage_backend/models/storage_backend.py:0 +#, python-format +msgid "Connection Test Failed!" +msgstr "¡Error en la Prueba de Conexión!" + +#. module: storage_backend +#. odoo-python +#: code:addons/storage_backend/models/storage_backend.py:0 +#, python-format +msgid "Connection Test Succeeded!" +msgstr "¡Conexión de Prueba Exitosa!" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__create_date +msgid "Created on" +msgstr "Creado el" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__directory_path +msgid "Directory Path" +msgstr "Ruta del Directorio" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__directory_path_env_default +msgid "Directory Path Env Default" +msgstr "Ruta del Directorio Ent Predet" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__directory_path_env_is_editable +msgid "Directory Path Env Is Editable" +msgstr "El Entorno de Ruta del Directorio es Editable" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__display_name +msgid "Display Name" +msgstr "Mostrar Nombre" + +#. module: storage_backend +#. odoo-python +#: code:addons/storage_backend/models/storage_backend.py:0 +#, python-format +msgid "Everything seems properly set up!" +msgstr "¡Todo parece correctamente configurado!" + +#. module: storage_backend +#: model:ir.model.fields.selection,name:storage_backend.selection__storage_backend__backend_type__filesystem +msgid "Filesystem" +msgstr "Sistema de archivos" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__has_validation +msgid "Has Validation" +msgstr "Tiene Validación" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__id +msgid "ID" +msgstr "ID(identificación)" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend____last_update +msgid "Last Modified on" +msgstr "Última Modifiación el" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__write_uid +msgid "Last Updated by" +msgstr "Última Actualización por" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__write_date +msgid "Last Updated on" +msgstr "Última Actualización el" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__name +msgid "Name" +msgstr "Nombre" + +#. module: storage_backend +#: model:ir.model.fields,help:storage_backend.field_storage_backend__directory_path +#: model:ir.model.fields,help:storage_backend.field_storage_backend__directory_path_env_default +msgid "Relative path to the directory to store the file" +msgstr "Ruta relativa al directorio para almacenar el archivo" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__server_env_defaults +msgid "Server Env Defaults" +msgstr "Valores por defecto del Entorno de Servidor" + +#. module: storage_backend +#: model:ir.actions.act_window,name:storage_backend.act_open_storage_backend_view +#: model:ir.model,name:storage_backend.model_storage_backend +#: model:ir.ui.menu,name:storage_backend.menu_storage +#: model:ir.ui.menu,name:storage_backend.menu_storage_backend +#: model_terms:ir.ui.view,arch_db:storage_backend.storage_backend_view_form +#: model_terms:ir.ui.view,arch_db:storage_backend.storage_backend_view_search +msgid "Storage Backend" +msgstr "Servidor de Almacenamiento" + +#. module: storage_backend +#: model_terms:ir.ui.view,arch_db:storage_backend.storage_backend_view_form +msgid "Test connection" +msgstr "Probar conexión" diff --git a/odoo-bringout-oca-storage-storage_backend/storage_backend/i18n/it.po b/odoo-bringout-oca-storage-storage_backend/storage_backend/i18n/it.po new file mode 100644 index 0000000..8f9ab23 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend/storage_backend/i18n/it.po @@ -0,0 +1,151 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * storage_backend +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2025-06-03 09:25+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.10.4\n" + +#. module: storage_backend +#. odoo-python +#: code:addons/storage_backend/components/filesystem_adapter.py:0 +#, python-format +msgid "Access to %s is forbidden" +msgstr "L'accesso a %s è vietato" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__backend_type +msgid "Backend Type" +msgstr "Tipo backend" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__backend_type_env_default +msgid "Backend Type Env Default" +msgstr "Tipo backend ambiente predefinito" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__backend_type_env_is_editable +msgid "Backend Type Env Is Editable" +msgstr "Il tipo backend ambiente è modificabile" + +#. module: storage_backend +#. odoo-python +#: code:addons/storage_backend/models/storage_backend.py:0 +#, python-format +msgid "Connection Test Failed!" +msgstr "Test connessione fallito!" + +#. module: storage_backend +#. odoo-python +#: code:addons/storage_backend/models/storage_backend.py:0 +#, python-format +msgid "Connection Test Succeeded!" +msgstr "Test connessione avvenuto con successo!" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__create_uid +msgid "Created by" +msgstr "Creato da" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__create_date +msgid "Created on" +msgstr "Creato il" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__directory_path +msgid "Directory Path" +msgstr "Percorso cartella" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__directory_path_env_default +msgid "Directory Path Env Default" +msgstr "Percorso cartella ambiente predefinito" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__directory_path_env_is_editable +msgid "Directory Path Env Is Editable" +msgstr "Percorso cartella ambiente è modificabile" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__display_name +msgid "Display Name" +msgstr "Nome visualizzato" + +#. module: storage_backend +#. odoo-python +#: code:addons/storage_backend/models/storage_backend.py:0 +#, python-format +msgid "Everything seems properly set up!" +msgstr "Tutto sembra impostato correttamente!" + +#. module: storage_backend +#: model:ir.model.fields.selection,name:storage_backend.selection__storage_backend__backend_type__filesystem +msgid "Filesystem" +msgstr "Filesystem" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__has_validation +msgid "Has Validation" +msgstr "Ha validazione" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__id +msgid "ID" +msgstr "ID" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend____last_update +msgid "Last Modified on" +msgstr "Ultima modifica il" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__write_uid +msgid "Last Updated by" +msgstr "Ultimo aggiornamento di" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__write_date +msgid "Last Updated on" +msgstr "Ultimo aggiornamento il" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__name +msgid "Name" +msgstr "Nome" + +#. module: storage_backend +#: model:ir.model.fields,help:storage_backend.field_storage_backend__directory_path +#: model:ir.model.fields,help:storage_backend.field_storage_backend__directory_path_env_default +msgid "Relative path to the directory to store the file" +msgstr "Percorso relativo alla cartella per archiviare il file" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__server_env_defaults +msgid "Server Env Defaults" +msgstr "Predefiniti ambiente server" + +#. module: storage_backend +#: model:ir.actions.act_window,name:storage_backend.act_open_storage_backend_view +#: model:ir.model,name:storage_backend.model_storage_backend +#: model:ir.ui.menu,name:storage_backend.menu_storage +#: model:ir.ui.menu,name:storage_backend.menu_storage_backend +#: model_terms:ir.ui.view,arch_db:storage_backend.storage_backend_view_form +#: model_terms:ir.ui.view,arch_db:storage_backend.storage_backend_view_search +msgid "Storage Backend" +msgstr "Backend deposito" + +#. module: storage_backend +#: model_terms:ir.ui.view,arch_db:storage_backend.storage_backend_view_form +msgid "Test connection" +msgstr "Prova connessione" diff --git a/odoo-bringout-oca-storage-storage_backend/storage_backend/i18n/storage_backend.pot b/odoo-bringout-oca-storage-storage_backend/storage_backend/i18n/storage_backend.pot new file mode 100644 index 0000000..9176cb1 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend/storage_backend/i18n/storage_backend.pot @@ -0,0 +1,148 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * storage_backend +# +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: storage_backend +#. odoo-python +#: code:addons/storage_backend/components/filesystem_adapter.py:0 +#, python-format +msgid "Access to %s is forbidden" +msgstr "" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__backend_type +msgid "Backend Type" +msgstr "" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__backend_type_env_default +msgid "Backend Type Env Default" +msgstr "" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__backend_type_env_is_editable +msgid "Backend Type Env Is Editable" +msgstr "" + +#. module: storage_backend +#. odoo-python +#: code:addons/storage_backend/models/storage_backend.py:0 +#, python-format +msgid "Connection Test Failed!" +msgstr "" + +#. module: storage_backend +#. odoo-python +#: code:addons/storage_backend/models/storage_backend.py:0 +#, python-format +msgid "Connection Test Succeeded!" +msgstr "" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__create_uid +msgid "Created by" +msgstr "" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__create_date +msgid "Created on" +msgstr "" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__directory_path +msgid "Directory Path" +msgstr "" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__directory_path_env_default +msgid "Directory Path Env Default" +msgstr "" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__directory_path_env_is_editable +msgid "Directory Path Env Is Editable" +msgstr "" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__display_name +msgid "Display Name" +msgstr "" + +#. module: storage_backend +#. odoo-python +#: code:addons/storage_backend/models/storage_backend.py:0 +#, python-format +msgid "Everything seems properly set up!" +msgstr "" + +#. module: storage_backend +#: model:ir.model.fields.selection,name:storage_backend.selection__storage_backend__backend_type__filesystem +msgid "Filesystem" +msgstr "" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__has_validation +msgid "Has Validation" +msgstr "" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__id +msgid "ID" +msgstr "" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend____last_update +msgid "Last Modified on" +msgstr "" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__write_date +msgid "Last Updated on" +msgstr "" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__name +msgid "Name" +msgstr "" + +#. module: storage_backend +#: model:ir.model.fields,help:storage_backend.field_storage_backend__directory_path +#: model:ir.model.fields,help:storage_backend.field_storage_backend__directory_path_env_default +msgid "Relative path to the directory to store the file" +msgstr "" + +#. module: storage_backend +#: model:ir.model.fields,field_description:storage_backend.field_storage_backend__server_env_defaults +msgid "Server Env Defaults" +msgstr "" + +#. module: storage_backend +#: model:ir.actions.act_window,name:storage_backend.act_open_storage_backend_view +#: model:ir.model,name:storage_backend.model_storage_backend +#: model:ir.ui.menu,name:storage_backend.menu_storage +#: model:ir.ui.menu,name:storage_backend.menu_storage_backend +#: model_terms:ir.ui.view,arch_db:storage_backend.storage_backend_view_form +#: model_terms:ir.ui.view,arch_db:storage_backend.storage_backend_view_search +msgid "Storage Backend" +msgstr "" + +#. module: storage_backend +#: model_terms:ir.ui.view,arch_db:storage_backend.storage_backend_view_form +msgid "Test connection" +msgstr "" diff --git a/odoo-bringout-oca-storage-storage_backend/storage_backend/models/__init__.py b/odoo-bringout-oca-storage-storage_backend/storage_backend/models/__init__.py new file mode 100644 index 0000000..f45f402 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend/storage_backend/models/__init__.py @@ -0,0 +1 @@ +from . import storage_backend diff --git a/odoo-bringout-oca-storage-storage_backend/storage_backend/models/storage_backend.py b/odoo-bringout-oca-storage-storage_backend/storage_backend/models/storage_backend.py new file mode 100644 index 0000000..c678b94 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend/storage_backend/models/storage_backend.py @@ -0,0 +1,185 @@ +# Copyright 2017 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# Copyright 2019 Camptocamp SA (http://www.camptocamp.com). +# @author Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import base64 +import fnmatch +import functools +import inspect +import logging +import warnings + +from odoo import _, fields, models + +_logger = logging.getLogger(__name__) + + +# TODO: useful for the whole OCA? +def deprecated(reason): + """Mark functions or classes as deprecated. + + Emit warning at execution. + + The @deprecated is used with a 'reason'. + + .. code-block:: python + + @deprecated("please, use another function") + def old_function(x, y): + pass + """ + + def decorator(func1): + + if inspect.isclass(func1): + fmt1 = "Call to deprecated class {name} ({reason})." + else: + fmt1 = "Call to deprecated function {name} ({reason})." + + @functools.wraps(func1) + def new_func1(*args, **kwargs): + warnings.simplefilter("always", DeprecationWarning) + warnings.warn( + fmt1.format(name=func1.__name__, reason=reason), + category=DeprecationWarning, + stacklevel=2, + ) + warnings.simplefilter("default", DeprecationWarning) + return func1(*args, **kwargs) + + return new_func1 + + return decorator + + +class StorageBackend(models.Model): + _name = "storage.backend" + _inherit = ["collection.base", "server.env.mixin"] + _backend_name = "storage_backend" + _description = "Storage Backend" + + name = fields.Char(required=True) + backend_type = fields.Selection( + selection=[("filesystem", "Filesystem")], required=True, default="filesystem" + ) + directory_path = fields.Char( + help="Relative path to the directory to store the file" + ) + has_validation = fields.Boolean(compute="_compute_has_validation") + + def _compute_has_validation(self): + for rec in self: + if not rec.backend_type: + # with server_env + # this code can be triggered + # before a backend_type has been set + # get_adapter() can't work without backend_type + rec.has_validation = False + continue + adapter = rec._get_adapter() + rec.has_validation = hasattr(adapter, "validate_config") + + @property + def _server_env_fields(self): + return {"backend_type": {}, "directory_path": {}} + + def add(self, relative_path, data, binary=True, **kwargs): + if not binary: + data = base64.b64decode(data) + return self._forward("add", relative_path, data, **kwargs) + + @deprecated("Use `add`") + def _add_bin_data(self, relative_path, data, **kwargs): + return self.add(relative_path, data, **kwargs) + + @deprecated("Use `add` with `binary=False`") + def _add_b64_data(self, relative_path, data, **kwargs): + return self.add(relative_path, data, binary=False, **kwargs) + + def get(self, relative_path, binary=True, **kwargs): + data = self._forward("get", relative_path, **kwargs) + if not binary and data: + data = base64.b64encode(data) + return data + + @deprecated("Use `get` with `binary=False`") + def _get_b64_data(self, relative_path, **kwargs): + return self.get(relative_path, binary=False, **kwargs) + + @deprecated("Use `get`") + def _get_bin_data(self, relative_path, **kwargs): + return self.get(relative_path, **kwargs) + + def list_files(self, relative_path="", pattern=False): + names = self._forward("list", relative_path) + if pattern: + names = fnmatch.filter(names, pattern) + return names + + @deprecated("Use `list_files`") + def _list(self, relative_path="", pattern=False): + return self.list_files(relative_path, pattern=pattern) + + def find_files(self, pattern, relative_path="", **kw): + return self._forward("find_files", pattern, relative_path=relative_path) + + @deprecated("Use `find_files`") + def _find_files(self, pattern, relative_path="", **kw): + return self.find_files(pattern, relative_path=relative_path, **kw) + + def move_files(self, files, destination_path, **kw): + return self._forward("move_files", files, destination_path, **kw) + + @deprecated("Use `move_files`") + def _move_files(self, files, destination_path, **kw): + return self.move_files(files, destination_path, **kw) + + def delete(self, relative_path): + return self._forward("delete", relative_path) + + @deprecated("Use `delete`") + def _delete(self, relative_path): + return self.delete(relative_path) + + def _forward(self, method, *args, **kwargs): + _logger.debug( + "Backend Storage ID: %s type %s: %s file %s %s", + self.backend_type, + self.id, + method, + args, + kwargs, + ) + self.ensure_one() + adapter = self._get_adapter() + return getattr(adapter, method)(*args, **kwargs) + + def _get_adapter(self): + with self.work_on(self._name) as work: + return work.component(usage=self.backend_type) + + def action_test_config(self): + if not self.has_validation: + raise AttributeError("Validation not supported!") + adapter = self._get_adapter() + try: + adapter.validate_config() + title = _("Connection Test Succeeded!") + message = _("Everything seems properly set up!") + msg_type = "success" + except Exception as err: + title = _("Connection Test Failed!") + message = str(err) + msg_type = "danger" + return { + "type": "ir.actions.client", + "tag": "display_notification", + "params": { + "title": title, + "message": message, + "type": msg_type, + "sticky": False, + }, + } diff --git a/odoo-bringout-oca-storage-storage_backend/storage_backend/readme/CONTRIBUTORS.rst b/odoo-bringout-oca-storage-storage_backend/storage_backend/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000..9b79250 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend/storage_backend/readme/CONTRIBUTORS.rst @@ -0,0 +1,8 @@ +* Sébastien BEAU +* Raphaël Reverdy +* Florian da Costa +* Cédric Pigeon +* Renato Lima +* Benoît Guillot +* Laurent Mignon +* Denis Roussel diff --git a/odoo-bringout-oca-storage-storage_backend/storage_backend/readme/DESCRIPTION.rst b/odoo-bringout-oca-storage-storage_backend/storage_backend/readme/DESCRIPTION.rst new file mode 100644 index 0000000..e69de29 diff --git a/odoo-bringout-oca-storage-storage_backend/storage_backend/readme/HISTORY.rst b/odoo-bringout-oca-storage-storage_backend/storage_backend/readme/HISTORY.rst new file mode 100644 index 0000000..e69de29 diff --git a/odoo-bringout-oca-storage-storage_backend/storage_backend/readme/USAGE.rst b/odoo-bringout-oca-storage-storage_backend/storage_backend/readme/USAGE.rst new file mode 100644 index 0000000..e69de29 diff --git a/odoo-bringout-oca-storage-storage_backend/storage_backend/security/ir.model.access.csv b/odoo-bringout-oca-storage-storage_backend/storage_backend/security/ir.model.access.csv new file mode 100644 index 0000000..dd245d4 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend/storage_backend/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_storage_backend_edit,storage_backend edit,model_storage_backend,base.group_system,1,1,1,1 diff --git a/odoo-bringout-oca-storage-storage_backend/storage_backend/static/description/icon.png b/odoo-bringout-oca-storage-storage_backend/storage_backend/static/description/icon.png new file mode 100644 index 0000000..3a0328b Binary files /dev/null and b/odoo-bringout-oca-storage-storage_backend/storage_backend/static/description/icon.png differ diff --git a/odoo-bringout-oca-storage-storage_backend/storage_backend/static/description/index.html b/odoo-bringout-oca-storage-storage_backend/storage_backend/static/description/index.html new file mode 100644 index 0000000..cbe6c40 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend/storage_backend/static/description/index.html @@ -0,0 +1,429 @@ + + + + + +Storage Bakend + + + +
+

Storage Bakend

+ + +

Production/Stable License: LGPL-3 OCA/storage Translate me on Weblate Try me on Runboat

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub 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.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Akretion
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/storage project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/odoo-bringout-oca-storage-storage_backend/storage_backend/tests/__init__.py b/odoo-bringout-oca-storage-storage_backend/storage_backend/tests/__init__.py new file mode 100644 index 0000000..2161dca --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend/storage_backend/tests/__init__.py @@ -0,0 +1,2 @@ +from . import common +from . import test_filesystem diff --git a/odoo-bringout-oca-storage-storage_backend/storage_backend/tests/common.py b/odoo-bringout-oca-storage-storage_backend/storage_backend/tests/common.py new file mode 100644 index 0000000..84f9167 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend/storage_backend/tests/common.py @@ -0,0 +1,78 @@ +# Copyright 2017 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import base64 +from unittest import mock + +from odoo.addons.component.tests.common import TransactionComponentCase + + +class BackendStorageTestMixin(object): + def _test_setting_and_getting_data(self): + # Check that the directory is empty + files = self.backend.list_files() + self.assertNotIn(self.filename, files) + + # Add a new file + self.backend.add( + self.filename, self.filedata, mimetype="text/plain", binary=False + ) + + # Check that the file exist + files = self.backend.list_files() + self.assertIn(self.filename, files) + + # Retrieve the file added + data = self.backend.get(self.filename, binary=False) + self.assertEqual(data, self.filedata) + + # Delete the file + self.backend.delete(self.filename) + files = self.backend.list_files() + self.assertNotIn(self.filename, files) + + def _test_setting_and_getting_data_from_root(self): + self._test_setting_and_getting_data() + + def _test_setting_and_getting_data_from_dir(self): + self.backend.directory_path = self.case_with_subdirectory + self._test_setting_and_getting_data() + + def _test_find_files( + self, + backend, + adapter_dotted_path, + mocked_filepaths, + pattern, + expected_filepaths, + ): + with mock.patch(adapter_dotted_path + ".list") as mocked: + mocked.return_value = mocked_filepaths + res = backend.find_files(pattern) + self.assertEqual(sorted(res), sorted(expected_filepaths)) + + def _test_move_files( + self, + backend, + adapter_dotted_path, + filename, + destination_path, + expected_filepaths, + ): + with mock.patch(adapter_dotted_path + ".move_files") as mocked: + mocked.return_value = expected_filepaths + res = backend.move_files(filename, destination_path) + self.assertEqual(sorted(res), sorted(expected_filepaths)) + + +class CommonCase(TransactionComponentCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.backend = cls.env.ref("storage_backend.default_storage_backend") + cls.filedata = base64.b64encode(b"This is a simple file") + cls.filename = "test_file.txt" + cls.case_with_subdirectory = "subdirectory/here" + cls.demo_user = cls.env.ref("base.user_demo") diff --git a/odoo-bringout-oca-storage-storage_backend/storage_backend/tests/test_filesystem.py b/odoo-bringout-oca-storage-storage_backend/storage_backend/tests/test_filesystem.py new file mode 100644 index 0000000..a136401 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend/storage_backend/tests/test_filesystem.py @@ -0,0 +1,65 @@ +# Copyright 2017 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +import os + +from odoo.exceptions import AccessError + +from .common import BackendStorageTestMixin, CommonCase + +ADAPTER_PATH = ( + "odoo.addons.storage_backend.components.filesystem_adapter.FileSystemStorageBackend" +) + + +class FileSystemCase(CommonCase, BackendStorageTestMixin): + def test_setting_and_getting_data_from_root(self): + self._test_setting_and_getting_data_from_root() + + def test_setting_and_getting_data_from_dir(self): + self._test_setting_and_getting_data_from_dir() + + def test_find_files(self): + good_filepaths = ["somepath/file%d.good" % x for x in range(1, 10)] + bad_filepaths = ["somepath/file%d.bad" % x for x in range(1, 10)] + mocked_filepaths = bad_filepaths + good_filepaths + backend = self.backend.sudo() + base_dir = backend._get_adapter()._basedir() + expected = [base_dir + "/" + path for path in good_filepaths] + self._test_find_files( + backend, ADAPTER_PATH, mocked_filepaths, r".*\.good$", expected + ) + + def test_move_files(self): + backend = self.backend.sudo() + base_dir = backend._get_adapter()._basedir() + expected = [base_dir + "/" + self.filename] + destination_path = os.path.join(base_dir, "destination") + self._test_move_files( + backend, ADAPTER_PATH, self.filename, destination_path, expected + ) + + +class FileSystemDemoUserAccessCase(CommonCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.backend = cls.backend.with_user(cls.demo_user) + + def test_cannot_add_file(self): + with self.assertRaises(AccessError): + self.backend.add( + self.filename, self.filedata, mimetype="text/plain", binary=False + ) + + def test_cannot_list_file(self): + with self.assertRaises(AccessError): + self.backend.list_files() + + def test_cannot_read_file(self): + with self.assertRaises(AccessError): + self.backend.get(self.filename, binary=False) + + def test_cannot_delete_file(self): + with self.assertRaises(AccessError): + self.backend.delete(self.filename) diff --git a/odoo-bringout-oca-storage-storage_backend/storage_backend/views/backend_storage_view.xml b/odoo-bringout-oca-storage-storage_backend/storage_backend/views/backend_storage_view.xml new file mode 100644 index 0000000..5cccc5a --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend/storage_backend/views/backend_storage_view.xml @@ -0,0 +1,81 @@ + + + + storage.backend + + + + + + + + + storage.backend + +
+
+ +
+ +
+
+ + + + +
+
+
+
+ + storage.backend + + + + + + + + Storage Backend + ir.actions.act_window + storage.backend + tree,form + + [] + {} + + + + + form + + + + + + tree + + + + +
diff --git a/odoo-bringout-oca-storage-storage_backend_sftp/README.md b/odoo-bringout-oca-storage-storage_backend_sftp/README.md new file mode 100644 index 0000000..4ba653f --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend_sftp/README.md @@ -0,0 +1,46 @@ +# Storage Backend SFTP + +Odoo addon: storage_backend_sftp + +## Installation + +```bash +pip install odoo-bringout-oca-storage-storage_backend_sftp +``` + +## Dependencies + +This addon depends on: +- storage_backend + +## Manifest Information + +- **Name**: Storage Backend SFTP +- **Version**: 16.0.1.0.1 +- **Category**: Storage +- **License**: LGPL-3 +- **Installable**: True + +## Source + +Based on [OCA/storage](https://github.com/OCA/storage) branch 16.0, addon `storage_backend_sftp`. + +## License + +This package maintains the original LGPL-3 license from the upstream Odoo project. + +## Documentation + +- Overview: doc/OVERVIEW.md +- Architecture: doc/ARCHITECTURE.md +- Models: doc/MODELS.md +- Controllers: doc/CONTROLLERS.md +- Wizards: doc/WIZARDS.md +- 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 diff --git a/odoo-bringout-oca-storage-storage_backend_sftp/doc/ARCHITECTURE.md b/odoo-bringout-oca-storage-storage_backend_sftp/doc/ARCHITECTURE.md new file mode 100644 index 0000000..50458ae --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend_sftp/doc/ARCHITECTURE.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 Storage_backend_sftp Module - storage_backend_sftp + 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. diff --git a/odoo-bringout-oca-storage-storage_backend_sftp/doc/CONFIGURATION.md b/odoo-bringout-oca-storage-storage_backend_sftp/doc/CONFIGURATION.md new file mode 100644 index 0000000..7753cb8 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend_sftp/doc/CONFIGURATION.md @@ -0,0 +1,3 @@ +# Configuration + +Refer to Odoo settings for storage_backend_sftp. Configure related models, access rights, and options as needed. diff --git a/odoo-bringout-oca-storage-storage_backend_sftp/doc/CONTROLLERS.md b/odoo-bringout-oca-storage-storage_backend_sftp/doc/CONTROLLERS.md new file mode 100644 index 0000000..f628e77 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend_sftp/doc/CONTROLLERS.md @@ -0,0 +1,3 @@ +# Controllers + +This module does not define custom HTTP controllers. diff --git a/odoo-bringout-oca-storage-storage_backend_sftp/doc/DEPENDENCIES.md b/odoo-bringout-oca-storage-storage_backend_sftp/doc/DEPENDENCIES.md new file mode 100644 index 0000000..985d1f7 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend_sftp/doc/DEPENDENCIES.md @@ -0,0 +1,5 @@ +# Dependencies + +This addon depends on: + +- [storage_backend](../../odoo-bringout-oca-storage-storage_backend) diff --git a/odoo-bringout-oca-storage-storage_backend_sftp/doc/FAQ.md b/odoo-bringout-oca-storage-storage_backend_sftp/doc/FAQ.md new file mode 100644 index 0000000..02fbfc9 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend_sftp/doc/FAQ.md @@ -0,0 +1,4 @@ +# FAQ + +- Q: Which Odoo version? A: 16.0 (OCA/OCB packaged). +- Q: How to enable? A: Start server with --addon storage_backend_sftp or install in UI. diff --git a/odoo-bringout-oca-storage-storage_backend_sftp/doc/INSTALL.md b/odoo-bringout-oca-storage-storage_backend_sftp/doc/INSTALL.md new file mode 100644 index 0000000..149b646 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend_sftp/doc/INSTALL.md @@ -0,0 +1,7 @@ +# Install + +```bash +pip install odoo-bringout-oca-storage-storage_backend_sftp" +# or +uv pip install odoo-bringout-oca-storage-storage_backend_sftp" +``` diff --git a/odoo-bringout-oca-storage-storage_backend_sftp/doc/MODELS.md b/odoo-bringout-oca-storage-storage_backend_sftp/doc/MODELS.md new file mode 100644 index 0000000..dac0f42 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend_sftp/doc/MODELS.md @@ -0,0 +1,12 @@ +# Models + +Detected core models and extensions in storage_backend_sftp. + +```mermaid +classDiagram + class storage_backend +``` + +Notes +- Classes show model technical names; fields omitted for brevity. +- Items listed under _inherit are extensions of existing models. diff --git a/odoo-bringout-oca-storage-storage_backend_sftp/doc/OVERVIEW.md b/odoo-bringout-oca-storage-storage_backend_sftp/doc/OVERVIEW.md new file mode 100644 index 0000000..e9616c8 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend_sftp/doc/OVERVIEW.md @@ -0,0 +1,6 @@ +# Overview + +Packaged Odoo addon: storage_backend_sftp. Provides features documented in upstream Odoo 16 under this addon. + +- Source: OCA/OCB 16.0, addon storage_backend_sftp +- License: LGPL-3 diff --git a/odoo-bringout-oca-storage-storage_backend_sftp/doc/REPORTS.md b/odoo-bringout-oca-storage-storage_backend_sftp/doc/REPORTS.md new file mode 100644 index 0000000..e0ea35f --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend_sftp/doc/REPORTS.md @@ -0,0 +1,3 @@ +# Reports + +This module does not define custom reports. diff --git a/odoo-bringout-oca-storage-storage_backend_sftp/doc/SECURITY.md b/odoo-bringout-oca-storage-storage_backend_sftp/doc/SECURITY.md new file mode 100644 index 0000000..e07da9d --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend_sftp/doc/SECURITY.md @@ -0,0 +1,8 @@ +# Security + +This module does not define custom security rules or access controls beyond Odoo defaults. + +Default Odoo security applies: +- Base user access through standard groups +- Model access inherited from dependencies +- No custom row-level security rules diff --git a/odoo-bringout-oca-storage-storage_backend_sftp/doc/TROUBLESHOOTING.md b/odoo-bringout-oca-storage-storage_backend_sftp/doc/TROUBLESHOOTING.md new file mode 100644 index 0000000..56853cb --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend_sftp/doc/TROUBLESHOOTING.md @@ -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. diff --git a/odoo-bringout-oca-storage-storage_backend_sftp/doc/USAGE.md b/odoo-bringout-oca-storage-storage_backend_sftp/doc/USAGE.md new file mode 100644 index 0000000..a966f86 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend_sftp/doc/USAGE.md @@ -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 storage_backend_sftp +``` diff --git a/odoo-bringout-oca-storage-storage_backend_sftp/doc/WIZARDS.md b/odoo-bringout-oca-storage-storage_backend_sftp/doc/WIZARDS.md new file mode 100644 index 0000000..48e790d --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend_sftp/doc/WIZARDS.md @@ -0,0 +1,3 @@ +# Wizards + +This module does not include UI wizards. diff --git a/odoo-bringout-oca-storage-storage_backend_sftp/pyproject.toml b/odoo-bringout-oca-storage-storage_backend_sftp/pyproject.toml new file mode 100644 index 0000000..89e78b3 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend_sftp/pyproject.toml @@ -0,0 +1,42 @@ +[project] +name = "odoo-bringout-oca-storage-storage_backend_sftp" +version = "16.0.0" +description = "Storage Backend SFTP - Implement SFTP Storage" +authors = [ + { name = "Ernad Husremovic", email = "hernad@bring.out.ba" } +] +dependencies = [ + "odoo-bringout-oca-storage-storage_backend>=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 = ["storage_backend_sftp"] + +[tool.rye] +managed = true +dev-dependencies = [ + "pytest>=8.4.1", +] diff --git a/odoo-bringout-oca-storage-storage_backend_sftp/storage_backend_sftp/README.rst b/odoo-bringout-oca-storage-storage_backend_sftp/storage_backend_sftp/README.rst new file mode 100644 index 0000000..9f082d8 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend_sftp/storage_backend_sftp/README.rst @@ -0,0 +1,44 @@ + +.. image:: https://img.shields.io/badge/licence-LGPL--3-blue.svg + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 + +===================== +Storage backend SFTP +===================== + +Add the possibility to store and get data from an SFTP for your storage backend + + + +Installation +============ + +To install this module, you need to: + +#. (root) pip install paramiko + + +Known issues / Roadmap +====================== + +Update README with the last model of README when migration to v11 in OCA + + +Credits +======= + + +Contributors +------------ + +* Sebastien Beau +* Raphaël Reverdy +* Cédric Pigeon +* Simone Orsi + + +Maintainer +---------- + +* Akretion diff --git a/odoo-bringout-oca-storage-storage_backend_sftp/storage_backend_sftp/__init__.py b/odoo-bringout-oca-storage-storage_backend_sftp/storage_backend_sftp/__init__.py new file mode 100644 index 0000000..0f00a67 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend_sftp/storage_backend_sftp/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import components diff --git a/odoo-bringout-oca-storage-storage_backend_sftp/storage_backend_sftp/__manifest__.py b/odoo-bringout-oca-storage-storage_backend_sftp/storage_backend_sftp/__manifest__.py new file mode 100644 index 0000000..7fcdbef --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend_sftp/storage_backend_sftp/__manifest__.py @@ -0,0 +1,17 @@ +# Copyright 2017 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +{ + "name": "Storage Backend SFTP", + "summary": "Implement SFTP Storage", + "version": "16.0.1.0.1", + "category": "Storage", + "website": "https://github.com/OCA/storage", + "author": " Akretion,Odoo Community Association (OCA)", + "license": "LGPL-3", + "installable": True, + "external_dependencies": {"python": ["paramiko"]}, + "depends": ["storage_backend"], + "data": ["views/backend_storage_view.xml"], +} diff --git a/odoo-bringout-oca-storage-storage_backend_sftp/storage_backend_sftp/components/__init__.py b/odoo-bringout-oca-storage-storage_backend_sftp/storage_backend_sftp/components/__init__.py new file mode 100644 index 0000000..76ddd15 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend_sftp/storage_backend_sftp/components/__init__.py @@ -0,0 +1 @@ +from . import sftp_adapter diff --git a/odoo-bringout-oca-storage-storage_backend_sftp/storage_backend_sftp/components/sftp_adapter.py b/odoo-bringout-oca-storage-storage_backend_sftp/storage_backend_sftp/components/sftp_adapter.py new file mode 100644 index 0000000..aaa65fe --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend_sftp/storage_backend_sftp/components/sftp_adapter.py @@ -0,0 +1,129 @@ +# Copyright 2017 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# Copyright 2019 Camptocamp SA (http://www.camptocamp.com). +# Copyright 2020 ACSONE SA/NV () +# @author Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +import errno +import logging +import os +from contextlib import contextmanager +from io import StringIO + +from odoo.addons.component.core import Component + +_logger = logging.getLogger(__name__) + +try: + import paramiko +except ImportError as err: # pragma: no cover + _logger.debug(err) + + +def sftp_mkdirs(client, path, mode=511): + try: + client.mkdir(path, mode) + except IOError as e: + if e.errno == errno.ENOENT and path: + sftp_mkdirs(client, os.path.dirname(path), mode=mode) + client.mkdir(path, mode) + else: + raise # pragma: no cover + + +def load_ssh_key(ssh_key_buffer): + for pkey_class in ( + paramiko.RSAKey, + paramiko.DSSKey, + paramiko.ECDSAKey, + paramiko.Ed25519Key, + ): + try: + return pkey_class.from_private_key(ssh_key_buffer) + except paramiko.SSHException: + ssh_key_buffer.seek(0) # reset the buffer "file" + raise Exception("Invalid ssh private key") + + +@contextmanager +def sftp(backend): + transport = paramiko.Transport((backend.sftp_server, backend.sftp_port)) + if backend.sftp_auth_method == "pwd": + transport.connect(username=backend.sftp_login, password=backend.sftp_password) + elif backend.sftp_auth_method == "ssh_key": + ssh_key_buffer = StringIO(backend.sftp_ssh_private_key) + private_key = load_ssh_key(ssh_key_buffer) + transport.connect(username=backend.sftp_login, pkey=private_key) + client = paramiko.SFTPClient.from_transport(transport) + yield client + transport.close() + + +class SFTPStorageBackendAdapter(Component): + _name = "sftp.adapter" + _inherit = "base.storage.adapter" + _usage = "sftp" + + def add(self, relative_path, data, **kwargs): + with sftp(self.collection) as client: + full_path = self._fullpath(relative_path) + dirname = os.path.dirname(full_path) + if dirname: + try: + client.stat(dirname) + except IOError as e: + if e.errno == errno.ENOENT: + sftp_mkdirs(client, dirname) + else: + raise # pragma: no cover + remote_file = client.open(full_path, "w") + remote_file.write(data) + remote_file.close() + + def get(self, relative_path, **kwargs): + full_path = self._fullpath(relative_path) + with sftp(self.collection) as client: + file_data = client.open(full_path, "r") + data = file_data.read() + # TODO: shouldn't we close the file? + return data + + def list(self, relative_path): + full_path = self._fullpath(relative_path) + with sftp(self.collection) as client: + try: + return client.listdir(full_path) + except IOError as e: + if e.errno == errno.ENOENT: + # The path do not exist return an empty list + return [] + else: + raise # pragma: no cover + + def move_files(self, files, destination_path): + _logger.debug("mv %s %s", files, destination_path) + fp = self._fullpath + with sftp(self.collection) as client: + for sftp_file in files: + dest_file_path = os.path.join( + destination_path, os.path.basename(sftp_file) + ) + # Remove existing file at the destination path (an error is raised + # otherwise) + try: + client.lstat(dest_file_path) + except FileNotFoundError: + _logger.debug("destination %s is free", dest_file_path) + else: + client.unlink(dest_file_path) + # Move the file using absolute filepaths + client.rename(fp(sftp_file), fp(dest_file_path)) + + def delete(self, relative_path): + full_path = self._fullpath(relative_path) + with sftp(self.collection) as client: + return client.remove(full_path) + + def validate_config(self): + with sftp(self.collection) as client: + client.listdir() diff --git a/odoo-bringout-oca-storage-storage_backend_sftp/storage_backend_sftp/i18n/bs.po b/odoo-bringout-oca-storage-storage_backend_sftp/storage_backend_sftp/i18n/bs.po new file mode 100644 index 0000000..eb35f09 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend_sftp/storage_backend_sftp/i18n/bs.po @@ -0,0 +1,87 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * storage_backend_sftp +# +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: storage_backend_sftp +#: model:ir.model.fields,field_description:storage_backend_sftp.field_storage_backend__backend_type +msgid "Backend Type" +msgstr "Tip pozadine" + +#. module: storage_backend_sftp +#: model:ir.model.fields,field_description:storage_backend_sftp.field_storage_backend__backend_type_env_default +msgid "Backend Type Env Default" +msgstr "Zadano okruženje tipa pozadine" + +#. module: storage_backend_sftp +#: model:ir.model.fields,help:storage_backend_sftp.field_storage_backend__sftp_ssh_private_key +msgid "" +"It's recommended to not store the key here but to provide it via secret env " +"variable. See `server_environment` docs." +msgstr "" + +#. module: storage_backend_sftp +#: model:ir.model.fields,help:storage_backend_sftp.field_storage_backend__sftp_login +msgid "Login to connect to sftp server" +msgstr "Prijava za povezivanje na SFTP server" + +#. module: storage_backend_sftp +#: model:ir.model.fields.selection,name:storage_backend_sftp.selection__storage_backend__sftp_auth_method__pwd +msgid "Password" +msgstr "Zaporka" + +#. module: storage_backend_sftp +#: model:ir.model.fields.selection,name:storage_backend_sftp.selection__storage_backend__sftp_auth_method__ssh_key +msgid "Private key" +msgstr "Privatni ključ" + +#. module: storage_backend_sftp +#: model:ir.model.fields.selection,name:storage_backend_sftp.selection__storage_backend__backend_type__sftp +#: model_terms:ir.ui.view,arch_db:storage_backend_sftp.storage_backend_view_form +msgid "SFTP" +msgstr "SFTP" + +#. module: storage_backend_sftp +#: model:ir.model.fields,field_description:storage_backend_sftp.field_storage_backend__sftp_auth_method +msgid "SFTP Authentification Method" +msgstr "SFTP metoda autentifikacije" + +#. module: storage_backend_sftp +#: model:ir.model.fields,field_description:storage_backend_sftp.field_storage_backend__sftp_server +msgid "SFTP Host" +msgstr "SFTP domaćin" + +#. module: storage_backend_sftp +#: model:ir.model.fields,field_description:storage_backend_sftp.field_storage_backend__sftp_login +msgid "SFTP Login" +msgstr "SFTP prijava" + +#. module: storage_backend_sftp +#: model:ir.model.fields,field_description:storage_backend_sftp.field_storage_backend__sftp_password +msgid "SFTP Password" +msgstr "SFTP Password" + +#. module: storage_backend_sftp +#: model:ir.model.fields,field_description:storage_backend_sftp.field_storage_backend__sftp_port +msgid "SFTP Port" +msgstr "SFTP Port" + +#. module: storage_backend_sftp +#: model:ir.model.fields,field_description:storage_backend_sftp.field_storage_backend__sftp_ssh_private_key +msgid "SSH private key" +msgstr "SSH privatni ključ" + +#. module: storage_backend_sftp +#: model:ir.model,name:storage_backend_sftp.model_storage_backend +msgid "Storage Backend" +msgstr "Pozadina skladištenja" diff --git a/odoo-bringout-oca-storage-storage_backend_sftp/storage_backend_sftp/i18n/it.po b/odoo-bringout-oca-storage-storage_backend_sftp/storage_backend_sftp/i18n/it.po new file mode 100644 index 0000000..4db22d1 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend_sftp/storage_backend_sftp/i18n/it.po @@ -0,0 +1,92 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * storage_backend_sftp +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2025-06-04 09:26+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.10.4\n" + +#. module: storage_backend_sftp +#: model:ir.model.fields,field_description:storage_backend_sftp.field_storage_backend__backend_type +msgid "Backend Type" +msgstr "Tipo backend" + +#. module: storage_backend_sftp +#: model:ir.model.fields,field_description:storage_backend_sftp.field_storage_backend__backend_type_env_default +msgid "Backend Type Env Default" +msgstr "Tipo backend ambiente predefinito" + +#. module: storage_backend_sftp +#: model:ir.model.fields,help:storage_backend_sftp.field_storage_backend__sftp_ssh_private_key +msgid "" +"It's recommended to not store the key here but to provide it via secret env " +"variable. See `server_environment` docs." +msgstr "" +"Si raccomanda si non salvare qui la chiave ma di fornirla attraverso una " +"variabile di ambiente segreta. Vedere documentazione 'server_enviroment'." + +#. module: storage_backend_sftp +#: model:ir.model.fields,help:storage_backend_sftp.field_storage_backend__sftp_login +msgid "Login to connect to sftp server" +msgstr "Accedere per collegarsi al server SFTP" + +#. module: storage_backend_sftp +#: model:ir.model.fields.selection,name:storage_backend_sftp.selection__storage_backend__sftp_auth_method__pwd +msgid "Password" +msgstr "Password" + +#. module: storage_backend_sftp +#: model:ir.model.fields.selection,name:storage_backend_sftp.selection__storage_backend__sftp_auth_method__ssh_key +msgid "Private key" +msgstr "Chiave privata" + +#. module: storage_backend_sftp +#: model:ir.model.fields.selection,name:storage_backend_sftp.selection__storage_backend__backend_type__sftp +#: model_terms:ir.ui.view,arch_db:storage_backend_sftp.storage_backend_view_form +msgid "SFTP" +msgstr "SFTP" + +#. module: storage_backend_sftp +#: model:ir.model.fields,field_description:storage_backend_sftp.field_storage_backend__sftp_auth_method +msgid "SFTP Authentification Method" +msgstr "Metodo autenticazione SFTP" + +#. module: storage_backend_sftp +#: model:ir.model.fields,field_description:storage_backend_sftp.field_storage_backend__sftp_server +msgid "SFTP Host" +msgstr "Host SFTP" + +#. module: storage_backend_sftp +#: model:ir.model.fields,field_description:storage_backend_sftp.field_storage_backend__sftp_login +msgid "SFTP Login" +msgstr "Accesso SFTP" + +#. module: storage_backend_sftp +#: model:ir.model.fields,field_description:storage_backend_sftp.field_storage_backend__sftp_password +msgid "SFTP Password" +msgstr "Password SFTP" + +#. module: storage_backend_sftp +#: model:ir.model.fields,field_description:storage_backend_sftp.field_storage_backend__sftp_port +msgid "SFTP Port" +msgstr "Porta SFTP" + +#. module: storage_backend_sftp +#: model:ir.model.fields,field_description:storage_backend_sftp.field_storage_backend__sftp_ssh_private_key +msgid "SSH private key" +msgstr "Chiave privata SSH" + +#. module: storage_backend_sftp +#: model:ir.model,name:storage_backend_sftp.model_storage_backend +msgid "Storage Backend" +msgstr "Backend deposito" diff --git a/odoo-bringout-oca-storage-storage_backend_sftp/storage_backend_sftp/i18n/storage_backend_sftp.pot b/odoo-bringout-oca-storage-storage_backend_sftp/storage_backend_sftp/i18n/storage_backend_sftp.pot new file mode 100644 index 0000000..7847f48 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend_sftp/storage_backend_sftp/i18n/storage_backend_sftp.pot @@ -0,0 +1,87 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * storage_backend_sftp +# +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: storage_backend_sftp +#: model:ir.model.fields,field_description:storage_backend_sftp.field_storage_backend__backend_type +msgid "Backend Type" +msgstr "" + +#. module: storage_backend_sftp +#: model:ir.model.fields,field_description:storage_backend_sftp.field_storage_backend__backend_type_env_default +msgid "Backend Type Env Default" +msgstr "" + +#. module: storage_backend_sftp +#: model:ir.model.fields,help:storage_backend_sftp.field_storage_backend__sftp_ssh_private_key +msgid "" +"It's recommended to not store the key here but to provide it via secret env " +"variable. See `server_environment` docs." +msgstr "" + +#. module: storage_backend_sftp +#: model:ir.model.fields,help:storage_backend_sftp.field_storage_backend__sftp_login +msgid "Login to connect to sftp server" +msgstr "" + +#. module: storage_backend_sftp +#: model:ir.model.fields.selection,name:storage_backend_sftp.selection__storage_backend__sftp_auth_method__pwd +msgid "Password" +msgstr "" + +#. module: storage_backend_sftp +#: model:ir.model.fields.selection,name:storage_backend_sftp.selection__storage_backend__sftp_auth_method__ssh_key +msgid "Private key" +msgstr "" + +#. module: storage_backend_sftp +#: model:ir.model.fields.selection,name:storage_backend_sftp.selection__storage_backend__backend_type__sftp +#: model_terms:ir.ui.view,arch_db:storage_backend_sftp.storage_backend_view_form +msgid "SFTP" +msgstr "" + +#. module: storage_backend_sftp +#: model:ir.model.fields,field_description:storage_backend_sftp.field_storage_backend__sftp_auth_method +msgid "SFTP Authentification Method" +msgstr "" + +#. module: storage_backend_sftp +#: model:ir.model.fields,field_description:storage_backend_sftp.field_storage_backend__sftp_server +msgid "SFTP Host" +msgstr "" + +#. module: storage_backend_sftp +#: model:ir.model.fields,field_description:storage_backend_sftp.field_storage_backend__sftp_login +msgid "SFTP Login" +msgstr "" + +#. module: storage_backend_sftp +#: model:ir.model.fields,field_description:storage_backend_sftp.field_storage_backend__sftp_password +msgid "SFTP Password" +msgstr "" + +#. module: storage_backend_sftp +#: model:ir.model.fields,field_description:storage_backend_sftp.field_storage_backend__sftp_port +msgid "SFTP Port" +msgstr "" + +#. module: storage_backend_sftp +#: model:ir.model.fields,field_description:storage_backend_sftp.field_storage_backend__sftp_ssh_private_key +msgid "SSH private key" +msgstr "" + +#. module: storage_backend_sftp +#: model:ir.model,name:storage_backend_sftp.model_storage_backend +msgid "Storage Backend" +msgstr "" diff --git a/odoo-bringout-oca-storage-storage_backend_sftp/storage_backend_sftp/models/__init__.py b/odoo-bringout-oca-storage-storage_backend_sftp/storage_backend_sftp/models/__init__.py new file mode 100644 index 0000000..f45f402 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend_sftp/storage_backend_sftp/models/__init__.py @@ -0,0 +1 @@ +from . import storage_backend diff --git a/odoo-bringout-oca-storage-storage_backend_sftp/storage_backend_sftp/models/storage_backend.py b/odoo-bringout-oca-storage-storage_backend_sftp/storage_backend_sftp/models/storage_backend.py new file mode 100644 index 0000000..f79870f --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend_sftp/storage_backend_sftp/models/storage_backend.py @@ -0,0 +1,48 @@ +# Copyright 2017 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# Copyright 2019 Camptocamp SA (http://www.camptocamp.com). +# @author Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import fields, models + + +class StorageBackend(models.Model): + _inherit = "storage.backend" + + backend_type = fields.Selection( + selection_add=[("sftp", "SFTP")], ondelete={"sftp": "set default"} + ) + sftp_server = fields.Char(string="SFTP Host") + sftp_port = fields.Integer(string="SFTP Port", default=22) + sftp_auth_method = fields.Selection( + string="SFTP Authentification Method", + selection=[("pwd", "Password"), ("ssh_key", "Private key")], + default="pwd", + required=True, + ) + sftp_login = fields.Char( + string="SFTP Login", help="Login to connect to sftp server" + ) + sftp_password = fields.Char(string="SFTP Password") + sftp_ssh_private_key = fields.Text( + string="SSH private key", + help="It's recommended to not store the key here " + "but to provide it via secret env variable. " + "See `server_environment` docs.", + ) + + @property + def _server_env_fields(self): + env_fields = super()._server_env_fields + env_fields.update( + { + "sftp_password": {}, + "sftp_login": {}, + "sftp_server": {}, + "sftp_port": {}, + "sftp_auth_method": {}, + "sftp_ssh_private_key": {}, + } + ) + return env_fields diff --git a/odoo-bringout-oca-storage-storage_backend_sftp/storage_backend_sftp/static/description/icon.png b/odoo-bringout-oca-storage-storage_backend_sftp/storage_backend_sftp/static/description/icon.png new file mode 100644 index 0000000..3a0328b Binary files /dev/null and b/odoo-bringout-oca-storage-storage_backend_sftp/storage_backend_sftp/static/description/icon.png differ diff --git a/odoo-bringout-oca-storage-storage_backend_sftp/storage_backend_sftp/tests/__init__.py b/odoo-bringout-oca-storage-storage_backend_sftp/storage_backend_sftp/tests/__init__.py new file mode 100644 index 0000000..c923f0a --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend_sftp/storage_backend_sftp/tests/__init__.py @@ -0,0 +1 @@ +from . import test_sftp diff --git a/odoo-bringout-oca-storage-storage_backend_sftp/storage_backend_sftp/tests/test_sftp.py b/odoo-bringout-oca-storage-storage_backend_sftp/storage_backend_sftp/tests/test_sftp.py new file mode 100644 index 0000000..fb01fab --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend_sftp/storage_backend_sftp/tests/test_sftp.py @@ -0,0 +1,120 @@ +# Copyright 2017 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# Copyright 2019 Camptocamp (http://www.camptocamp.com). +# @author Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +# pylint: disable=missing-manifest-dependency +# disable warning on 'vcr' missing in manifest: this is only a dependency for +# dev/tests + +import errno +import logging +import os +from unittest import mock + +from odoo.addons.storage_backend.tests.common import BackendStorageTestMixin, CommonCase + +_logger = logging.getLogger(__name__) + +MOD_PATH = "odoo.addons.storage_backend_sftp.components.sftp_adapter" +ADAPTER_PATH = MOD_PATH + ".SFTPStorageBackendAdapter" +PARAMIKO_PATH = MOD_PATH + ".paramiko" + + +class SftpCase(CommonCase, BackendStorageTestMixin): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.backend.write( + { + "backend_type": "sftp", + "sftp_login": "foo", + "sftp_password": "pass", + "sftp_server": os.environ.get("SFTP_HOST", "localhost"), + "sftp_port": os.environ.get("SFTP_PORT", "2222"), + "directory_path": "upload", + } + ) + cls.case_with_subdirectory = "upload/subdirectory/here" + + @mock.patch(MOD_PATH + ".sftp_mkdirs") + @mock.patch(PARAMIKO_PATH) + def test_add(self, mocked_paramiko, mocked_mkdirs): + client = mocked_paramiko.SFTPClient.from_transport() + # simulate errors + exc = IOError() + # general + client.stat.side_effect = exc + with self.assertRaises(IOError): + self.backend.add("fake/path", b"fake data") + # not found + exc.errno = errno.ENOENT + client.stat.side_effect = exc + fakefile = open("/tmp/fakefile.txt", "w+b") + client.open.return_value = fakefile + self.backend.add("fake/path", b"fake data") + # mkdirs has been called + mocked_mkdirs.assert_called() + # file has been written and closed + self.assertTrue(fakefile.closed) + with open("/tmp/fakefile.txt", "r") as thefile: + self.assertEqual(thefile.read(), "fake data") + + @mock.patch(PARAMIKO_PATH) + def test_get(self, mocked_paramiko): + client = mocked_paramiko.SFTPClient.from_transport() + with open("/tmp/fakefile2.txt", "w+b") as fakefile: + fakefile.write(b"filecontent") + client.open.return_value = open("/tmp/fakefile2.txt", "r") + self.assertEqual(self.backend.get("fake/path"), "filecontent") + + @mock.patch(PARAMIKO_PATH) + def test_list(self, mocked_paramiko): + client = mocked_paramiko.SFTPClient.from_transport() + # simulate errors + exc = IOError() + # general + client.listdir.side_effect = exc + with self.assertRaises(IOError): + self.backend.list_files() + # not found + exc.errno = errno.ENOENT + client.listdir.side_effect = exc + self.assertEqual(self.backend.list_files(), []) + + def test_find_files(self): + good_filepaths = ["somepath/file%d.good" % x for x in range(1, 10)] + bad_filepaths = ["somepath/file%d.bad" % x for x in range(1, 10)] + mocked_filepaths = bad_filepaths + good_filepaths + backend = self.backend.sudo() + expected = good_filepaths[:] + expected = [backend.directory_path + "/" + path for path in good_filepaths] + self._test_find_files( + backend, ADAPTER_PATH, mocked_filepaths, r".*\.good$", expected + ) + + @mock.patch(PARAMIKO_PATH) + def test_move_files(self, mocked_paramiko): + client = mocked_paramiko.SFTPClient.from_transport() + # simulate file is not already there + client.lstat.side_effect = FileNotFoundError() + to_move = "move/from/path/myfile.txt" + to_path = "move/to/path" + self.backend.move_files([to_move], to_path) + # no need to delete it + client.unlink.assert_not_called() + # rename gets called + client.rename.assert_called_with( + "upload/" + to_move, "upload/" + to_move.replace("from", "to") + ) + # now try to override destination + client.lstat.side_effect = None + client.lstat.return_value = True + self.backend.move_files([to_move], to_path) + # client will delete it first + client.unlink.assert_called_with(to_move.replace("from", "to")) + # then move it + client.rename.assert_called_with( + "upload/" + to_move, "upload/" + to_move.replace("from", "to") + ) diff --git a/odoo-bringout-oca-storage-storage_backend_sftp/storage_backend_sftp/views/backend_storage_view.xml b/odoo-bringout-oca-storage-storage_backend_sftp/storage_backend_sftp/views/backend_storage_view.xml new file mode 100644 index 0000000..341631f --- /dev/null +++ b/odoo-bringout-oca-storage-storage_backend_sftp/storage_backend_sftp/views/backend_storage_view.xml @@ -0,0 +1,31 @@ + + + + storage.backend + + + + + + + + + + + + + + + diff --git a/odoo-bringout-oca-storage-storage_file/README.md b/odoo-bringout-oca-storage-storage_file/README.md new file mode 100644 index 0000000..f1099d8 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_file/README.md @@ -0,0 +1,46 @@ +# Storage File + +Odoo addon: storage_file + +## Installation + +```bash +pip install odoo-bringout-oca-storage-storage_file +``` + +## Dependencies + +This addon depends on: +- storage_backend + +## Manifest Information + +- **Name**: Storage File +- **Version**: 16.0.1.0.1 +- **Category**: Storage +- **License**: LGPL-3 +- **Installable**: True + +## Source + +Based on [OCA/storage](https://github.com/OCA/storage) branch 16.0, addon `storage_file`. + +## License + +This package maintains the original LGPL-3 license from the upstream Odoo project. + +## Documentation + +- Overview: doc/OVERVIEW.md +- Architecture: doc/ARCHITECTURE.md +- Models: doc/MODELS.md +- Controllers: doc/CONTROLLERS.md +- Wizards: doc/WIZARDS.md +- 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 diff --git a/odoo-bringout-oca-storage-storage_file/doc/ARCHITECTURE.md b/odoo-bringout-oca-storage-storage_file/doc/ARCHITECTURE.md new file mode 100644 index 0000000..55b6734 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_file/doc/ARCHITECTURE.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 Storage_file Module - storage_file + 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. diff --git a/odoo-bringout-oca-storage-storage_file/doc/CONFIGURATION.md b/odoo-bringout-oca-storage-storage_file/doc/CONFIGURATION.md new file mode 100644 index 0000000..8b2d929 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_file/doc/CONFIGURATION.md @@ -0,0 +1,3 @@ +# Configuration + +Refer to Odoo settings for storage_file. Configure related models, access rights, and options as needed. diff --git a/odoo-bringout-oca-storage-storage_file/doc/CONTROLLERS.md b/odoo-bringout-oca-storage-storage_file/doc/CONTROLLERS.md new file mode 100644 index 0000000..ff097c0 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_file/doc/CONTROLLERS.md @@ -0,0 +1,17 @@ +# Controllers + +HTTP routes provided by this module. + +```mermaid +sequenceDiagram + participant U as User/Client + participant C as Module Controllers + participant O as ORM/Views + + U->>C: HTTP GET/POST (routes) + C->>O: ORM operations, render templates + O-->>U: HTML/JSON/PDF +``` + +Notes +- See files in controllers/ for route definitions. diff --git a/odoo-bringout-oca-storage-storage_file/doc/DEPENDENCIES.md b/odoo-bringout-oca-storage-storage_file/doc/DEPENDENCIES.md new file mode 100644 index 0000000..985d1f7 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_file/doc/DEPENDENCIES.md @@ -0,0 +1,5 @@ +# Dependencies + +This addon depends on: + +- [storage_backend](../../odoo-bringout-oca-storage-storage_backend) diff --git a/odoo-bringout-oca-storage-storage_file/doc/FAQ.md b/odoo-bringout-oca-storage-storage_file/doc/FAQ.md new file mode 100644 index 0000000..207002f --- /dev/null +++ b/odoo-bringout-oca-storage-storage_file/doc/FAQ.md @@ -0,0 +1,4 @@ +# FAQ + +- Q: Which Odoo version? A: 16.0 (OCA/OCB packaged). +- Q: How to enable? A: Start server with --addon storage_file or install in UI. diff --git a/odoo-bringout-oca-storage-storage_file/doc/INSTALL.md b/odoo-bringout-oca-storage-storage_file/doc/INSTALL.md new file mode 100644 index 0000000..6d32267 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_file/doc/INSTALL.md @@ -0,0 +1,7 @@ +# Install + +```bash +pip install odoo-bringout-oca-storage-storage_file" +# or +uv pip install odoo-bringout-oca-storage-storage_file" +``` diff --git a/odoo-bringout-oca-storage-storage_file/doc/MODELS.md b/odoo-bringout-oca-storage-storage_file/doc/MODELS.md new file mode 100644 index 0000000..b4c1199 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_file/doc/MODELS.md @@ -0,0 +1,14 @@ +# Models + +Detected core models and extensions in storage_file. + +```mermaid +classDiagram + class storage_file + class ir_actions_report + class storage_backend +``` + +Notes +- Classes show model technical names; fields omitted for brevity. +- Items listed under _inherit are extensions of existing models. diff --git a/odoo-bringout-oca-storage-storage_file/doc/OVERVIEW.md b/odoo-bringout-oca-storage-storage_file/doc/OVERVIEW.md new file mode 100644 index 0000000..449a79c --- /dev/null +++ b/odoo-bringout-oca-storage-storage_file/doc/OVERVIEW.md @@ -0,0 +1,6 @@ +# Overview + +Packaged Odoo addon: storage_file. Provides features documented in upstream Odoo 16 under this addon. + +- Source: OCA/OCB 16.0, addon storage_file +- License: LGPL-3 diff --git a/odoo-bringout-oca-storage-storage_file/doc/REPORTS.md b/odoo-bringout-oca-storage-storage_file/doc/REPORTS.md new file mode 100644 index 0000000..e0ea35f --- /dev/null +++ b/odoo-bringout-oca-storage-storage_file/doc/REPORTS.md @@ -0,0 +1,3 @@ +# Reports + +This module does not define custom reports. diff --git a/odoo-bringout-oca-storage-storage_file/doc/SECURITY.md b/odoo-bringout-oca-storage-storage_file/doc/SECURITY.md new file mode 100644 index 0000000..361623f --- /dev/null +++ b/odoo-bringout-oca-storage-storage_file/doc/SECURITY.md @@ -0,0 +1,41 @@ +# Security + +Access control and security definitions in storage_file. + +## Access Control Lists (ACLs) + +Model access permissions defined in: +- **[ir.model.access.csv](../storage_file/security/ir.model.access.csv)** + - 2 model access rules + +## Record Rules + +Row-level security rules defined in: + +## Security Groups & Configuration + +Security groups and permissions defined in: +- **[storage_file.xml](../storage_file/security/storage_file.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: +- **[ir.model.access.csv](../storage_file/security/ir.model.access.csv)** + - Model access permissions (CRUD rights) +- **[storage_file.xml](../storage_file/security/storage_file.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 diff --git a/odoo-bringout-oca-storage-storage_file/doc/TROUBLESHOOTING.md b/odoo-bringout-oca-storage-storage_file/doc/TROUBLESHOOTING.md new file mode 100644 index 0000000..56853cb --- /dev/null +++ b/odoo-bringout-oca-storage-storage_file/doc/TROUBLESHOOTING.md @@ -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. diff --git a/odoo-bringout-oca-storage-storage_file/doc/USAGE.md b/odoo-bringout-oca-storage-storage_file/doc/USAGE.md new file mode 100644 index 0000000..11862ee --- /dev/null +++ b/odoo-bringout-oca-storage-storage_file/doc/USAGE.md @@ -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 storage_file +``` diff --git a/odoo-bringout-oca-storage-storage_file/doc/WIZARDS.md b/odoo-bringout-oca-storage-storage_file/doc/WIZARDS.md new file mode 100644 index 0000000..48e790d --- /dev/null +++ b/odoo-bringout-oca-storage-storage_file/doc/WIZARDS.md @@ -0,0 +1,3 @@ +# Wizards + +This module does not include UI wizards. diff --git a/odoo-bringout-oca-storage-storage_file/pyproject.toml b/odoo-bringout-oca-storage-storage_file/pyproject.toml new file mode 100644 index 0000000..648728d --- /dev/null +++ b/odoo-bringout-oca-storage-storage_file/pyproject.toml @@ -0,0 +1,42 @@ +[project] +name = "odoo-bringout-oca-storage-storage_file" +version = "16.0.0" +description = "Storage File - Storage file in storage backend" +authors = [ + { name = "Ernad Husremovic", email = "hernad@bring.out.ba" } +] +dependencies = [ + "odoo-bringout-oca-storage-storage_backend>=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 = ["storage_file"] + +[tool.rye] +managed = true +dev-dependencies = [ + "pytest>=8.4.1", +] diff --git a/odoo-bringout-oca-storage-storage_file/storage_file/README.rst b/odoo-bringout-oca-storage-storage_file/storage_file/README.rst new file mode 100644 index 0000000..ef955ca --- /dev/null +++ b/odoo-bringout-oca-storage-storage_file/storage_file/README.rst @@ -0,0 +1,85 @@ +============ +Storage File +============ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:d386a4285cd08b7d08b3efef78ebf6dd22cf97ddca2c8fd8fe2a17f7caae8c3b + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png + :target: https://odoo-community.org/page/development-status + :alt: Production/Stable +.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstorage-lightgray.png?logo=github + :target: https://github.com/OCA/storage/tree/16.0/storage_file + :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-storage_file + :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| + +External file management depending on Storage Backend module. + +It include these features: +* link to any Odoo model/record +* store metadata like: checksum, mimetype + +Use cases (with help of additional modules): +- store pdf (like invoices) on a file server with high SLA +- access attachments with read/write on prod environment and only read only on dev / testing + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub 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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Akretion + +Contributors +~~~~~~~~~~~~ + +* Sebastien Beau +* Raphaël Reverdy + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/storage `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/odoo-bringout-oca-storage-storage_file/storage_file/__init__.py b/odoo-bringout-oca-storage-storage_file/storage_file/__init__.py new file mode 100644 index 0000000..91c5580 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_file/storage_file/__init__.py @@ -0,0 +1,2 @@ +from . import controllers +from . import models diff --git a/odoo-bringout-oca-storage-storage_file/storage_file/__manifest__.py b/odoo-bringout-oca-storage-storage_file/storage_file/__manifest__.py new file mode 100644 index 0000000..b758404 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_file/storage_file/__manifest__.py @@ -0,0 +1,26 @@ +# Copyright 2017 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +{ + "name": "Storage File", + "summary": "Storage file in storage backend", + "version": "16.0.1.0.1", + "category": "Storage", + "website": "https://github.com/OCA/storage", + "author": " Akretion, Odoo Community Association (OCA)", + "license": "LGPL-3", + "development_status": "Production/Stable", + "application": False, + "installable": True, + "external_dependencies": {"python": ["python_slugify"]}, + "depends": ["storage_backend"], + "data": [ + "views/storage_file_view.xml", + "views/storage_backend_view.xml", + "security/ir.model.access.csv", + "security/storage_file.xml", + "data/ir_cron.xml", + "data/storage_backend.xml", + ], +} diff --git a/odoo-bringout-oca-storage-storage_file/storage_file/controllers/__init__.py b/odoo-bringout-oca-storage-storage_file/storage_file/controllers/__init__.py new file mode 100644 index 0000000..12a7e52 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_file/storage_file/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/odoo-bringout-oca-storage-storage_file/storage_file/controllers/main.py b/odoo-bringout-oca-storage-storage_file/storage_file/controllers/main.py new file mode 100644 index 0000000..be1d6c2 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_file/storage_file/controllers/main.py @@ -0,0 +1,20 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + + +from odoo import http +from odoo.http import request + + +class StorageFileController(http.Controller): + @http.route( + ["/storage.file/"], type="http", auth="public" + ) + def content_common(self, slug_name_with_id, token=None, download=None, **kw): + storage_file = request.env["storage.file"].get_from_slug_name_with_id( + slug_name_with_id + ) + return ( + request.env["ir.binary"] + ._get_image_stream_from(storage_file, "data") + .get_response() + ) diff --git a/odoo-bringout-oca-storage-storage_file/storage_file/data/ir_cron.xml b/odoo-bringout-oca-storage-storage_file/storage_file/data/ir_cron.xml new file mode 100644 index 0000000..297a92c --- /dev/null +++ b/odoo-bringout-oca-storage-storage_file/storage_file/data/ir_cron.xml @@ -0,0 +1,15 @@ + + + + Clean Storage File + + + 1 + days + -1 + + + code + model._clean_storage_file() + + diff --git a/odoo-bringout-oca-storage-storage_file/storage_file/data/storage_backend.xml b/odoo-bringout-oca-storage-storage_file/storage_file/data/storage_backend.xml new file mode 100644 index 0000000..cb39acf --- /dev/null +++ b/odoo-bringout-oca-storage-storage_file/storage_file/data/storage_backend.xml @@ -0,0 +1,8 @@ + + + + name_with_id + odoo + + + diff --git a/odoo-bringout-oca-storage-storage_file/storage_file/i18n/bs.po b/odoo-bringout-oca-storage-storage_file/storage_file/i18n/bs.po new file mode 100644 index 0000000..b546110 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_file/storage_file/i18n/bs.po @@ -0,0 +1,331 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * storage_file +# +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: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__active +msgid "Active" +msgstr "Aktivan" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_backend__backend_view_use_internal_url +msgid "Backend View Use Internal Url" +msgstr "Pregled pozadine koristi internu URL" + +#. module: storage_file +#: model_terms:ir.ui.view,arch_db:storage_file.storage_backend_view_form +msgid "Base URL used for files" +msgstr "Osnovna URL korišten za datoteke" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_backend__base_url +msgid "Base Url" +msgstr "Osnovna URL" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_backend__base_url_for_files +msgid "Base Url For Files" +msgstr "Osnovna URL za datoteke" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__checksum +msgid "Checksum/SHA1" +msgstr "Kontrolni zbroj/SHA1" + +#. module: storage_file +#: model:ir.actions.server,name:storage_file.ir_cron_clean_storage_file_ir_actions_server +#: model:ir.cron,cron_name:storage_file.ir_cron_clean_storage_file +msgid "Clean Storage File" +msgstr "Očisti datoteku skladištenja" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__company_id +msgid "Company" +msgstr "Preduzeće" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__create_uid +msgid "Created by" +msgstr "Kreirao" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__create_date +msgid "Created on" +msgstr "Kreirano" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__data +msgid "Data" +msgstr "Podaci" + +#. module: storage_file +#: model:ir.model.fields,help:storage_file.field_storage_file__data +msgid "Datas" +msgstr "Podaci" + +#. module: storage_file +#: model:ir.model.fields,help:storage_file.field_storage_backend__backend_view_use_internal_url +msgid "" +"Decide if Odoo backend views should use the external URL (usually a CDN) or " +"the internal url with direct access to the storage. This could save you some" +" money if you pay by CDN traffic." +msgstr "" + +#. module: storage_file +#: model:ir.model.fields,help:storage_file.field_storage_backend__is_public +msgid "" +"Define if every files stored into this backend are public or not. Examples:\n" +"Private: your file/image can not be displayed is the user is not logged (not available on other website);\n" +"Public: your file/image can be displayed if nobody is logged (useful to display files on external websites)" +msgstr "" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__display_name +msgid "Display Name" +msgstr "Prikazani naziv" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__extension +msgid "Extension" +msgstr "Ekstenzija" + +#. module: storage_file +#: model:ir.model.fields.selection,name:storage_file.selection__storage_backend__served_by__external +msgid "External" +msgstr "Vanjski" + +#. module: storage_file +#: model:ir.actions.act_window,name:storage_file.act_open_storage_file_view +#: model:ir.ui.menu,name:storage_file.menu_storage_file +#: model_terms:ir.ui.view,arch_db:storage_file.storage_file_view_form +#: model_terms:ir.ui.view,arch_db:storage_file.storage_file_view_search +msgid "File" +msgstr "Datoteka" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__file_size +msgid "File Size" +msgstr "Veličine datoteke" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__file_type +msgid "File Type" +msgstr "Tip datoteke" + +#. module: storage_file +#. odoo-python +#: code:addons/storage_file/models/storage_file.py:0 +#, python-format +msgid "File can not be updated, remove it and create a new one" +msgstr "Datoteka se ne može ažurirati, uklonite je i kreirajte novu" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_backend__filename_strategy +msgid "Filename Strategy" +msgstr "Strategija naziva datoteka" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__filename +msgid "Filename without extension" +msgstr "Naziv datoteke bez ekstenzije" + +#. module: storage_file +#: model:ir.model.fields,help:storage_file.field_storage_file__internal_url +msgid "HTTP URL to load the file directly from storage." +msgstr "HTTP URL za direktno učitavanje datoteke iz skladištenja." + +#. module: storage_file +#: model:ir.model.fields,help:storage_file.field_storage_file__url +msgid "HTTP accessible path to the file" +msgstr "HTTP dostupna putanja do datoteke" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__human_file_size +msgid "Human File Size" +msgstr "Ljudska veličina datoteke" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__id +msgid "ID" +msgstr "ID" + +#. module: storage_file +#: model_terms:ir.ui.view,arch_db:storage_file.storage_backend_view_form +msgid "" +"If you have changed parameters via server env settings the URL might look " +"outdated." +msgstr "" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__internal_url +msgid "Internal Url" +msgstr "Interna URL" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_backend__is_public +msgid "Is Public" +msgstr "Je javno" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file____last_update +msgid "Last Modified on" +msgstr "Zadnje mijenjano" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__write_uid +msgid "Last Updated by" +msgstr "Zadnji ažurirao" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__write_date +msgid "Last Updated on" +msgstr "Zadnje ažurirano" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__mimetype +msgid "Mime Type" +msgstr "Mime Tip" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__name +msgid "Name" +msgstr "Naziv:" + +#. module: storage_file +#: model:ir.model.fields.selection,name:storage_file.selection__storage_backend__filename_strategy__name_with_id +msgid "Name and ID" +msgstr "Naziv i ID" + +#. module: storage_file +#: model:ir.model.fields,help:storage_file.field_storage_backend__url_include_directory_path +msgid "" +"Normally the directory_path it's for internal usage. If this flag is enabled" +" the path will be used to compute the public URL." +msgstr "" + +#. module: storage_file +#: model:ir.model.fields.selection,name:storage_file.selection__storage_backend__served_by__odoo +msgid "Odoo" +msgstr "Odoo" + +#. module: storage_file +#: model_terms:ir.ui.view,arch_db:storage_file.storage_backend_view_form +msgid "Recompute base URL for files" +msgstr "Ponovno izračunaj osnovnu URL za datoteke" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__relative_path +msgid "Relative Path" +msgstr "Relativna putanja" + +#. module: storage_file +#: model:ir.model.fields,help:storage_file.field_storage_file__relative_path +msgid "Relative location for backend" +msgstr "Relativna lokacija za pozadinu" + +#. module: storage_file +#: model:ir.model,name:storage_file.model_ir_actions_report +msgid "Report Action" +msgstr "Akcija izvještaja" + +#. module: storage_file +#: model:ir.model.fields.selection,name:storage_file.selection__storage_backend__filename_strategy__hash +msgid "SHA hash" +msgstr "SHA hash" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_backend__served_by +msgid "Served By" +msgstr "Poslužuje" + +#. module: storage_file +#: model_terms:ir.ui.view,arch_db:storage_file.storage_backend_view_form +msgid "" +"Served by Odoo option will use `web.base.url` as the base URL.\n" +"
Make sure this parameter is properly configured and accessible\n" +" from everwhere you want to access the service." +msgstr "" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__slug +msgid "Slug" +msgstr "Slug" + +#. module: storage_file +#: model:ir.model.fields,help:storage_file.field_storage_file__slug +msgid "Slug-ified name with ID for URL" +msgstr "Slug-ificirani naziv s ID-om za URL" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__backend_id +msgid "Storage" +msgstr "Skladištenje" + +#. module: storage_file +#: model:ir.model,name:storage_file.model_storage_backend +msgid "Storage Backend" +msgstr "Pozadina skladištenja" + +#. module: storage_file +#: model:ir.model,name:storage_file.model_storage_file +msgid "Storage File" +msgstr "Datoteka skladištenja" + +#. module: storage_file +#: model:ir.model.fields,help:storage_file.field_storage_backend__filename_strategy +msgid "" +"Strategy to build the name of the file to be stored.\n" +"Name and ID: will store the file with its name + its id.\n" +"SHA Hash: will use the hash of the file as filename (same method as the native attachment storage)" +msgstr "" + +#. module: storage_file +#. odoo-python +#: code:addons/storage_file/models/storage_file.py:0 +#, python-format +msgid "" +"The filename strategy is empty for the backend %s.\n" +"Please configure it" +msgstr "" + +#. module: storage_file +#: model:ir.model.constraint,message:storage_file.constraint_storage_file_path_uniq +msgid "The private path must be uniq per backend" +msgstr "Privatna putanja mora biti jedinstvena po pozadini" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__to_delete +msgid "To Delete" +msgstr "Za brisanje" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__url +msgid "Url" +msgstr "Url" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_backend__url_include_directory_path +msgid "Url Include Directory Path" +msgstr "URL uključuje putanju direktorija" + +#. module: storage_file +#: model_terms:ir.ui.view,arch_db:storage_file.storage_backend_view_form +msgid "" +"When served by external service you might have special environment configuration\n" +" for building final files URLs.\n" +"
For performance reasons, the base URL is computed and stored.\n" +" If you change some parameters (eg: in local dev environment or special instances)\n" +" and you still want to see the images you might need to refresh this URL\n" +" to make sure images and/or files are loaded correctly." +msgstr "" diff --git a/odoo-bringout-oca-storage-storage_file/storage_file/i18n/es.po b/odoo-bringout-oca-storage-storage_file/storage_file/i18n/es.po new file mode 100644 index 0000000..c9864ca --- /dev/null +++ b/odoo-bringout-oca-storage-storage_file/storage_file/i18n/es.po @@ -0,0 +1,368 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * storage_file +# +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 \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: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__active +msgid "Active" +msgstr "Activo" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_backend__backend_view_use_internal_url +msgid "Backend View Use Internal Url" +msgstr "Vista del Servidor Usar Url Interna" + +#. module: storage_file +#: model_terms:ir.ui.view,arch_db:storage_file.storage_backend_view_form +msgid "Base URL used for files" +msgstr "URL base utilizada para los archivos" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_backend__base_url +msgid "Base Url" +msgstr "Url Base" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_backend__base_url_for_files +msgid "Base Url For Files" +msgstr "Url base Para Archivos" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__checksum +msgid "Checksum/SHA1" +msgstr "Verificación de suma/SHA1" + +#. module: storage_file +#: model:ir.actions.server,name:storage_file.ir_cron_clean_storage_file_ir_actions_server +#: model:ir.cron,cron_name:storage_file.ir_cron_clean_storage_file +msgid "Clean Storage File" +msgstr "Archivo de almacenamiento limpio" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__company_id +msgid "Company" +msgstr "Companía" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__create_date +msgid "Created on" +msgstr "Creado el" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__data +msgid "Data" +msgstr "Datos" + +#. module: storage_file +#: model:ir.model.fields,help:storage_file.field_storage_file__data +msgid "Datas" +msgstr "Datos" + +#. module: storage_file +#: model:ir.model.fields,help:storage_file.field_storage_backend__backend_view_use_internal_url +msgid "" +"Decide if Odoo backend views should use the external URL (usually a CDN) or " +"the internal url with direct access to the storage. This could save you some" +" money if you pay by CDN traffic." +msgstr "" +"Decida si las vistas del servidor de Odoo deben usar la URL externa (" +"usualmente un CDN) o la URL interna con acceso directo al almacenamiento. " +"Esto podría ahorrarte algo de dinero si pagas por tráfico CDN." + +#. module: storage_file +#: model:ir.model.fields,help:storage_file.field_storage_backend__is_public +msgid "" +"Define if every files stored into this backend are public or not. Examples:\n" +"Private: your file/image can not be displayed is the user is not logged (not available on other website);\n" +"Public: your file/image can be displayed if nobody is logged (useful to display files on external websites)" +msgstr "" +"Define si todos los archivos almacenados en este servidor son públicos o no. " +"Ejemplos:\n" +"Privado: su archivo/imagen no puede mostrarse si el usuario no está " +"registrado (no disponible en otros sitios web);\n" +"Público: su archivo/imagen puede mostrarse si nadie ha iniciado sesión (útil " +"para mostrar archivos en sitios web externos)" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__display_name +msgid "Display Name" +msgstr "Mostrar Nombre" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__extension +msgid "Extension" +msgstr "Extensión" + +#. module: storage_file +#: model:ir.model.fields.selection,name:storage_file.selection__storage_backend__served_by__external +msgid "External" +msgstr "Externo" + +#. module: storage_file +#: model:ir.actions.act_window,name:storage_file.act_open_storage_file_view +#: model:ir.ui.menu,name:storage_file.menu_storage_file +#: model_terms:ir.ui.view,arch_db:storage_file.storage_file_view_form +#: model_terms:ir.ui.view,arch_db:storage_file.storage_file_view_search +msgid "File" +msgstr "Fichero" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__file_size +msgid "File Size" +msgstr "Tamaño del Archivo" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__file_type +msgid "File Type" +msgstr "Tipo de Archivo" + +#. module: storage_file +#. odoo-python +#: code:addons/storage_file/models/storage_file.py:0 +#, python-format +msgid "File can not be updated, remove it and create a new one" +msgstr "El archivo no se puede actualizar, elimínelo y cree uno nuevo" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_backend__filename_strategy +msgid "Filename Strategy" +msgstr "Estrategia de Archivos" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__filename +msgid "Filename without extension" +msgstr "Nombre de archivo sin extensión" + +#. module: storage_file +#: model:ir.model.fields,help:storage_file.field_storage_file__internal_url +msgid "HTTP URL to load the file directly from storage." +msgstr "URL HTTP para cargar el archivo directamente desde el almacenamiento." + +#. module: storage_file +#: model:ir.model.fields,help:storage_file.field_storage_file__url +msgid "HTTP accessible path to the file" +msgstr "Ruta de acceso HTTP al archivo" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__human_file_size +msgid "Human File Size" +msgstr "Tamaño del Archivo Humano" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__id +msgid "ID" +msgstr "ID (identificación)" + +#. module: storage_file +#: model_terms:ir.ui.view,arch_db:storage_file.storage_backend_view_form +msgid "" +"If you have changed parameters via server env settings the URL might look " +"outdated." +msgstr "" +"Si ha cambiado los parámetros a través de la configuración del entorno del " +"servidor, la URL puede parecer obsoleta." + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__internal_url +msgid "Internal Url" +msgstr "Url Interna" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_backend__is_public +msgid "Is Public" +msgstr "Es Público" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file____last_update +msgid "Last Modified on" +msgstr "Última Modificación el" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__write_uid +msgid "Last Updated by" +msgstr "Última actualización por" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__write_date +msgid "Last Updated on" +msgstr "Última Actualización el" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__mimetype +msgid "Mime Type" +msgstr "Tipo Mimo" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__name +msgid "Name" +msgstr "Nombre" + +#. module: storage_file +#: model:ir.model.fields.selection,name:storage_file.selection__storage_backend__filename_strategy__name_with_id +msgid "Name and ID" +msgstr "Nombre e ID" + +#. module: storage_file +#: model:ir.model.fields,help:storage_file.field_storage_backend__url_include_directory_path +msgid "" +"Normally the directory_path it's for internal usage. If this flag is enabled" +" the path will be used to compute the public URL." +msgstr "" +"Normalmente el directory_path es para uso interno. Si se activa esta opción, " +"la ruta se utilizará para calcular la URL pública." + +#. module: storage_file +#: model:ir.model.fields.selection,name:storage_file.selection__storage_backend__served_by__odoo +msgid "Odoo" +msgstr "Odoo" + +#. module: storage_file +#: model_terms:ir.ui.view,arch_db:storage_file.storage_backend_view_form +msgid "Recompute base URL for files" +msgstr "Recalcular la URL base de los archivos" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__relative_path +msgid "Relative Path" +msgstr "Trayectoria Relativa" + +#. module: storage_file +#: model:ir.model.fields,help:storage_file.field_storage_file__relative_path +msgid "Relative location for backend" +msgstr "Ubicación relativa del servidor" + +#. module: storage_file +#: model:ir.model,name:storage_file.model_ir_actions_report +msgid "Report Action" +msgstr "Informar Acción" + +#. module: storage_file +#: model:ir.model.fields.selection,name:storage_file.selection__storage_backend__filename_strategy__hash +msgid "SHA hash" +msgstr "Clave SHA" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_backend__served_by +msgid "Served By" +msgstr "Servido Por" + +#. module: storage_file +#: model_terms:ir.ui.view,arch_db:storage_file.storage_backend_view_form +msgid "" +"Served by Odoo option will use `web.base.url` as the base URL.\n" +"
Make sure this parameter is properly configured and accessible\n" +" from everwhere you want to access the service." +msgstr "" +"La opción servida por Odoo utilizará `web.base.url` como URL base.\n" +"
Asegúrese de que este parámetro esté configurado " +"correctamente y sea accesible\n" +" desde cualquier lugar que desee acceder al servicio." + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__slug +msgid "Slug" +msgstr "Babosa" + +#. module: storage_file +#: model:ir.model.fields,help:storage_file.field_storage_file__slug +msgid "Slug-ified name with ID for URL" +msgstr "Nombre slug-ificado con ID para URL" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__backend_id +msgid "Storage" +msgstr "Almacenamiento" + +#. module: storage_file +#: model:ir.model,name:storage_file.model_storage_backend +msgid "Storage Backend" +msgstr "Servidor de Almacenamiento" + +#. module: storage_file +#: model:ir.model,name:storage_file.model_storage_file +msgid "Storage File" +msgstr "Fichero de Almacenamiento" + +#. module: storage_file +#: model:ir.model.fields,help:storage_file.field_storage_backend__filename_strategy +msgid "" +"Strategy to build the name of the file to be stored.\n" +"Name and ID: will store the file with its name + its id.\n" +"SHA Hash: will use the hash of the file as filename (same method as the native attachment storage)" +msgstr "" +"Estrategia para construir el nombre del fichero a almacenar.\n" +"Nombre e ID: almacenará el fichero con su nombre + su id.\n" +"SHA Hash: utilizará el hash del archivo como nombre de archivo (mismo método " +"que el almacenamiento nativo de archivos adjuntos)" + +#. module: storage_file +#. odoo-python +#: code:addons/storage_file/models/storage_file.py:0 +#, python-format +msgid "" +"The filename strategy is empty for the backend %s.\n" +"Please configure it" +msgstr "" +"La estrategia de nombre de archivo está vacía para el servidor %s.\n" +"Por favor, configúrela" + +#. module: storage_file +#: model:ir.model.constraint,message:storage_file.constraint_storage_file_path_uniq +msgid "The private path must be uniq per backend" +msgstr "La ruta privada debe ser única por servidor" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__to_delete +msgid "To Delete" +msgstr "Para Eliminar" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__url +msgid "Url" +msgstr "Url" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_backend__url_include_directory_path +msgid "Url Include Directory Path" +msgstr "Url Incluir Ruta Directorio" + +#. module: storage_file +#: model_terms:ir.ui.view,arch_db:storage_file.storage_backend_view_form +msgid "" +"When served by external service you might have special environment configuration\n" +" for building final files URLs.\n" +"
For performance reasons, the base URL is computed and stored.\n" +" If you change some parameters (eg: in local dev environment or special instances)\n" +" and you still want to see the images you might need to refresh this URL\n" +" to make sure images and/or files are loaded correctly." +msgstr "" +"Cuando el servicio es atendido por un servicio externo, es posible que tenga " +"una configuración de entorno especial.\n" +" para crear URLs de archivos finales.\n" +"
Por motivos de rendimiento, la URL base se calcula " +"y almacena.\n" +" Si cambia algunos parámetros (por ejemplo: en el entorno " +"de desarrollo local o en instancias especiales)\n" +" y aún desea ver las imágenes, es posible que necesite " +"actualizar esta URL\n" +" para asegurarse de que las imágenes y/o archivos se " +"carguen correctamente." diff --git a/odoo-bringout-oca-storage-storage_file/storage_file/i18n/it.po b/odoo-bringout-oca-storage-storage_file/storage_file/i18n/it.po new file mode 100644 index 0000000..f3fbae2 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_file/storage_file/i18n/it.po @@ -0,0 +1,368 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * storage_file +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2025-02-11 11:06+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.6.2\n" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__active +msgid "Active" +msgstr "Attivo" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_backend__backend_view_use_internal_url +msgid "Backend View Use Internal Url" +msgstr "Vista backend utilizza URL interno" + +#. module: storage_file +#: model_terms:ir.ui.view,arch_db:storage_file.storage_backend_view_form +msgid "Base URL used for files" +msgstr "URL base utilizzato per i file" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_backend__base_url +msgid "Base Url" +msgstr "URL base" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_backend__base_url_for_files +msgid "Base Url For Files" +msgstr "URL base per i file" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__checksum +msgid "Checksum/SHA1" +msgstr "Checksum/SHA1" + +#. module: storage_file +#: model:ir.actions.server,name:storage_file.ir_cron_clean_storage_file_ir_actions_server +#: model:ir.cron,cron_name:storage_file.ir_cron_clean_storage_file +msgid "Clean Storage File" +msgstr "Pulisci deposito file" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__company_id +msgid "Company" +msgstr "Azienda" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__create_uid +msgid "Created by" +msgstr "Creato da" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__create_date +msgid "Created on" +msgstr "Creato il" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__data +msgid "Data" +msgstr "Dati" + +#. module: storage_file +#: model:ir.model.fields,help:storage_file.field_storage_file__data +msgid "Datas" +msgstr "Dati" + +#. module: storage_file +#: model:ir.model.fields,help:storage_file.field_storage_backend__backend_view_use_internal_url +msgid "" +"Decide if Odoo backend views should use the external URL (usually a CDN) or " +"the internal url with direct access to the storage. This could save you some" +" money if you pay by CDN traffic." +msgstr "" +"Decidere se le viste backend Odoo devono usare l'URL esterno (normalmente un " +"CDN) o l'URL interno con accesso diretto al deposito. Questo può far " +"risparmiare denaro se si paga il traffico CDN." + +#. module: storage_file +#: model:ir.model.fields,help:storage_file.field_storage_backend__is_public +msgid "" +"Define if every files stored into this backend are public or not. Examples:\n" +"Private: your file/image can not be displayed is the user is not logged (not available on other website);\n" +"Public: your file/image can be displayed if nobody is logged (useful to display files on external websites)" +msgstr "" +"Definisce se ogni file archiviato in questo bcackend è pubblico o no. Esempi:" +"\n" +"Privato: il file/immagine non può essere visualizzato se l'utente non ha " +"effettuato l'accesso (non disponibile nel sito web);\n" +"Pubblico: il file/immagine può essere visualizzato se nessuno ha effettuato " +"l'accesso (utile per visualizzare i file in siti web esterni)" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__display_name +msgid "Display Name" +msgstr "Nome visualizzato" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__extension +msgid "Extension" +msgstr "Estensione" + +#. module: storage_file +#: model:ir.model.fields.selection,name:storage_file.selection__storage_backend__served_by__external +msgid "External" +msgstr "Esterno" + +#. module: storage_file +#: model:ir.actions.act_window,name:storage_file.act_open_storage_file_view +#: model:ir.ui.menu,name:storage_file.menu_storage_file +#: model_terms:ir.ui.view,arch_db:storage_file.storage_file_view_form +#: model_terms:ir.ui.view,arch_db:storage_file.storage_file_view_search +msgid "File" +msgstr "File" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__file_size +msgid "File Size" +msgstr "Dimensione file" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__file_type +msgid "File Type" +msgstr "Tipo file" + +#. module: storage_file +#. odoo-python +#: code:addons/storage_file/models/storage_file.py:0 +#, python-format +msgid "File can not be updated, remove it and create a new one" +msgstr "Il file non può essere caricato, rimuoverlo e crearne uno nuovo" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_backend__filename_strategy +msgid "Filename Strategy" +msgstr "Strategia nome file" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__filename +msgid "Filename without extension" +msgstr "Nome file senza estensione" + +#. module: storage_file +#: model:ir.model.fields,help:storage_file.field_storage_file__internal_url +msgid "HTTP URL to load the file directly from storage." +msgstr "URL HTTP per caricare il file direttamente dal deposito." + +#. module: storage_file +#: model:ir.model.fields,help:storage_file.field_storage_file__url +msgid "HTTP accessible path to the file" +msgstr "Percorso HTTP al file accessibile" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__human_file_size +msgid "Human File Size" +msgstr "Dimensione file umana" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__id +msgid "ID" +msgstr "ID" + +#. module: storage_file +#: model_terms:ir.ui.view,arch_db:storage_file.storage_backend_view_form +msgid "" +"If you have changed parameters via server env settings the URL might look " +"outdated." +msgstr "" +"Se si sono modificati i parametri attraverso le impostazione ambiente server " +"l'URL può risultare vecchio." + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__internal_url +msgid "Internal Url" +msgstr "URL interno" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_backend__is_public +msgid "Is Public" +msgstr "È pubblico" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file____last_update +msgid "Last Modified on" +msgstr "Ultima modifica il" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__write_uid +msgid "Last Updated by" +msgstr "Ultimo aggiornamento di" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__write_date +msgid "Last Updated on" +msgstr "Ultimo aggiornamento il" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__mimetype +msgid "Mime Type" +msgstr "Tipo MIME" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__name +msgid "Name" +msgstr "Nome" + +#. module: storage_file +#: model:ir.model.fields.selection,name:storage_file.selection__storage_backend__filename_strategy__name_with_id +msgid "Name and ID" +msgstr "Nome e ID" + +#. module: storage_file +#: model:ir.model.fields,help:storage_file.field_storage_backend__url_include_directory_path +msgid "" +"Normally the directory_path it's 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: storage_file +#: model:ir.model.fields.selection,name:storage_file.selection__storage_backend__served_by__odoo +msgid "Odoo" +msgstr "Odoo" + +#. module: storage_file +#: model_terms:ir.ui.view,arch_db:storage_file.storage_backend_view_form +msgid "Recompute base URL for files" +msgstr "Ricalcola l'URL base per i file" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__relative_path +msgid "Relative Path" +msgstr "Percorso relativo" + +#. module: storage_file +#: model:ir.model.fields,help:storage_file.field_storage_file__relative_path +msgid "Relative location for backend" +msgstr "Posizione relativa per il backend" + +#. module: storage_file +#: model:ir.model,name:storage_file.model_ir_actions_report +msgid "Report Action" +msgstr "Azione resoconto" + +#. module: storage_file +#: model:ir.model.fields.selection,name:storage_file.selection__storage_backend__filename_strategy__hash +msgid "SHA hash" +msgstr "Hash SHA" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_backend__served_by +msgid "Served By" +msgstr "Fornito da" + +#. module: storage_file +#: model_terms:ir.ui.view,arch_db:storage_file.storage_backend_view_form +msgid "" +"Served by Odoo option will use `web.base.url` as the base URL.\n" +"
Make sure this parameter is properly configured and accessible\n" +" from everwhere you want to access the service." +msgstr "" +"Fornito dall'opzione Odoo userà `web.base.url` come URL base.\n" +"
Assicurarsi che questo parametro sia correttamente " +"configurato e accessibile\n" +" da ovunque si voglia accedere a questo servizio." + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__slug +msgid "Slug" +msgstr "Frazione" + +#. module: storage_file +#: model:ir.model.fields,help:storage_file.field_storage_file__slug +msgid "Slug-ified name with ID for URL" +msgstr "Nome frazionato con ID per URL" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__backend_id +msgid "Storage" +msgstr "Deposito" + +#. module: storage_file +#: model:ir.model,name:storage_file.model_storage_backend +msgid "Storage Backend" +msgstr "Backend deposito" + +#. module: storage_file +#: model:ir.model,name:storage_file.model_storage_file +msgid "Storage File" +msgstr "File deposito" + +#. module: storage_file +#: model:ir.model.fields,help:storage_file.field_storage_backend__filename_strategy +msgid "" +"Strategy to build the name of the file to be stored.\n" +"Name and ID: will store the file with its name + its id.\n" +"SHA Hash: will use the hash of the file as filename (same method as the native attachment storage)" +msgstr "" +"Strategia per costruire il nome del file da archiviare.\n" +"Nome e ID: archivierà il file con nome + il suo ID\n" +"Hash SHA: utilizzerà l'hash del file come nome del file (stesso metodo del " +"deposito allegati nativo)" + +#. module: storage_file +#. odoo-python +#: code:addons/storage_file/models/storage_file.py:0 +#, python-format +msgid "" +"The filename strategy is empty for the backend %s.\n" +"Please configure it" +msgstr "" +"La strategia del nome file è vuota per il backend %s.\n" +"Configurarlo" + +#. module: storage_file +#: model:ir.model.constraint,message:storage_file.constraint_storage_file_path_uniq +msgid "The private path must be uniq per backend" +msgstr "Il percorso privato deve essere univoco per backend" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__to_delete +msgid "To Delete" +msgstr "Da cancellare" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__url +msgid "Url" +msgstr "URL" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_backend__url_include_directory_path +msgid "Url Include Directory Path" +msgstr "L'URL include il percorso cartella" + +#. module: storage_file +#: model_terms:ir.ui.view,arch_db:storage_file.storage_backend_view_form +msgid "" +"When served by external service you might have special environment configuration\n" +" for building final files URLs.\n" +"
For performance reasons, the base URL is computed and stored.\n" +" If you change some parameters (eg: in local dev environment or special instances)\n" +" and you still want to see the images you might need to refresh this URL\n" +" to make sure images and/or files are loaded correctly." +msgstr "" +"Quando fornito da servizio esterno serve avere una configurazione ambiente " +"speciale\n" +" per costruire gli URL finali dei file.\n" +"
Per motivi di prestazione, l'URL base è calcolato e " +"salvato.\n" +" Se si cambiano alcun parametri (es. nell'ambiente " +"sviluppo locale o istanze speciali)\n" +" e si vuole continuare a vedere le immagini, bisogna " +"aggiornare l'URL\n" +" per essere sicuri che immagini e/o file siano caricati " +"correttamente." diff --git a/odoo-bringout-oca-storage-storage_file/storage_file/i18n/storage_file.pot b/odoo-bringout-oca-storage-storage_file/storage_file/i18n/storage_file.pot new file mode 100644 index 0000000..37b7624 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_file/storage_file/i18n/storage_file.pot @@ -0,0 +1,331 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * storage_file +# +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: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__active +msgid "Active" +msgstr "" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_backend__backend_view_use_internal_url +msgid "Backend View Use Internal Url" +msgstr "" + +#. module: storage_file +#: model_terms:ir.ui.view,arch_db:storage_file.storage_backend_view_form +msgid "Base URL used for files" +msgstr "" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_backend__base_url +msgid "Base Url" +msgstr "" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_backend__base_url_for_files +msgid "Base Url For Files" +msgstr "" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__checksum +msgid "Checksum/SHA1" +msgstr "" + +#. module: storage_file +#: model:ir.actions.server,name:storage_file.ir_cron_clean_storage_file_ir_actions_server +#: model:ir.cron,cron_name:storage_file.ir_cron_clean_storage_file +msgid "Clean Storage File" +msgstr "" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__company_id +msgid "Company" +msgstr "" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__create_uid +msgid "Created by" +msgstr "" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__create_date +msgid "Created on" +msgstr "" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__data +msgid "Data" +msgstr "" + +#. module: storage_file +#: model:ir.model.fields,help:storage_file.field_storage_file__data +msgid "Datas" +msgstr "" + +#. module: storage_file +#: model:ir.model.fields,help:storage_file.field_storage_backend__backend_view_use_internal_url +msgid "" +"Decide if Odoo backend views should use the external URL (usually a CDN) or " +"the internal url with direct access to the storage. This could save you some" +" money if you pay by CDN traffic." +msgstr "" + +#. module: storage_file +#: model:ir.model.fields,help:storage_file.field_storage_backend__is_public +msgid "" +"Define if every files stored into this backend are public or not. Examples:\n" +"Private: your file/image can not be displayed is the user is not logged (not available on other website);\n" +"Public: your file/image can be displayed if nobody is logged (useful to display files on external websites)" +msgstr "" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__display_name +msgid "Display Name" +msgstr "" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__extension +msgid "Extension" +msgstr "" + +#. module: storage_file +#: model:ir.model.fields.selection,name:storage_file.selection__storage_backend__served_by__external +msgid "External" +msgstr "" + +#. module: storage_file +#: model:ir.actions.act_window,name:storage_file.act_open_storage_file_view +#: model:ir.ui.menu,name:storage_file.menu_storage_file +#: model_terms:ir.ui.view,arch_db:storage_file.storage_file_view_form +#: model_terms:ir.ui.view,arch_db:storage_file.storage_file_view_search +msgid "File" +msgstr "" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__file_size +msgid "File Size" +msgstr "" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__file_type +msgid "File Type" +msgstr "" + +#. module: storage_file +#. odoo-python +#: code:addons/storage_file/models/storage_file.py:0 +#, python-format +msgid "File can not be updated, remove it and create a new one" +msgstr "" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_backend__filename_strategy +msgid "Filename Strategy" +msgstr "" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__filename +msgid "Filename without extension" +msgstr "" + +#. module: storage_file +#: model:ir.model.fields,help:storage_file.field_storage_file__internal_url +msgid "HTTP URL to load the file directly from storage." +msgstr "" + +#. module: storage_file +#: model:ir.model.fields,help:storage_file.field_storage_file__url +msgid "HTTP accessible path to the file" +msgstr "" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__human_file_size +msgid "Human File Size" +msgstr "" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__id +msgid "ID" +msgstr "" + +#. module: storage_file +#: model_terms:ir.ui.view,arch_db:storage_file.storage_backend_view_form +msgid "" +"If you have changed parameters via server env settings the URL might look " +"outdated." +msgstr "" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__internal_url +msgid "Internal Url" +msgstr "" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_backend__is_public +msgid "Is Public" +msgstr "" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file____last_update +msgid "Last Modified on" +msgstr "" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__write_date +msgid "Last Updated on" +msgstr "" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__mimetype +msgid "Mime Type" +msgstr "" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__name +msgid "Name" +msgstr "" + +#. module: storage_file +#: model:ir.model.fields.selection,name:storage_file.selection__storage_backend__filename_strategy__name_with_id +msgid "Name and ID" +msgstr "" + +#. module: storage_file +#: model:ir.model.fields,help:storage_file.field_storage_backend__url_include_directory_path +msgid "" +"Normally the directory_path it's for internal usage. If this flag is enabled" +" the path will be used to compute the public URL." +msgstr "" + +#. module: storage_file +#: model:ir.model.fields.selection,name:storage_file.selection__storage_backend__served_by__odoo +msgid "Odoo" +msgstr "" + +#. module: storage_file +#: model_terms:ir.ui.view,arch_db:storage_file.storage_backend_view_form +msgid "Recompute base URL for files" +msgstr "" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__relative_path +msgid "Relative Path" +msgstr "" + +#. module: storage_file +#: model:ir.model.fields,help:storage_file.field_storage_file__relative_path +msgid "Relative location for backend" +msgstr "" + +#. module: storage_file +#: model:ir.model,name:storage_file.model_ir_actions_report +msgid "Report Action" +msgstr "" + +#. module: storage_file +#: model:ir.model.fields.selection,name:storage_file.selection__storage_backend__filename_strategy__hash +msgid "SHA hash" +msgstr "" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_backend__served_by +msgid "Served By" +msgstr "" + +#. module: storage_file +#: model_terms:ir.ui.view,arch_db:storage_file.storage_backend_view_form +msgid "" +"Served by Odoo option will use `web.base.url` as the base URL.\n" +"
Make sure this parameter is properly configured and accessible\n" +" from everwhere you want to access the service." +msgstr "" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__slug +msgid "Slug" +msgstr "" + +#. module: storage_file +#: model:ir.model.fields,help:storage_file.field_storage_file__slug +msgid "Slug-ified name with ID for URL" +msgstr "" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__backend_id +msgid "Storage" +msgstr "" + +#. module: storage_file +#: model:ir.model,name:storage_file.model_storage_backend +msgid "Storage Backend" +msgstr "" + +#. module: storage_file +#: model:ir.model,name:storage_file.model_storage_file +msgid "Storage File" +msgstr "" + +#. module: storage_file +#: model:ir.model.fields,help:storage_file.field_storage_backend__filename_strategy +msgid "" +"Strategy to build the name of the file to be stored.\n" +"Name and ID: will store the file with its name + its id.\n" +"SHA Hash: will use the hash of the file as filename (same method as the native attachment storage)" +msgstr "" + +#. module: storage_file +#. odoo-python +#: code:addons/storage_file/models/storage_file.py:0 +#, python-format +msgid "" +"The filename strategy is empty for the backend %s.\n" +"Please configure it" +msgstr "" + +#. module: storage_file +#: model:ir.model.constraint,message:storage_file.constraint_storage_file_path_uniq +msgid "The private path must be uniq per backend" +msgstr "" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__to_delete +msgid "To Delete" +msgstr "" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_file__url +msgid "Url" +msgstr "" + +#. module: storage_file +#: model:ir.model.fields,field_description:storage_file.field_storage_backend__url_include_directory_path +msgid "Url Include Directory Path" +msgstr "" + +#. module: storage_file +#: model_terms:ir.ui.view,arch_db:storage_file.storage_backend_view_form +msgid "" +"When served by external service you might have special environment configuration\n" +" for building final files URLs.\n" +"
For performance reasons, the base URL is computed and stored.\n" +" If you change some parameters (eg: in local dev environment or special instances)\n" +" and you still want to see the images you might need to refresh this URL\n" +" to make sure images and/or files are loaded correctly." +msgstr "" diff --git a/odoo-bringout-oca-storage-storage_file/storage_file/models/__init__.py b/odoo-bringout-oca-storage-storage_file/storage_file/models/__init__.py new file mode 100644 index 0000000..49b6a5d --- /dev/null +++ b/odoo-bringout-oca-storage-storage_file/storage_file/models/__init__.py @@ -0,0 +1,3 @@ +from . import storage_file +from . import storage_backend +from . import ir_actions_report diff --git a/odoo-bringout-oca-storage-storage_file/storage_file/models/ir_actions_report.py b/odoo-bringout-oca-storage-storage_file/storage_file/models/ir_actions_report.py new file mode 100644 index 0000000..5285c03 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_file/storage_file/models/ir_actions_report.py @@ -0,0 +1,14 @@ +# Copyright 2021 Camptocamp SA (http://www.camptocamp.com). +# @author Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import models + + +class IrActionsReport(models.Model): + _inherit = "ir.actions.report" + + def render_qweb_pdf(self, res_ids=None, data=None): + return super( + IrActionsReport, self.with_context(print_report_pdf=True) + ).render_qweb_pdf(res_ids=res_ids, data=data) diff --git a/odoo-bringout-oca-storage-storage_file/storage_file/models/storage_backend.py b/odoo-bringout-oca-storage-storage_file/storage_file/models/storage_backend.py new file mode 100644 index 0000000..9871674 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_file/storage_file/models/storage_backend.py @@ -0,0 +1,167 @@ +# Copyright 2017 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# Copyright 2019 Camptocamp SA (http://www.camptocamp.com). +# @author Simone Orsi +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import logging + +from odoo import api, fields, models + +_logger = logging.getLogger(__name__) + + +class StorageBackend(models.Model): + _inherit = "storage.backend" + + filename_strategy = fields.Selection( + selection=[("name_with_id", "Name and ID"), ("hash", "SHA hash")], + default="name_with_id", + help=( + "Strategy to build the name of the file to be stored.\n" + "Name and ID: will store the file with its name + its id.\n" + "SHA Hash: will use the hash of the file as filename " + "(same method as the native attachment storage)" + ), + ) + served_by = fields.Selection( + selection=[("odoo", "Odoo"), ("external", "External")], + required=True, + default="odoo", + ) + base_url = fields.Char(default="") + is_public = fields.Boolean( + default=False, + help="Define if every files stored into this backend are " + "public or not. Examples:\n" + "Private: your file/image can not be displayed is the user is " + "not logged (not available on other website);\n" + "Public: your file/image can be displayed if nobody is " + "logged (useful to display files on external websites)", + ) + url_include_directory_path = fields.Boolean( + default=False, + help="Normally the directory_path it's 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) + backend_view_use_internal_url = fields.Boolean( + help="Decide if Odoo backend views should use the external URL (usually a CDN) " + "or the internal url with direct access to the storage. " + "This could save you some money if you pay by CDN traffic." + ) + + def write(self, vals): + # Ensure storage file URLs are up to date + clear_url_cache = False + url_related_fields = ( + "served_by", + "base_url", + "directory_path", + "url_include_directory_path", + ) + for fname in url_related_fields: + if fname in vals: + clear_url_cache = True + break + res = super().write(vals) + if clear_url_cache: + self.action_recompute_base_url_for_files() + return res + + @property + def _server_env_fields(self): + env_fields = super()._server_env_fields + env_fields.update( + { + "filename_strategy": {}, + "served_by": {}, + "base_url": {}, + "url_include_directory_path": {}, + } + ) + return env_fields + + _default_backend_xid = "storage_backend.default_storage_backend" + + @classmethod + def _get_backend_id_from_param(cls, env, param_name, default_fallback=True): + backend_id = None + param = env["ir.config_parameter"].sudo().get_param(param_name) + if param: + if param.isdigit(): + backend_id = int(param) + elif "." in param: + backend = env.ref(param, raise_if_not_found=False) + if backend: + backend_id = backend.id + if not backend_id and default_fallback: + backend = env.ref(cls._default_backend_xid, raise_if_not_found=False) + if backend: + backend_id = backend.id + else: + _logger.warn("No backend found, no default fallback found.") + return backend_id + + @api.depends( + "served_by", + "base_url", + "directory_path", + "url_include_directory_path", + ) + def _compute_base_url_for_files(self): + for record in self: + record.base_url_for_files = record._get_base_url_for_files() + + def _get_base_url_for_files(self): + """Retrieve base URL for files.""" + backend = self.sudo() + parts = [] + if backend.served_by == "external": + parts = [backend.base_url or ""] + if backend.url_include_directory_path and backend.directory_path: + parts.append(backend.directory_path) + return "/".join(parts) + + def action_recompute_base_url_for_files(self): + """Refresh base URL for files. + + Rationale: all the params for computing this URL might come from server env. + When this is the case, the URL - being stored - might be out of date. + This is because changes to server env fields are not detected at startup. + Hence, let's offer an easy way to promptly force this manually when needed. + """ + self._compute_base_url_for_files() + self.env["storage.file"].invalidate_model(["url"]) + + def _get_base_url_from_param(self): + base_url_param = ( + "report.url" if self.env.context.get("print_report_pdf") else "web.base.url" + ) + return self.env["ir.config_parameter"].sudo().get_param(base_url_param) + + def _get_url_for_file(self, storage_file): + """Return final full URL for given file.""" + backend = self.sudo() + if backend.served_by == "odoo": + parts = [ + self._get_base_url_from_param(), + "storage.file", + storage_file.slug, + ] + else: + parts = [backend.base_url_for_files or "", storage_file.relative_path or ""] + return "/".join([x.rstrip("/") for x in parts if x]) + + def _register_hook(self): + super()._register_hook() + backends = self.search([]).filtered( + lambda x: x._get_base_url_for_files() != x.base_url_for_files + ) + if not backends: + return + sql = f"SELECT id FROM {self._table} WHERE ID IN %s FOR UPDATE" + self.env.cr.execute(sql, (tuple(backends.ids),), log_exceptions=False) + backends.action_recompute_base_url_for_files() + _logger.info("storage.backend base URL for files refreshed") diff --git a/odoo-bringout-oca-storage-storage_file/storage_file/models/storage_file.py b/odoo-bringout-oca-storage-storage_file/storage_file/models/storage_file.py new file mode 100644 index 0000000..ff625e6 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_file/storage_file/models/storage_file.py @@ -0,0 +1,213 @@ +# Copyright 2017 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import base64 +import hashlib +import logging +import mimetypes +import os +import re + +from odoo import api, fields, models +from odoo.exceptions import UserError +from odoo.tools import human_size +from odoo.tools.translate import _ + +_logger = logging.getLogger(__name__) + +try: + from slugify import slugify +except ImportError: # pragma: no cover + _logger.debug("Cannot `import slugify`.") + + +REGEX_SLUGIFY = r"[^-a-z0-9_]+" + + +class StorageFile(models.Model): + _name = "storage.file" + _description = "Storage File" + + name = fields.Char(required=True, index=True) + backend_id = fields.Many2one( + "storage.backend", "Storage", index=True, required=True + ) + url = fields.Char(compute="_compute_url", help="HTTP accessible path to the file") + internal_url = fields.Char( + compute="_compute_internal_url", + help="HTTP URL to load the file directly from storage.", + ) + slug = fields.Char( + compute="_compute_slug", help="Slug-ified name with ID for URL", store=True + ) + relative_path = fields.Char(readonly=True, help="Relative location for backend") + file_size = fields.Integer() + human_file_size = fields.Char(compute="_compute_human_file_size", store=True) + checksum = fields.Char("Checksum/SHA1", size=40, index=True, readonly=True) + filename = fields.Char( + "Filename without extension", compute="_compute_extract_filename", store=True + ) + extension = fields.Char(compute="_compute_extract_filename", store=True) + mimetype = fields.Char("Mime Type", compute="_compute_extract_filename", store=True) + data = fields.Binary( + help="Datas", inverse="_inverse_data", compute="_compute_data", store=False + ) + to_delete = fields.Boolean() + active = fields.Boolean(default=True) + company_id = fields.Many2one( + "res.company", "Company", default=lambda self: self.env.user.company_id.id + ) + file_type = fields.Selection([]) + + _sql_constraints = [ + ( + "path_uniq", + "unique(relative_path, backend_id)", + "The private path must be uniq per backend", + ) + ] + + def write(self, vals): + if "data" in vals: + for record in self: + if record.data: + raise UserError( + _("File can not be updated, remove it and create a new one") + ) + return super(StorageFile, self).write(vals) + + @api.depends("file_size") + def _compute_human_file_size(self): + for record in self: + record.human_file_size = human_size(record.file_size) + + @api.depends("filename", "extension") + def _compute_slug(self): + for record in self: + record.slug = record._slugify_name_with_id() + + def _slugify_name_with_id(self): + return "{}{}".format( + slugify( + "{}-{}".format(self.filename, self.id), regex_pattern=REGEX_SLUGIFY + ), + self.extension, + ) + + def _build_relative_path(self, checksum): + self.ensure_one() + strategy = self.sudo().backend_id.filename_strategy + if not strategy: + raise UserError( + _( + "The filename strategy is empty for the backend %s.\n" + "Please configure it" + ) + % self.backend_id.name + ) + if strategy == "hash": + return checksum[:2] + "/" + checksum + elif strategy == "name_with_id": + return self.slug + + def _prepare_meta_for_file(self): + bin_data = base64.b64decode(self.data) + checksum = hashlib.sha1(bin_data).hexdigest() + relative_path = self._build_relative_path(checksum) + return { + "checksum": checksum, + "file_size": len(bin_data), + "relative_path": relative_path, + } + + def _inverse_data(self): + for record in self: + record.write(record._prepare_meta_for_file()) + record.backend_id.sudo().add( + record.relative_path, + record.data, + mimetype=record.mimetype, + binary=False, + ) + + def _compute_data(self): + for rec in self: + if self._context.get("bin_size"): + rec.data = rec.file_size + elif rec.relative_path: + rec.data = rec.backend_id.sudo().get(rec.relative_path, binary=False) + else: + rec.data = None + + @api.depends("relative_path", "backend_id") + def _compute_url(self): + for record in self: + record.url = record._get_url() + + def _get_url(self): + """Retrieve file URL based on backend params.""" + return self.backend_id._get_url_for_file(self) + + @api.depends("slug") + def _compute_internal_url(self): + for record in self: + record.internal_url = record._get_internal_url() + + def _get_internal_url(self): + """Retrieve file URL to load file directly from the storage. + + It is recommended to use this for Odoo backend internal usage + to not generate traffic on external services. + """ + return f"/storage.file/{self.slug}" + + @api.depends("name") + def _compute_extract_filename(self): + for rec in self: + if rec.name: + rec.filename, rec.extension = os.path.splitext(rec.name) + mime, __ = mimetypes.guess_type(rec.name) + else: + rec.filename = rec.extension = mime = False + rec.mimetype = mime + + def unlink(self): + if self._context.get("cleanning_storage_file"): + super(StorageFile, self).unlink() + else: + self.write({"to_delete": True, "active": False}) + return True + + @api.model + def _clean_storage_file(self): + # we must be sure that all the changes are into the DB since + # we by pass the ORM + self.flush_model() + self._cr.execute( + """SELECT id + FROM storage_file + WHERE to_delete=True FOR UPDATE""" + ) + ids = [x[0] for x in self._cr.fetchall()] + for st_file in self.browse(ids): + st_file.backend_id.sudo().delete(st_file.relative_path) + st_file.with_context(cleanning_storage_file=True).unlink() + # commit is required since the backend could be an external system + # therefore, if the record is deleted on the external system + # we must be sure that the record is also deleted into Odoo + st_file._cr.commit() + + @api.model + def get_from_slug_name_with_id(self, slug_name_with_id): + """ + Return a browse record from a string generated by the method + _slugify_name_with_id + :param slug_name_with_id: + :return: a BrowseRecord (could be empty...) + """ + # id is the last group of digit after '-' + _id = re.findall(r"-([0-9]+)", slug_name_with_id)[-1:] + if _id: + _id = int(_id[0]) + return self.browse(_id) diff --git a/odoo-bringout-oca-storage-storage_file/storage_file/readme/CONTRIBUTORS.rst b/odoo-bringout-oca-storage-storage_file/storage_file/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000..3cd6620 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_file/storage_file/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Sebastien Beau +* Raphaël Reverdy diff --git a/odoo-bringout-oca-storage-storage_file/storage_file/readme/DESCRIPTION.rst b/odoo-bringout-oca-storage-storage_file/storage_file/readme/DESCRIPTION.rst new file mode 100644 index 0000000..ca71fff --- /dev/null +++ b/odoo-bringout-oca-storage-storage_file/storage_file/readme/DESCRIPTION.rst @@ -0,0 +1,9 @@ +External file management depending on Storage Backend module. + +It include these features: +* link to any Odoo model/record +* store metadata like: checksum, mimetype + +Use cases (with help of additional modules): +- store pdf (like invoices) on a file server with high SLA +- access attachments with read/write on prod environment and only read only on dev / testing diff --git a/odoo-bringout-oca-storage-storage_file/storage_file/security/ir.model.access.csv b/odoo-bringout-oca-storage-storage_file/storage_file/security/ir.model.access.csv new file mode 100644 index 0000000..21c0347 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_file/storage_file/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_storage_file_edit,storage_file edit,model_storage_file,base.group_system,1,1,1,1 +access_storage_file_read_public,storage_file public read,model_storage_file,,1,0,0,0 diff --git a/odoo-bringout-oca-storage-storage_file/storage_file/security/storage_file.xml b/odoo-bringout-oca-storage-storage_file/storage_file/security/storage_file.xml new file mode 100644 index 0000000..2510d3d --- /dev/null +++ b/odoo-bringout-oca-storage-storage_file/storage_file/security/storage_file.xml @@ -0,0 +1,16 @@ + + + + + + Storage file public + + + [('backend_id.is_public', '=', True)] + + + + + + diff --git a/odoo-bringout-oca-storage-storage_file/storage_file/static/description/icon.png b/odoo-bringout-oca-storage-storage_file/storage_file/static/description/icon.png new file mode 100644 index 0000000..3a0328b Binary files /dev/null and b/odoo-bringout-oca-storage-storage_file/storage_file/static/description/icon.png differ diff --git a/odoo-bringout-oca-storage-storage_file/storage_file/static/description/index.html b/odoo-bringout-oca-storage-storage_file/storage_file/static/description/index.html new file mode 100644 index 0000000..a61b5ad --- /dev/null +++ b/odoo-bringout-oca-storage-storage_file/storage_file/static/description/index.html @@ -0,0 +1,430 @@ + + + + + +Storage File + + + +
+

Storage File

+ + +

Production/Stable License: LGPL-3 OCA/storage Translate me on Weblate Try me on Runboat

+

External file management depending on Storage Backend module.

+

It include these features: +* link to any Odoo model/record +* store metadata like: checksum, mimetype

+

Use cases (with help of additional modules): +- store pdf (like invoices) on a file server with high SLA +- access attachments with read/write on prod environment and only read only on dev / testing

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub 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.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Akretion
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/storage project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/odoo-bringout-oca-storage-storage_file/storage_file/tests/__init__.py b/odoo-bringout-oca-storage-storage_file/storage_file/tests/__init__.py new file mode 100644 index 0000000..f2d7ae7 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_file/storage_file/tests/__init__.py @@ -0,0 +1 @@ +from . import test_storage_file diff --git a/odoo-bringout-oca-storage-storage_file/storage_file/tests/test_storage_file.py b/odoo-bringout-oca-storage-storage_file/storage_file/tests/test_storage_file.py new file mode 100644 index 0000000..9ff1277 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_file/storage_file/tests/test_storage_file.py @@ -0,0 +1,281 @@ +# Copyright 2017 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +import base64 +from unittest import mock +from urllib import parse + +from odoo.exceptions import AccessError, UserError + +from odoo.addons.component.tests.common import TransactionComponentCase + + +class StorageFileCase(TransactionComponentCase): + def setUp(self): + super().setUp() + self.backend = self.env.ref("storage_backend.default_storage_backend") + data = b"This is a simple file" + self.filesize = len(data) + self.filedata = base64.b64encode(data) + self.filename = "test of my_file.txt" + + def _create_storage_file(self): + return self.env["storage.file"].create( + { + "name": self.filename, + "backend_id": self.backend.id, + "data": self.filedata, + } + ) + + def test_create_and_read_served_by_odoo(self): + stfile = self._create_storage_file() + self.assertEqual(stfile.data, self.filedata) + self.assertEqual(stfile.mimetype, "text/plain") + self.assertEqual(stfile.extension, ".txt") + self.assertEqual(stfile.filename, "test of my_file") + self.assertEqual(stfile.relative_path, "test-of-my_file-%s.txt" % stfile.id) + url = parse.urlparse(stfile.url) + self.assertEqual(url.path, "/storage.file/test-of-my_file-%s.txt" % stfile.id) + self.assertEqual(stfile.file_size, self.filesize) + + def test_get_from_slug_name_with_id(self): + stfile = self._create_storage_file() + stfile2 = self.env["storage.file"].get_from_slug_name_with_id( + "test-of-my_file-%s.txt" % stfile.id + ) + self.assertEqual(stfile, stfile2) + # the method parse the given string to find the id. The id is the + # last sequence of digit starting with '-' + stfile2 = self.env["storage.file"].get_from_slug_name_with_id( + "test-999-%s.txt2" % stfile.id + ) + self.assertEqual(stfile, stfile2) + stfile2 = self.env["storage.file"].get_from_slug_name_with_id( + "test-999-%s" % stfile.id + ) + self.assertEqual(stfile, stfile2) + + def test_slug(self): + stfile = self._create_storage_file() + self.assertEqual( + stfile.slug, + "test-of-my_file-{}.txt".format(stfile.id), + ) + stfile.name = "Name has changed.png" + self.assertEqual( + stfile.slug, + "name-has-changed-{}.png".format(stfile.id), + ) + + def test_internal_url(self): + stfile = self._create_storage_file() + self.assertEqual( + stfile.internal_url, + "/storage.file/test-of-my_file-{}.txt".format(stfile.id), + ) + stfile.name = "Name has changed.png" + self.assertEqual( + stfile.slug, + "name-has-changed-{}.png".format(stfile.id), + ) + self.assertEqual( + stfile.internal_url, + "/storage.file/name-has-changed-{}.png".format(stfile.id), + ) + + def test_url(self): + stfile = self._create_storage_file() + params = self.env["ir.config_parameter"].sudo() + base_url = params.get_param("web.base.url") + # served by odoo + self.assertEqual( + stfile.url, + "{}/storage.file/test-of-my_file-{}.txt".format(base_url, stfile.id), + ) + # served by external + stfile.backend_id.update( + { + "served_by": "external", + "base_url": "https://foo.com", + "directory_path": "baz", + } + ) + # path not included + self.assertEqual( + stfile.url, "https://foo.com/test-of-my_file-{}.txt".format(stfile.id) + ) + # path included + stfile.backend_id.url_include_directory_path = True + self.assertEqual( + stfile.url, "https://foo.com/baz/test-of-my_file-{}.txt".format(stfile.id) + ) + + def test_url_for_report(self): + stfile = self._create_storage_file() + params = self.env["ir.config_parameter"].sudo() + params.set_param("report.url", "http://report.url") + # served by odoo + self.assertEqual( + stfile.with_context(print_report_pdf=True).url, + "http://report.url/storage.file/test-of-my_file-{}.txt".format(stfile.id), + ) + + def test_create_store_with_hash(self): + self.backend.filename_strategy = "hash" + stfile = self._create_storage_file() + self.assertEqual(stfile.data, self.filedata) + self.assertEqual(stfile.mimetype, "text/plain") + self.assertEqual(stfile.extension, ".txt") + self.assertEqual(stfile.filename, "test of my_file") + self.assertEqual( + stfile.relative_path, "13/1322d9ccb3d257095185b205eadc9307aae5dc84" + ) + + def test_missing_name_strategy(self): + self.backend.filename_strategy = None + with self.assertRaises(UserError): + self._create_storage_file() + + def test_create_and_read_served_by_external(self): + self.backend.write( + {"served_by": "external", "base_url": "https://cdn.example.com"} + ) + stfile = self._create_storage_file() + self.assertEqual(stfile.data, self.filedata) + self.assertEqual( + stfile.url, "https://cdn.example.com/test-of-my_file-%s.txt" % stfile.id + ) + self.assertEqual(stfile.file_size, self.filesize) + + def test_read_bin_size(self): + stfile = self._create_storage_file() + self.assertEqual(stfile.with_context(bin_size=True).data, b"21.00 bytes") + + def test_cannot_update_data(self): + stfile = self._create_storage_file() + data = base64.b64encode(b"This is different data") + with self.assertRaises(UserError): + stfile.write({"data": data}) + + # check that the file have been not modified + self.assertEqual(stfile.read()[0]["data"], self.filedata) + + def test_unlink(self): + # Do not commit during the test + self.cr.commit = lambda: True + stfile = self._create_storage_file() + + backend = stfile.backend_id + relative_path = stfile.relative_path + stfile.unlink() + + # Check the the storage file is set to delete + # and the file still exist on the storage + self.assertEqual(stfile.to_delete, True) + self.assertIn(relative_path, backend.list_files()) + + # Run the method to clean the storage.file + self.env["storage.file"]._clean_storage_file() + + # Check that the file is deleted + files = ( + self.env["storage.file"] + .with_context(active_test=False) + .search([("id", "=", stfile.id)]) + ) + self.assertEqual(len(files), 0) + self.assertNotIn(relative_path, backend.list_files()) + + def test_public_access1(self): + """ + Test the public access (when is_public on the backend). + When checked, the public user should have access to every content + (storage.file). + For this case, we use this public user and try to read a field on + no-public storage.file. + An exception should be raised because the backend is not public + :return: bool + """ + storage_file = self._create_storage_file() + # Ensure it's False (we shouldn't specify a is_public = False on the + # storage.backend creation because False must be the default value) + self.assertFalse(storage_file.backend_id.is_public) + # Public user used on the controller when authentication is 'public' + public_user = self.env.ref("base.public_user") + with self.assertRaises(AccessError): + # BUG OR NOT with_user doesn't invalidate the cache... + # force cache invalidation + self.env.cache.invalidate() + self.env[storage_file._name].with_user(public_user).browse( + storage_file.ids + ).name + return True + + def test_public_access2(self): + """ + Test the public access (when is_public on the backend). + When checked, the public user should have access to every content + (storage.file). + For this case, we use this public user and try to read a field on + no-public storage.file. + This public user should have access because the backend is public + :return: bool + """ + storage_file = self._create_storage_file() + storage_file.backend_id.write({"is_public": True}) + self.assertTrue(storage_file.backend_id.is_public) + # Public user used on the controller when authentication is 'public' + public_user = self.env.ref("base.public_user") + env = self.env(user=public_user) + storage_file_public = env[storage_file._name].browse(storage_file.ids) + self.assertTrue(storage_file_public.name) + return True + + def test_public_access3(self): + """ + Test the public access (when is_public on the backend). + When checked, the public user should have access to every content + (storage.file). + For this case, we use the demo user and try to read a field on + no-public storage.file (no exception should be raised) + :return: bool + """ + storage_file = self._create_storage_file() + # Ensure it's False (we shouldn't specify a is_public = False on the + # storage.backend creation because False must be the default value) + self.assertFalse(storage_file.backend_id.is_public) + demo_user = self.env.ref("base.user_demo") + env = self.env(user=demo_user) + storage_file_public = env[storage_file._name].browse(storage_file.ids) + self.assertTrue(storage_file_public.name) + return True + + def test_get_backend_from_param(self): + storage_file = self._create_storage_file() + with mock.patch.object( + type(self.env["ir.config_parameter"]), "get_param" + ) as mocked: + mocked.return_value = str(storage_file.backend_id.id) + self.assertEqual( + self.env["storage.backend"]._get_backend_id_from_param( + self.env, "foo.baz" + ), + storage_file.backend_id.id, + ) + with mock.patch.object( + type(self.env["ir.config_parameter"]), "get_param" + ) as mocked: + mocked.return_value = "storage_backend.default_storage_backend" + self.assertEqual( + self.env["storage.backend"]._get_backend_id_from_param( + self.env, "foo.baz" + ), + storage_file.backend_id.id, + ) + + def test_empty(self): + # get_url is called on new records + empty = self.env["storage.file"].new({})._get_url() + self.assertEqual(empty, "") diff --git a/odoo-bringout-oca-storage-storage_file/storage_file/views/storage_backend_view.xml b/odoo-bringout-oca-storage-storage_file/storage_file/views/storage_backend_view.xml new file mode 100644 index 0000000..3234c09 --- /dev/null +++ b/odoo-bringout-oca-storage-storage_file/storage_file/views/storage_backend_view.xml @@ -0,0 +1,62 @@ + + + + storage.backend + + + + + + + + + + + + + + +