Initial commit: OCA Storage packages (17 packages)

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

View file

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

View file

@ -0,0 +1,32 @@
# Architecture
```mermaid
flowchart TD
U[Users] -->|HTTP| V[Views and QWeb Templates]
V --> C[Controllers]
V --> W[Wizards Transient Models]
C --> M[Models and ORM]
W --> M
M --> R[Reports]
DX[Data XML] --> M
S[Security ACLs and Groups] -. enforces .-> M
subgraph Fs_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.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 <https://odoo-community.org/page/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
<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:
.. 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
<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.
.. code-block:: python
from odoo import models
class MyModel(models.Model):
_name = 'my.model'
_inherit = ['fs_image.mixin']
.. code-block:: xml
<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**
- (`#305 <https://github.com/OCA/storage/issues/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 <https://github.com/OCA/storage/issues/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 <https://github.com/OCA/storage/issues/307>`_)
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/storage/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/storage/issues/new?body=module:%20fs_image%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* ACSONE SA/NV
Contributors
~~~~~~~~~~~~
* Laurent Mignon <laurent.mignon@acsone.eu>
Maintainers
~~~~~~~~~~~
This module is maintained by the OCA.
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
.. |maintainer-lmignon| image:: https://github.com/lmignon.png?size=40px
:target: https://github.com/lmignon
:alt: lmignon
Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
|maintainer-lmignon|
This module is part of the `OCA/storage <https://github.com/OCA/storage/tree/16.0/fs_image>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View file

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

View file

@ -0,0 +1,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/**/*",
],
},
}

View file

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

View file

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

View file

@ -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 <informatica@totmaterial.es>\n"
"Language-Team: none\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.17\n"
#. module: fs_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"

View file

@ -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)\" <benjamin.willig@acsone.eu>\n"
"Language-Team: none\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 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 ""

View file

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

View file

@ -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 <stefano.consolaro@mymage.it>\n"
"Language-Team: none\n"
"Language: it\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.6.2\n"
#. module: 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"

View file

@ -0,0 +1,2 @@
from . import ir_attachment
from . import fs_image_mixin

View file

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

View file

@ -0,0 +1,15 @@
# Copyright 2023 ACSONE SA/NV
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models
class 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,
)

View file

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

View file

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

View file

@ -0,0 +1,36 @@
16.0.1.0.3 (2024-02-23)
~~~~~~~~~~~~~~~~~~~~~~~
**Bugfixes**
- (`#305 <https://github.com/OCA/storage/issues/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 <https://github.com/OCA/storage/issues/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 <https://github.com/OCA/storage/issues/307>`_)

View file

@ -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
<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:
.. 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
<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.
.. code-block:: python
from odoo import models
class MyModel(models.Model):
_name = 'my.model'
_inherit = ['fs_image.mixin']
.. code-block:: xml
<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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View file

@ -0,0 +1,591 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>Fs Image</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: gray; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic, pre.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="fs-image">
<h1 class="title">Fs Image</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:720789db007b07811c46c77857a24c41551a6f2554c9517630613347c8447f80
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Alpha" src="https://img.shields.io/badge/maturity-Alpha-red.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/storage/tree/16.0/fs_image"><img alt="OCA/storage" src="https://img.shields.io/badge/github-OCA%2Fstorage-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/storage-16-0/storage-16-0-fs_image"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/storage&amp;target_branch=16.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This addon defines a new field <strong>FSImage</strong> to use in your models. It is a
subclass of the <strong>FSFile</strong> field and comes with the same features. It extends
the <strong>FSFile</strong> field with specific properties dedicated to images. On the field
definition, the following additional properties are available:</p>
<ul class="simple">
<li><strong>max_width</strong> (int): maximum width of the image in pixels (default: <tt class="docutils literal">0</tt>, no limit)</li>
<li><strong>max_height</strong> (int): maximum height of the image in pixels (default: <tt class="docutils literal">0</tt>, no limit)</li>
<li><strong>verify_resolution</strong> (bool):whether the image resolution should be verified
to ensure it doesnt go over the maximum image resolution (default: <tt class="docutils literal">True</tt>).
See <cite>odoo.tools.image.ImageProcess</cite> for maximum image resolution (default: <tt class="docutils literal">50e6</tt>).</li>
</ul>
<p>On the fields value side, the value is an instance of a subclass of
<cite>odoo.addons.fs_file.fields.FSFileValue</cite>. 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.</p>
<div class="admonition important">
<p class="first admonition-title">Important</p>
<p class="last">This is an alpha version, the data model and design can change at any time without warning.
Only for development or testing purpose, do not use in production.
<a class="reference external" href="https://odoo-community.org/page/development-status">More details on development status</a></p>
</div>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#usage" id="toc-entry-1">Usage</a></li>
<li><a class="reference internal" href="#changelog" id="toc-entry-2">Changelog</a><ul>
<li><a class="reference internal" href="#section-1" id="toc-entry-3">16.0.1.0.3 (2024-02-23)</a></li>
<li><a class="reference internal" href="#section-2" id="toc-entry-4">16.0.1.0.2 (2023-12-02)</a></li>
<li><a class="reference internal" href="#section-3" id="toc-entry-5">16.0.1.0.1 (2023-12-02)</a></li>
</ul>
</li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-6">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-7">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-8">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-9">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-10">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#toc-entry-1">Usage</a></h1>
<p>This new field type can be used in the same way as the odoo Image field type.</p>
<pre class="code python literal-block">
<span class="kn">from</span> <span class="nn">odoo</span> <span class="kn">import</span> <span class="n">models</span><span class="w">
</span><span class="kn">from</span> <span class="nn">odoo.addons.fs_image.fields</span> <span class="kn">import</span> <span class="n">FSImage</span><span class="w">
</span><span class="k">class</span> <span class="nc">MyModel</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span><span class="w">
</span> <span class="n">_name</span> <span class="o">=</span> <span class="s1">'my.model'</span><span class="w">
</span> <span class="n">image</span> <span class="o">=</span> <span class="n">FSImage</span><span class="p">(</span><span class="s1">'Image'</span><span class="p">,</span> <span class="n">max_width</span><span class="o">=</span><span class="mi">1920</span><span class="p">,</span> <span class="n">max_height</span><span class="o">=</span><span class="mi">1920</span><span class="p">)</span>
</pre>
<pre class="code xml literal-block">
<span class="nt">&lt;record</span><span class="w"> </span><span class="na">id=</span><span class="s">&quot;my_model_form&quot;</span><span class="w"> </span><span class="na">model=</span><span class="s">&quot;ir.ui.view&quot;</span><span class="nt">&gt;</span><span class="w">
</span><span class="nt">&lt;field</span><span class="w"> </span><span class="na">name=</span><span class="s">&quot;name&quot;</span><span class="nt">&gt;</span>my.model.form<span class="nt">&lt;/field&gt;</span><span class="w">
</span><span class="nt">&lt;field</span><span class="w"> </span><span class="na">name=</span><span class="s">&quot;model&quot;</span><span class="nt">&gt;</span>my.model<span class="nt">&lt;/field&gt;</span><span class="w">
</span><span class="nt">&lt;field</span><span class="w"> </span><span class="na">name=</span><span class="s">&quot;arch&quot;</span><span class="w"> </span><span class="na">type=</span><span class="s">&quot;xml&quot;</span><span class="nt">&gt;</span><span class="w">
</span><span class="nt">&lt;form&gt;</span><span class="w">
</span><span class="nt">&lt;sheet&gt;</span><span class="w">
</span><span class="nt">&lt;group&gt;</span><span class="w">
</span><span class="nt">&lt;field</span><span class="w"> </span><span class="na">name=</span><span class="s">&quot;image&quot;</span><span class="w"> </span><span class="na">class=</span><span class="s">&quot;oe_avatar&quot;</span><span class="nt">/&gt;</span><span class="w">
</span><span class="nt">&lt;/group&gt;</span><span class="w">
</span><span class="nt">&lt;/sheet&gt;</span><span class="w">
</span><span class="nt">&lt;/form&gt;</span><span class="w">
</span><span class="nt">&lt;/field&gt;</span><span class="w">
</span><span class="nt">&lt;/record&gt;</span>
</pre>
<p>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.</p>
<p>A mode advanced and useful example is the following:</p>
<pre class="code python literal-block">
<span class="kn">from</span> <span class="nn">odoo</span> <span class="kn">import</span> <span class="n">models</span><span class="w">
</span><span class="kn">from</span> <span class="nn">odoo.addons.fs_image.fields</span> <span class="kn">import</span> <span class="n">FSImage</span><span class="w">
</span><span class="k">class</span> <span class="nc">MyModel</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span><span class="w">
</span> <span class="n">_name</span> <span class="o">=</span> <span class="s1">'my.model'</span><span class="w">
</span> <span class="n">image_1920</span> <span class="o">=</span> <span class="n">FSImage</span><span class="p">(</span><span class="s1">'Image'</span><span class="p">,</span> <span class="n">max_width</span><span class="o">=</span><span class="mi">1920</span><span class="p">,</span> <span class="n">max_height</span><span class="o">=</span><span class="mi">1920</span><span class="p">)</span><span class="w">
</span> <span class="n">image_128</span> <span class="o">=</span> <span class="n">FSImage</span><span class="p">(</span><span class="s1">'Image'</span><span class="p">,</span> <span class="n">max_width</span><span class="o">=</span><span class="mi">128</span><span class="p">,</span> <span class="n">max_height</span><span class="o">=</span><span class="mi">128</span><span class="p">,</span> <span class="n">related</span><span class="o">=</span><span class="s1">'image_1920'</span><span class="p">,</span> <span class="n">store</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</pre>
<pre class="code xml literal-block">
<span class="nt">&lt;record</span><span class="w"> </span><span class="na">id=</span><span class="s">&quot;my_model_form&quot;</span><span class="w"> </span><span class="na">model=</span><span class="s">&quot;ir.ui.view&quot;</span><span class="nt">&gt;</span><span class="w">
</span><span class="nt">&lt;field</span><span class="w"> </span><span class="na">name=</span><span class="s">&quot;name&quot;</span><span class="nt">&gt;</span>my.model.form<span class="nt">&lt;/field&gt;</span><span class="w">
</span><span class="nt">&lt;field</span><span class="w"> </span><span class="na">name=</span><span class="s">&quot;model&quot;</span><span class="nt">&gt;</span>my.model<span class="nt">&lt;/field&gt;</span><span class="w">
</span><span class="nt">&lt;field</span><span class="w"> </span><span class="na">name=</span><span class="s">&quot;arch&quot;</span><span class="w"> </span><span class="na">type=</span><span class="s">&quot;xml&quot;</span><span class="nt">&gt;</span><span class="w">
</span><span class="nt">&lt;form&gt;</span><span class="w">
</span><span class="nt">&lt;sheet&gt;</span><span class="w">
</span><span class="nt">&lt;group&gt;</span><span class="w">
</span><span class="nt">&lt;field</span><span class="w">
</span><span class="na">name=</span><span class="s">&quot;image_1920&quot;</span><span class="w">
</span><span class="na">class=</span><span class="s">&quot;oe_avatar&quot;</span><span class="w">
</span><span class="na">options=</span><span class="s">&quot;{'preview_image': 'image_128', 'zoom': true}&quot;</span><span class="w">
</span><span class="nt">/&gt;</span><span class="w">
</span><span class="nt">&lt;/group&gt;</span><span class="w">
</span><span class="nt">&lt;/sheet&gt;</span><span class="w">
</span><span class="nt">&lt;/form&gt;</span><span class="w">
</span><span class="nt">&lt;/field&gt;</span><span class="w">
</span><span class="nt">&lt;/record&gt;</span>
</pre>
<p>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 its 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.</p>
<p>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 its the most common use case.
When using an image field in a model, its recommended to use this mixin class
in order ensure that the image_medium field is always defined. A good practice
is to use the <cite>image_medium</cite> field as a preview image for the <cite>image</cite> field in
the form view to avoid to overload the form view with a large image and consume
too much bandwidth.</p>
<pre class="code python literal-block">
<span class="kn">from</span> <span class="nn">odoo</span> <span class="kn">import</span> <span class="n">models</span><span class="w">
</span><span class="k">class</span> <span class="nc">MyModel</span><span class="p">(</span><span class="n">models</span><span class="o">.</span><span class="n">Model</span><span class="p">):</span><span class="w">
</span> <span class="n">_name</span> <span class="o">=</span> <span class="s1">'my.model'</span><span class="w">
</span> <span class="n">_inherit</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'fs_image.mixin'</span><span class="p">]</span>
</pre>
<pre class="code xml literal-block">
<span class="nt">&lt;record</span><span class="w"> </span><span class="na">id=</span><span class="s">&quot;my_model_form&quot;</span><span class="w"> </span><span class="na">model=</span><span class="s">&quot;ir.ui.view&quot;</span><span class="nt">&gt;</span><span class="w">
</span><span class="nt">&lt;field</span><span class="w"> </span><span class="na">name=</span><span class="s">&quot;name&quot;</span><span class="nt">&gt;</span>my.model.form<span class="nt">&lt;/field&gt;</span><span class="w">
</span><span class="nt">&lt;field</span><span class="w"> </span><span class="na">name=</span><span class="s">&quot;model&quot;</span><span class="nt">&gt;</span>my.model<span class="nt">&lt;/field&gt;</span><span class="w">
</span><span class="nt">&lt;field</span><span class="w"> </span><span class="na">name=</span><span class="s">&quot;arch&quot;</span><span class="w"> </span><span class="na">type=</span><span class="s">&quot;xml&quot;</span><span class="nt">&gt;</span><span class="w">
</span><span class="nt">&lt;form&gt;</span><span class="w">
</span><span class="nt">&lt;sheet&gt;</span><span class="w">
</span><span class="nt">&lt;group&gt;</span><span class="w">
</span><span class="nt">&lt;field</span><span class="w">
</span><span class="na">name=</span><span class="s">&quot;image&quot;</span><span class="w">
</span><span class="na">class=</span><span class="s">&quot;oe_avatar&quot;</span><span class="w">
</span><span class="na">options=</span><span class="s">&quot;{'preview_image': 'image_medium', 'zoom': true}&quot;</span><span class="w">
</span><span class="nt">/&gt;</span><span class="w">
</span><span class="nt">&lt;/group&gt;</span><span class="w">
</span><span class="nt">&lt;/sheet&gt;</span><span class="w">
</span><span class="nt">&lt;/form&gt;</span><span class="w">
</span><span class="nt">&lt;/field&gt;</span><span class="w">
</span><span class="nt">&lt;/record&gt;</span>
</pre>
</div>
<div class="section" id="changelog">
<h1><a class="toc-backref" href="#toc-entry-2">Changelog</a></h1>
<div class="section" id="section-1">
<h2><a class="toc-backref" href="#toc-entry-3">16.0.1.0.3 (2024-02-23)</a></h2>
<p><strong>Bugfixes</strong></p>
<ul class="simple">
<li>(<a class="reference external" href="https://github.com/OCA/storage/issues/305">#305</a>)</li>
</ul>
</div>
<div class="section" id="section-2">
<h2><a class="toc-backref" href="#toc-entry-4">16.0.1.0.2 (2023-12-02)</a></h2>
<p><strong>Bugfixes</strong></p>
<ul>
<li><p class="first">Fix view crash when uploading an image</p>
<p>The rawCacheKey is appropriately managed by the base class and reflects the
records 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. (<a class="reference external" href="https://github.com/OCA/storage/issues/305">#305</a>)</p>
</li>
</ul>
</div>
<div class="section" id="section-3">
<h2><a class="toc-backref" href="#toc-entry-5">16.0.1.0.1 (2023-12-02)</a></h2>
<p><strong>Bugfixes</strong></p>
<ul>
<li><p class="first">Avoid to generate an SQL update query when an image field is read.</p>
<p>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. (<a class="reference external" href="https://github.com/OCA/storage/issues/307">#307</a>)</p>
</li>
</ul>
</div>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#toc-entry-6">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/storage/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/storage/issues/new?body=module:%20fs_image%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#toc-entry-7">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#toc-entry-8">Authors</a></h2>
<ul class="simple">
<li>ACSONE SA/NV</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#toc-entry-9">Contributors</a></h2>
<ul class="simple">
<li>Laurent Mignon &lt;<a class="reference external" href="mailto:laurent.mignon&#64;acsone.eu">laurent.mignon&#64;acsone.eu</a>&gt;</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#toc-entry-10">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org">
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
</a>
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.</p>
<p>Current <a class="reference external" href="https://odoo-community.org/page/maintainer-role">maintainer</a>:</p>
<p><a class="reference external image-reference" href="https://github.com/lmignon"><img alt="lmignon" src="https://github.com/lmignon.png?size=40px" /></a></p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/storage/tree/16.0/fs_image">OCA/storage</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,5 @@
.fs_file_download_button {
top: 10% !important;
left: 50% !important;
position: absolute !important;
}

View file

@ -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},
};

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="fs_image.AltTextDialog" owl="1">
<Dialog size="'md'" title="props.title">
<div class="form-group row">
<t t-if="props.readonly">
<span t-esc="props.value or ''" />
</t>
<div class="col-sm-12 o_field_widget o_field_text">
<input
type="text"
id="altText"
t-ref="altText"
t-att-value="props.altText"
class="o_input"
/>
</div>
</div>
<t t-set-slot="footer" owl="1">
<button
class="btn btn-primary"
t-ref="btn-confirm"
t-on-click="onConfirm"
>Save changes</button>
<button
class="btn btn-secondary"
t-ref="btn-close"
t-on-click="onClose"
>Cancel</button>
</t>
</Dialog>
</t>
</templates>

View file

@ -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);

View file

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="fs_image.FSImageField" owl="1">
<div class="d-inline-block position-relative opacity-trigger-hover">
<div
t-attf-class="position-absolute d-flex justify-content-between w-100 bottom-0 opacity-0 opacity-100-hover {{isMobile ? 'o_mobile_controls' : ''}}"
aria-atomic="true"
t-att-style="sizeStyle"
>
<t t-if="!props.readonly">
<FileUploader
acceptedFileExtensions="props.acceptedFileExtensions"
t-key="props.record.resId"
onUploaded.bind="onFileUploaded"
type="'image'"
>
<t t-set-slot="toggler">
<button
class="o_select_file_button btn btn-light border-0 rounded-circle m-1 p-1"
data-tooltip="Edit"
aria-label="Edit"
>
<i class="fa fa-pencil fa-fw" />
</button>
</t>
<t t-if="props.value and state.isValid">
<button
class="o_alt_text_file_button btn btn-light border-0 rounded-circle m-1 p-1"
data-tooltip="Alt Text"
aria-label="Set Alt Text"
t-on-click="onAltTextEdit"
>
<i class="fa fa-blind fa-fw" />
</button>
<button
class="o_clear_file_button btn btn-light border-0 rounded-circle m-1 p-1"
data-tooltip="Clear"
aria-label="Clear"
t-on-click="onFileRemove"
>
<i class="fa fa-trash-o fa-fw" />
</button>
</t>
</FileUploader>
</t>
</div>
<img
class="img img-fluid w-100"
alt="Binary file"
t-att-src="this.getUrl(props.previewImage or props.name)"
t-att-name="props.name"
t-att-height="props.height"
t-att-width="props.width"
t-att-style="sizeStyle"
t-att-alt="props.alt"
t-on-error.stop="onLoadFailed"
t-att-data-tooltip-template="hasTooltip and tooltipAttributes.template"
t-att-data-tooltip-info="hasTooltip and tooltipAttributes.info"
t-att-data-tooltip-delay="hasTooltip and props.zoomDelay"
/>
<button
t-if="props.value and state.isValid"
class="fs_file_download_button btn btn-light border-0 rounded-circle m-1 p-1 translate-middle opacity-0 opacity-100-hover"
data-tooltip="Download"
aria-label="Download"
t-on-click="onFileDownload"
>
<i class="fa fa-download fa-fw" />
</button>
</div>
</t>
</templates>

View file

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

View file

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

View file

@ -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()

View file

@ -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",
]